本文参考:https://learnopengl.com/。
长文预警 (〃'▽'〃) (。・ω・。) φ(>ω<*)
Advance
Depth testing
深度缓冲会以 16、24 或 32 位 float 的形式储存每个片段中的深度值,通常和颜色缓冲有着一样的宽度和高度。
大部分的系统中,深度缓冲的精度都是 24 位的。
当深度测试(Depth Testing)被启用,OpenGL会将一个片段的深度值与深度缓冲的内容进行对比,执深度测试。如果测试通过,深度缓冲将会更新为新的深度值;如果测试失败,片段会被丢弃。
深度缓冲是在片段着色器运行之后,在屏幕空间中运行的。屏幕空间坐标与 glViewport
定义的视口密切相关,可以使用 gl_FragCoord 从片段着色器中直接访问,x 和 y 分量代表了片段的屏幕空间坐标,(0, 0)位于左下角。
gl_FragCoord 的 z 分量,是片段真正的深度值,也是与深度缓冲内容所对比的那个值。
大部分 GPU 都提供提前深度测试(Early Depth Testing)的硬件特性,允许深度测试在片段着色器之前运行。
只要清楚一个片段永远不会是可见的(在其他物体之后),就能提前丢弃这个片段。
片段着色器开销很大,应该尽可能避免运行。当使用提前深度测试时,片段着色器的限制是不能写入片段的深度值。如果片段着色器对它的深度值进行了写入,就不能提前深度测试,OpenGL 不能提前知道深度值。
深度测试默认禁用,用 GL_DEPTH_TEST 选项来启用:
glEnable(GL_DEPTH_TEST);
当启用深度测试后,如果一个片段通过了深度测试,OpenGL 会在深度缓冲中储存该片段的 z 值;如果没有通过,会丢弃该片段。如果启用了深度缓冲,应该在每个渲染迭代之前使用 GL_DEPTH_BUFFER_BIT 来清除深度缓冲,否则会使用上一次渲染迭代中的写入的深度值。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
某些情况下需要对所有片段都执行深度测试并丢弃相应的片段,但不希望更新深度缓冲,禁用深度缓冲的写入。
glDepthMask(GL_FALSE); // 深度测试被启用才有效。
OpenGL 允许修改深度测试使用的比较运算符,来控制什么时候该通过或丢弃一个片段,什么时候更新深度缓冲。
glDepthFunc(GL_LESS); // 默认使用
- GL_ALWAYS:永远通过深度测试
- GL_NEVER:永远不通过深度测试
- GL_LESS:在片段深度值小于缓冲的深度值时通过测试
- GL_EQUAL:在片段深度值等于缓冲区的深度值时通过测试
- GL_LEQUAL: 在片段深度值小于等于缓冲区的深度值时通过测试
- GL_GREATER:在片段深度值大于缓冲区的深度值时通过测试
- GL_NOTEQUAL:在片段深度值不等于缓冲区的深度值时通过测试
- GL_GEQUAL:在片段深度值大于等于缓冲区的深度值时通过测试
下面的线性方程将 z 值变换到了[0,1]
之间的深度值: \[
depth = \frac{z-near}{far-near}
\]
将非常近的物体的深度值设置为接近 0,非常接近远平面的的深度值会非常接近1。
实际使用时,需要 z 很小(距离很近)的时候提供较大的精度值,需要一个非线性变换: \[ depth = \frac{z^{-1}-near^{-1}}{far^{-1}-near^{-1}} \] 深度缓冲中的值在屏幕空间中不是线性的(下面是 near = 0.1、far = 100 的图像):
near = 0.1
far = 100
z = np.arange(0.1, 100, 0.1)
depth = (1 / z - 1 / near) / (1 / far - 1 / near)
plt.plot(z, depth)
深度值很大一部分是由很小的 z 值所决定的,这给了近处的物体很大的深度精度。
内建 gl_FragCoord 向量的 z 值包含了那个特定片段的深度值,可以可视化深度值。
FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
两个平面或三角形非常紧密地平行排列在一起时会发生深度冲突(Z-fighting)。深度缓冲没有足够的精度来决定两个形状哪个在前面,结果就不断地在切换前后顺序,导致奇怪现象。解决方法如下:
- 不要把多个物体摆得太靠近,防止一些三角形重叠。
- 将近平面设置远一些。
- 使用更高精度的深度缓冲。
Stencil Test
当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行;模板测试可能会丢弃片段,保留的片段会进入深度测试。
模板测试是根据模板缓冲(Stencil Buffer)进行的。每个模板值(Stencil Value)是8位的,可以将这些模板值设置为想要的值,然后当某一个片段有某一个模板值的时候,就可以选择丢弃或保留这个片段。
每个窗口库都需要配置一个模板缓冲。GLFW自动做了这件事,,其它窗口库可能不会默认创建一个模板库。
模板缓冲首先被清除为 0,之后在模板缓冲中使用1填充了部分,场景中片段只在片段的模板值为1的时候被渲染。
启用 GL_STENCIL_TEST 来启用模板测试:
glEnable(GL_STENCIL_TEST);
在每次迭代之前清除模板缓冲:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glStencilMask
设置一个位掩码(Bitmask),与将要写入缓冲的模板值进行与(AND)运算:
glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)
glStencilFunc(GLenum func, GLint ref, GLuint mask);
func
:设置模板测试函数,会应用到已储存的模板值和glStencilFunc
函数的ref
值上,可用函数与深度缓冲类似。ref
:模板测试的参考值,模板缓冲的内容将会与这个值进行比较。mask
:会与参考值和储存的模板值在测试比较它们之前进行与(AND)运算,默认所有位都为 1。
上面的例子中:片段的模板值等于(GL_EQUAL
)参考值 1,会通过测试被绘制,否则被丢弃。
glStencilFunc(GL_EQUAL, 1, 0xFF);
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass);
// 默认: glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
sfail
:模板测试失败时采取的行为。dpfail
:模板测试通过,但深度测试失败时采取的行为。dppass
:模板测试和深度测试都通过时采取的行为。
行为 描述 GL_KEEP 保持当前储存的模板值 GL_ZERO 将模板值设置为0 GL_REPLACE 将模板值设置为 glStencilFunc
函数设置的ref
值GL_INCR 如果模板值小于最大值则将模板值加1 GL_INCR_WRAP 与GL_INCR一样,但如果模板值超过了最大值则归零 GL_DECR 如果模板值大于最小值则将模板值减1 GL_DECR_WRAP 与GL_DECR一样,但如果模板值小于0则将其设置为最大值 GL_INVERT 按位翻转当前的模板缓冲值
物体边框
创建一个边框颜色片段着色器:
FragColor = vec4(0.26, 0.12, 0.34, 1.0);
启用模板测试
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
- 模板测试和深度测试有一个测试失败:保留当前储存在模板缓冲中的值。
- 模板测试和深度测试都通过:储存的模板值设置为参考值,用
glStencilFunc
设置为 1。
在 rendering loop 中清除模板缓冲。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
绘制不需要边框的物体:( 禁用模板缓冲,不更新)
shader.active();
glStencilMask(0x00);
glBindVertexArray(planeVAO);
glBindTexture(GL_TEXTURE_2D, texturePlane);
shader.setMat4("model", glm::mat4(1.0f));
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
绘制需要边框的物体:(将模板缓冲清除为0,对所有绘制的片段,将模板值更新为1)
shader.active();
// 所有的片段都更新模板缓冲
glStencilFunc(GL_ALWAYS, 1, 0xFF);
// 启用模板缓冲写入
glStencilMask(0xFF);
glBindVertexArray(cubeVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureCube);
model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
使用 GL_ALWAYS 模板测试函数,保证需要绘制物体的每个片段都会将模板缓冲的模板值更新为1。
绘制放大的物体:(禁用模板写入和深度测试)
border_shader.active();
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
float scale = 1.1;
glBindVertexArray(cubeVAO);
glBindTexture(GL_TEXTURE_2D, textureCube);
model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
model = glm::scale(model, glm::vec3(scale, scale, scale));
border_shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
使用 GL_NOTEQUAL模板测试函数,保证只绘制需要绘制边框的物体的模板值不为 1 的部分。
禁用深度测试,让放大的地方(边框),不会被不需要绘制边框的物体覆盖。
重启深度缓冲和模板测试:
glStencilMask(0xFF);
glStencilFunc(GL_ALWAYS, 0, 0xFF);
glEnable(GL_DEPTH_TEST);
下面是测试的情况:
Blending
透明是由 alpha 值来控制的。
片段丢弃
使用 RGBA 加载含 alpha 通道的图像,在着色器中简单判断一下就行。
vec4 texColor = texture(texture1, TexCoords);
if(texColor.a < 0.1)
discard;
FragColor = texColor;
OpenGL 默认不知道怎么处理 alpha 值,不知道什么时候丢弃片段。
discard
指令被调用,会保证片段不会被进一步处理,不会进入颜色缓冲。能够在片段着色器中检测一个片段的 alpha 值是否低于某个阈值,满足条件就丢弃这个片段,
使用透明值,纹理图像的顶部将会与底部边缘的纯色值进行插值,结果是一个半透明的有色边框。
使用 alpha 纹理时,环绕方式设置为 GL_CLAMP_TO_EDGE。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, format == GL_RGBA ? GL_CLAMP_TO_EDGE : GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, format == GL_RGBA ? GL_CLAMP_TO_EDGE : GL_REPEAT);
颜色混合
启用 GL_BLEND 来启用混合。
glEnable(GL_BLEND);
\[ C_{RES}=C_{SRC}*P_{SRC}+C_{DST}*P_{DST} \]
- \(C_{SRC}\):纹理颜色向量。
- \(P_{SRC}\):alpha 值对\(C_{SRC}\)的影响。
- \(C_{DST}\):颜色缓冲中的颜色向量。
- \(P_{DST}\):alpha 值对\(C_{DST}\)的影响。
用下面的函数设置上面的参数。(glBlendColor
设置颜色常量 \(C_{CONST}\))
glBlendFunc(GLenum sfactor, GLenum dfactor);
- GL_ZERO:\(0\)
- GL_ONE:\(1\)
- GL_SRC_COLOR:\(C_{SRC}\)
- GL_ONE_MINUS_SRC_COLOR:\(1-C_{SRC}\)
- GL_DST_COLOR:\(C_{DST}\)
- GL_ONE_MINUS_DST_COLOR:\(1-C_{DST}\)
- GL_SRC_ALPHA:\(C_{SRC}.{\alpha}\)
- GL_ONE_MINUS_SRC_ALPHA:\(1-C_{SRC}.{\alpha}\)
- GL_DST_ALPHA:\(C_{DST}.{\alpha}\)
- GL_ONE_MINUS_DST_ALPHA:\(1-C_{DST}.{\alpha}\)
- GL_CONSTANT_COLOR:\(C_{CONST}\)
- GL_ONE_MINUS_CONSTANT_COLOR:\(1-C_{CONST}\)
- GL_CONSTANT_ALPHA:\(C_{CONST}.{\alpha}\)
- GL_ONE_MINUS_CONSTANT_ALPHA:\(1-C_{CONST}.{\alpha}\)
比如\(P_{SRC} = C_{SRC}.{\alpha},~~ P_{DST} = 1-C_{SRC}.{\alpha}\)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
使用 glBlendFuncSeparate
为 RGB 和 alpha 通道设置不同的选项:
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
glBlendEquation(GLenum mode)
允许设置运算符
- GL_FUNC_ADD:默认。
- GL_FUNC_SUBTRACT:相减。
- GL_FUNC_REVERSE_SUBTRACT:交换再相减。
半透明纹理
始化时启用混合,并设定相应的混合函数:
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
启用了混合,不需要丢弃片段,把片段着色器还原:
FragColor = texture(texture1, TexCoords);
注意到最前面的窗户遮挡了后面的窗户(下图左)。
深度测试和混合一起使用,当写入深度缓冲时,深度缓冲不会检查片段是否是透明,所以透明部分会和其它值一样写入到深度缓冲中。即使透明的部分应该显示背后的窗户,深度测试仍然丢弃了它们。
对于草贴图这种全透明的物体,可以选择丢弃透明的片段而不是混合它们,这样就没有深度问题。
绘制一个有不透明和透明物体的场景顺序:
- 绘制所有不透明物体。
- 观察者视角获取物体的距离,对所有透明物体按距离排序。
- 按顺序绘制所有透明物体(先绘制远处的)。
std::map<float, glm::vec3> _map;
glBindVertexArray(windowVAO);
glBindTexture(GL_TEXTURE_2D, textureWindow);
for (int i = 0; i < arraySize(window_pos); i++)
{
_map[glm::length(camera.getPosition() - window_pos[i])] = window_pos[i];
}
for (auto&& it = _map.rbegin(); it != _map.rend(); ++it)
{
model = glm::mat4(1.0f);
model = glm::translate(model, it->second);
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
glBindVertexArray(0);
- map 会按照 Key 对值排序(小到大)。
- 逆序遍历 map,auto 实际上是
std::map<float,glm::vec3>::reverse_iterator
,绘制物体。
Face culling
只渲染观察者能看到的面,丢弃剩下的面;何一个面都有两侧,面向和背对观察者的面,只绘制面向观察者的面。
OpenGL 能检查所有面向(Front Facing)观察者的面,并渲染它们;丢弃那些背向(Back Facing)的面,节省片段着色器调用(开销很大)。OpenGL使用了一个技巧,分析顶点数据的环绕顺序。
下面是按照顺时针和逆时针定义的三角形:
float vertices[] = { // 顺时针 vertices[0], // 1 vertices[1], // 2 vertices[2], // 3 // 逆时针 vertices[0], // 1 vertices[2], // 3 vertices[1] // 2 };
默认环绕顺序为逆时针定义的三角形为正向三角形(观察者看过去为逆时针)。
会剔除所有顺时针渲染的面(背向观察者的面)。
启用 OpenGL 的 GL_CULL_FACE 选项来开启面剔除。
glEnable(GL_CULL_FACE);
只对立方体这样的封闭形状有效,绘制草之类的物体,要禁用面剔除,因为其正向面和背向面都应该可见。
调用 glCullFace
改变需要剔除的面的类型。
GL_BACK
:只剔除背面(默认)。GL_FRONT
:只剔除正面。GL_FRONT_AND_BACK
:剔除正面和背面。
将顺时针的面定义为正向面:
glFrontFace(GL_CW); // 默认GL_CCW代表是逆时针
面剔除可以很好的提高程序性能,但应该明确什么物体应该面剔除,什么物体禁止面剔除。
FrameBuffers
帧缓冲FrameBuffers = 颜色缓冲ColorBuffers + 深度缓冲DepthBuffer + 模板缓冲StencilBuffer
目前的操作都是在默认帧缓冲的渲染缓冲上进行的,默认的帧缓冲是在创建窗口的时候生成和配置的。
使用 glGenFramebuffers
创建帧缓冲对象FBO(Frame Buffer Object)。
unsigned FBO;
glGenFramebuffers(1, &FBO);
使用 glBindFramebuffer
绑定帧缓冲。
glBindFramebuffer(GL_FRAMEBUFFER, FBO);
绑定到 GL_FRAMEBUFFER 目标之后,所有读取和写入帧缓冲的操作将会影响当前绑定的帧缓冲。
可以使用 GL_READ_FRAMEBUFFER 或 GL_DRAW_FRAMEBUFFER一个帧缓冲分别绑定到读取或写入目标。
- 绑定到 GL_READ_FRAMEBUFFER 的帧缓冲将会使用在读取操作中。
- 绑定到 GL_DRAW_FRAMEBUFFER 的帧缓冲将会被用作渲染、清除等写入操作的目标。
大部分情况不需要区分,使用 GL_FRAMEBUFFER 绑定。
完整的帧缓冲
- 至少有一个缓冲(颜色、深度、模板缓冲)。
- 至少有一个颜色附件(Attachment)。
- 所有的附件都必须是完整的(保留了内存)。
- 每个缓冲都应该有相同的样本数。
下面的代码可以判断帧缓冲是否完整:
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
之后所有的渲染操作将会渲染到当前绑定帧缓冲的附件中。
当前绑定的帧缓冲不是默认帧缓冲,渲染指令不会对窗口的视觉输出有任何影响。渲染到一个不同帧缓冲即离屏渲染(Off-screen Rendering)。要让所有渲染操作在主窗口中有视觉效果,要再次激活默认帧缓冲,并绑定到 0。
glBindFramebuffer(GL_FRAMEBUFFER, 0);
完成所有的帧缓冲操作之后,要删除这个帧缓冲对象。
glDeleteFramebuffers(1, &FBO);
附件是一个内存位置,它能作为帧缓冲的一个缓冲。
当创建一个附件的时候有两个选项:纹理或渲染缓冲对象(Renderbuffer Object)。
纹理附件
把一个纹理附加到帧缓冲,所有渲染指令会写入到这个纹理中。使用纹理的优点是,可以把所有渲染操作的结果将会被储存在一个纹理图像中,可以在着色器中很方便地使用。
unsigned texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- 维度设置为了屏幕大小(不是必须的)。
- data 参数设置为 NULL,仅仅分配内存而不填充。
- 大多数情况下不需要关心环绕方式或多级渐远纹理。
- 要屏幕渲染到一个更小或更大的纹理上,需要在渲染到帧缓冲之前再次调用冲
glViewport
,使用纹理的新维度作为参数,否则只有小部分的纹理或屏幕会被渲染到这个纹理上。
附加到帧缓冲上:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
- param1:target 帧缓冲的目标(绘制、读取)。
- param2:attachment 件类型(可以添加多个),这里是颜色附件。
- param3:textarget 附加的纹理类型。
- param4:texture 附加的纹理本身。
- param5:level 多级渐远纹理的级别(保留0)。
附加一个深度缓冲纹理:
- 附件类型:GL_DEPTH_ATTACHMENT
- 纹理格式(Format)、内部格式(Internalformat):GL_DEPTH_COMPONENT
附加一个深度模板纹理:
- 附件类型: GL_STENCIL_ATTACHMENT
- 纹理格式(Format)、内部格式(Internalformat):GL_STENCIL_INDEX
可以将深度缓冲和模板缓冲附加为一个单独的纹理。
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0,
GL_DEPTH_STENCIL,GL_UNSIGNED_INT_24_8,NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);
渲染缓冲对象
渲染缓冲对象(RenderBuffer Object)是一个真正的缓冲,即一系列的字节、整数、像素,它会将数据储存为 OpenGL 原生渲染格式,是为离屏渲染到帧缓冲优化过的。
创建一个渲染缓冲对象。
unsigned RBO;
glGenRenderbuffers(1, &RBO);
绑定渲染缓冲对象。
glBindRenderbuffer(GL_RENDERBUFFER, RBO);
由于渲染缓冲对象通常是只写的,会经常用于深度和模板附件,因为一般不需要从深度和模板缓冲中读取值,只关心深度和模板测试。
用 glRenderbufferStorage
创建深度和模板缓冲。
创建一个渲染缓冲对象是专门被设计作为图像使用的,而不是纹理作为通用数据缓冲。
这里选择 GL_DEPTH24_STENCIL8 作为内部格式,它封装了 24 位的深度和 8 位的模板缓冲。
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
附加这个渲染缓冲对象。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, RBO);
渲染到纹理
这里将场景渲染到一个附加到帧缓冲对象上的颜色纹理中,在整个屏幕的四边形上绘制这个纹理。
需要一个覆盖屏幕的平面和平面 VAO:
float surface[] =
{
// positions // texCoords
-1.0f, 1.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, -1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
1.0f, -1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f
};
unsigned int surfaceVAO, surfaceVBO;
glGenVertexArrays(1, &surfaceVAO);
glGenBuffers(1, &surfaceVBO);
glBindVertexArray(surfaceVAO);
glBindBuffer(GL_ARRAY_BUFFER, surfaceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(surface), &surface, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glBindVertexArray(0);
创建一个帧缓冲对象:
unsigned FBO;
glGenFramebuffers(1, &FBO);
glBindFramebuffer(GL_FRAMEBUFFER, FBO);
创建一个纹理图像并绑定到当前帧缓冲对象:
unsigned TBO;
glGenTextures(1, &TBO);
glBindTexture(GL_TEXTURE_2D, TBO);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, TBO, 0);
创建一个渲染缓冲对象让OpenGL 能够进行深度测试(模板测试):
unsigned RBO;
glGenRenderbuffers(1, &RBO);
glBindRenderbuffer(GL_RENDERBUFFER, RBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, RBO);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
m_log("Framebuffer can't complete.");
解绑帧缓冲:
glBindFramebuffer(GL_FRAMEBUFFER, 0);
创建简单着色器绘制四边形屏幕:
#version 330 core
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.0, 1.0);
TexCoords = aTexCoords;
}
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D surfaceTexture;
void main()
{
FragColor = texture(surfaceTexture, TexCoords);
}
in rendering loop:
// 绑定帧缓冲
glBindFramebuffer(GL_FRAMEBUFFER, FBO)
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
//glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
// 绘制场景中的物体到帧缓冲中,绘制代码不变(之前绘制到默认帧缓冲中)
// 解绑帧缓冲并清除
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 绘制屏幕
surface_shader.active();
glBindVertexArray(surfaceVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, TBO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
下图是效果(右边是线框模式,可以看出只绘制了两个三角形)。
后期处理
负片:
FragColor = vec4(1.0 - vec3(texture(surfaceTexture, TexCoords)), 1.0);
灰度:
vec3 t = texture(surfaceTexture, TexCoords).rgb;
float gray = 0.2126 * t.r + 0.7152 * t.g + 0.0722 * t.b;
FragColor = vec4(gray, gray, gray, 1.0);
锐化(Sharpen)核:
大部分核将所有权重加起来之后都应该会等于1,如果不等于1,最终的纹理颜色将会比原来更亮或者更暗。
核在对屏幕纹理的边缘进行采样的时候,还会对中心像素周围的 8 个像素进行采样,会取到纹理之外的像素。环绕方式默认是 GL_REPEAT,在没有设置的情况下取到的是屏幕另一边的像素,而另一边的像素本不应该对中心像素产生影响。因此可以将屏幕纹理的环绕方式设置为 GL_CLAMP_TO_EDGE。在取到纹理外的像素时,能重复边缘的像素来更精确地估计最终的值。
const float offset = 1.0 / 300.0;
const vec2 offsets[9] =
{
vec2(-offset, offset), vec2( 0.0f, offset), vec2( offset, offset),
vec2(-offset, 0.0f), vec2( 0.0f, 0.0f), vec2( offset, 0.0f),
vec2(-offset, -offset), vec2( 0.0f, -offset), vec2( offset, -offset)
};
const float kernel[9] =
{
-1, -1, -1,
-1, 9, -1,
-1, -1, -1
};
void main()
{
vec3 sampleTex[9];
for(int i = 0; i < 9; i++)
{
sampleTex[i] = vec3(texture(surfaceTexture, TexCoords.st + offsets[i]));
}
vec3 col = vec3(0.0);
for(int i = 0; i < 9; i++)
col += sampleTex[i] * kernel[i];
FragColor = vec4(col, 1.0);
}
模糊核:
const float kernel[9] =
{
1.0 / 16, 2.0 / 16, 1.0 / 16,
2.0 / 16, 4.0 / 16, 2.0 / 16,
1.0 / 16, 2.0 / 16, 1.0 / 16
};
边缘检测(Edge-detection)核:
const float kernel[9] =
{
1, 1, 1,
1, -8, 1,
1, 1, 1
};
Soble边缘检测:
// 水平梯度/垂直轮廓
const float kernel[9] =
{
-1, 0, 1,
-2, 0, 2,
-1, 0, 1,
};
// 垂直梯度/水平轮廓
const float kernel[9] =
{
-1,-2, -1,
-0, 0, 0,
-1, 2, 1,
};
Cubemap
立方体贴图(Cubemap)是一个有 6 个 2D 纹理的纹理。
立方体贴图创建要绑定到 GL_TEXTURE_CUBE_MAP。
unsigned cubemap;
glGenTextures(1, &cubemap);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemap);
调用 glTexImage2D
函数 6 次为每个面添加贴图。OpenGL 提供 6 个特殊纹理目标,对应立方体贴图的 6 个面。
- GL_TEXTURE_CUBE_MAP_POSITIVE_X 右
- GL_TEXTURE_CUBE_MAP_NEGATIVE_X 左
- GL_TEXTURE_CUBE_MAP_POSITIVE_Y 上
- GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 下
- GL_TEXTURE_CUBE_MAP_POSITIVE_Z 后
- GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 前
下面是加载立方体贴图的函数实现:
unsigned m_load_img3D(const char* paths[], const size_t N = 6, bool flip_verticlal = false)
{
unsigned texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
stbi_set_flip_vertically_on_load(flip_verticlal);
int width, height, channels;
for (int i = 0; i < N; i++)
{
unsigned char* data = stbi_load(paths[i], &width, &height, &channels, 0);
if (data)
{
GLenum format = get_format(channels);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
stbi_image_free(data);
}
else
{
stbi_image_free(data);
#ifdef USE_LOG
m_log("Failed to load texture!");
m_log(paths[i]);
#endif
}
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
return texture;
}
渲染天空盒小优化
先渲染天空盒,会对屏幕上的每一个像素运行一遍片段着色器,即使只有小部分的天空盒最终是可见的。
最后渲染天空盒,深度缓冲会填充满所有物体的深度值,只需要在提前深度测试通过的地方渲染天空盒的片段就可以了,减少了片段着色器的调用。
问题是,天空盒只是一个 1x1x1 的立方体,会不通过大部分的深度测试,导致渲染失败。
注意到透视除法是在顶点着色器运行之后执行的,将
gl_Position
的xyz
坐标除以w
分量,结果z
分量是顶点深度值。让输出的z
分量等于w
分量,当执行透视除法后,z
分量会变为w / w = 1.0
,即最大的深度值,天空盒只会在没有可见物体的地方渲染。(samplerCube
用于片段着色器采样立方体贴图)#version 330 core layout (location = 0) in vec3 aPos; out vec3 TexCoords; uniform mat4 projection; uniform mat4 view; void main() { TexCoords = aPos; vec4 pos = projection * view * vec4(aPos, 1.0); gl_Position = pos.xyww; } #version 330 core out vec4 FragColor; in vec3 TexCoords; uniform samplerCube skybox; void main() { FragColor = texture(skybox, TexCoords); }
深度缓冲将会填充上天空盒1.0 的值,深度函数从默认的 GL_LESS 改为GL_LEQUAL,保证天空盒在值小于或等于深度缓冲而不是小于时通过深度测试,渲染完成后改回 GL_LESS 。
glDepthFunc(GL_LEQUAL); skybox_shader.active(); view = glm::mat4(glm::mat3(camera.getViewMat())); skybox_shader.setMat4("view", view); skybox_shader.setMat4("projection", projection); glBindVertexArray(skyboxVAO); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, skybox_cubemap); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthFunc(GL_LESS);
不希望移动影响天空盒子,因此设置矩阵第四个维度为0。
view = glm::mat4(glm::mat3(camera.getViewMat()));
反射
在物体的片段着色器上,根据观察向量 D
、物体法向量 N
来计算反射向量 R
。( reflect
函数)
需要物体有法向量,并更新物体的顶点着色器和片段着色器。
Position 输出向量是一个世界空间的位置向量,用来在片段着色器内计算观察方向向量。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 Normal;
out vec3 Position;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
Normal = mat3(transpose(inverse(model))) * aNormal;
Position = vec3(model * vec4(aPos, 1.0));
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
#version 330 core
out vec4 FragColor;
in vec3 Normal;
in vec3 Position;
uniform vec3 cameraPos;
uniform samplerCube skybox;
void main()
{
vec3 D = normalize(Position - cameraPos);
vec3 R = reflect(D, normalize(Normal));
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}
渲染物体之前要绑定立方体贴图:
glBindVertexArray(mirrorVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, skybox_cubemap);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
不需要改变 skybox。
折射
修改下片段着色器即可。(refract
函数)
float refractive_index = 1.00 / 1.55;
vec3 D = normalize(Position - cameraPos);
vec3 R = refract(D, normalize(Normal), refractive_index);
FragColor = vec4(texture(skybox, R).rgb, 1.0);
Buffer
OpenGL 的缓冲是管理特定内存块的对象,需要绑定到对应的缓冲目标。
- 绑定到 GL_ARRAY_BUFFER:顶点数组缓冲。
- 绑定到 GL_ELEMENT_ARRAY_BUFFER:顶点索引缓冲。
OpenGL内部会为每个目标储存一个缓冲,根据目标的不同,以不同的方式处理缓冲。
数据导入缓冲区
glBufferData
:填充缓冲对象所管理的内存,如果设置 data 为 NULL,只分配内存,不进行填充。
glBufferSubData
填充缓冲特定区域:
- 可以指定偏移量。
- 确保有足够的内存分配(先调用
glBufferData
)。
glBufferSubData(GL_ARRAY_BUFFER, 1 << 4, sizeof(data), &data); //[16, 16 + sizeof(data)]
调用 glMapBuffer
函数返回当前绑定缓冲的内存指针:
// 绑定缓冲区
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 获取
void *ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
// 内存拷贝
memcpy(ptr, data, sizeof(data));
// 解除内存指针绑定,ptr不再可用
glUnmapBuffer(GL_ARRAY_BUFFER);
直接映射数据到缓冲(比如从文件中读取数据,并直接复制到内存缓冲中),可以用
glMapBuffer
。
顶点属性分批
const float positions[] = {};
const float normals[] = {};
const float texCoords[] = {};
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals),
sizeof(texCoords), &texCoords);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(sizeof(positions)));
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float),
(void*)(sizeof(positions) + sizeof(normals)));
缓冲复制
使用 glCopyBufferSubData
把一个缓冲中的数据复制到另一个缓冲中:
void glCopyBufferSubData(GLenum readtarget, GLenum writetarget,
GLintptr readoffset, GLintptr writeoffset, GLsizeiptr size);
- readtarget:源缓冲目标(比如 VERTEX_ARRAY_BUFFER)。
- writetarget:目标缓冲目标(比如 VERTEX_ELEMENT_ARRAY_BUFFER)。
- readoffset:源偏移量。
- writeoffset:目标偏移量。
- size:数据大小。
读写数据的两个不同缓冲都为顶点数组缓冲:GL_COPY_READ_BUFFER 和 GL_COPY_WRITE_BUFFER。
const float vertex_data[] = {};
// 1
glBindBuffer(GL_COPY_READ_BUFFER, VBO1);
glBindBuffer(GL_COPY_WRITE_BUFFER, VBO2);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertex_data));
// 2
glBindBuffer(GL_ARRAY_BUFFER, VBO1);
glBindBuffer(GL_COPY_WRITE_BUFFER, VBO2);
glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertex_data));
Advanced GLSL
内建变量(Built-in Variable)
顶点着色器:
gl_Position:(输出变量)裁剪空间输出位置向量。
gl_PointSize:(输出变量)顶点着色器中修改点大小的功能默认是禁用的,启用方法如下:
glEnable(GL_PROGRAM_POINT_SIZE);
点的大小设置为裁剪空间位置的 z 值,即顶点距观察者的距离,它会随着观察者距顶点距离变远而变大。
gl_Position = projection * view * model * vec4(aPos, 1.0); gl_PointSize = gl_Position.z;
这个可以用来做粒子特效。
gl_VertexID:(输入变量)储存了正在绘制的顶点 ID。
片段着色器:
gl_FragCoord:(只读变量)在片段着色器中,x、y 为片段屏幕空间坐标,z 是片段深度值。
可以对比不同片段计算的视觉输出效果。
gl_FrontFacing:当前片段是属于正向面的一部分还是背向面的一部分。
可以用来输出正面和背面不同的效果。
gl_FragDepth:在着色器内设置片段深度值,会禁用所有的提前深度测试。
OpenGL 4.2 起,在片段着色器的顶部使用深度条件重新声明 gl_FragDepth 变量:
layout (depth_<condition>) out float gl_FragDepth;
- any:默认值,禁用提前深度测试,会损失很多性能。
- greater:只能让深度值比
gl_FragCoord.z
更大。- less:只能让深度值比
gl_FragCoord.z
更小。- unchanged:要写入
gl_FragDepth
,只能写入gl_FragCoord.z
的值。
接口块
用 in 或 out 关键字来定义。只要两个接口块的名字一样,对应的输入和输出会匹配起来。
out VS_OUT
{
vec2 TexCoords;
vec3 Normal;
} vs_out;
in VS_OUT
{
vec2 TexCoords;
vec3 Normal;
} fs_in;
uniform缓冲对象(Uniform Buffer Object)
UBO 允许定义一系列在多个着色器中相同的全局Uniform变量,使用 UBO 只需要设置相关uniform 一次。
UBO 是一个缓冲,使用 glGenBuffers
创建,绑定到 GL_UNIFORM_BUFFER 缓冲目标。
使用顶点着色器,将 projection 和 view 存储到 Uniform 块中:
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
之前在每个渲染迭代中,对每个着色器设置 projection 和 view 的 Uniform 矩阵。
利用 UBO,只需要存储这些矩阵一次。声明了 Matrices 的 Uniform 块,Uniform 块中的变量可以直接访问,不需要加块名为前缀。OpenGL 代码中将矩阵值存入缓冲中,每个声明了这个Uniform 块的着色器都能够访问这些矩阵。
Uniform 块的内容是储存在一个缓冲对象中的,实际上只是一块预留内存。需要告诉 OpenGL 内存的哪一部分对应着色器中的哪一个 uniform 变量。
layout (std140)
当前定义的 Uniform 块对它的内容使用一个特定的内存布局。std140 布局声明了每个变量的偏移量都是由一系列规则所决定的,声明了每个变量类型的内存布局。td140 布局不是最高效的布局,但保证了内存布局在每个声明了这个 Uniform 块的程序中是一致的。
还有
shared
和packed
布局,这里不赘述。
基准对齐量(Base Alignment)等于一个变量在 Uniform 块中所占据的空间(包括填充量(Padding))。
对齐偏移量(Aligned Offset)是一个变量从块起始位置的字节偏移量,必须是基准对齐量的倍数。
每个变量(int、float和bool)被定义为 4 字节量,令 N = 4 。
- 标量:基准对齐量 N。
- 向量:2N 或 4N(vec3)。
- 标量数组或向量数组:与 vec4 相同。
- 矩阵:储存为列向量的数组,向量基准对齐量与 vec4 相同。
- 结构体:所有元素根据规则计算后的大小,会填充到 vec4 大小的倍数。
layout (std140) uniform M_Block
{ // total 152
// 基准对齐量 // 对齐偏移量
float value; // 4 // 0
vec3 vector; // 16 // 16 (16的倍数,所以 4->16)
mat4 matrix; // 16 // 32 (列 0)
// 16 // 48 (列 1)
// 16 // 64 (列 2)
// 16 // 80 (列 3)
float values[3]; // 16 // 96 (values[0])
// 16 // 112 (values[1])
// 16 // 128 (values[2])
bool boolean; // 4 // 144
int integer; // 4 // 148
};
使用 UBO,调用 glGenBuffers
创建 UBO,并绑定到 GL_UNIFORM_BUFFER,再用 glBufferData
分配内存。
unsigned UBO;
glGenBuffers(1, &UBO);
glBindBuffer(GL_UNIFORM_BUFFER, UBO);
glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
需要对缓冲更新或者插入数据,绑定到对应 UBO,用 glBufferSubData
更新内存。
在 OpenGL 上下文中,定义了一些绑定点(Binding Point),可以将一个 Uniform 缓冲链接至它,并将着色器中的Uniform 块绑定到相同的绑定点,把它们连接到一起。
将图中的 Lights Uniform 块链接到绑定点2:(需要对每个着色器重复这一步骤)
unsigned lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");
glUniformBlockBinding(shaderA.ID, lights_index, 2);
OpenGL 4.2起,可以添加一个布局标识符,显式地将 Uniform 块的绑定点储存在着色器中。
layout(std140, binding = 2) uniform Lights { ... };
绑定UBO 到相同的绑定点上:
glBindBufferBase(GL_UNIFORM_BUFFER, 2, UBO);
// or
glBindBufferRange(GL_UNIFORM_BUFFER, 2, UBO, 0, 152);
用以下方式更新 Uniform 缓冲对象:
glBindBuffer(GL_UNIFORM_BUFFER, UBO);
int b = true; // GLSL bool 4 byte.
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
示例
#version 330 core
layout (location = 0) in vec3 aPos;
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
uniform mat4 model;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
模型矩阵在不同的着色器中会不断改变,使用Uniform缓冲对象并不方便。
创建UBO,并绑定到绑定点0。
unsigned UBO;
glGenBuffers(1, &UBO);
glBindBuffer(GL_UNIFORM_BUFFER, UBO);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
glBindBufferRange(GL_UNIFORM_BUFFER, 0, UBO, 0, 2 * sizeof(glm::mat4));
顶点着色器 Uniform 块设置为绑定点0。
unsigned uidx1 = glGetUniformBlockIndex(shader1.ID, "Matrices");
unsigned uidx2 = glGetUniformBlockIndex(shader2.ID, "Matrices");
glUniformBlockBinding(shader1.ID, uidx1, 0);
glUniformBlockBinding(shader2.ID, uidx2, 0);
使用 glBufferSubData
在进入rendering loop 前存储投影矩阵。
glm::mat4 projection = glm::perspective(glm::radians(45.0f), width/height, 0.1f, 100.0f);
glBindBuffer(GL_UNIFORM_BUFFER, UBO);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
每次渲染迭代中绘制物体之前,将观察矩阵更新到缓冲的后半部分。
glm::mat4 view = camera.GetViewMat();
glBindBuffer(GL_UNIFORM_BUFFER, UBO);
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
UBO 的优势
- 一次设置多个 uniform 会比一个个设置 uniform 快。
- 在 Uniform 缓冲中修改一次 uniform 比在多个着色器中修改同样的 uniform 更容易。
- OpenGL 限制了最多能处理的 uniform 数量,通过 GL_MAX_VERTEX_UNIFORM_COMPONENTS 查询,使用 UBO 数量会更多。
Geometry Shader
几何着色器(Geometry Shader) 在顶点和片段着色器之间,输入是一个图元的一组顶点,它能够将顶点变换为完全不同的图元,还能生成比原来更多的顶点。
几何着色器顶部,声明从顶点着色器输入的图元类型。
layout (points) in;
points
:绘制 GL_POINTS。(1)- ‘
lines
:绘制 GL_LINES 或 GL_LINE_STRIP。(2)lines_adjacency
:GL_LINES_ADJACENCY 或 GL_LINE_STRIP_ADJACENCY。(4)triangles
:GL_TRIANGLES、GL_TRIANGLE_STRIP 或 GL_TRIANGLE_FAN。(3)triangles_adjacency
:GL_TRIANGLES_ADJACENCY、GL_TRIANGLES_ADJACENCY 或GL_TRIANGLE_STRIP_ADJACENCY。(6)()里面的数字是一个图元包含的最少顶点数。
声明几何着色器输出的图元类型,并设置最多输出的顶点数量。
points
line_strip
triangle_strip
GLSL 提供内建变量(几何着色器输入是一个图元所有顶点,一般多于 1 个,因此被声明为一个数组)。
in gl_Vertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
} gl_in[];
几何着色器的使用
准备四个点(不需要 z 方向坐标):
const float points[] =
{
// position color
-0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 1.0f, 1.0f, 0.0f
};
顶点着色器
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
out VS_OUT {
vec3 v_color;
} vs_out;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
vs_out.v_color = aColor;
}
在几何着色器上绘制图案(几何着色器编译链接与其他着色器一样):
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;
in VS_OUT {
vec3 v_color;
} gs_in[];
out vec3 g_color;
const float scale = 0.25;
void main()
{
g_color = gs_in[0].v_color;
gl_Position = gl_in[0].gl_Position + vec4(-scale, -scale, 0.0, 0.0);
g_color = vec3(1.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( scale, -scale, 0.0, 0.0);
g_color = vec3(1.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4(-scale, scale, 0.0, 0.0);
g_color = gs_in[0].v_color;
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( scale, scale, 0.0, 0.0);
EmitVertex();
g_color = gs_in[0].v_color;
gl_Position = gl_in[0].gl_Position + vec4( 0.0, 2 * scale, 0.0, 0.0);
// top point color
g_color = vec3(1.0);
EmitVertex();
EndPrimitive();
}
调用
EmitVertex
时,gl_Position 向量会被添加到图元中。调用EndPrimitive
时,发射出的(Emitted)顶点会合成为指定输出渲染图元。EmitVertex
调用之后重复调用EndPrimitive
能够生成多个图元。
片段着色器
#version 330 core
out vec4 FragColor;
in vec3 g_color;
void main()
{
FragColor = vec4(g_color, 1.0);
}
物体爆炸
把每个三角形沿着法向量的方向移动一小段距离。
使用 3 个输入顶点坐标来获取法向量,使用叉乘来获取垂直于其它两个向量的一个向量:
vec3 get_normal()
{
vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
return normalize(cross(a, b));
}
用法向量和顶点位置向量作为参数,会返回一个新的向量,即位置向量沿着法线向量进行位移之后的结果。
vec4 explode(vec4 position, vec3 normal)
{
float magnitude = 2.0;
vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude;
return position + vec4(direction, 0.0);
}
完整的物体爆炸几何着色器
#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
in VS_OUT {
vec2 texCoords;
} gs_in[];
out vec2 TexCoords;
// shader.setFloat("time", glfwGetTime());
uniform float time;
vec3 get_normal()
{
vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
return normalize(cross(a, b));
}
vec4 explode(vec4 position, vec3 normal)
{
float magnitude = 2.0;
vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude;
return position + vec4(direction, 0.0);
}
void main() {
vec3 normal = get_normal();
gl_Position = explode(gl_in[0].gl_Position, normal);
TexCoords = gs_in[0].texCoords;
EmitVertex();
gl_Position = explode(gl_in[1].gl_Position, normal);
TexCoords = gs_in[1].texCoords;
EmitVertex();
gl_Position = explode(gl_in[2].gl_Position, normal);
TexCoords = gs_in[2].texCoords;
EmitVertex();
EndPrimitive();
}
法向量可视化
使用模型提供的顶点法线
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out VS_OUT {
vec3 normal;
} vs_out;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
mat3 normalMatrix = mat3(transpose(inverse(view * model)));
vs_out.normal = normalize(vec3(projection * vec4(normalMatrix * aNormal, 0.0)));
}
几何着色器会接收每一个顶点(包括一个位置向量和一个法向量),并在每个位置向量处绘制一个法线向量:
#version 330 core
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;
in VS_OUT {
vec3 normal;
} gs_in[];
const float MAGNITUDE = 0.4;
void draw_line(int index)
{
gl_Position = gl_in[index].gl_Position;
EmitVertex();
gl_Position = gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0) * MAGNITUDE;
EmitVertex();
EndPrimitive();
}
void main()
{
draw_line(0);
draw_line(1);
draw_line(2);
}
使用片段着色器,将其显示为单色的线。
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
该几何着色器也经常用于给物体添加毛发。
Instancing
将数据一次性发送给 GPU,使用一个绘制函数让 OpenGL 利用这些数据绘制多个物体,即实例化(Instancing)。
glDrawArrays
:glDrawArraysInstanced
glDrawElements
:glDrawElementsInstanced
渲染函数实例化版本需要一个额外的参数,实例数量来设置需要渲染的实例个数。GPU 会直接渲染实例,而不用频繁地与 CPU 进行通信。
GLSL 在顶点着色器中嵌入了一个内建变量 gl_InstanceID,从0开始,在每个实例被渲染时递增 1。
实例化数组
定义为一个顶点属性,仅在顶点着色器渲染一个新的实例时才会更新。
实例位置:
glm::vec2 translations[100];
int index = 0;
float offset = 0.1f;
for(int y = -10; y < 10; y += 2)
{
for(int x = -10; x < 10; x += 2)
{
glm::vec2 translation;
translation.x = (float)x / 10.0f + offset;
translation.y = (float)y / 10.0f + offset;
translations[index++] = translation;
}
}
实例缓冲:
glBindVertexArray(quadVAO);
unsigned instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations, GL_STATIC_DRAW);
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1);
glVertexAttribDivisor
告诉 OpenGL 什么情况下更新顶点属性的内容至新一组数据。
- param1:顶点属性。
- param2:属性除数。默认 0,在顶点着色器的每次迭代时更新顶点属性;设置为 n,染 n 个新实例的时候更新顶点属性。
顶点着色器:
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;
out vec3 v_color;
void main()
{
gl_Position = vec4(aPos * gl_InstanceID / 100.0 + aOffset, 0.0, 1.0);
v_color = aColor;
}
片段着色器:
#version 330 core
out vec4 FragColor;
in vec3 v_color;
void main()
{
FragColor = vec4(v_color, 1.0);
}
绘制方式:
shader.active();
glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
glBindVertexArray(0);
Anti Aliasing
抗锯齿(Anti-aliasing)是为了产生更平滑的边缘。
多重采样(Multisampling)
光栅器将一个图元所有顶点作为输入,并将它转换为一系列的片段。顶点坐标与片段之间基本不会有一对一映射,因此光栅器需要以某种方式来决定每个顶点最终所在的片段(屏幕坐标)。
多重采样是将单一采样点变为多个采样点,不使用像素中心的单一采样点,而是以特定图案排列的 4 个子采样点。用这些子采样点来决定像素的覆盖度,颜色缓冲的大小会随着子采样点的增加而增加。
采样点的数量可以是任意的,更多的采样点能带来更精确的覆盖率。
MSAA(Multisample Anti-aliasing)工作方式:不管三角形覆盖了多少子采样点,每个图元中每个像素只运行一次片段着色器,片段着色器使用的顶点数据会插值到每个像素的中心,结果颜色被储存在每个被覆盖的子采样点中。
当颜色缓冲的子样本被图元的所有颜色填满时,所有的颜色会在每个像素内部平均化。
MSAA in OpenGL
在 OpenGL 中使用 MSAA,要使用一个能在每个像素中存储大于 1 个颜色值的颜色缓冲(多重采样为每个采样点储存一个颜色)。存储特定数量的多重采样样本的多重采样缓冲(Multisample Buffer)。
在创建窗口之前调用 glfwWindowHint
来完成。
glfwWindowHint(GLFW_SAMPLES, 4);
调用glfwCreateWindow
创建渲染窗口时,每个屏幕坐标就会使用一个包含 4 个子采样点的颜色缓冲。
GLFW 会自动创建一个每像素 4 个子采样点的深度和样本缓冲,因此对应缓冲的大小都变为 4 倍。
启用多重采样。
glEnable(GL_MULTISAMPLE);
离屏 MSAA
用自己的帧缓冲进行离屏渲染,要动手生成多重采样缓冲。
创建支持储存多个采样点的纹理,使用 glTexImage2DMultisample
,纹理目标 GL_TEXTURE_2D_MULTISAPLE。
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, texture);
// samples是采样个数,最后一个参数为GL_TRUE,图像对每个纹素使用相同的样本位置、相同数量的子采样点个数。
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
使用 glFramebufferTexture2D
将多重采样纹理附加到帧缓冲上,纹理类型 GL_TEXTURE_2D_MULTISAMPLE。
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D_MULTISAMPLE, texture, 0);
当前绑定的帧缓冲就有了一个纹理图像形式的多重采样颜色缓冲。
多重采样渲染缓冲对象:在指定(当前绑定的)渲染缓冲的内存存储时,将 glRenderbufferStorage
改为glRenderbufferStorageMultisample
。
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);
渲染到多重采样帧缓冲:
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
自定义抗锯齿算法
将纹理 uniform 采样器设置为 sampler2DMS
uniform sampler2DMS screenTextureMS;
使用 texelFetch
获取每个子样本的颜色值:
vec4 colorSample = texelFetch(screenTextureMS, TexCoords, i); // 第i+1子样本
- 本文链接:https://morisa66.github.io/2021/01/21/OpenGL2/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。