顶点位置的 Vulkan 默认坐标系

Vulkan default coord system for vertex positions

我正在通过在玩具渲染器上工作来研究 Vulkan 坐标系。 我对顶点位置的坐标感到困惑。 在线 Vulkan 信息,例如: https://matthewwellings.com/blog/the-new-vulkan-coordinate-system/

...提到+X是对的,+Y是下的,+Z是背的。

在我的渲染器中,+Z 指向前方,我不明白为什么。 我有一个这样定义的三角形:

    // CCW is facing forward
    std::vector<PosColorVertex> vertexBuffer = {
        {{ 0.0f, -1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}},
        {{-1.0f, 1.0f, 0.0f},  {0.0f, 1.0f, 0.0f}},
        {{ 1.0f, 1.0f, -5.0f}, {0.0f, 0.0f, 1.0f}},
    };

-5(Z) 将顶点移回屏幕。应该是 +5 才行。

坐标系好像是这样的:

如果我把相机放在原点,它看起来像这样:

另一个镜头,相机远离三角形(视图在 Z 轴上平移 -4)。

一些相关代码。模型和视图矩阵都是恒等的。

对比:

 outColor = inColor;
 gl_Position = ubo.projectionMatrix * ubo.viewMatrix * ubo.modelMatrix * vec4(inPos.xyz, 1.0);

FS:

outFragColor = vec4(inColor, 1.0);

投影是:

glm::perspective(glm::radians(60.0f), w/h, 0.1, 256.0);

裁剪和归一化设备坐标

剪辑坐标是我们从顶点着色器获得的坐标。归一化设备坐标 (NDC) 相同,但除以 w。有两种常见的用户选项(左撇子和右撇子):

“向上”的含义实际上取决于您。但是,如果您希望它与几乎所有的表示引擎兼容,您希望“up”在视口变换之后表示 -y(因此在 NDH 中,您的“up”应该是 -y 在香草视口变换的情况下,或者它应该是 +y 以防视口变换稍后翻转它)。

在 framebuffer\image 坐标中选择“向上”总是 -y 是因为几乎所有演示引擎上的表面坐标都假定原点在左上:

许多图像文件格式也假定相同。

世界坐标

世界坐标完全由您决定。通过顶点着色器,您可以将世界坐标转换为 Vulkan 可以处理的剪辑坐标。

您是通过 glm::perspective 完成的。

让我们先实际看看我们有什么:

std::vector<PosColorVertex> vertexBuffer = {
    {{ 0.0f, -1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}},
    {{-1.0f, 1.0f, 0.0f},  {0.0f, 1.0f, 0.0f}},
    {{ 1.0f, 1.0f, -5.0f}, {0.0f, 0.0f, 1.0f}},
};

现在,这又取决于对这实际上意味着什么的解释。我们需要另一个“向上”参考方向。

为了保持理智,我们可能希望“向上”处于 y 增加的方向。所以这意味着我们在底部有一些红色的角。我们在左上角有一个绿色角。我们在右上角有一个蓝色的角。或者我认为是作者的意图。

此外,为了保持理智,我们更喜欢右手坐标系。所以,如果我们选择 +y 表示“向上”,而 +x 表示“向右”,那么 -z 必须是“前面”(并且 +z 必须是“后面”):

(这与 Blender 具有世界坐标的方式相匹配。)现在我们陷入了困境。我们的 z 是负数而不是 NDH 要求的正数。与“向上”相比,我们的 y 指向与 NDH 不同的方向。无论我们做什么转换,我们都需要使它们匹配。

glm::perspective() 做什么

glm::perspective() 主要是让它看起来很好看。但为此它需要做几个假设。

最简单的是深度。在 NDC 的 Vulkan 中,它是零比一。方便的是 GLM_FORCE_DEPTH_ZERO_TO_ONE。这指示 perspective() 它应该将您的 nearfar 分别映射到 01。 (默认值为 -1 到 1,除非手动更正,否则在 Vulkan 中不起作用。)xy 仍然始终为 -1 到1.

第二选择是惯用手。默认为右手。左撇子需求GLM_FORCE_LEFT_HANDED。或者它可以明确地用于单个功能,例如perspectiveRH()。这有点误导。这实际上意味着“右手”意味着“前”是 -z。而“左撇子”意味着投影假设“前”是正方向 z:

第三个选择是什么是“向上”。 glm::perspective() 实际上并没有对此做任何事情,并且 y 的极性在整个变换过程中保持不变。如果我们想让 +y 表示“向上”,我们需要手动执行此操作。我们可以利用视口翻转功能,也可以将其烘焙到视图投影矩阵中:proj[1][1] = -proj[1][1].

如何测试这些东西

测试这个实际上非常简单。此代码可用于以下目的:

#include <cmath>
#include <iostream>

#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
//#define GLM_FORCE_LEFT_HANDED
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

int main(){
#ifdef GLM_FORCE_LEFT_HANDED
    const float near = 1.0f;
    const float far = 2.0f;
#else //right-handed
    const float near = -1.0f;
    const float far = -2.0f;
#endif

    glm::vec3 right { 1.0f,  0.0f, near};
    glm::vec3 left  {-1.0f,  0.0f, near};
    glm::vec3 up    { 0.0f,  1.0f, near};
    glm::vec3 down  { 0.0f, -1.0f, near};
    glm::vec3 front { 0.0f,  0.0f, (far-near) + near };
    glm::vec3 back  { 0.0f,  0.0f, (near-far) + near };


    const auto xform = glm::perspective(glm::radians(60.0f), 1.0f, std::abs(near), std::abs(far));

    auto r = xform * glm::vec4(right , 1.0f);
    auto l = xform * glm::vec4(left  , 1.0f);
    auto u = xform * glm::vec4(up    , 1.0f);
    auto d = xform * glm::vec4(down  , 1.0f);
    auto f = xform * glm::vec4(front   , 1.0f);
    auto b = xform * glm::vec4(back  , 1.0f);


    std::cout << "Right to clip: ("  << r.x << ", " << r.y << ", " << r.z << ", " << r.w << ")\n";
    std::cout << "Left to clip: ("   << l.x << ", " << l.y << ", " << l.z << ", " << l.w << ")\n";
    std::cout << "Up to clip: ("     << u.x << ", " << u.y << ", " << u.z << ", " << u.w << ")\n";
    std::cout << "Down to clip: ("   << d.x << ", " << d.y << ", " << d.z << ", " << d.w << ")\n";
    std::cout << "Front to clip: ("  << f.x << ", " << f.y << ", " << f.z << ", " << f.w << ")\n";
    std::cout << "Back to clip: ("   << b.x << ", " << b.y << ", " << b.z << ", " << b.w << ")\n";
}

对于左右手设置,我们得到:

Right to clip: (1.73205, 0, 0, 1)
Left to clip: (-1.73205, 0, 0, 1)
Up to clip: (0, 1.73205, 0, 1)
Down to clip: (0, -1.73205, 0, 1)
Front to clip: (0, 0, 2, 2)
Back to clip: (0, 0, -2, 0)

Ups,这一切都被剪掉了。但无论如何“右”和“上”都是正数。所以是的,我们可能想要翻转 y 某种方式以与表示引擎坐标兼容。 "front" 是 (0,0,1) 方向,"back" 停止存在。

注意代码中的变化是世界坐标中使用的 z 的方向。