你如何计算相机的变换矩阵?

Posted

技术标签:

【中文标题】你如何计算相机的变换矩阵?【英文标题】:How do you calculate the transformation matrix for a camera? 【发布时间】:2016-08-07 23:56:14 【问题描述】:

我有一个摄像机,其位置、向前、向右和向下向量定义为类成员 _position_forward_right_down。对于问题的旋转部分,我只需使用_forward_right_down 加载目标坐标系(视图空间)的行,然后应用旋转矩阵进入“OpenGL”视图空间。

在此之前,我用_position 的否定翻译。这是正确的,还是我需要做更多的数学运算来确定旋转前乘或后乘的实际平移?

以下是我拥有的似乎不起作用的代码。具体来说,我在世界空间的原点渲染一个对象(一个面向 z 轴的小四边形),当我运行程序时,该对象出现扭曲。 (请注意,mat4 构造函数以 ROW 主要顺序获取元素,尽管它在内部将它们存储在主要列中)。

mat4 Camera::matrix() const

    return
        mat4(
            0.0f, 0.0f, -1.0f, 0.0f,
            1.0f, 0.0f, 0.0f, 0.0f,
            0.0f, -1.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f) *
        mat4(
            _forward.x, _forward.y, _forward.z, 0.0f,
            _right.x, _right.y, _right.z, 0.0f,
            _down.x, _down.y, _down.z, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f) *
        translate(
            -_position.x,
            -_position.y,
            -_position.z
        );

这是在上述之前使用的查看功能的代码。

void Camera::look_at(const vec3& position, const vec3& point)

    _position = position;
    _forward = normalize(point - position);
    _right = cross(_forward, vec3(0.0f, 1.0f, 0.0f));
    _down = cross(_forward, _right);

在我的初始化中,我使用了这段代码:

camera.look_at(vec3(1.0f, 1.0f, 1.0f), vec3(0.0f, 0.0f, 0.0f));
view = camera.matrix();
proj = perspective(60.0f, SCREEN_WIDTH / (float)SCREEN_HEIGHT, 0.001f, 100.0f);
view_proj = proj * view;

【问题讨论】:

您可以通过重新排序中间矩阵并在必要时分配- 来摆脱第一个矩阵乘法。应该是(right, -down, -forward)。究竟什么似乎不起作用?总体思路看起来不错。其余的取决于您对坐标系的定义。 这不是意味着你的构造函数以列的主要顺序接受参数吗? “mat4 构造函数以 ROW 主要顺序获取元素(尽管它在内部将它们存储在主要列中)”在您的示例中,_forward 是矩阵中的列向量,对吗?它只是看起来像行主要排序,因为您实际上不能像在列中那样编写它们。 另外,matrix() 方法是试图获取lookAt 矩阵,还是相机的模型矩阵? (不同的是lookAt矩阵是模型矩阵的逆矩阵,用于将物体从世界空间变换到相机空间) @NicoSchertler:谢谢。你是对的,我可以摆脱第一个矩阵,这会使这更快。我这样写是为了让第三方更清楚发生了什么,但如果我按照你的方式做,我希望这样的第三方能够理解为什么需要为 OpenGL 重新排序前/右/下。至于不起作用的是产生的图像似乎失真。感谢下面的答案,我意识到在我的 look_at 函数中(我添加到问题中)并没有标准化所有向量,所以我必须在今天晚些时候尝试这个项目。 @MatthewWoo:感谢您的帮助。 mat4 构造函数的参数主要是行(m0、m4、m7、m12、m1、m5、m9、m13 等),但写入内部 1d 浮点数组为 m[0] = m0, m[1] = m1 等(即主要列),除非我使用这些术语行/列主要错误我认为出于可读性目的这是正确的(如果在文本编辑器中指定了矩阵元素列表,则可以在文本编辑器中正确格式化它们)行专业)。 Camera::matrix() 方法应该返回视图矩阵,即 worldToView 矩阵。所以它应该返回 viewToWorld 矩阵的倒数 【参考方案1】:

计算视图矩阵的问题(具体问题“如何计算相机的变换矩阵?”的答案)是将向量和点从世界空间转换到视图空间的问题。这与从任何 source 空间转换到 destination 空间相同。 (对于这个答案的其余部分,源空间指的是世界空间,目标空间指的是视图空间。)我们给出了相对的目标基向量(旋转向量)到源(_right x、_up y 和 _backward z 相机向量作为vec3s)和目标空间相对于源空间的原点(_position 点作为vec3)。虽然问题使用_forward_right_down,但首先我们将查看_right_up_backward 以与OpenGL 视图空间保持一致。最后,我们将看看使用前向量的解决方案。此外,术语空间将指代坐标系基础(方向向量)以及框架原点(位置向量)。

从源空间到目标空间的变换矩阵可以表示为旋转,使所有向量和点相对于目标空间正确定向,然后进行平移,使所有点相对于目标正确定位空间。任何变换矩阵乘以一个点始终是旋转(矩阵的行和向量的线性组合)和平移(将旋转的结果偏移第四列向量)。请注意,方向向量(在齐次坐标中由向量的第四个分量设置为零表示)不受平移的影响。我们现在将研究两种方法来推导这样的矩阵,其中第二种方法计算效率更高。

让偏移向量是指向从目标空间到源空间的整个距离的向量。在问题提供的代码 sn-p 中使用的第一种方法是首先通过相对于源空间的偏移向量进行平移。此操作重新定位所有点(但不是向量)以将目标原点作为参考点。其次,通过将目标空间相对于源空间的基向量作为行(即_right_up_backward)加载到前三列来应用旋转。为什么是行?相对于任何坐标系的向量仅表示坐标系的基础基向量的线性组合,4x4 矩阵乘以 4 分量向量的结果只是第一个矩阵的行乘以列的线性组合第二个矩阵(在列向量的情况下,这只是一个 4x1 矩阵)。这种线性组合正是我们所追求的,也是齐次坐标起作用的原因。以下是实现此方法的示例代码。请注意,translate() 函数返回一个平移矩阵。

mat4 Camera::matrix() const

    return
        mat4(
               _right.x,    _right.y,    _right.z, 0.0f,
                  _up.x,       _up.y,       _up.z, 0.0f,
            _backward.x, _backward.y, _backward.z, 0.0f,
                   0.0f,        0.0f,        0.0f, 1.0f) *
        translate(
            -_position.x,
            -_position.y,
            -_position.z
        );

第二种更有效的方法是直接生成矩阵,而不是乘以两个矩阵,而无需将旋转矩阵和平移矩阵相乘。首先,和以前一样,前三列将目标基向量作为行加载。最后,使用每个基向量的点积计算平移向量(第四列中的前三个元素)。

mat4 Camera::matrix() const

    return mat4(
           _right.x,    _right.y,    _right.z, -dot(   _right, _position),
              _up.x,       _up.y,       _up.z, -dot(      _up, _position),
        _backward.x, _backward.y, _backward.z, -dot(_backward, _position),
               0.0f,        0.0f,        0.0f,                      1.0f);

注意点积应该是dot(_right, -_position)等,但是我们可以通过将点积之外的否定移动到-dot(_right, _position)来保存乘法。

这些点积与之前将旋转矩阵乘以平移矩阵完全相同。例如,下面的矩阵乘法演示了第一种方法中的情况。将其与上面的代码与点积进行比较。

| x0 x1 x2 0 |   | 1 0 0 -t0 |   | x0 x1 x2 -(x0*t0 + x1*t1 + x2*t2) |
| y0 y1 y2 0 |   | 0 1 0 -t1 |   | y0 y1 y2 -(y0*t0 + y1*t1 + y2*t2) |
| z0 z1 z2 0 | * | 0 0 1 -t2 | = | z0 z1 z2 -(z0*t0 + z1*t1 + z2*t2) |
|  0  0  0 1 |   | 0 0 0   1 |   |  0  0  0                       1  |

请注意,_right_up_backward 向量(或问题中的 _forward_right_down 向量)必须始终进行归一化,否则一些无意将产生缩放。

为了考虑使用_forward_right_down 向量,需要应用额外的旋转才能到达 OpenGL 视图空间。下面列出了这个矩阵:

|  0  1  0  0 |
|  0  0 -1  0 |
| -1  0  0  0 |
|  0  0  0  1 |

左上角 3x3 子矩阵中的行是 OpenGL 视图空间(右 x,上 y,后 z)相对于所需相机空间(前 x,右 y,下 z)的基向量。然后我们将这个矩阵与之前的矩阵相乘,再得到点积。这样的矩阵乘法类似于:

|  0  1  0  0 |   | x0 x1 x2 t0 |   |  y0  y1  y2  y3 |
|  0  0 -1  0 |   | y0 y1 y2 t1 |   | -z0 -z1 -z2 -t2 |
| -1  0  0  0 | * | z0 z1 z2 t2 | = | -x0 -x1 -x2 -t0 |
|  0  0  0  1 |   |  0  0  0  1 |   |   0   0   0   1 |

最后的矩阵函数可以是:

mat4 Camera::matrix() const

    float tx =  dot(_forward, _position);
    float ty = -dot(_right, _position);
    float tz =  dot(_down, _position);

    return mat4(
           _right.x,    _right.y,    _right.z,   ty,
           -_down.x,    -_down.y,    -_down.z,   tz,
        -_forward.x, -_forward.y, -_forward.z,   tx,
               0.0f,        0.0f,        0.0f, 1.0f);

所以除了一些性能问题之外,问题中的所有代码都是正确的,除了向量没有look_at()函数中归一化。

【讨论】:

哪些向量需要归一化? _position 只是一个职位; _forward 已标准化; _right 是 2 个单位向量的叉积,因此是一个单位向量;与_down 相同。除非你暗示浮点精度是这里的问题? @MatthewWoo 两个单位向量的叉积只有当操作数垂直时才有单位长度。通常,长度为sin(phi),其中phi 是向量之间的角度。【参考方案2】:

为了方便,我建议使用glm::lookAt()

更多信息请参考https://glm.g-truc.net/0.9.2/api/a00245.html#ga2d6b6c381f047ea4d9ca4145fed9edd5

但这里是如何从位置、目标和向上向量构造它的。

请注意,这与 lookAt() 函数相同。

//creates a lookat matrix
mat4 lookAt(Vec3 eye, Vec3 center, Vec3 up)

    Vec3 left, up2, forward;

    // make rotation matrix

    // forward vector
    forward = center - eye;
    forward.Normalize();

    // up2 vector (assuming up is normalized)
    up2 = up;

    // left vector = up2 cross forward
    left = up2.Cross(forward);

    // Recompute up2 = forward cross left (in case up and forward were not orthogonal)
    up2 = forward.Cross(left);

    // cross product gives area of parallelogram, which is < 1.0 for
    // non-perpendicular unit-length vectors; so normalize left, up2 here
    left.Normalize();
    up2.Normalize();

     mat4(
        left.x, left.y, left.z, left.Dot(-eye),
        up2.x, up2.y, up2.z, up2.Dot(-eye),
        -forward.x, -forward.y, -forward.z, -forward.Dot(-eye),
        0.0f, 0.0f, 0.0f, 1.0f)


注意 up2 = up 如果 up 和 forward 是正交的,否则 up2 是一个正交的 up 向量。

【讨论】:

感谢您的回复。我在问题中添加了我的 look_at 代码。阅读您的 lookAt 实现让我意识到我忘记对计算的 _right 和 _down 向量进行标准化。另外,我注意到您加载矩阵的平移部分不是使用 -eye 而是使用行(旋转部分)和 -eye 的线性组合(即 left.Dot(-eye), up2.Dot(-eye) , -forward.Dot(-eye)。今晚有时间处理代码时,我将不得不尝试这个。你能告诉我为什么它是后者(线性组合)而不是前者(-eye)吗? 在考虑了更多问题之后,我意识到您提供的代码创建了一个矩阵,该矩阵旋转然后翻译,而我的代码(似乎不正确)尝试translate 然后rotate。任何仿射变换矩阵乘以 4 分量向量首先是旋转(仿射矩阵的行和输入向量的线性组合),然后是平移(仿射矩阵的最后一列偏移)。 如果您有相对于源坐标系(世界空间)的目标坐标系(相机空间)的基本向量相对于源的目标原点(世界空间中的相机位置),然后从源到目的地的矩阵是作为行的目的地基础,然后通过从目的地原点到源原点相对的向量转换为正确位置(旋转)到目的地。因此,为什么 -eye 向量通过代码中的点积转换为相机空间并然后加载到最后一列。 是的,我将矩阵乘法简化为 1 步,只是为了减少行数。它确实使理解底层数学有点困难,但可能效率更高一些。旋转然后平移显然是这里的意图。

以上是关于你如何计算相机的变换矩阵?的主要内容,如果未能解决你的问题,请参考以下文章

如何根据两个连续帧的投影变换矩阵估计相机姿态?

有没有办法从glm中的视图矩阵中提取变换矩阵?

✠OpenGL-3-数学基础

从单应性中提取变换和旋转矩阵?

解释 OpenGL 如何管理它的变换矩阵

具有基本矩阵变换 (WebGL) 的类似 FPS 的相机移动