本文参考: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); 

image-20210315161038560

颜色混合

启用 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  
};

winding

默认环绕顺序为逆时针定义的三角形为正向三角形(观察者看过去为逆时针)。

会剔除所有顺时针渲染的面(背向观察者的面)。

启用 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_FRAMEBUFFERGL_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_Positionxyz坐标除以 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);

reflect

不需要改变 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_BUFFERGL_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 块的程序中是一致的。

还有 sharedpacked 布局,这里不赘述。

基准对齐量(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 块绑定到相同的绑定点,把它们连接到一起。

binding_points

将图中的 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);   
}
image-20210316185435057
image-20210316185435057

物体爆炸

把每个三角形沿着法向量的方向移动一小段距离。

使用 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)。

  • glDrawArraysglDrawArraysInstanced
  • glDrawElementsglDrawElementsInstanced

渲染函数实例化版本需要一个额外的参数,实例数量来设置需要渲染的实例个数。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子样本