opengl
可编程渲染管线
先来说说什么是渲染管线。管线的英文是pipeline,其实正确的翻译应该是流水线,或者一系列流程。那么渲染管线就可以看作我可以输入一堆原始图形数据,经过这个流水线,能得到每个像素颜色的输出。
渲染管线大致分为两个部分:一是将3D信息转换为2D信息,二是将2D信息转换为一系列有颜色的像素。具体流程如下图:

所有的这些阶段在不同的图形API(例如本文涉及的OpenGL)上都是高度专门化的,并且它们十分容易并行执行,因此可以将各个阶段的程序搬到GPU上去运行,由这些程序操纵被绑到GPU显存上的顶点数据,完成渲染工作。
其中有几个阶段是用户可编程的(上图蓝色部分),例如最常用到的vertex shader(将3D坐标转换为另一种3D坐标)和fragment shader(计算某个像素的颜色)。我们可以向OpenGL注入我们自己写的shader,所使用的语言就是GLSL(Games202需用)。
crash course
NDC(Normalized Device Coordinates)
这个在Games101提到MVP变换中有提及,它的中文叫做标准化设备坐标,就是x,y,z都是在[-1,1]的空间,对应的就是MVP中的Projection。
前面提到,Vertex shader的作用是将3D坐标装换成另外的3D坐标,因此可以推测出,MVP变换通常是在Vertex shader中进行的。
VBO(Vertex Buffer Objects)
顶点缓冲对象,听上去是拿来存储顶点信息的,事实也正是如此,只不过后面加了个对象的概念。
对于OpenGL中的对象,它们都对应着一个ID。对于VBO,可以使用
glGenBuffers
来生成并获得对应ID:
1 | unsigned int VBO; |
关于OpenGL的API详细文档,推荐查阅 docs.gl
既然VBO内部其实是Buffer,那么就会有很多其它的Buffer Object,因此需要分个类,俗称缓冲类型。VBO的缓冲类型是GL_ARRAY_BUFFER(没毛病,其实就是数组)。
OpenGL实质上是一个状态机,因此在渲染流程中,要渲染的VBO应该属于当前的OpenGL上下文,即被绑定。要绑定某个VBO,可以:
1 | glBindBuffer(GL_ARRAY_BUFFER, VBO); |
绑定完成后,我们对于任何VBO(GL_ARRAY_BUFFER)的修改,都会影响到这个被绑定的VBO。
说了半天,我们VBO内部是要有顶点的,那么数据在哪呢?我们可以自己定义一个float数组:
1 | float vertices[] = { |
这里有三个顶点,顶点属性尚且只有坐标(其实可以加上颜色、法向等)。可以观察到所有坐标都是在NDC范围内的。
好了,现在我们有了直接在cpp里hard code的顶点属性,我们需要和当前绑定的VBO(并不是指定VBO哦,记住opengl的逻辑是一个状态机):
1 | glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); |
这里的最后一个参数涉及到API实现方的优化,具体见 docs.gl
Vertex shader
这里的Shader language是GLSL,对于当前的Vertex shader,我们并不需要给它进行什么复杂的变换(例如MVP变换),因此直接返回对应的齐次坐标即可:
1 |
|
第一行是版本说明,也明确表示我们使用核心模式。第二行指定了输入数据。main
函数内则指定了输出位置 gl_Position
。
接下来我们要在openGL创建Vertex shader(注意没有绑定,后面会提到,绑定的是整个program):
1 | unsigned int vertexShader; |
Fragment shader
和Vertex shader相似:
1 |
|
Fragment shader只需要一个输出变量RGBA,我们可以在开头声明。
链接着色器程序
创建一个program对象,然后将shader链接进去即可,最后如果不需要的话可以删掉前面创建的shader,我们只需要program:
1
2
3
4
5
6
7
8
9
10unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
解释顶点属性分布
我们前面用 glBufferData
绑定了VBO,现在需要解释顶点属性分布,这样shader才知道输入数据。
1 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); |
这里第一个参数对应着Vertex shader的输入变量
layout (location = 0) in vec3 aPos;
,第二个参数是顶点属性的大小,第三个参数是类型,第四个参数表示是否希望数据被标准化,第五个参数是步长,即两个顶点属性之间的间隔,第六个参数是偏移量。
glEnableVertexAttribArray
表示启用顶点属性,这里0表示第一个顶点属性,即
layout (location = 0) in vec3 aPos;
。
至此其实已经具备了渲染一个简单三角形的能力,代码会长的像这样:
1 | // 0. 复制顶点数组到缓冲中供OpenGL使用 |
VAO (Vertex Array Object)
VAO会记录以下顶点属性调用: 1、glEnableVertexAttribArray和glDisableVertexAttribArray的调用 2、通过glVertexAttribPointer设置的顶点属性配置 3、通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象VBO
创建一个VAO: 1
2unsigned int VAO;
glGenVertexArrays(1, &VAO);
因此,在绘制一个物体的时候,我们可以首先绑定一个VAO,然后绑定配置VBO及其属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();