简单的 3D 投影和方向处理?

发布于 2024-12-07 10:28:18 字数 340 浏览 1 评论 0原文

我目前正在开发一款古怪的复古飞行模拟游戏,我的 3D 项目遇到了一些问题,因为我找不到任何关于该主题的可靠通用文档。

给定以下信息,如何将游戏世界中的简单位置向量转换为屏幕上的二维向量:

摄像机位置 相机方向(见下文) 视野 屏幕的高度和宽度(以及纵横比)

我也在寻找一种存储方向的方法,我已经编写了一个基本的矢量库,但我不确定如何存储在两个相机中使用的旋转(和投影代码)以及游戏中对象旋转的实际处理。我目前正在考虑使用四元数,但是是否可以(并且容易)使用四元数而不是矩阵进行投影变换?

有没有关于代码中实现四元数的好资源?我是否必须为复数编写一个单独的库?

感谢您的时间和任何帮助:)

I'm currently working on a quirky retro flight sim and I've run into a few problems with my 3d projects, in so much as I can't find any solid generic documentation on the subject.

How do I convert simple positional vectors in my game world into 2d vectors on screen given the following information:

The cameras position
a cameras orientation ( see below)
a field of view
a height and width of the screen (and aspect ratio)

I'm also looking for a way to store orientations, I've already written a basic vector library but I'm unsure as to how to store rotations for use in both the camera (and projection code) as well as actual handling of rotations of in-game objects. I'm currently looking at using quaternions but it is it possible (and easy) to use quaternions instead of matrices for projection transformations?

Are there any good sources on implementation quaternions in code? Will I have to write a seperate library for complex numbers?

Thank you for your time and any help :)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

若水般的淡然安静女子 2024-12-14 10:28:18

注意:长答案!

我在 Love2D 中完成了一个类似的项目,它运行得非常快,所以我不认为自己在 Lua 中进行数学计算而不是使用 OpenGL(OpenGL 不是)有什么问题。反正没暴露)。

与评论相反,您不应该灰心。一旦您掌握了 3D 方向和透视背后的数学原理,实际上就非常简单了。

对于方向来说,四元数可能有点过分了。我发现要进行旋转 3D 投影,只需要 Vec2Vec3Camera 类。虽然在数学上存在一些细微的差异,但在实用中,向量的向量可以构成完全合适的变换矩阵,而变换矩阵可以构成完全合适的方向。矩阵是向量的向量,其优点是您只需要编写一个类来处理两者。

要投影矢量 v,请考虑相机的 3 个参数:

  • loc、表示相机位置
  • trans Vec3 code>,一个 Mat3by3(也称为 Vec3Vec3),用于相机的方向
    • (免责声明:使用矩阵进行相机方向在技术上被认为是有害的,因为小的舍入误差可能会累积,但在实际使用中没有问题)
  • zoom,用于确定视角的缩放因子。 z(相对于相机)的zoom相当于处于2D;也就是说,从角度来看没有缩放。

投影的工作原理如下:

function Camera:project(v)
    local relv -- v positioned relative to the camera, both in orientation and location
    relv = self.trans * (v - self.loc) -- here '*' is vector dot product
    if relv.z > 0 then
        -- v is in front of the camera
        local w -- perspective scaling factor
        w = self.zoom / relv.z
        local projv -- projected vector
        projv = Vec2(relv.x * w, relv.y * w)
        return projv
    else
        -- v is behind the camera
        return nil
    end
end

假设 Vec2(0, 0) 对应于窗口的中心,而不是角落。设置它是一个简单的翻译。

trans 应以单位矩阵开始:Vec3(Vec3(1, 0, 0), Vec3(0, 1, 0), Vec3(0, 0, 1)) 并增量计算,每次方向改变时进行小的调整。

我感觉你已经了解矩阵的基础知识,但如果你不知道,想法是这样的:
矩阵是向量的向量,至少在这种情况下,可以将其视为坐标系。每个向量都可以被视为坐标系的一个轴。通过更改矩阵的元素(它们是向量,被视为矩阵的列),您可以更改该坐标系中坐标的含义。在正常使用中,向量的第一个分量意味着向右移动,第二个分量意味着向上,第三个分量意味着向前。但是,使用矩阵,您可以使每个分量指向任意方向。 点积的定义为

function Vec3.dot(a, b) return ax * bx + ay + by + az * bz end

对于矩阵

Vec3(axis1, axis2, axis3)<, /code>

给定点积的定义,用向量 v 点缀的矩阵将产生

axis1 * vx + axis2 * vy + axis3 * vz

这意味着第一个v 的元素表示要移动多少个 axis1,第二个元素表示要移动多少个 axis2,第三个元素表示要移动多少个 axis2表示要移动多少个 axis3,如果以标准坐标而不是矩阵坐标表示,则最终结果为 v。当我们将矩阵与向量相乘时,我们正在改变向量分量的含义。从本质上讲,它是诸如“任何以前向右的东西现在都不再向右且更向前”或类似的陈述的数学表达式。在一句话中,矩阵变换了空间。

回到手头的任务,要使用矩阵以角度 theta 表示“俯仰”(即围绕 x 轴)的旋转,您可以编写:

function pitchrotation(theta)
    return Vec3(
        -- axis 1
        -- rotated x axis
        -- we're rotating *around* the x axis, so it stays the same
        Vec3(
            1,
            0,
            0
        ),
        -- axis 2
        -- rotated y axis
        Vec3(
            0,
            math.cos(theta),
            math.sin(theta)
        ),
        -- axis 3
        -- rotated z axis
        Vec3(
            0,
            -math.sin(theta),
            math.cos(theta)
        )
    )
end

对于“偏航”(围绕 x 轴),您可以编写: y 轴):

function yawrotation(theta)
    return Vec3(
        -- axis 1
        -- rotated x axis
        Vec3(
            math.cos(theta),
            0,
            math.sin(theta)
        ),
        -- axis 2
        -- rotated y axis
        -- we're rotating *around* the y axis, so it stays the same
        Vec3(
            0,
            1,
            0
        ),
        -- axis 3
        -- rotated z axis
        Vec3(
            -math.sin(theta),
            0,
            math.cos(theta)
        )
    )
end

最后“滚动”(绕 z 轴),这在飞行模拟中特别有用:

function rollrotation(theta)
    return Vec3(
        -- axis 1
        -- rotated x axis
        Vec3(
            math.cos(theta),
            math.sin(theta),
            0
        ),
        -- axis 2
        -- rotated y axis
        Vec3(
            -math.sin(theta),
            math.cos(theta),
            0
        ),
        -- axis 3
        -- rotated z axis
        -- we're rotating *around* the z axis, so it stays the same
        Vec3(
            0,
            0,
            1
        )
    )
end

如果您在脑海中想象这对 x、y 和 z 轴的影响,所有这些余弦和正弦以及标志翻转可能会开始 感觉。他们都在那里是有原因的。

最后,我们到达了难题的最后一步,即应用这些旋转。矩阵的一个很好的特性是很容易复合它们。您可以非常轻松地进行变换 - 您只需变换每个轴即可!将现有矩阵 A 转换为矩阵 B

function combinematrices(a, b)
    return Vec3(b * a.x, b * a.y, b * a.z) -- x y and z are the first second and third axes
end

这意味着,如果您想对相机应用更改,只需使用此矩阵组合机制即可每帧稍微旋转方向。相机类的这些函数将提供一种简单的更改方法:

function Camera:rotateyaw(theta)
    self.trans = combinematrices(self.trans, yawrotation(-theta))
end

我们使用负 theta,因为我们希望 trans 与相机方向相反,以进行投影。您可以使用俯仰和滚动来实现类似的功能。

准备好所有这些构建块后,您就应该准备好在 Lua 中编写 3D 图形代码了。您将想要尝试使用 zoom - 我通常使用 500,但这实际上取决于应用程序。

缺少的一件事是深度测试,如果没有 OpenGL,这确实无法完成。如果您绘制的是线框点以外的任何内容,则没有真正的好方法来确保所有内容都按正确的顺序绘制。您可以进行排序,但效率很低,并且它无法处理某些必须逐像素进行排序的极端情况,而这正是 OpenGL 所做的。

快乐编码!希望这有帮助!

CAUTION: LONG ANSWER!

I've done a similar project in Love2D, and it runs perfectly fast, so I don't see the problem with doing the math yourself in Lua rather than using OpenGL (which isn't exposed anyway).

Contrary to the comments, you should not be discouraged. The math behind 3D orientation and perspective is in fact quite simple, once you get the feel for it.

For orientation, quaternions are probably overkill. I've found that to do 3D projection with rotation, only Vec2, Vec3, and Camera classes are needed. While mathematically there are a few subtle differences, in practicality Vectors of Vectors make perfectly suitable transformation matrices, and transformation matrices make perfectly suitable orientations. A matrix being a vector of vectors has the upside that you only need to write one class to handle both.

To project a vector v, take into account 3 parameters of the camera:

  • loc, a Vec3 for the camera's position
  • trans, a Mat3by3 (also known as a Vec3 of Vec3's) for the inverse of the camera's orientation
    • (disclaimer: using matrices for camera orientation is technically considered harmful, because small rounding errors can accumulate, but in actual use it's fine)
  • zoom, a scaling factor used to determine the perspective. A z (relative to the camera) of zoom is equivalent to being in 2D; that is, no scaling from perspective.

projection works like this:

function Camera:project(v)
    local relv -- v positioned relative to the camera, both in orientation and location
    relv = self.trans * (v - self.loc) -- here '*' is vector dot product
    if relv.z > 0 then
        -- v is in front of the camera
        local w -- perspective scaling factor
        w = self.zoom / relv.z
        local projv -- projected vector
        projv = Vec2(relv.x * w, relv.y * w)
        return projv
    else
        -- v is behind the camera
        return nil
    end
end

this assumes that Vec2(0, 0) corresponds to the center of the window, not a corner. Setting that up is a simple translation.

trans should start out as the identity matrix: Vec3(Vec3(1, 0, 0), Vec3(0, 1, 0), Vec3(0, 0, 1)) and be calculated incrementally, making small adjustments every time an orientation change is made.

I get the feeling you already know the basics of matrices, but if you don't, the idea is this:
A matrix is a Vector of Vectors, which can be thought of, at least in this case, as a coordinate system. Each vector can be thought of as one axis of the coordinate system. By changing the elements of the matrix (which are vectors and are thought of as the matrix's columns), you change the meaning of coordinates in that coordinate system. In normal usage, the first component of a vector means move right, the second component means up, and the third component means forward. However, with a matrix, you can make each component point in an arbitrary direction. The definition of dot product is

function Vec3.dot(a, b) return a.x * b.x + a.y + b.y + a.z * b.z end

for a matrix

Vec3(axis1, axis2, axis3)

given the definition of dot product, that matrix dotted with a vector v would yield

axis1 * v.x + axis2 * v.y + axis3 * v.z

which means that the first element of v says how many axis1s to move by, the second element says how many axis2's to move by, and the third element says how many axis3's to move by, with the end result being v, if it were expressed in standard coordinates instead of the coordinates of the matrix. When we multiply a matrix with a vector, we're changing the meaning of the components of the vector. Essentially, its the mathematical expression of a statement like "anything that was to the right is now less to the right and more forward" or anything similar. In one sentence, a matrix transforms a space.

Getting back to the task at hand, to represent a rotation in "pitch" (meaning around the x axis) by an angle theta using a matrix, you could write:

function pitchrotation(theta)
    return Vec3(
        -- axis 1
        -- rotated x axis
        -- we're rotating *around* the x axis, so it stays the same
        Vec3(
            1,
            0,
            0
        ),
        -- axis 2
        -- rotated y axis
        Vec3(
            0,
            math.cos(theta),
            math.sin(theta)
        ),
        -- axis 3
        -- rotated z axis
        Vec3(
            0,
            -math.sin(theta),
            math.cos(theta)
        )
    )
end

and for "yaw" (around the y axis):

function yawrotation(theta)
    return Vec3(
        -- axis 1
        -- rotated x axis
        Vec3(
            math.cos(theta),
            0,
            math.sin(theta)
        ),
        -- axis 2
        -- rotated y axis
        -- we're rotating *around* the y axis, so it stays the same
        Vec3(
            0,
            1,
            0
        ),
        -- axis 3
        -- rotated z axis
        Vec3(
            -math.sin(theta),
            0,
            math.cos(theta)
        )
    )
end

and finally "roll" (around the z axis), which is especially useful in a flight sim:

function rollrotation(theta)
    return Vec3(
        -- axis 1
        -- rotated x axis
        Vec3(
            math.cos(theta),
            math.sin(theta),
            0
        ),
        -- axis 2
        -- rotated y axis
        Vec3(
            -math.sin(theta),
            math.cos(theta),
            0
        ),
        -- axis 3
        -- rotated z axis
        -- we're rotating *around* the z axis, so it stays the same
        Vec3(
            0,
            0,
            1
        )
    )
end

If you visualize what that does to the x, y, and z axes in your head, all those cosines and sines and sign flips might start to make sense. They're all in there for a reason.

Finally, we reach the last step of the puzzle, which is applying these rotations. A nice feature of matrices is that it's easy to compound them. You can transform a transformation very easily - you simply transform each axis! To transform an existing matrix A by a matrix B:

function combinematrices(a, b)
    return Vec3(b * a.x, b * a.y, b * a.z) -- x y and z are the first second and third axes
end

what this means is that if you want to apply a change to your camera, you can simply use this matrix combination mechanism to rotate the orientation a little bit each frame. These functions for your camera class will provide an easy way to make changes:

function Camera:rotateyaw(theta)
    self.trans = combinematrices(self.trans, yawrotation(-theta))
end

we use negative theta because we want trans to be the opposite of the orientation of the camera, for projection. You can make similar functions with pitch and roll.

With all of these building blocks in place, you should be all set to write 3D graphics code in Lua. You're going to want to experiment with zoom - I generally use 500, but it really depends of the application.

The one piece missing, that really can't be accomplished without OpenGL, is depth testing. If you're drawing anything but points of wireframe, there isn't really a good way to make sure everything draws in the right order. You can sort, but that's inefficient, and it doesn't handle some corner cases where you have to do it pixel by pixel, which is what OpenGL does.

Happy coding! Hope that was helpful!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文