learnopengl(8) 坐标系统

发布时间 2023-08-17 16:37:38作者: Clemens

Opengl希望我们在顶点着色器处理之后,我们可见的所有的顶点都为标准化设备坐标,也就是说,每个顶点的xyz坐标都在[-1.0, 1.0]范围内,超出这个坐标范围的都不可见。

我们通常会自己设定一个坐标范围,然后再在顶点着色器中将这些坐标变为标准化坐标,之后进入光栅化阶段,将他们变为屏幕上的二维坐标或者像素。

将坐标变换为标准化设备坐标,再将标准化设备坐标变换为屏幕坐标,是分多个步骤进行的,像流水线一样。在这个流水线中,物体顶点在转换为屏幕坐标之间还会经历多个过渡坐标系统,优点在于:在这些特定的坐标系统中,可以更方便的进行一些操作和运算。有以下5个比较重要的坐标系统:

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

 

一、概述

从上图可以看出,从一个空间变换到另一个空间需要用到几个重要的矩阵:我们的顶点从局部空间开始,这里是局部坐标系统,经过模型(Model)矩阵变换到世界空间,这里变为世界坐标系统。从世界空间,经过观察(View)矩阵变换到观察空间,这里变为观察坐标系统;从观察空间又经过投影(Projection)矩阵变换到裁剪空间,这里变为裁剪坐标系统;最后经过视口变换到屏幕空间,以屏幕坐标的形式结束。

  1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
  2. 下一步是将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
  3. 接下来我们将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
  4. 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
  5. 最后,我们将裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。

 

二、局部空间

局部空间是物体所在的坐标空间,即对象最开始的地方。

 

三、世界空间

世界空间中的坐标,指顶点相对于世界的坐标。

模型矩阵是一种变换矩阵,它能通过位移、缩放、旋转将物体置于它本该在的位置和朝向。

 

四、观察空间

观察空间经常被人们称为Opengl的摄像机(Camera),所以有时候也称为摄像机空间或者视觉空间。观察空间是将世界空间转换为用户视野前方的的坐标而产生的结果。

观察矩阵将世界空间转换为观察坐标。

 

五、裁剪空间

Opengl希望所有的坐标都落在一个坐标范围内,而坐标范围外的都裁剪掉,剩下的坐标将变为屏幕上的可见的片段。这就是裁剪空间的由来。

投影矩阵指定了一个范围坐标,投影矩阵会将指定范围内的坐标映射到标准化设备范围(-1.0, 1.0)内,超过该坐标范围的都将被裁剪掉。

注:如果只是图元(Primitive),例如三角形,的一部分超出了裁剪体积(Clipping Volume),则OpenGL会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围。

由投影矩阵创建的观察箱(Viewing Box)被称为平头截体(Frustum),出现在平头截体范围内的坐标最终都会出现在屏幕上。

将特定范围内的坐标转化到标准化设备坐标系的过程,被称为投影(Projection)。因为使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中。

将所有的顶点变换到裁剪空间后,最终的操作透视除法(Perspective Division)将会执行。这个过程中,会把位置向量的x,y,z分量除以w齐次分量;透视除法是将4D裁剪空间坐标变换为3D标准化坐标的过程。这一步是在顶点着色器最后自动执行。

在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用glViewport中的设定),并被变换成片段。

将观察坐标变换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。

 

正射投影

正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。

上面的平截头体定义了可见的坐标,它由由宽、高、近(Near)平面和远(Far)平面所指定。任何出现在近平面之前或远平面之后的坐标都会被裁剪掉。正射平截头体直接将平截头体内部的所有坐标映射为标准化设备坐标,因为每个向量的w分量都没有进行改变;如果w分量等于1.0,透视除法则不会改变这个坐标。

要创建一个正射投影矩阵,我们可以使用GLM的内置函数glm::ortho

glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);

前两个参数指定了平截头体的左右坐标,第三和第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小,然后第五和第六个参数则定义了近平面和远平面的距离。这个投影矩阵会将处于这些x,y,z值范围内的坐标变换为标准化设备坐标。

正射投影矩阵直接将坐标映射到2D平面中,即你的屏幕,但实际上一个直接的投影矩阵会产生不真实的结果,因为这个投影没有将透视(Perspective)考虑进去。所以我们需要透视投影矩阵来解决这个问题。

 

透视投影

实际生活中,近大远小的效果成为透视(Perspective)。

透视投影矩阵,修改了每个顶点的w值,越远的顶点坐标的w值越大,具体计算参考:

一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间上的一个点。下面是一张透视平截头体的图片:

在GLM中可以这样创建一个透视投影矩阵:

glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);

它的第一个参数定义了fov的值,它表示的是视野(Field of View),并且设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f,但想要一个末日风格的结果你可以将其设置一个更大的值。第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。

透视投影和正视投影的对比,左边为透视投影,右边为正视投影:

 

六、组合到一起

根据流程的顺序,一个顶点坐标将会根据以下过程变换到裁剪坐标:

$$ V_{clib}=M_{projection}*M_{view}*M_{model}*V_{local} $$

最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。

顶点着色器的输出要求所有的顶点都在裁剪空间内,这正是我们刚才使用变换矩阵所做的。OpenGL然后对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。OpenGL会使用glViewPort内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点(在我们的例子中是一个800x600的屏幕)。这个过程称为视口变换。

 

七、进入3D

在learnopengl(6)的基础上,

定义三个矩阵,并将他们传入顶点着色器:

    // 定义观察矩阵,移动
    glm::mat4 view(1.0);
    view = glm::translate(view, glm::vec3(0, 0, -3));
    // 定义投影矩阵,
    glm::mat4 projection(1.0f);
    projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
    // 将矩阵传入着色器
    int modelLoc = glGetUniformLocation(ourShader.ID, "model");
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
    int viewLoc = glGetUniformLocation(ourShader.ID, "view");
    glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
    int projectionLoc = glGetUniformLocation(ourShader.ID, "projection");
    glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));

同时修改着色器程序:

#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTextCoord;

out vec3 ourColor;
out vec2 TexCoord;

uniform mat4 transform;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
	gl_Position = projection * view * model * vec4(aPos, 1.0);
	ourColor = aColor;
	TexCoord = vec2(aTextCoord.x, aTextCoord.y);
}

运行结果:

和预期的一致。