目录
前言
上一篇回顾: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+dy,z+dz,1)
我们再来看第四维度为 0 的情况:
[ 1 0 0 d x 0 1 0 d y 0 0 1 d z 0 0 0 1 ] ∗ ( x , y , z , 0 ) = ( x , y , z , 0 ) \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, 0\right)=\left(x,y,z, 0\right) ⎣⎢⎢⎡100001000010dxdydz1⎦⎥⎥⎤∗(x,y,z,0)=(x,y,z,0)
什么都没有发生。因为对于第四维度为 0 的情况,我们认为它是一个方向向量。对于方向向量的平移操作为无效,因为向量始终表示方向!
于是得到我们的平移变换矩阵:
T ( d x , d y , d z ) = [ 1 0 0 d x 0 1 0 d y 0 0 1 d z 0 0 0 1 ] \boldsymbol{T}_{}(d_{x},d_{y},d_{z})=\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] T(dx,dy,dz)=⎣⎢⎢⎡100001000010dxdydz1⎦⎥⎥⎤
旋转变换矩阵
虽然短,但是能够旋转
旋转也是一种线性变换。旋转分为三个部分:绕 xyz 轴旋转,所以我们理应有三个旋转变换矩阵。首先我们来看绕着 z 轴的旋转:
绕着 z 轴旋转相当于 z 不变。通过推理我们可以得到点的变换规律:
x ′ = x cos θ − y sin θ y ′ = x sin θ + y cos θ z ′ = z \begin{array}{l} x^{\prime}=x \cos \theta-y \sin \theta \\ y^{\prime}=x \sin \theta+y \cos \theta \\ z^{\prime}=z \end{array} x′=xcosθ−ysinθy′=xsinθ+ycosθz′=z
于是有绕着 z 轴的旋转矩阵:
R z ( θ ) = [ cos θ − sin θ 0 0 sin θ cos θ 0 0 0 0 1 0 0 0 0 1 ] \boldsymbol{R}_{z}(\theta)=\left[\begin{array}{cccc} \cos \theta & -\sin \theta & 0 & 0 \\ \sin \theta & \cos \theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] Rz(θ)=⎣⎢⎢⎡cosθsinθ00−sinθcosθ0000100001⎦⎥⎥⎤
同理我们可以得到绕 xy 轴的旋转矩阵:
R x ( θ ) = [ 1 0 0 0 0 cos θ − sin θ 0 0 sin θ cos θ 0 0 0 0 1 ] \boldsymbol{R}_{x}(\theta)=\left[\begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & \cos \theta & -\sin \theta & 0 \\ 0 & \sin \theta & \cos \theta & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] Rx(θ)=⎣⎢⎢⎡10000cosθsinθ00−sinθcosθ00001⎦⎥⎥⎤
R y ( θ ) = [ cos θ 0 sin θ 0 0 1 0 0 − sin θ 0 cos θ 0 0 0 0 1 ] \boldsymbol{R}_{y}(\theta)=\left[\begin{array}{cccc} \cos \theta & 0 & \sin \theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin \theta & 0 & \cos \theta & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] Ry(θ)=⎣⎢⎢⎡cosθ0−sinθ00100sinθ0cosθ00001⎦⎥⎥⎤
⚠
此处默认模型的中心点在原点,才能够实现绕 xyz 轴的旋转。如果中心不在原点,就会进行错误的变换
缩放变换矩阵
缩放的变换和平移类似,也是各个坐标乘一个系数(平移是加上一个系数)
v ′ = v ∗ s v' = v * s v′=v∗s
于是我们能够很快的得出缩放变换的变换矩阵:
S ( s x , s y , s z ) = [ s x 0 0 0 0 s y 0 0 0 0 s z 0 0 0 0 1 ] \mathbf{S}\left(s_{x}, s_{y}, s_{z}\right)=\left[\begin{array}{cccc} s_{x} & 0 & 0 & 0 \\ 0 & s_{y} & 0 & 0 \\ 0 & 0 & s_{z} & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] S(sx,sy,sz)=⎣⎢⎢⎡sx0000sy0000sz00001⎦⎥⎥⎤
⚠
此处默认模型的中心点在原点,才能够实现等比缩放。如果中心不在原点,就会进行错误的变换
变换的级联
对于上述的三种变换:平移 旋转 缩放,我们得到了 5 个变换矩阵,那么我们该怎么利用他们呢?
我们当然可以逐个逐个的对顶点坐标进行变换:
v = Rx * v // x旋转
v = Ry * v // y旋转
v = Rz * v // z旋转
v = S * v // 缩放
v = T * v // 平移
但是这一切值得吗?
这里就再次体现出利用矩阵对齐次坐标的巧妙性:我们可以先将所有的变换矩阵都乘起来,形成最终的模型变换矩阵,再利用模型变换矩阵对顶点坐标进行变换
model = T * R * S // 变换的级联 -- 模型变换矩阵
v = model * v
我们首先进行缩放变换,然后是旋转,最后是平移。注意我们书写矩阵的顺序和实际的运算顺序相反。
⚠
注意变换的先后顺序,因为旋转和缩放默认模型中心在原点
我们必须先进行缩放旋转,之后再进行平移。否则会形成错误的变换!
xyz轴的旋转级联过程并不是直接相乘,因为旋转会互相干扰,但是我们使用glm的API可以避免我们手动计算它。
让立方体换个姿势
我们已经知晓了如何构造我们的模型变换矩阵,现在开始让立方体换个姿势摆放。
构建模型变换矩阵
矩阵的构造是很辛苦的,好在 glm 库已经帮我们封装好了。我们首先引入如下的两个头文件,他们的作用分别是矩阵变换和矩阵寻址:
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
然后我们可以开始在初始化(init 函数)中,构造一个模型变换矩阵:
// 构造模型变换矩阵
glm::mat4 unit( // 单位矩阵
glm::vec4(1, 0, 0, 0),
glm::vec4(0, 1, 0, 0),
glm::vec4(0, 0, 1, 0),
glm::vec4(0, 0, 0, 1)
);
glm::mat4 scale = glm::scale(unit, glm::vec3(0.6, 0.6, 0.6)); // xyz缩放0.6倍
glm::mat4 translate = glm::translate(unit, glm::vec3(-0.3, 0.0, 0.0)); // 平移(-0.3, 0.0, 0.0)
glm::mat4 rotate = unit; // 旋转
rotate = glm::rotate(rotate, glm::radians(-30.0f), glm::vec3(1, 0, 0)); // 绕x轴转-30度
rotate = glm::rotate(rotate, glm::radians(20.0f), glm::vec3(0, 1, 0)); // 绕y轴转20度
rotate = glm::rotate(rotate, glm::radians(0.0f), glm::vec3(0, 0, 1)); // 绕z轴转0度
glm::mat4 model = translate * rotate * scale; // 变换级联 -- 生成模型变换矩阵
至此,模型矩阵到手,可是我们如何对顶点进行变换呢?一种简单的方式是写一个for循环,对所有的顶点都进行一次矩阵乘法,可是这么做相当于把工作交给cpu。
一种更加流行的方式是我们将模型变换矩阵传递到GPU中,让GPU对顶点进行变换。
向GPU传递模型变换矩阵
因为我们要对所有的顶点都进行一次模型变换。这意味着对所有顶点来说,他们看到的模型变换矩阵必须是一致的!
为了解决这个问题,我们引入 uniform 变量。那么什么是 uniform 呢?
对不起放错图了。。。
唔。。。uniform 变量指的是一致变量。还记得上一篇博客中我我们提到的,对输入的每一个顶点,都会运行顶点着色器程序。在顶点着色器中,每个顶点的坐标是不同的,所有的顶点着色器程序看到的 uniform 变量是相同的,这就是一致变量的意思。
彳亍! 我们开始向GPU传递 uniform 变量,在 init 函数中。和顶点数据的输入类似,我们要指定 model 矩阵在着色器中的位置索引(约定变量名称),然后向GPU传递数据:
// 传递uniform变量
GLuint mlocation = glGetUniformLocation(program, "model"); // 名为model的uniform变量的位置索引
glUniformMatrix4fv(mlocation, 1, GL_FALSE, glm::value_ptr(model)); // 列优先矩阵
其中我们必须通过 glGetUniformLocation
函数来获取uniform变量的位置。此外, glUniformMatrix4fv
函数 的第三个参数是表示该矩阵是列优先还是行优先。
⚠
关于行列优先变量:
我们使用的是 glm 库进行模型矩阵的构造,我们填 false。
如果是自己手写矩阵构造,我们填 true。
在GPU中进行矩阵变换
我们修改顶点着色器的代码为如下的的代码:
#version 330 core
in vec3 vPosition; // cpu传入的顶点坐标
in vec3 vColor; // cpu传入的顶点颜色
out vec3 vColorOut; // 向片元着色器传递顶点颜色
uniform mat4 model; // 模型变换矩阵
void main()
{
gl_Position = model * vec4(vPosition, 1.0); // 指定ndc坐标
vColorOut = vColor; // 这个颜色经过线性插值生成片元颜色
}
别紧张,改动的地方很少。我们只是将传过来的uniform变量,和我们的顶点坐标进行了一次矩阵乘法运算(相当于对顶点施加模型变换)。红色部分为我们增加的代码:
片元着色器不需要任何改动,嗯。
深度测试
如果你按部就班的完成了上面的代码,那么你会发现我们的程序结果是这样的:
我们的立方体好像被掏空一般,这是因为我们的测试策略。我们没有开启任何绘制测试策略,那么三角形就会按照三角面片的绘制顺序互相覆盖!我们的立方体正面显然被立方体的背面覆盖了。。。
为了还原正确的覆盖方式,我们需要开启深度测试。我们只渲染离相机最近的像素点,其他像素点会被忽略。
开启的方式也很简单。我们在 init 函数的最后加一句:
glEnable(GL_DEPTH_TEST); // 开启深度测试
此外,我们在 main 函数创建窗体时(放在前后都可),启用深度模式:
将:
glutInitDisplayMode(GLUT_RGBA);
改为:
glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
然后我们修改 display 函数中的代码。在每一次绘制之前,我们都需要清空两个缓冲:
- 颜色缓冲
- 深度缓冲(方便进行深度测试)
改动如下:
将:
glClear(GL_COLOR_BUFFER_BIT);
改为:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
最后重新运行程序,可以看到绘制正常辣:
ohhhhhh
完整代码
c++
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <GL/glew.h>
#include <GL/freeglut.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
// 正方形的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存储顶点在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)
};
// 顶点坐标 / 颜色
std::vector<glm::vec3> points, colors;
// 读取文件并且返回一个长字符串表示文件内容
std::string readShaderFile(std::string filepath)
{
std::string res, line;
std::ifstream fin(filepath);
if (!fin.is_open())
{
std::cout << "文件 " << filepath << " 打开失败" << std::endl;
exit(-1);
}
while (std::getline(fin, line))
{
res += line + '\n';
}
fin.close();
return res;
}
// 获取着色器对象
GLuint getShaderProgram(std::string fshader, std::string vshader)
{
// 读取shader源文件
std::string vSource = readShaderFile(vshader);
std::string fSource = readShaderFile(fshader);
const char* vpointer = vSource.c_str();
const char* fpointer = fSource.c_str();
// 容错
GLint success;
GLchar infoLog[512];
// 创建并编译顶点着色器
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, (const GLchar**)(&vpointer), NULL);
glCompileShader(vertexShader);
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); // 错误检测
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "顶点着色器编译错误\n" << infoLog << std::endl;
exit(-1);
}
// 创建并且编译片段着色器
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, (const GLchar**)(&fpointer), NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); // 错误检测
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "片段着色器编译错误\n" << infoLog << std::endl;
exit(-1);
}
// 链接两个着色器到program对象
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return shaderProgram;
}
// 初始化
void init()
{
// 由面片信息生成三角形面片顶点
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]);
}
// 生成vbo对象并且绑定vbo
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// 先确定vbo的总数据大小(画饼???) 传NULL指针表示我们暂时不传数据
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec3) * (points.size() + colors.size()), NULL, GL_STATIC_DRAW);
// 传送数据到vbo 分别传递 顶点位置 和 顶点颜色
GLuint pointDataOffset = 0;
GLuint colorDataOffset = sizeof(glm::vec3) * points.size();
glBufferSubData(GL_ARRAY_BUFFER, pointDataOffset, sizeof(glm::vec3) * points.size(), &points[0]);
glBufferSubData(GL_ARRAY_BUFFER, colorDataOffset, sizeof(glm::vec3) * colors.size(), &colors[0]);
// 生成vao对象并且绑定vao
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// 生成着色器程序对象
std::string fshaderPath = "shaders/fshader.fsh";
std::string vshaderPath = "shaders/vshader.vsh";
GLuint program = getShaderProgram(fshaderPath, vshaderPath);
glUseProgram(program); // 使用着色器
// 建立顶点变量vPosition在着色器中的索引 同时指定vPosition变量的数据解析格式
GLuint vlocation = glGetAttribLocation(program, "vPosition"); // vPosition变量的位置索引
glEnableVertexAttribArray(vlocation);
glVertexAttribPointer(vlocation, 3, GL_FLOAT, GL_FALSE, 0, (GLvoid*)0); // vao指定vPosition变量的数据解析格式
// 建立颜色变量vColor在着色器中的索引 同时指定vColor变量的数据解析格式
GLuint clocation = glGetAttribLocation(program, "vColor"); // vColor变量的位置索引
glEnableVertexAttribArray(clocation);
glVertexAttribPointer(clocation, 3, GL_FLOAT, GL_FALSE, 0, (GLvoid*)(sizeof(glm::vec3) * points.size())); // 注意指定offset参数
// 构造模型变换矩阵
glm::mat4 unit( // 单位矩阵
glm::vec4(1, 0, 0, 0),
glm::vec4(0, 1, 0, 0),
glm::vec4(0, 0, 1, 0),
glm::vec4(0, 0, 0, 1)
);
glm::mat4 scale = glm::scale(unit, glm::vec3(0.6, 0.6, 0.6)); // xyz缩放0.6倍
glm::mat4 translate = glm::translate(unit, glm::vec3(-0.3, 0.0, 0.0)); // 平移(-0.3, 0.0, 0.0)
glm::mat4 rotate = unit; // 旋转
rotate = glm::rotate(rotate, glm::radians(-30.0f), glm::vec3(1, 0, 0)); // 绕x轴转-30度
rotate = glm::rotate(rotate, glm::radians(20.0f), glm::vec3(0, 1, 0)); // 绕y轴转20度
rotate = glm::rotate(rotate, glm::radians(0.0f), glm::vec3(0, 0, 1)); // 绕z轴转0度
glm::mat4 model = translate * rotate * scale; // 变换级联 -- 生成模型变换矩阵
// 传递uniform变量
GLuint mlocation = glGetUniformLocation(program, "model"); // 名为model的uniform变量的位置索引
glUniformMatrix4fv(mlocation, 1, GL_FALSE, glm::value_ptr(model)); // 列优先矩阵
glEnable(GL_DEPTH_TEST); // 开启深度测试
glClearColor(0.0, 0.0, 0.0, 1.0); // 背景颜色 -- 黑
}
// 显示回调函数
void display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清空窗口颜色缓存
glDrawArrays(GL_TRIANGLES, 0, points.size()); // 绘制n个点
glutSwapBuffers(); // 交换缓冲区
}
int main(int argc, char** argv)
{
glutInit(&argc, argv); // glut初始化
glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
glutInitWindowSize(512, 512); // 窗口大小
glutCreateWindow("2 - model"); // 创建OpenGL上下文
#ifdef __APPLE__
#else
glewInit();
#endif
init();
glutDisplayFunc(display); // 设置显示回调函数 -- 每帧执行
glutMainLoop(); // 进入主循环
return 0;
}
着色器
顶点:
#version 330 core
in vec3 vPosition; // cpu传入的顶点坐标
in vec3 vColor; // cpu传入的顶点颜色
out vec3 vColorOut; // 向片元着色器传递顶点颜色
uniform mat4 model; // 模型变换矩阵
void main()
{
gl_Position = model * vec4(vPosition, 1.0); // 指定ndc坐标
vColorOut = vColor; // 这个颜色经过线性插值生成片元颜色
}
片元:
#version 330 core
in vec3 vColorOut; // 顶点着色器传递的颜色
out vec4 fColor; // 片元输出像素的颜色
void main()
{
fColor = vec4(vColorOut, 1.0);
}
小结
三维的绘制并没有什么大不了的,我们传递三维的顶点数据就是了。值得注意的是物体的模型变换矩阵。
我们一定是通过模型变换矩阵来调整三维物体的姿态。注意现代的解决方案是传递模型变换矩阵到着色器中,让GPU帮我们进行运算。
此外,在绘制三维物体的时候,我们不要忘记开启深度测试,这样才能够准确的描绘物体的覆盖关系。
好了。。。这次的内容应该足够多了。我本来还想再加上键鼠控制和OFF文件的读取的,放到下一篇罢(摸了