深度测试 深度缓冲Depth Buffer 由窗口系统自动创建,和颜色缓冲有着一样的宽度和高度。深度值以16、24或32位float的形式储存(大部分都是24位的),范围[0.0~1.0]
执行过程 深度测试是在片段着色器运行之后(以及模板测试(Stencil Testing)运行之后)在屏幕空间中运行。在片段着色器中gl_FragCoord
的x和y保存了片段的屏幕空间坐标,z是深度值
深度测试就是将片段的深度值与深度缓冲的内容进行对比(执行一个测试函数),如果测试通过,深度缓冲将会更新为新的深度值,否则片段被丢弃
深度测试函数 可以使用glDepthFunc
设置测试函数,默认是GL_LESS
,深度值更小时通过测试。还可以设置其它的比较运算符,甚至还有GL_ALWAYS
和GL_NEVER
精度 经过模型变换和观测变换得到摄像机视角下的观测坐标\(pos_{view}\) ,然后经过透视投影 、透视除法、视口变换得到屏幕空间坐标\(pos_{screen}\) ,坐标的z就是深度值 \[ F_{depth} = \frac{1/z-1/near}{1/far-1/near} \] 因为是反比例函数,所以当z比较大时,深度变化幅度小;当z比较小时,深度变换比较快
如果采用的是正交投影 ,则最后得到的深度值与z之间是线性关系
防止深度冲突 因为精度问题,当两个面非常接近时,深度缓冲无法决定哪个在前面。结果就是在两者之间不断切换前后顺序,从而导致很奇怪的花纹。因为采用了非线性的变换,在远处的深度精度比较差,更容易发生深度冲突。
解决办法
不要把物体放的太近 把摄像机的近平面near设置得远一些,从而在稍微远点的距离也能获得高精度 使用更高精度的深度缓冲,把24位换成32位 模板测试 模板缓冲Stencil Buffer 模板缓冲也是由 GLFW自动配置。模板值一般是8位的,一共可以取256种数值
执行过程 在片段着色器处理完一个片段之后开始执行,执行模板测试函数并根据设置的操作更新缓冲。通过模板测试的片段会进入之后的深度测试
模板函数 模板测试也可以像深度测试一样通过glStencilFunc
指定测试函数,不同的是可以在参数里提供一个参考值ref
,而且还可以通过glStencilOp
设置测试成功和失败时如何更新模板值,比深度测试更灵活。
实现物体描边 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 ); glStencilFunc(GL_ALWAYS, 1 , 0xFF ); glStencilMask(0xFF ); glStencilFunc(GL_NOTEQUAL, 1 , 0xFF ); glStencilMask(0x00 ); glStencilMask(0xFF ); glStencilFunc(GL_ALWAYS, 1 , 0xFF );
实现透视 在绘制放大一点的物体之前关闭深度测试,绘制之后开启,也就是上面代码注释掉的部分
混合 添加植被 植被的贴图中透明度要么是0(完全透明)要么是1(完全不透明)。要添加植被到场景中,只需要在片段着色器里将alpha小于阈值的片段丢弃
1 2 3 4 vec4 texColor = texture (texture1, TexCoords);if (texColor.a < 0.1 ) discard ; FragColor = texColor;
对于有透明度的贴图,如果设置为GL_REPEAT
,边缘部分会与另一头进行混合,如果边缘是透明的,而另一头不透明,结果就会产生一个边框。所以需要设置为GL_CLAMP_TOEDGE
混合方程 窗户的玻璃是半透明的,需要开启混合,按照混合方程对窗户玻璃的颜色\(Color\_{source}\) 和后面物体的颜色\(Color\_{destination}\) 进行加权求和 \[ Color_{result} = Color_{source}\times Frac_{source} + Color_{destination}\times Frac_{destination} \] 具体的混合系数可以通过glBlendFunc
设置
通常用的glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
就是指\(Frac_{source} = Alpha_{source}\) ,\(Frac_{destination} = 1-Alpha_{source}\) ,按照要写入的颜色的alpha值进行混合,如果是alpha=0完全透明,就直接使用原来的颜色;如果是alpha=1完全不透明,就直接使用新的颜色。
glBlendFunc(GL_ONE, GL_ZERO)
则表示直接覆盖原有颜色
绘制顺序 应该按照“不透明物体-比较远的透明物体-比较近的透明物体”的顺序绘制。
按照混合方程,混合用的alpha是透明物体的,需要不透明物体先被绘制出来才能混合,否则后绘制不透明物体会直接覆盖透明物体的颜色,不管哪个在前哪个在后。
如果同样是透明物体,如果先绘制了近距离的,那么在绘制远距离的时候会因为深度测试失败而导致后面的根本绘制不出来,也就不会混合。
所以透明物体需要按照与摄像机的距离进行排序,为了方便动态添加删除,采用红黑树map
,以距离为键,物体索引为值。
如果存在旋转、形变等变换的话,还是会露馅。
面剔除 在顶点着色器运行之后
默认逆时针顺序(从观察者的角度来看)的顶点组成的面是正向的,默认剔除背面。可以用glCullFace
设置要剔除的面,glFrontFace(GL_CW)
设置顺时针为正面(默认是GL_CCW
)
对于非封闭的物体,不能开启面剔除,比如双面的植被,开启之后就只能看到一面
帧缓冲 包括颜色缓冲、深度缓冲和模板缓冲。对应有帧缓冲对象,可以添加纹理附件(data参数传递空指针只分配内存而没有数据)作为颜色缓冲,将渲染结果写入到纹理中;可以添加渲染缓冲对象附件(只写),适合保存深度值和模板值(共32位)
渲染到纹理上 先创建帧缓冲对象,生成并附加纹理,在渲染循环中将屏幕渲染到纹理上,然后恢复默认的缓冲,把纹理绘制到屏幕上(使用的一个屏幕四边形顶点数据,shader里不需要进行坐标变换,直接采样纹理并输出),看起来和之前效果一样,但是开启线框模式会发现屏幕上只绘制了一个矩形。
OpenGL代码
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 unsigned int fbo;glGenFramebuffers(1 , &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); unsigned int texColorBuffer;glGenTextures(1 , &texColorBuffer); glBindTexture(GL_TEXTURE_2D, texColorBuffer); glTexImage2D(GL_TEXTURE_2D, 0 , GL_RGB, SCREEN_WIDTH, SCREEN_HEIGHT, 0 , GL_RGB, GL_UNSIGNED_BYTE, nullptr ); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0 ); unsigned int rbo;glGenRenderbuffers(1 , &rbo); glBindRenderbuffer(GL_RENDERBUFFER, rbo); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, SCREEN_WIDTH, SCREEN_HEIGHT); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE){ std ::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std ::endl ; } glBindFramebuffer(GL_FRAMEBUFFER, 0 ); constexpr float vertices[] = { -1.0f , -1.0f , 0.0f , 0.0f , 1.0f , -1.0f , 1.0f , 0.0f , 1.0f , 1.0f , 1.0f , 1.0f , 1.0f , 1.0f , 1.0f , 1.0f , -1.0f , 1.0f , 0.0f , 1.0f , -1.0f , -1.0f , 0.0f , 0.0f , }; unsigned int screenVAO, screenVBO; glGenVertexArrays(1 , &screenVAO); glGenBuffers(1 , &screenVBO); glBindVertexArray(screenVAO); glBindBuffer(GL_ARRAY_BUFFER, screenVBO); glBufferData(GL_ARRAY_BUFFER, sizeof (vertices), vertices, GL_STATIC_DRAW); glEnableVertexAttribArray(0 ); glVertexAttribPointer(0 , 2 , GL_FLOAT, GL_FALSE, 4 * sizeof (float ), nullptr ); glEnableVertexAttribArray(1 ); glVertexAttribPointer(1 , 2 , GL_FLOAT, GL_FALSE, 4 * sizeof (float ), (void *)(2 * sizeof (float ))); Shader screenShader ("res/shaders/framebuffer.vert" , "res/shaders/framebuffer.frag" ) ;screenShader.Bind(); screenShader.SetUniform1i("screenTexture" , 0 ); { glBindFramebuffer(GL_FRAMEBUFFER, fbo); glClearColor(0 , 0 , 0 , 1 ); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); glEnable(GL_DEPTH_TEST); glBindFramebuffer(GL_FRAMEBUFFER, 0 ); glClearColor(1.0f , 1.0f , 1.0f , 1.0f ); glClear(GL_COLOR_BUFFER_BIT); screenShader.Bind(); glBindVertexArray(screenVAO); glBindTexture(GL_TEXTURE_2D, texColorBuffer); glDisable(GL_DEPTH_TEST); glDrawArrays(GL_TRIANGLES, 0 , 6 ); }
framebuffer.vert
1 2 3 4 5 6 7 8 9 10 #version 330 layout (location = 0 ) in vec2 aPos;layout (location = 1 ) in vec2 aTexCoords;out vec2 TexCoords;void main(){ gl_Position = vec4 (aPos.x, aPos.y, 0 , 1 ); TexCoords = aTexCoords; }
framebuffer.frag
1 2 3 4 5 6 7 8 9 10 11 #version 330 in vec2 TexCoords;uniform sampler2D screenTexture; out vec4 FragColor;void main(){ FragColor = texture (screenTexture, TexCoords); }
后期处理 将屏幕绘制到纹理上之后,就可以动用各种图像处理技术来处理这个纹理,然后再绘制到屏幕上,从而实现后处理效果。
片段着色器
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #version 330 in vec2 TexCoords;uniform sampler2D screenTexture; out vec4 FragColor;void main(){ const float offset = 1.0 / 300 ; vec2 offsets[] = { vec2 (-offset , offset ), vec2 (0 , offset ), vec2 (offset , offset ), vec2 (-offset , 0 ), vec2 (0 , 0 ), vec2 (offset , 0 ), vec2 (-offset , -offset ), vec2 (0 , -offset ), vec2 (offset , -offset ) }; float kernel[] = { 1 , 1 , 1 , 1 , -9 , 1 , 1 , 1 , 1 }; vec3 sampleTex[9 ]; for (int i = 0 ; i < 9 ; i++){ sampleTex[i] = vec3 (texture (screenTexture, TexCoords.st + offsets[i])); } vec3 col = vec3 (0 ); for (int i = 0 ; i < 9 ; i++){ col += sampleTex[i] * kernel[i]; } FragColor = vec4 (col, 1 ); }
MRT 多渲染目标,给一个帧缓冲对象同时绑定多个颜色附件,在glFramebufferTexture2D
的第二个参数指定对应的附加槽位,如GL_COLOR_ATTACHMENT1
。然后用glDrawBuffers
指定这些槽位的数组顺序,之后在片段着色器中输出对应的值到对应槽位
1 layout (location = 1 ) out vec4 outColor;
平时输出的FragColor
就是默认帧缓冲的0号槽位的颜色附件。
这里需要注意的是,像FragColor
一样,声明的输出变量都要是vec4
(不管颜色附件的实际格式如何),否则之后在用texture
采样出来的结果会不对,比如声明为vec3
,那么之后的第四个分量会默认为0!,如果之后是采样出来作为颜色,那就看不到啦!
立方体贴图 包含了立方体的六个面,只需要从立方体的中心取一个方向向量即可进行采样。
天空盒 先定义一个立方体,在shader中定义一个samplerCube
,采样使用的uv坐标是这个立方体上的顶点坐标(由顶点着色器直接传递过来)
为了天空盒永远在物体后面,也就是让天空盒深度值为1,将z设为w,从而执行透视除法之后得到的深度z/w永远为1,并设置GL_LEQUAL
通过测试
为了防止天空盒移动,需要去掉观察矩阵的位移量
1 glm::mat4 skyboxViewMat = glm::mat4(glm::mat3(viewMat));
注意天空盒仍需要在透明物体之前绘制
顶点着色器
1 2 3 4 5 6 7 8 9 10 11 #version 330 layout (location = 0 ) in vec3 aPos;uniform mat4 projMat;uniform mat4 viewMat;out vec3 TexCoords;void main(){ TexCoords = aPos; gl_Position = (projMat * viewMat * vec4 (aPos, 1 )).xyww; }
片段着色器
1 2 3 4 5 6 7 8 9 10 11 #version 330 core in vec3 TexCoords;uniform samplerCube skybox;out vec4 FragColor;void main(){ FragColor = texture (skybox, TexCoords); }
反射天空盒 在片段着色器里,用观察方向和法向(逆转置计算过的)求出反射方向,并用来对天空盒采样,作为输出的颜色
顶点着色器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #version 330 core layout (location = 0 ) in vec3 aPos;layout (location = 1 ) in vec3 aNormal;uniform mat4 modelMat;uniform mat4 viewMat;uniform mat4 projMat;out vec3 Normal;out vec3 Position;void main(){ Normal = mat3 (transpose (inverse (modelMat))) * aNormal; Position = vec3 (modelMat * vec4 (aPos, 1 )); gl_Position = projMat * viewMat * modelMat * vec4 (aPos, 1 ); }
片段着色器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #version 330 core in vec3 Normal;in vec3 Position;uniform vec3 cameraPos;uniform samplerCube skybox;out vec4 FragColor;void main(){ vec3 I = normalize (Position - cameraPos); vec3 R = reflect (I, normalize (Normal)); FragColor = vec4 (texture (skybox, R).rgb, 1 ); }
折射天空盒 用观察方向和法向求出折射方向(需要折射率之比,比如玻璃材质需要空气1.0/玻璃1.52),然后对天空盒采样
片段着色器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #version 330 core in vec3 Normal;in vec3 Position;uniform vec3 cameraPos;uniform samplerCube skybox;out vec4 FragColor;void main(){ float ratio = 1.0 / 1.52 ; vec3 I = normalize (Position - cameraPos); vec3 R = refract (I, normalize (Normal), ratio); FragColor = vec4 (texture (skybox, R).rgb, 1 ); }
高级数据 glBufferData
填充缓冲的内存,如果数据参数传空指针则只分配内存
glBufferSubData
只填充部分缓冲区
glMapBuffer
直接获取缓冲区指针,用指针赋值数据,之后glUnMapBuffer
令指针无效
glVertexAttribPointer
结合glBufferSubData
可以实现分批顶点属性(先先存放所有顶点的位置,然后是所有顶点的uv等),处理起来简单、看起来更整洁
glCopyBufferSubData
复制缓冲
高级GLSL 着色器的所有内建变量
内建变量在使用时并不需要声明,下面的gl_Position: out vec4
表示:gl_Position
这个内建变量是浮点型输出变量,输出变量需要赋值,输入变量是只读的
顶点着色器变量 gl_Position: out vec4
:设置剪裁空间位置
gl_PointSize: out float
:设置图元GL_POINTS
的大小,需要glEnable(GL_PROGRAM_POINT_SIZE)
开启。也可以用glPointSize
函数来设置。可以用于实现粒子效果
gl_VertexID: in int
:储存了正在绘制顶点的当前ID。如果绘制调用的是glDrawElements
,则ID是当前顶点索引;如果是glDrawArrays
,则ID是已处理顶点数量
片段着色器变量 gl_FragCoord: in vec3
:x和y分量是片段的屏幕空间坐标,z分量等于对应片段的深度值(屏幕空间)。可以对屏幕的不同部分进行不同处理gl_FrontFacing: in bool
:当前片段是否为正向面的一部分。可以用于对模型内外表面进行不同处理(要关闭剔除)gl_FragCoord: out float
:设置片段的深度值[0.0, 1.0]。没有设置的话会等于gl_FragCoord.z
,设置的话OpenGL会禁用提前深度测试,除非采用OpenGL 4.2以上才支持的深度条件做重新声明,比如layout (depth_greater) out float gl_FragDepth;
声明只会把深度增大接口块 类似于struct
,把几个变量打包进行输入输出,但是这里省掉了struct
关键字,而且只需要类型相同即可传递数据,而不需要实例名相同
1 2 3 4 5 6 7 8 9 10 out VS_OUT{ vec2 TexCoord; }vs_out; in VS_OUT { vec2 TexCoords; } fs_in;
定义相同的Uniform块的着色器可以共享数据
1 2 3 4 5 6 layout (std140 ) uniform Matrices{ mat4 projection; mat4 view; };
像C++的结构体有内存对齐一样,Uniform也有内存布局。Uniform块保存在缓冲对象上,默认采用共享布局来节省空间(程序中保持一致,但不一定是变量定义的顺序)
而上面采用的是std140
布局,对应有一个偏移量计算规则,在不同的shader里的布局相同,从而可以统一设置数值,这也就是接下来要用的Uniform缓冲
使用Uniform块之后可以在不同shader中共享数据,而不用每次都要单独设置,尤其是经常用到的viewMat
和projMat
。
首先把顶点着色器中的
1 2 uniform mat4 viewMat;uniform mat4 projMat;
改成
1 2 3 4 layout (std140 ) uniform Matrices{ mat4 viewMat; mat4 projMat; };
在C++程序里创建Uniform缓冲对象并和Uniform块链接到相同的绑定点
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 unsigned int uniformBlockIndexShader = glGetUniformBlockIndex(shader.GetID(), "Matrices" );unsigned int uniformBlockIndexBorderShader = glGetUniformBlockIndex(borderShader.GetID(), "Matrices" );unsigned int uniformBlockIndexGrassShader = glGetUniformBlockIndex(grassShader.GetID(), "Matrices" );unsigned int uniformBlockIndexSkyboxReflectionShader = glGetUniformBlockIndex(skyboxReflectionShader.GetID(), "Matrices" );unsigned int uniformBlockIndexSkyboxRefractionShader = glGetUniformBlockIndex(skyboxRefractionShader.GetID(), "Matrices" );glUniformBlockBinding(shader.GetID(), uniformBlockIndexShader, 0 ); glUniformBlockBinding(borderShader.GetID(), uniformBlockIndexBorderShader, 0 ); glUniformBlockBinding(grassShader.GetID(), uniformBlockIndexGrassShader, 0 ); glUniformBlockBinding(skyboxReflectionShader.GetID(), uniformBlockIndexSkyboxReflectionShader, 0 ); glUniformBlockBinding(skyboxRefractionShader.GetID(), uniformBlockIndexSkyboxRefractionShader, 0 ); unsigned int uboMatrices;glGenBuffers(1 , &uboMatrices); glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices); glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof (glm::mat4), nullptr , GL_STATIC_DRAW); glBindBuffer(GL_UNIFORM_BUFFER, 0 ); glBindBufferRange(GL_UNIFORM_BUFFER, 0 , uboMatrices, 0 , 2 * sizeof (glm::mat4));
上面把天空盒的shader注释掉了,因为天空盒的观察矩阵需要去掉位移部分,不应该跟其它shader共享。
然后在渲染循环中去掉原先单独为每个shader设置两个矩阵的操作,改为
1 2 3 glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices); glBufferSubData(GL_UNIFORM_BUFFER, 0 , sizeof (glm::mat4), glm::value_ptr(viewMat)); glBufferSubData(GL_UNIFORM_BUFFER, sizeof (glm::mat4), sizeof (glm::mat4), glm::value_ptr(projMat));
这样每个shader就都能拿到这两个矩阵的数据了
几何着色器 顶点和片段着色器之间运行。输入一个图元(如点或三角形)的所有顶点,可以随意变换,然后输出不同图元或更多的点
跟另外两种着色器一样需要创建(类型为GL_GEOMETRY_SHADER
)、编译、附加、链接,可以在着色器之间传递数据
示例 以每个输入点为中心创建一条水平线
1 2 3 4 5 6 7 8 9 10 11 12 13 #version 330 core layout (points ) in ; layout (line_strip , max_vertices = 2 ) out ; void main() { gl_Position = gl_in [0 ].gl_Position + vec4 (-0.1 , 0.0 , 0.0 , 0.0 ); EmitVertex (); gl_Position = gl_in [0 ].gl_Position + vec4 ( 0.1 , 0.0 , 0.0 , 0.0 ); EmitVertex (); EndPrimitive (); }
内建变量gl_in
是一个数组,是一个图元的所有顶点
可能的定义形式
1 2 3 4 5 6 in gl_Vertex { vec4 gl_Position ; float gl_PointSize ; float gl_ClipDistance []; } gl_in [];
如果需要其它顶点数据,可以使用接口块从顶点着色器传过来,但是要声明为数组(加个[]
),如果是传递给片段着色器的变量,比如顶点颜色,在调用EmitVertex
之前,这个变量最后的值就是这个顶点的颜色,从而可以为每个点指定颜色。
实现爆破 在几何着色器中,由输入的三角形的三个顶点计算出法向量(同平面内的两个向量叉积),然后把三个点都沿着法向量位移sin(time)+1
,从而可以把模型上的每个三角形都分离出来。
因为这个操作实际上是作用在模型本体的,应该算作是模型变换,所以在顶点着色器里不使用MVP矩阵,而是直接把顶点数据传递给几何着色器,在几何着色器中计算完位移的顶点坐标后再应用MVP矩阵计算出gl_Position
、用M矩阵计算出FragPos
传递给片段着色器。
法向量可视化 先正常绘制,然后再由几何着色器绘制一遍。在几何着色器里对三角形的每个顶点都按照顶点法向位移出另一个点,同样因为位移操作视为模型变换,所以MVP放到几何着色器里做
顶点着色器直接传递法向量
1 2 gl_Position = vec4 (aPos, 1.0 ); vs_out.normal = aNormal;
几何着色器在每个点绘制法向线段(位移出一个点)
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const float MAGNITUDE = 0.4 ;void GenerateLine(mat3 normalMatrix,int index ){ gl_Position = projMat * viewMat * modelMat * gl_in [index ].gl_Position ; EmitVertex (); vec4 ptOnNormal = viewMat * modelMat * gl_in [index ].gl_Position + vec4 ( normalize (normalMatrix * gs_in[index ].normal) * MAGNITUDE, 0 ); gl_Position = projMat * ptOnNormal; EmitVertex (); EndPrimitive (); } void main(){ mat3 nMat = mat3 (transpose (inverse (viewMat * modelMat))); GenerateLine(nMat,0 ); GenerateLine(nMat,1 ); GenerateLine(nMat,2 ); }
实例化 绘制调用的开销 glDrawArrays
或glDrawElements
函数告诉GPU去绘制你的顶点数据会消耗更多的性能,因为OpenGL在绘制顶点数据之前需要做很多准备工作(比如告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU to GPU Bus)上进行的)
实例化绘制 改用glDrawArraysInstanced
和glDrawElementsInstanced
,告诉GPU要绘制的物体实例数量,每个实例有对应的gl_InstanceID
,从0开始。于是可以向顶点着色器传递数组,然后使用gl_InstanceID
进行索引。
但是Uniform数据大小有限制,需要改用实例化数组,其实就是作为顶点属性绑定上去,所以基本和普通的顶点属性一样需要创建缓冲对象并绑定属性指针。不同的是最后需要调用glVertexAttribDivisor(属性位置号,属性除数)
。默认情况属性除数是0,在顶点着色器的每次迭代时更新顶点属性;设置为1时,在渲染一个新实例的时候更新顶点属性,也就是说对应位置的属性是一个实例化数组。在着色器中还是可以使用gl_InstanceID
取得实例索引。
小行星带 在顶点着色器中把小行星的modelMat
定义为顶点属性,比如layout(location=3) in mat4 instanceMatrix
,但是顶点属性最大为vec4
,所以在外部需要把mat4
分成四列分别绑定到位置3、4、5、6(都要设置glVertexAttribDivisor
)。因为内存连续分布,矩阵本来也是按列存放,所以并不影响着色器里使用矩阵。
实例化渲染适用于渲染草、植被、粒子等大量重复的形状。
抗锯齿 光栅器 运行在顶点着色器之后、片段着色器之前,输入一个图元的所有顶点,输出一系列片段。片段坐标就是屏幕像素点坐标,需要用像素点中心作为采样点来判断与三角形的内外关系。之后得到像素点组成的三角形,由于像素点本身的大小,锯齿很明显。
多重采样 原本在像素里只取中心的一个采样点改为使用多个采样点,用这些子采样点来决定像素的遮盖度(会增大颜色缓冲)。之后如果直接对几个采样点分别运行片段着色器,最后给像素点取平均值,那就会多次运行片段着色器。但是实际上一个像素点只运行一次片段着色器,每个采样点都有颜色、深度和模板值。
glfwWindowHint(GLFW_SAMPLES, 4)
提示GLFW使用多重采样缓冲,glEnable(GL_MULTISAMPLE)
启用多重采样(默认是启用的)。
如果要做离屏渲染,跟之前在帧缓冲中做的差不多,可以手动创建多重采样缓冲对象、附加多重采样纹理并渲染到多重采样缓冲。不一样的是一个多重采样的图像包含比普通图像更多的信息,需要缩小或者还原(Resolve)图像。如果要做后期处理,需要将多重采样缓冲位块传送到一个没有使用多重采样纹理附件的FBO中。然后用这个普通的颜色附件来做后期处理。