LearnOpenGL note - Getting started:Coordinate Systems

OpenGL入门系列5

Posted by Tao on Friday, February 25, 2022

坐标变换相关内容及各变换矩阵推导我们已经在Games101-MVP一章中详细介绍过了,本章侧重结合OpenGL流程来分析坐标变换的内容。

0 坐标变换总览

本节笔记对应LearnOpenGL坐标系统一章,并依于此对坐标变换相关概念做一个扩展和厘清。

首先看两张图片,介绍了OpenGL中坐标变换的整体过程(图一来源,图二[来源])。

transformation_pipeline.png
图1 Transformation Pipeline

newtranspipe.png
图2 Transformation Pipeline

在上面的图中,注意,OpenGL只定义了裁剪坐标系、规范化设备坐标系和屏幕坐标系,而局部坐标系(模型坐标系)、世界坐标系和照相机坐标系都是为了方便用户设计而自定义的坐标系,它们的关系如下图所示(来自Chapter 7. World in Motion):

TransformPipeline.svg
图3 Full Vertex Transformation Pipeline

图中左边的过程包括模型变换、视变换,投影变换,这些变换可以由用户根据需要自行指定,这些内容在顶点着色器中完成;而图中右边的两个步骤,包括透视除法、视口变换,这两个步骤是OpenGL自动执行的,在顶点着色器处理后的阶段完成。

OpenGL希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。即每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标或像素。

综上,总共有6个不同的坐标系统:

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space)或相机坐标系)
  • 裁剪空间(Clip Space)
  • 标准化设备坐标系空间(NDC space)
  • 屏幕空间(Screen Space)

这就是一个顶点在最终被转化为片段之前需要经历的所有不同状态。(6个阶段5个变换)

各个变换阶段的详细介绍

下面分别对每个阶段的变换做一个总结,以帮助理解。

1 概述

我们的顶点坐标起始于局部空间(Local Space),在这里它称为局部坐标(Local Coordinate),它在之后会变为世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Coordinate)的形式结束。最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。另外ViewPort Transform变换OpenGL替我们完成。下面的这张图展示了整个流程以及各个变换过程做了什么:
coordinate_systems

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

坐标变换相关内容,各变换矩阵推导详见Games101-MVP一节

2 代码实现

我们为上述的每一个步骤都创建了一个变换矩阵:模型矩阵、观察矩阵和投影矩阵。
一个顶点坐标将会根据以下过程被变换到裁剪坐标:
\[V_{clip}=M_{projection}⋅M_{view}⋅M_{model}⋅V_{local}\] 最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。

  • 我们首先创建一个模型变换矩阵。
    这个模型矩阵包含了位移、缩放与旋转操作,它们会被应用到所有物体的顶点上,使它们变换到全局的世界空间。
    这个模型矩阵看起来是这样的:
glm::mat4 modelMat;
modelMat = glm::rotate(modelMat, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
  • 接下来我们需要创建一个观察矩阵。
    我们想要在场景里面稍微往后移动,以使得物体变成可见(当在世界空间时,我们位于原点(0,0,0))。
    将摄像机向后移动,和将整个场景向前移动是一样的。
    这正是观察矩阵所做的,我们以相反于摄像机移动的方向移动整个场景。
    因为我们想要往后移动,并且OpenGL是一个右手坐标系(Right-handed System),所以我们需要沿着z轴的正方向移动。
    我们会通过将场景沿着z轴负方向平移来实现,它会给我们一种我们在往后移动的感觉。
glm::mat4 viewMat;
// 注意,我们将矩阵向我们要进行移动场景的反方向移动。
viewMat = glm::translate(viewMat, glm::vec3(0.0f, 0.0f, -3.0f));
  • 最后我们需要做的是定义一个投影矩阵。我们希望在场景中使用透视投影,所以像这样声明一个投影矩阵:
glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f), screenWidth / screenHeight, 0.1f, 100.0f);

合并起来即

//M
glm::mat4 modelMat;
modelMat = glm::rotate(modelMat, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
//V
glm::mat4 viewMat;
// 注意,我们将矩阵向我们要进行移动场景的反方向移动。
viewMat = glm::translate(viewMat, glm::vec3(0.0f, 0.0f, -3.0f));
//P
glm::mat4 projMat;
projMat = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);

修改顶点着色器接口

#version 330 core           
layout(location = 0) in vec3 aPos;   // 位置变量的属性位置值为 0
layout(location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
layout(location = 2) in vec2 aTexCoord; // uv变量的属性位置值为 2

out vec4 vertexColor;
out vec2 TexCoord;

//uniform mat4 transform;
uniform mat4 modelMat;
uniform mat4 viewMat;
uniform mat4 projMat;
void main(){             
   gl_Position =  projMat * viewMat * modelMat * vec4(aPos.x, aPos.y, aPos.z, 1.0);   
   vertexColor = vec4(aColor,1.0); 
   TexCoord = aTexCoord;
}

数据传入顶点着色器:

glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "modelMat"), 1, GL_FALSE, glm::value_ptr(modelMat));
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "viewMat"), 1, GL_FALSE, glm::value_ptr(viewMat));
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "projMat"), 1, GL_FALSE, glm::value_ptr(projMat));

最终的物体应该会: coordinate_systems_result

2.1 立方体

到目前为止,我们一直都在使用一个2D平面,而且在3D空间里!所以,让我们大胆地拓展我们的2D平面为一个3D立方体。要想渲染一个立方体,我们一共需要36个顶点(6个面 x 每个面有2个三角形组成 x 每个三角形有3个顶点),这36个顶点的位置你可以从这里获取。

修改顶点属性

// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// uv属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(2);

把和EBO有关的全部注释掉,用绘制数组形式画出图形

glDrawArrays(GL_TRIANGLES, 0, 36);

并且在开启窗口(glViewport(0, 0, 800, 600);)的后面启动深度缓存

glEnable(GL_DEPTH_TEST);

在渲染循环中,每次渲染迭代之前清除深度缓冲(否则前一帧的深度信息仍然保存在缓冲中)

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Z-buffer

OpenGL存储它的所有深度信息于一个Z缓冲(Z-buffer)中,也被称为深度缓冲(Depth Buffer)。GLFW会自动为你生成这样一个缓冲(就像它也有一个颜色缓冲来存储输出图像的颜色)。深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing),它是由OpenGL自动完成的。

2.2 更多的立方体

现在我们想在屏幕上显示10个立方体。
每个立方体看起来都是一样的,区别在于它们在世界的位置及旋转角度不同。
立方体的图形布局已经定义好了,所以当渲染更多物体的时候我们不需要改变我们的缓冲数组和属性数组,我们唯一需要做的只是改变每个对象的模型矩阵来将立方体变换到世界坐标系中。

首先,让我们为每个立方体定义一个位移向量来指定它在世界空间的位置。我们将在一个glm::vec3数组中定义10个立方体位置

glm::vec3 cubePositions[] = {
  glm::vec3( 0.0f,  0.0f,  0.0f), 
  glm::vec3( 2.0f,  5.0f, -15.0f), 
  glm::vec3(-1.5f, -2.2f, -2.5f),  
  glm::vec3(-3.8f, -2.0f, -12.3f),  
  glm::vec3( 2.4f, -0.4f, -3.5f),  
  glm::vec3(-1.7f,  3.0f, -7.5f),  
  glm::vec3( 1.3f, -2.0f, -2.5f),  
  glm::vec3( 1.5f,  2.0f, -2.5f), 
  glm::vec3( 1.5f,  0.2f, -1.5f), 
  glm::vec3(-1.3f,  1.0f, -1.5f)  
};

现在,在渲染循环中,我们调用glDrawArrays 10次,但这次在我们渲染之前每次传入一个不同的模型矩阵到顶点着色器中。
我们也对每个箱子也稍微加了一点旋转:

for (unsigned int i = 0; i < 10; i++)
{
 glm::mat4 modelMat;
 modelMat = glm::translate(modelMat, cubePositions[i]);
 float angle = 20.0f * i;
 modelMat = glm::rotate(modelMat, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));

 myShader->use();
 glUniform1i(glGetUniformLocation(myShader->ID, "ourTexture"), 0);
 glUniform1i(glGetUniformLocation(myShader->ID, "ourFace"), 1);

 //glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "transform"), 1, GL_FALSE, glm::value_ptr(trans));
 glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "modelMat"), 1, GL_FALSE, glm::value_ptr(modelMat));
 glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "viewMat"), 1, GL_FALSE, glm::value_ptr(viewMat));
 glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "projMat"), 1, GL_FALSE, glm::value_ptr(projMat));

 //glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
 glDrawArrays(GL_TRIANGLES, 0, 36);
}

morecube

2.2.1 关于DrawCall

我们调用glDrawArrays 10次,对应Unity中有个叫DrawCall的东西
我们调用glDrawArrays 10次也就是会发出10个DrawCall,每画一次物体就是呼叫一个DrawCall,后期可利用实例化渲染合并DrawCall

完整代码

Reference

OpenGL学习脚印: 坐标变换过程(vertex transformation)

「如果这篇文章对你有用,请随意打赏」

Heisenberg Blog

如果这篇文章对你有用,请随意打赏

使用微信扫描二维码完成支付


comments powered by Disqus