OpenGL学习(三)三维绘制与模型变换矩阵

前言

上一篇回顾: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θ00sinθ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θ00sinθ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θ0sinθ00100sinθ0cosθ00001


此处默认模型的中心点在原点,才能够实现绕 xyz 轴的旋转。如果中心不在原点,就会进行错误的变换

缩放变换矩阵

缩放的变换和平移类似,也是各个坐标乘一个系数(平移是加上一个系数)

v ′ = v ∗ s v' = v * s v=vs

于是我们能够很快的得出缩放变换的变换矩阵:

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 函数中的代码。在每一次绘制之前,我们都需要清空两个缓冲:

  1. 颜色缓冲
  2. 深度缓冲(方便进行深度测试)

改动如下:

将:
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文件的读取的,放到下一篇罢(摸了

猜你喜欢

转载自blog.csdn.net/weixin_44176696/article/details/109993082