renderPipeline - 一个三维场景的渲染过程
1 渲染流水线概览
1.1 几何阶段
- 1 模型和视图变换阶段:模型变换的目的是将模型变换到适合渲染的空间当中,而视图变换的目的是将摄像机放置于坐标原点,方便后续步骤的操作。
- 2 顶点着色阶段(vertex shading过程):顶点着色的目的在于确定模型上顶点处材质的光照效果。
- 3 投影阶段:投影阶段是将模型从三维空间投射到二维的空间中的过程。投影阶段也可以理解为将视体变换到一个对角顶点分别是(-1,-1,-1)和(1,1,1)单位立方体内的过程。
- 4 几何着色器:几何着色器可以在顶点发送到下一着色器阶段之前对它们随意变换。几何着色器输出的形式只能是点,折线和三角形条。
- 5 裁剪阶段:裁剪阶段的目的,是对部分位于视体内部的图元进行裁剪操作。
- 6 屏幕映射阶段:屏幕映射阶段的主要目的,是将之前步骤得到的坐标映射到对应的屏幕坐标系上。
1.1.1 结合具体代码描述vertex shading过程
- 1 顶点着色器:是完全可编程的阶段,是专门处理传入的顶点信息的着色器,顶点着色器可以对每个顶点 进行诸如变换和变形在内的很多操作。顶点着色器一般不处理附加信息,也就是说,顶点着色器提供 了修改,创建,或者忽略与每个多边形顶点相关的值的方式,例如其颜色,法线,纹理坐标和位置。 通常,顶点着色器程序将顶点从模型空间(Model Space)变换到齐次裁剪空间(Homogeneous Clip Space),并且,一个顶点着色器至少且必须输出此变换位置(以便于像素着色阶段使用)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// 顶点shading的具体过程
// -------------------------------- 控制 mvp --------------------------------------------
// pass projection matrix to shader (as projection matrix rarely changes there's no need to do this per frame)
// -----------------------------------------------------------------------------------------------------------
glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
ourShader.setMat4("projection", projection);
...
// camera/view transformation
glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
ourShader.setMat4("view", view);
...
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
ourShader.setMat4("model", model);
// -------------------------------- vertex shader 代码 --------------------------------
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
TexCoords = aTexCoords;
gl_Position = projection * view * vec4(FragPos, 1.0); // 转换以后给opengl绘制顶点的坐标
}
1.1.2 结合具体代码说明geometry shader的代码
1 |
|
1.2 光栅阶段
- 1 三角形设定阶段(不可配置):三角形设定阶段主要用来计算三角形表面的差异和三角形表面的其他相关数据。
- 2 三角形遍历阶段(不可配置):找到哪些采样点或像素在三角形中的过程通常叫三角形遍历。
- 3 像素着色阶段(fragment shading过程):像素着色阶段的主要目的是计算所有需逐像素计算操作的过程。
- 4 融合阶段:融合阶段的主要任务是合成当前储存于缓冲器中的由之前的像素着色阶段产生的片段颜色。此外,融合阶段还负责可见性问题(Z 缓冲相关)的处理。
1.2.1 像素着色
fragment shading和融合阶段过程,有较多的步骤:
- 1 像素着色:用来处理场景光照和与之相关的效果,如凸凹纹理映射和调色。名称片断着色器似乎更为准确,因为对于着色器的调用和屏幕上像素的显示并非一一对应。举个例子,对于一个像素,片断着色器可能会被调用若干次来决定它最终的颜色,那些被遮挡的物体也会被计算,直到最后的深度缓冲才将各物体前后排序。在最终合并阶段设置片段颜色以进行合并,而深度值也可以由像素着色器修改。模板缓冲(stencil buffer)值是不可修改的,而是将其传递到合并阶段(Merge Stage)。
1.2.3 融合阶段
- 1 融合阶段:是将像素着色器中生成的各个片段的深度和颜色与帧缓冲结合在一起的地方。这个阶段也就是进行模板缓冲(Stencil-Buffer)和 Z 缓冲(Z-buffer)操作的地方。最常用于透明处理(Transparency)和合成操作(Compositing)的颜色混合(Color Blending)操作也是在这个阶段进行的。一下
- 1.1 模板缓冲:大概就是GLFW给每个窗口库都配置一个模板缓冲,默认情况下,启用模板缓冲写入,就可以把物体对应的顶点的哪些像素位置的模板缓冲值写为1,然后模板缓冲为1的位置,场景对应的片段才会被渲染,通过模板缓冲才会进入深度缓冲阶段,很容易想到的就是为什么是这样的顺序?显然深度模板每一个像素的位置上很可能有多个buffer信息需要处理,计算量远大于模板缓冲,于是自然先进行模板缓冲
- 1.1.1 比如具体的:轮廓算法,就可以通过模板缓冲来实现:简单解释:
- 当你有一个物体,你渲染前,先开启模板缓冲,把这个物体对应的模板缓冲都写成1,然后关闭模板缓冲,避免用于轮廓的物体也去写模板缓冲了
- 然后你把这个物体稍微放大,绘制之前, glStencilFunc(GL_NOTEQUAL, 1, 0xFF);也就是不为1的地方,glStencilFunc描述了OpenGL应该对模板缓冲内容做什么,也就是不为1的地方,会通过模板测试,之后用一个简单的带有颜色的片段着色器着色画出这个物体即可形成了轮廓
在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
渲染物体。
禁用模板写入以及深度测试。
将每个物体缩放一点点。
使用一个不同的片段着色器,输出一个单独的(边框)颜色。
再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
再次启用模板写入和深度测试。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 轮廓算法代码
glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glStencilMask(0x00); // 记得保证我们在绘制地板的时候不会更新模板缓冲
normalShader.use();
DrawFloor()
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
DrawTwoContainers();
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use();
DrawTwoScaledUpContainers();
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);
- 1.1.1 比如具体的:轮廓算法,就可以通过模板缓冲来实现:简单解释:
- 1.2 深度缓冲:对于每一个像素,有多个顶点信息,通过计算顶点和当前camera距离的位置,我们决定谁去渲染,通常有这几种策略:默认情况下使用的深度函数是GL_LESS,它将会丢弃深度值大于等于当前深度缓冲值的所有片段。
GL_ALWAYS 永远通过深度测试
GL_NEVER 永远不通过深度测试
GL_LESS 在片段深度值小于缓冲的深度值时通过测试
GL_EQUAL 在片段深度值等于缓冲区的深度值时通过测试
GL_LEQUAL 在片段深度值小于等于缓冲区的深度值时通过测试
GL_GREATER 在片段深度值大于缓冲区的深度值时通过测试
GL_NOTEQUAL 在片段深度值不等于缓冲区的深度值时通过测试
GL_GEQUAL 在片段深度值大于等于缓冲区的深度值时通过测试
- 1.1 模板缓冲:大概就是GLFW给每个窗口库都配置一个模板缓冲,默认情况下,启用模板缓冲写入,就可以把物体对应的顶点的哪些像素位置的模板缓冲值写为1,然后模板缓冲为1的位置,场景对应的片段才会被渲染,通过模板缓冲才会进入深度缓冲阶段,很容易想到的就是为什么是这样的顺序?显然深度模板每一个像素的位置上很可能有多个buffer信息需要处理,计算量远大于模板缓冲,于是自然先进行模板缓冲
结合具体代码描述(pixel)fragment shading过程和融合阶段
1 | // --------------------------------- fragment(pixel) shader 代码 ---------------------------- |