跳转至

11 数学库与投影矩阵

标准化设备坐标系

引言

我们先做几个试验。

  1. 在原先的代码中,长方形的四个顶点是0.5

    //顶点数据
    float positions[] = {
        //顶点位置xy, UV坐标xy
        -0.5f, -0.5f, 0.0f, 0.0f,   // 0
         0.5f, -0.5f, 1.0f, 0.0f,    // 1
         0.5f,  0.5f, 1.0f, 1.0f,    // 2
        -0.5f,  0.5f, 0.0f, 1.0f    // 3
    };
    

  2. 将长方形的四个顶点改成1

    //顶点数据
    float positions[] = {
        //顶点位置xy, UV坐标xy
        -1.0f, -1.0f, 0.0f, 0.0f,   // 0
         1.0f, -1.0f, 1.0f, 0.0f,    // 1
         1.0f,  1.0f, 1.0f, 1.0f,    // 2
        -1.0f,  1.0f, 0.0f, 1.0f    // 3
    };
    

  3. 将长方形的四个顶点改成2

    //顶点数据
    float positions[] = {
        //顶点位置xy, UV坐标xy
        -2.0f, -2.0f, 0.0f, 0.0f,   // 0
         2.0f, -2.0f, 1.0f, 0.0f,    // 1
         2.0f,  2.0f, 1.0f, 1.0f,    // 2
        -2.0f,  2.0f, 0.0f, 1.0f    // 3
    };
    

不难发现,无论我们怎么更改顶点的坐标值,在窗口中,每次只显示了x∈[-1, 1],y∈[-1, 1]的内容。

回想,在这份代码中(basic.shader),positions的顶点坐标直接赋值给了gl_Position

#shader vertex
//...

void main()
{
    gl_Position = position;
    v_TexCoord = texCoord;
};

#shader fragment
//...

因此,引出了一个背景知识:gl_Position实际上被GPU当成 [-1, 1] 的坐标(标准化设备坐标)进行处理了。GPU在之后,会将**标准化设备坐标系** 映射成 屏幕坐标系,从而绘制到我们的屏幕上。

标准化设备坐标系

标准化设备坐标系(normalized device coordinate system, NDC)

  • 是一个二维的空间直角坐标系
  • 它的X、Y的范围都在[-1, 1]之间

GPU就会将 标准化设备坐标系 映射成 屏幕坐标系(window device coordinate system),从而将我们的图案显示到屏幕当中。

glm数学库

glm第三方库的全程叫做OpenGL Mathematics。Yes,它就是OpenGL的数学库,所以有很多标准都是基于OpenGL标准编写的。glm旨在模仿GLSL并与OpenGL一起使用。

例如:glm矩阵的内存布局与OpenGL一致,都是按列优先来存储的!

  • 因此在使用glUniformMatrix4fv时,就无需转置,直接将glm的矩阵内存拷贝到GPU即可。
void Shader::SetUniformMat4f(const std::string& name, const glm::mat4& matrix) {
    GLCall(
        glUniformMatrix4fv(
            GetUniformLocation(name),
            1,          //1个矩阵
            GL_FALSE,   //true需要转置,false不需要转置
            &matrix[0][0]  //传入矩阵的内存地址
        )
    );
    //f表示float
    //v表示我们在传入一个数组    
}
  • 但如果其他数学库是以行优先来存储矩阵的,那么就需要传入GL_TRUE,OpenGL会自行对其进行转置

投影矩阵

介绍

假设,你有一个3D世界,里面有山丘、地形、人物、建筑物等等,这个3D世界是以何种方式显示在2D的屏幕当中的?

  • 投影矩阵

投影矩阵,本质上是将三维空间投影到二维屏幕上的矩阵,即将三维坐标转换为标准化设备坐标。

它有两种:

  1. 透视投影:近大远小,常用于3D世界
  2. 正交投影:远近一样,常用语2D世界当中,但是3D中也会有应用(例如关卡编辑器、3D建模程序)

在代码中使用投影矩阵

  1. Application.cpp

    #include "glm/gml.hpp"
    #include "glm/gtc/matrix_transform.hpp"
    
    int main()
    {
        //...
        IndexBuffer ib(indices, 6);
    
        //正交矩阵(把所有坐标映射到2D平面上,离的远的物体并不会变小)
        glm::mat4 proj = glm::ortho(
            -2.0f, //左边
            2.0f,  //右边
            -1.5f, //底部
            1.5f,   //顶部
            -1.0f,  //远
            1.0f    //近
        );
        //x∈[-2, 2]会被映射到NDC的[-1, 1]上,其他的会被删掉。因此x=0.5即在窗口的1/4处
        //y∈[-1.5, 1.5]会被映射到NDC的[-1, 1]上,其他的会被删掉
        //注:遵循4x3的纵横比(将这些数字乘以2,就得到了4x3)
        //顶部到底部有3个单位距离;从左到右4个单位距离
    
        //...
        shader.SetUniformMat4f("u_MVP", proj); //传入着色器
    }
    

  2. basic.shader

    #shader vertex
    #version 330 core
    
    layout(location = 0)in vec4 position;
    layout(location = 1)in vec2 textCoord;
    
    out vec2 v_TextCoord;
    
    uniform mat4 u_MVP; //模型视图矩阵,目前只传入了一个投影矩阵
    
    void main()
    {
        gl_Position = u_MVP * position; //将坐标做一个投影变换
        v_TextCoord = textCoord;
    };
    
    #shader fragment
    #version 330 core
    
    layout(location = 0)out vec4 color;
    
    in vec2 v_TextCoord;
    
    uniform vec4 u_Color;
    uniform sampler2D u_Texture;
    
    void main()
    {
        vec4 textColor = texture(u_Texture, v_TextCoord);
        color = textColor;
    };
    

  3. shader.cpp

    //glm::mat4是列式存储,而OpenGL也是列式存储,因此无需转置
    //如果matrix是行式存储,才需要转置
    void Shader::SetUniformMat4f(const std::string& name, const glm::mat4& matrix) {
        GLCall(
            glUniformMatrix4fv(
                GetUniformLocation(name),
                1,          //1个矩阵
                GL_FALSE,   //true需要转置,false不需要转置
                &matrix[0][0]  //传入矩阵的内存地址
            )
        );
        //f表示float
        //v表示我们在传入一个数组    
    }
    

在完成代码之后,我们可以做几个试验来加深对投影矩阵的理解

  1. 我们可以在CPU中计算投影之后的坐标是多少,然后观察这个点在窗口的位置是否正确
glm::vec4 vp(100, 100, 0, 1);
glm::vec4 result = proj * vp;
  1. 更改glm::ortho中x、y的阈值