【问题标题】:How to apply a transformation matrix?如何应用变换矩阵?
【发布时间】:2010-10-25 09:52:01
【问题描述】:

我正在尝试获取 3D 空间中某个点的 2D 屏幕坐标,即我知道相机的平移、倾斜和滚动的位置,并且我有我希望的点的 3D x、y、z 坐标项目。

我很难理解转换/投影矩阵,我希望这里的一些聪明人可以帮助我;)

这是我迄今为止整理的测试代码:

public class TransformTest {

public static void main(String[] args) {

    // set up a world point (Point to Project)
    double[] wp = {100, 100, 1};
    // set up the projection centre (Camera Location)
    double[] pc = {90, 90, 1};

    double roll = 0;
    double tilt = 0;
    double pan = 0;

    // translate the point
    vSub(wp, pc, wp);

    // create roll matrix
    double[][] rollMat = {
            {1, 0, 0},
            {0, Math.cos(roll), -Math.sin(roll)},
            {0, Math.sin(roll), Math.cos(roll)},
    };
    // create tilt matrix
    double[][] tiltMat = {
            {Math.cos(tilt), 0, Math.sin(tilt)},
            {0, 1, 0},
            {-Math.sin(tilt), 0, Math.cos(tilt)},
    };
    // create pan matrix
    double[][] panMat = {
            {Math.cos(pan), -Math.sin(pan), 0},
            {Math.sin(pan), Math.cos(pan), 0},
            {0, 0, 1},
    };

    // roll it
    mvMul(rollMat, wp, wp);
    // tilt it
    mvMul(tiltMat, wp, wp);
    // pan it
    mvMul(panMat, wp, wp);

}

public static void vAdd(double[] a, double[] b, double[] c) {
    for (int i=0; i<a.length; i++) {
        c[i] = a[i] + b[i];
    }
}

public static void vSub(double[] a, double[] b, double[] c) {
    for (int i=0; i<a.length; i++) {
        c[i] = a[i] - b[i];
    }      
}

public static void mvMul(double[][] m, double[] v, double[] w) {

    // How to multiply matrices?
} }

基本上,我需要获取 3D 点相交的给定屏幕的 2D XY 坐标。我不确定如何使用滚动、倾斜和平移矩阵来转换世界点 (wp)。

非常感谢任何帮助!

【问题讨论】:

标签: 3d projection


【解决方案1】:

这是复杂的东西。请阅读有关该主题的书,以获取所有数学和细节。如果你打算长期玩这些东西,你需要知道这些东西。这个答案只是为了让你可以弄湿你的脚并四处乱窜。

乘法矩阵

首先要做的事情。乘法矩阵是reasonably simple affair

假设您有矩阵 ABC,其中 AB = C 。假设您想计算矩阵 C 在第 3 行第 2 列的值。

  • A的第三行和B的第二列。您现在应该从 AB 获得相同数量的值。 (如果你没有为这两个矩阵定义矩阵乘法。你不能这样做。)如果两者都是 4×4 矩阵,你应该有来自 A 的 4 个值(第 3 行) 和 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)。这些是笛卡尔坐标。我们称它们为(xyz)。

齐次坐标意味着您在笛卡尔坐标的末尾写了一个额外的 1。 (5, 2, 1) 变为 (5, 2, 1, 1)。我们称它们为(xyzw)。

每当您进行使 w ≠ 1 的变换时,您将坐标的每个分量除以 w。这会改变您的 xyz,并使 w = 1。 (即使你的转换没有改变w,这样做也没有什么坏处。它只是将所有内容除以 1,什么都不做。)

你可以用齐次坐标做一些非常酷的事情,即使它们背后的数学并不完全有意义。正是在这一点上,我要求您再次查看此答案顶部的建议。


转换一个点

我将在本节和接下来的部分中使用 OpenGL 术语和方法。如果有什么不清楚或似乎与您的目标相冲突(因为这对我来说似乎有点像家庭作业:P),请发表评论。

我还将首先假设您的滚动、倾斜和平移矩阵是正确的。

当您想使用变换矩阵变换点时,您可以将该矩阵右乘以表示您的点的列向量。假设您想通过某个变换矩阵 A 翻译 (5, 2, 1)。您首先定义 v = [5, 2, 1, 1]T。 (我用 [x, y, z, w]T小T表示应该把它写成列向量。)

// Your point in 3D
double v[4][5] = {{5}, {2}, {1}, {1}}

在这种情况下,Av = v1,其中 v1是你的转变点。像矩阵乘法一样进行这种乘法,其中 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);

现在,如果您要应用多个变换矩阵,首先将它们组合成一个变换矩阵。通过按照您希望它们应用的顺序将矩阵相乘来做到这一点。

以编程方式,您应该从单位矩阵开始,然后对每个变换矩阵进行右乘。令I4为4×4单位矩阵,令A1,A2, A3, ... 成为您的转换矩阵。让你的最终转换矩阵为 Afinal

AfinalI4
A最终A最终A1
Afinal ← AfinalA2
AfinalAfinalA3

请注意,我使用该箭头表示分配。当你实现这个时,确保在矩阵乘法计算中仍然使用它时不要覆盖 Afinal!复制一份。

// 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);

最后,做和上面一样的乘法:Afinalv = v1

// Use the above matrix to transform v
mmMul(a_final, v, v_1);

从头到尾

相机变换应表示为视图矩阵。在此处执行您的 Aviewv = v1 操作。 (v 将您的世界坐标表示为 4×1 列向量,Afinal 是您的 A查看。)

// World coordinates to eye coordinates
// A_view is a_final from above
mmMult(a_view, v_world, v_view);

投影变换描述了透视变换。这就是使较近的物体变大而使较远的物体变小的原因。这是在相机转换之后执行的。如果您还不需要透视,只需使用单位矩阵作为投影矩阵。无论如何,在这里执行 A v1 = v2

// Eye coordinates to clip coordinates
// If you don't care about perspective, SKIP THIS STEP
mmMult(a_projection, v_view, v_eye);

接下来,您需要进行透视划分。这深入研究了我尚未描述的同质坐标。无论如何,将 v2 的每个组件除以 v2 的最后一个组件。如果 v2 = [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];
}

最后,带上你的v2。前两个坐标是您的 xy 坐标。第三个是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 coordinate systems 的集合非常相似。不同之处在于您从世界坐标开始,而 OpenGL 从对象坐标开始。区别如下:

  • 你从世界坐标开始
    • OpenGL 从对象坐标开始
  • 您使用视图矩阵将世界坐标转换为眼睛坐标
    • OpenGL 使用 ModelView 矩阵将对象坐标转换为眼睛坐标

从那以后,一切都一样了。

【讨论】:

  • 你熟悉齐次坐标吗?代表 3D 变换的 4×4 矩阵和 4 向量?
  • 这是一个很好的答案;但也感谢您指出,对于这个领域,“相当简单”与“简单”的正常定义并不完全重叠(我并没有试图以任何方式诋毁您的答案;只是指出长度单独使这不完全“简单”)。
  • 感谢您的详细回复。对于像我这样的完整初学者来说,这是一个很大的问题。这实际上不是家庭作业,而是我正在研究的更大系统中的一个小元素,因此我希望找到一个快速的解决方案并继续前进。但是,我发现很难理解您的描述,因为我对矩阵知之甚少。理想情况下,我会花时间阅读它们,我相信它会看起来更清晰。我不认为,当你有时间的时候,你可以用伪代码的形式解释一下吗?如果以时尚之类的代码呈现,我发现更容易理解事物!
  • 阅读了您的上一条评论并查看了 PDF,我想我正在回答一个与您提出的问题不同的问题。 GPS坐标是什么形式的? (纬度、对数、高度?x、y、z 与 0、0、0 是地球的中心?还有别的吗?)照片是否“包裹”了整个地球? (如果不是,它覆盖什么区域?它是弯曲的吗?)
  • 好吧,很明显我的答案不是你想要的。话虽如此,继续澄清可能不值得您花时间,特别是如果您转而寻找其他资源来解决您的问题。 [评论拆分]
【解决方案2】:

这个范围太大了,无法在这里得到一个好的答案:我建议阅读关于这个主题的一个很好的参考。我一直很喜欢Foley and VanDam...

【讨论】:

  • 除了将平移、倾斜和滚动矩阵应用于世界点之外,还有很多其他功能吗?我的印象是这是一个相对简单的过程,还需要什么来实现我的需要?谢谢
  • 这实际上是一个相对简单的过程,但在此过程中,我真的建议您阅读很多基础理论。我学到了很多东西,我只能说我的经验告诉我,掌握基本理论真的很重要。
【解决方案3】:

我已经发布了一些代码here,可以满足您的大部分需求。

它包含 OpenGL gluPerspective()gluLookAt() 函数的 Java 实现:

Camera camera = new Camera();

Point3d eye = new Point3d(3, 4, 8);
Point3d center = new Point3d(0, 0, 0);
Vector3d up = new Vector3d(0, 1, 0);

camera.perspective(60.0, 1.6, 0.1, 20); // vertical fov, aspect ratio, znear, zfar
camera.lookAt(eye, center, up);

要在其中使用project() 函数,请使用:

void plot(Camera camera, Point4d p) {
    Point4d q = Camera.project(p);
    float x = q.x / q.w;
    float y = q.y / q.w;
    ...
}

返回的 xy 值在 -0.5 ... 0.5 范围内

【讨论】:

  • 感谢 Alnitak,感谢您帮助我解决这个问题。几个问题:返回的 x、y 值,如何将它们转换为屏幕像素? IE。如果我有一个分辨率为 480x360 的屏幕,将 x、y 值转换为实际像素的方法是什么?另外,当我将 GPS 球坐标转换为笛卡尔坐标以供此处使用时,它们非常大,这对转换有影响吗?最后,Ray、Matrix4d、Point3d 和 Vector3d 类在哪里?您发布的代码还调用具有不同构造函数的 Camera 类吗?我无法让它工作......谢谢!
  • 抱歉,请忽略关于相机类具有不同构造函数的最后一条评论——我误读了它。哦,我假设我需要这些类,即 Point3d、Point4d 等可以在某处下载?
  • 好的,我在 Java 3D javax.vecmath 包中找到了所需的类。我现在有最后一个问题——plot() 方法接受一个 Camera 参数和一个 Point4d 参数,这个 Point4d 对象是从哪里来的?
  • Point4d 参数只是 Wesley 描述的那些同质向量之一。只需使用
  • 谢谢,这似乎有效。您如何将返回的 X、Y 值转换为实际的屏幕协调?
猜你喜欢
  • 1970-01-01
  • 2012-04-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多