目录
前言
上一篇回顾:OpenGL学习(二)渲染流水线与三角形绘制
在上一篇博客中我们实现了二维平面上三角形的绘制,今天我们来绘制一个立方体,同时我们将会利用模型变换矩阵对立方体进行旋转,平移,缩放等操作,最后我们会通过阅读OFF格式的模型来读取更加复杂的三维模型。
⚠
该部分的绘制代码基于上一篇博客:OpenGL学习(二)渲染流水线与三角形绘制
博客内容因为篇幅关系,不会完整的列出所有的代码
完整代码会放在文章末尾
绘制立方体
立方体的绘制,比起二维的三角形来说,要相对复杂。立方体有 6 个面,而我们的绘制是基于基本的三角形图元,这意味着我们需要用 2 个三角形来描述正方体的一个面。
此外,一个立方体有 6 个面,这意味着我们需要有 12 个三角面片,而每个三角面片有 3 个顶点,所以我们总共要生成 36 个顶点。
你可能注意到哪里不对了,毕竟一个立方体才 8 个顶点啊,你搁这整 36 个,啥啊?
注意到我们的绘制函数 glDrawArrays
,在该函数中如果第一个参数指定的绘制模式为 GL_TRIANGLES
,表示三个顶点一组绘制三角形,所以尽管会有一些点的冗余,但我们应该按照规则来给定顶点。下面以绘制一个正方形为例,解释为何立方体会有 36 个顶点:
可以看到,尽管一个正方形只有 4 个顶点,但是为了用两个三角形去绘制它,我们生成了 6 个顶点,将两个三角形拼凑为正方形。
所以我们首先需要定义正方体的 8 个顶点,和其顶点的颜色。我个人更加倾向于把它定义为全局变量:
// 正方形的8个顶点位置坐标
std::vector<glm::vec3> vertexPosition = {
glm::vec3(-0.5, -0.5, -0.5),
glm::vec3(0.5, -0.5, -0.5),
glm::vec3(-0.5, 0.5, -0.5),
glm::vec3(0.5, 0.5, -0.5),
glm::vec3(-0.5, -0.5, 0.5),
glm::vec3(0.5, -0.5, 0.5),
glm::vec3(-0.5, 0.5, 0.5),
glm::vec3(0.5, 0.5, 0.5)
};
// 正方形的8个顶点的颜色
std::vector<glm::vec3> vertexColor = {
glm::vec3(1.0, 1.0, 1.0), // White
glm::vec3(1.0, 1.0, 0.0), // Yellow
glm::vec3(0.0, 1.0, 0.0), // Green
glm::vec3(0.0, 1.0, 1.0), // Cyan
glm::vec3(1.0, 0.0, 1.0), // Magenta
glm::vec3(1.0, 0.0, 0.0), // Red
glm::vec3(0.5, 0.5, 0.5), // Gray
glm::vec3(0.0, 0.0, 1.0) // Blue
};
随后我们需要指定正方形的 6 个面,即 12 个三角面片。我们创建一个数组 faces,其中第 i 个元素 faces[i]
表示第 i 个三角面片的顶点下标:
// 正方形由6个面 -- 12个三角形面片组成 faces存储顶点在vertexPosition中的下标
std::vector<glm::vec3> faces = {
glm::vec3(0,2,1),
glm::vec3(1,2,3),
glm::vec3(1,3,7),
glm::vec3(7,5,1),
glm::vec3(0,1,5),
glm::vec3(5,4,0),
glm::vec3(0,4,2),
glm::vec3(4,6,2),
glm::vec3(4,5,7),
glm::vec3(7,6,4),
glm::vec3(6,7,3),
glm::vec3(3,2,6)
};
如图:通过 faces 数组指定三角面片的顶点:
知晓了 faces 中定义立方体三角面片的方式之后,我们就可以利用 faces 数组,生成顶点属性的索引:
首先我们建立两个变量,分别表示顶点的位置坐标和顶点的颜色
// 顶点坐标 / 颜色
std::vector<glm::vec3> points, colors;
然后我们在初始化(上一篇博客的 init 函数)中,通过 faces 数组,生成顶点属性:
// 由面片信息生成三角形面片顶点
for (int i = 0; i < faces.size(); i++)
{
// 取得第 i 个三角面片的三个顶点的下标
int index1 = faces[i].x;
int index2 = faces[i].y;
int index3 = faces[i].z;
// 生成顶点
points.push_back(vertexPosition[index1]);
points.push_back(vertexPosition[index2]);
points.push_back(vertexPosition[index3]);
// 生成顶点颜色
colors.push_back(vertexColor[index1]);
colors.push_back(vertexColor[index2]);
colors.push_back(vertexColor[index3]);
}
剩下的步骤和我们在上一篇博客:OpenGL学习(二)渲染流水线与三角形绘制 中的操作一样,创建vao vbo,读取着色器,指定vao格式,传输数据…
对了,别忘了改 display 中的绘制函数,我们绘制的顶点数不再是3个了:
重新运行程序,我们得到了一个。。。唔。。。正方形,而不是立方体?
结果是意料之中的,因为我们从立方体的正面看过去,那么就应该是一个正方形。如果我们想看到更多的面,我们就应该让正方体旋转起来!
模型变换矩阵
提到旋转,就不得不提一下模型变换。事实上,建模师在建立3D模型的时候,是以一个叫 模型坐标系 为参考建立的。比如立方体,如下的图展示了立方体的模型坐标。
但是事实上,在我们建立 3D 场景的时候,我们不同的三维模型具有不同的位置。我们不能强求建模师在建模时就确定模型的绝对位置,况且我们还会实时地对模型进行移动旋转等操作,这就意味着,对模型的平移旋转缩放必须是由程序完成的!
于是我们引入 模型变换矩阵 这个概念。我们通过对模型的坐标进行变换,得到我们想要的效果。
齐次坐标
在开始构建我们的模型变换矩阵前,首先了解到齐次坐标的概念。通常情况下,我们都可以用三维向量来描述三维空间下的点,或者是一个方向:
glm::vec3(0,2,1)
glm::vec3(1,2,3)
可是我们如何区分一个三维向量是坐标还是方向向量?
如果是坐标,那么我们平移这个向量,对应的坐标需要发生改变。如果是方向向量,那么我们平移这个向量,对应的坐标不能发生改变。
这就带来了难题。于是数学家们通过巧妙而猥琐的构造方式,想出了一个完美的解决方案:”即然没法区分,那就增加一个维度来保存向量的属性信息“,这就是齐次坐标。
齐次坐标在三维坐标的基础上,拓展了一个维度,变为四维的向量。那么增加了一个维度就能够区分 坐标点 和 方向向量 了吗?
直接说结论:第四维度为0则为方向向量,第四维度为1则为坐标
注:这其中涉及巧妙的构造,但是我们暂时记住结论。接下来我们会验证这种构造的正确性。
通过矩阵进行变换
平移旋转和缩放都是线性变换,我们观察矩阵乘法的定义式:
我们发现齐次坐标左乘一个矩阵,对于齐次坐标的四个维度而言,全都是 力士 线性变换!
线性变换意味着我们可以通过将其次坐标和一个矩阵进行乘法,从而实现平移旋转和缩放等线性变换。
平移变换矩阵
平移变换是最简单的线性变换!我们只需要将一个坐标加上一定的偏移就可以实现。所以我们有
v ′ = v + d v' = v + d v′=v+d
如果写成齐次坐标的矩阵乘法的形式,我们通过一个平移矩阵,对坐标进行变换(注意这里第四个维度为 1 表示这是一个点):
[ 1 0 0 d x 0 1 0 d y 0 0 1 d z 0 0 0 1 ] ∗ ( x , y , z , 1 ) = ( x + d x , y + d y , z + d z , 1 ) \left[\begin{array}{cccc} 1 & 0 & 0 & d_{x} \\ 0 & 1 & 0 & d_{y} \\ 0 & 0 & 1 & d_{z} \\ 0 & 0 & 0 & 1 \end{array}\right] * \left(x,y,z, 1\right)=\left(x+d_{x},y+d_{y},z+d_{z}, 1\right) ⎣⎢⎢⎡100001000010dxdydz1⎦⎥⎥⎤∗(x,y,z,1)=(x+dx,y+d