注意:长答案!
我在 Love2D 中做过一个类似的项目,它运行得非常快,所以我看不出在 Lua 中自己做数学而不是使用 OpenGL(无论如何都没有公开)的问题。
与评论相反,您不应该气馁。3D 方向和透视背后的数学原理实际上非常简单,只要您对它有所了解。
对于方向,四元数可能是矫枉过正。我发现要使用旋转进行 3D 投影,只需要Vec2
、Vec3
和Camera
类。虽然在数学上存在一些细微的差异,但实际上,向量的向量构成了非常合适的变换矩阵,而变换矩阵则构成了非常合适的方向。作为向量向量的矩阵的好处是您只需要编写一个类来处理两者。
要投影一个向量v
,请考虑相机的 3 个参数:
loc
, aVec3
为相机的位置
trans
, a Mat3by3
(也称为 a Vec3
of Vec3
's) 表示相机方向
的倒数
- (免责声明:使用矩阵进行相机定位在技术上被认为是有害的,因为小的舍入误差会累积,但在实际使用中没问题)
zoom
,用于确定透视的比例因子。A 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 a.x * b.x + a.y + b.y + a.z * b.z end
对于矩阵
Vec3(axis1, axis2, axis3)
给定点积的定义,用向量点缀的矩阵v
将产生
axis1 * v.x + axis2 * v.y + axis3 * v.z
这意味着 的第一个元素v
表示axis1
要移动多少个,第二个元素表示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
对于“偏航”(围绕 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 所做的。
快乐编码!希望这有帮助!