[OpenGL] 模型与渲染—— OpenGL的特性与坑
技术概述
OpenGL,是一种 3D 图形库的规范。我们可以直接使用 OpenGL 简单、高效地在屏幕上绘制图形
因此,OpenGL 可用于以下方面:
- 游戏
- 模型
- 仿真
……的图形显示部分。再加上自行编写的或第三方库的代码才可以组成一个完整的程序
虽然 OpenGL 一般是用 C 编写的,但它却是面向对象的
但是,几乎所有的对象在使用前,都必须先自行绑定
同时每种对象能够绑定的数量是固定的(一般是1个)。绑定新的就会令旧的失效
请体会以下代码:
obj a;
obj b;
bind(a);
objDoSomething();// 这里操作的是 a
bind(b);
objDoSomething();// 这里操作的是 b
低情商:全局变量;高情商:状态机
基于此,对象的绑定,成为了学习的一大问题。比如在未绑定对象的情况下操作对象,什么也不会发生
因此,需要小心翼翼地使用原本的代码,或者自行封装
比如这样:
obj a;
obj b;
a.doSomething();
b.doSomething();
...
void obj::doSomething()
{
bind(a);
objDoSomething();
bind(NULL);
}
另外一点,就是对这些对象的理解
一开始,接触VAO、着色器、FBO这些概念令人眼花缭乱。如何理解这些对象也是一个问题
技术详述
如何表示一个模型?
模型,由各个顶点,以及顶点所携带的信息构成
比如,我们可以这样表示一个三角形
float[] triangle
{
1.0f, 1.0f,
1.0f, 0.0f,
0.0f, 1.0f
};
这些数据是什么意思?事实上,这需要由你自己决定
VBO 与 VAO
VBO(顶点缓冲对象),用于将数据保存到某个可以高效读取的位置
而 VAO(顶点数组对象), 则是用来表示如何读取这些数据的
使用的效果大致是这样的:
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);// 前文提到的绑定来了
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);// 没有上一行的绑定,这一行就什么也不做
GLuint vao;
glGenVertexArrays(1, &vao);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
具体来说VAO就是通过 glVertexAttribPointer
这一函数来指示数据的存储方式的。比如上文中的参数表示的是:
顶点的第0个属性由2个 float 组成,两个顶点属性之间相距2 * sizeof(float)
,第一个顶点直接从头((void*)0
)开始读取
至此,我们已经有了一个表示模型数据的对象了
模型之后
有了模型,我们应该如何渲染模型呢?
OpenGL 通过程序员自定的着色器与管线来绘制模型
大致地说,管线由多个着色器组成,每个着色器负责处理不同的信息
有2个着色器是必须由程序员自己定义的
- 顶点着色器:对顶点的坐标进行处理
- 片段着色器:对图元(比如,三角形)所占据的像素的颜色进行处理
还有一个默认提供,但可以自行设置的图元着色器,可以把某种图元处理成其他图元
比如把点处理成一个三角形
有了管线,我们就可以把它和 VAO 一起使用,绘制图形了
(严谨地说,程序员将以上着色器(Shader)合并成 Program,由管线使用 Program)
更加地多姿多彩
之前我们只提到了模型的坐标、颜色等内容,其实我们也可以通过着色器将图片中的颜色附着到模型上
而这种方式需要用到的就是 OpenGL 中的材质(Texture)
类似于 VAO,我们需要把图片读取到程序中,并绑定和设定信息
之后我们就可以像用 VAO 一样地使用 Texture 了
不过 VAO 同时只能绑定1个,而 Texture 可以绑定 32 个甚至更多
(相当于开了个全局变量存绑定的 VAO,而开了个全局数组存绑定的 Texture)
另外除了用于存储图片的颜色,配合 FBO(下面会提到)使用,Texture 可以作为缓冲存储颜色以外的信息
比如深度——图元在空间中的前后关系
其他对象和特性
接下来简单介绍一下上面没有提及到的对象
- EBO:用时间换空间,节省 VBO 占用的空间。有时 VBO 中可能会有多个重复的顶点,使用 EBO 可以把重复的顶点并成一个
- Skybox(OpenGL 里面叫 Texture Cube Box):用于实现天空盒
- FBO:用于实现离屏渲染/图像后期处理
- RBO:一种只写不读的缓冲,比直接使用 Texture 当缓冲更快(当然需要读取的话只能用 Texture 了)
- UBO:Program 中可以指定全局的数据,但不同的 Program 的全局数据并不共享。如果多个 Program 需要使用相同的数据的话,只要使用 UBO 就可以做到不同的 Program 共享 UBO 中的数据了(但还是要自己指定每个 Program 用的是哪个 UBO)
还有一些特性用于解放程序员的双手
- 深度测试:用于体现图元前后关系
- 模板测试:可以用于实现诸如镜子之类的效果
- Mipmap:加快远端图元显示材质速度的技术
- 各项异性(需要扩展):防止远端图元材质失真的技术(但是会占用更多空间,多占用3倍。作为对比,Mipmap多占用1/3)
- MSAA:以多次计算作为代价,防止图元边缘出现锯齿。还有与之配套的抗锯齿材质,可以与 FBO 配合使用
OpenGL 之外的技术
有些技术虽然 OpenGL 没有直接包含,但编写代码时又用得上的技术。这些技术需要自己去编(C)写(V):
- 光照:比如布林-冯光照模型,一种快速而又看起来比较逼真的光照模型
- 阴影贴图:利用 FBO,我们可以实现一个实时渲染中可以使用的,不够逼真、但速度可以接受的阴影
把前面的流程串起来
一个简单的 OpenGL 程序的结构,大致上是下面这样的:
初始化();
创建模型();
创建着色器();
创建材质();
while(程序未退出())
{
获取输入();
处理信息();
绘制图形();
}
可以看到,有些函数与 OpenGL 无关,比如获取输入(用 GLFW 实现)、处理信息(自己写)
模型、着色器、材质的创建顺序一般是无所谓的
主要是循环中的内容,几乎所有的游戏、模拟软件,都是通过这种方法实现绘制的
问题与解决方案
下面提一下自己编写代码时遇到的问题:
Q:为什么绘制不出图形
有以下可能
- VBO、VAO 没设置好(先绑定 VAO,再绑定 VBO,为了保险起见,再依次解绑 VAO、VBO)
- 绘制时没有绑定 VAO 和 Program,这两个必须同时绑定才能绘制
- 顶点着色器中,顶点坐标的第四分量(w)设置成了零(应该设置成非零值)
Q:绘制带材质的图形时,只绘制了一个白色的矩形或者黑色的图形
- 同上一个问题的第一个回答
- 没有绑定材质
- 绑定了材质,没有指定着色器读取这个材质
Q:绘制带材质图形时,图形是花的
图片通道与材质通道不匹配,比如图片有4个通道,材质却指定为 GL_RGB
(3个通道)
Q: 绘制带材质图形时,图形是歪的
请看这篇文章
Q:绘制带材质图形时,图形莫名其妙偏移了一段距离,换台电脑就没问题
请检查你编写的着色器中有没有出现未定义就直接使用的变量。有的显卡驱动会直接把未定义局部变量初始化为零,有的不会。以下是例子
float a;// 应该这样写:float a = 0.0f;
a += 0.5;// Oops...
Q:我的 OpenGL 怎么没有各向异性相关的宏?
这个需要作为扩展自行添加(比如 GLAD 的下载页面中就可以指定需要这个扩展)
总结
学习 OpenGL,不仅仅是学习 OpenGL 本身那么简单,或多或少地还涉及到以下内容(括号内是现成的库):
- 计算机图形学
- 面向对象编程
- 线性代数(glm)
- 文件处理(stb_image, assimp)
- 物理模型
- GUI(ImGUI)
此外,如果是制作游戏的话,还需要有音效(OpenAL),等等
另外,即使是 OpenGL 本身,也还有许许多多的特性在上文没有提及(比如OpenGL 4的特性)。需要学习的地方还有很多