这是复杂的东西。请阅读有关该主题的书,以获取所有数学和细节。如果你打算长期玩这些东西,你需要知道这些东西。这个答案只是为了让你可以弄湿你的脚并四处乱窜。
乘法矩阵
第一件事。矩阵相乘是一件相当简单的事情。
假设您有矩阵A、B和C,其中AB = C。假设您想计算矩阵C在第 3 行第 2 列的值。
- 取A的第三行和B的第二列。您现在应该从A和B中获得相同数量的值。(如果你没有为这两个矩阵定义矩阵乘法。你不能这样做。)如果两者都是 4×4 矩阵,你应该有来自A(第 3 行)的 4 个值和来自B的 4 个值(第 2 列)。
- 将A的每个值与B的每个值相乘。您最终应该得到 4 个新值。
- 添加这些值。
您现在在第 3 行第 2 列有矩阵C的值。当然,挑战在于以编程方式执行此操作。
/* AB = C
Row-major ordering
a[0][0] a[0][2] a[0][3]...
a[1][0] a[1][4] ...
a[2][0] ...
...*/
public static mmMul(double[][] a, double[][] b, double[][] c) {
c_height = b.length; // Height of b
c_width = a[0].length; // Width of a
common_side = a.length; // Height of a, width of b
for (int i = 0; i < c_height; i++) {
for (int j = 0; j < c_width; j++) {
// Ready to calculate value of c[i][j]
c[i][j] = 0;
// Iterate through ith row of a, jth col of b in lockstep
for (int k = 0; k < common_side; k++) {
c[i][j] += a[i][k] * b[k][j];
}
}
}
}
齐次坐标
您有 3D 坐标。假设你有 (5, 2, 1)。这些是笛卡尔坐标。我们称它们为 ( x , y , z )。
齐次坐标意味着您在笛卡尔坐标的末尾写了一个额外的 1。(5, 2, 1) 变为 (5, 2, 1, 1)。我们称它们为(x、y、z、w)。
每当您进行使w ≠ 1 的变换时,您将坐标的每个分量除以w。这会改变您的x、y和z,并再次使w = 1。(即使你的转换没有改变w ,这样做也没有什么坏处。它只是将所有内容除以 1,什么都不做。)
您可以使用同质坐标做一些非常酷的事情,即使它们背后的数学并不完全有意义。正是在这一点上,我要求您再次查看此答案顶部的建议。
转换一个点
我将在本节和以下各节中使用 OpenGL 术语和方法。如果有什么不清楚或似乎与您的目标相冲突(因为这对我来说似乎有点像家庭作业:P),请发表评论。
我还将首先假设您的滚动、倾斜和平移矩阵是正确的。
当您想使用变换矩阵变换一个点时,将该矩阵右乘以表示您的点的列向量。假设您想通过某个变换矩阵A翻译 (5, 2, 1) 。您首先定义v = [5, 2, 1, 1] T。(我用小 T写 [ x , y , z , w ] T表示你应该把它写成列向量。)
// Your point in 3D
double v[4][5] = {{5}, {2}, {1}, {1}}
在这种情况下,Av = v 1,其中v 1是您的转换点。像矩阵乘法一样进行此乘法运算,其中A为 4×4,v为 4×1。您最终将得到一个 4×1 矩阵(这是另一个列向量)。
// Transforming a single point with a roll
double v_1[4][6];
mmMul(rollMat, v, v_1);
现在,如果要应用多个变换矩阵,首先将它们组合成一个变换矩阵。通过按照您希望它们应用的顺序将矩阵相乘来做到这一点。
以编程方式,您应该从单位矩阵开始,然后对每个变换矩阵进行右乘。令I 4为 4×4 单位矩阵,并让A 1、A 2、A 3 ……成为您的变换矩阵。让您的最终转换矩阵为A final
最终← I 4最终← A最终A 1
A最终← A最终A 2最终← A最终A 3 _ _ _
请注意,我使用该箭头表示分配。当你实现这个时,确保在矩阵乘法计算中仍在使用A final时不要覆盖它! 复印一份。
// A composite transformation matrix (roll, then tilt)
double a_final[4][4] =
{
{1, 0, 0, 0},
{0, 1, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1}
}; // the 4 x 4 identity matrix
double a_final_copy[4][4];
mCopy(a_final, a_final_copy); // make a copy of a_final
mmMul(rollMat, a_final_copy, a_final);
mCopy(a_final, a_final_copy); // update the copy
mmMul(tiltMat, a_final_copy, a_final);
最后,做与上面相同的乘法:A final v = v 1
// Use the above matrix to transform v
mmMul(a_final, v, v_1);
从开始到结束
相机变换应表示为视图矩阵。在此处执行您的A视图 v = v 1操作。(v将您的世界坐标表示为 4×1 列向量,A final是您的A视图。)
// World coordinates to eye coordinates
// A_view is a_final from above
mmMult(a_view, v_world, v_view);
投影变换描述了透视变换。这就是使较近的物体变大而使较远的物体变小的原因。这是在相机转换之后执行的。如果您还不需要透视,只需使用单位矩阵作为投影矩阵。无论如何,在这里执行A v 1 = v 2。
// Eye coordinates to clip coordinates
// If you don't care about perspective, SKIP THIS STEP
mmMult(a_projection, v_view, v_eye);
接下来,您需要进行透视划分。这深入研究了我尚未描述的同质坐标。无论如何,将v 2的每个分量除以v 2的最后一个分量。如果v 2 = [ x , y , z , w ] T,则将每个分量除以w(包括w本身)。你应该以w = 1 结束。(如果你的投影矩阵是单位矩阵,就像我之前描述的那样,这一步应该什么都不做。)
// Clip coordinates to normalized device coordinates
// If you skipped the previous step, SKIP THIS STEP
for (int i = 0; i < 4; i++) {
v_ndc[i] = v_eye[i] / v[3];
}
最后,拿上你的v 2。前两个坐标是您的x和y坐标。第三个是z,你可以扔掉它。(稍后,一旦你变得非常先进,你可以使用这个z值来确定哪个点在某个其他点的前面或后面。)此时,最后一个分量是w = 1,所以你不需要完全没有了。
x = v_ndc[0]
y = v_ndc[1]
z = v_ndc[2] // unused; your screen is 2D
如果您跳过了透视和透视分割步骤,请使用上面v_view
的代替v_ndc
。
这与OpenGL 坐标系的集合非常相似。不同之处在于您从世界坐标开始,而 OpenGL 从对象坐标开始。区别如下:
- 你从世界坐标开始
- 您使用视图矩阵将世界坐标转换为眼睛坐标
- OpenGL 使用 ModelView 矩阵将对象坐标转换为眼睛坐标
从那以后,一切都一样了。