图形API中的几个基础概念

Stencil#

我们通常见到深度图的格式是D24S8,S8的意思就是占用8个bit的模板缓存。它起的作用有点像深度缓存,决定像素是否可见,但比深度更加丰富,能让开发者做出更加丰富的选择效果。

在渲染管线中,stencil test是在depth test之前的。根据stencil test和depth test的结果来决定是否绘制像素以及模板缓存如何保存。

Stencil test分为两步:

  1. 比较
  2. 写入模板缓存

比较

需要设置参考值ref、比较时对模板值操作的readMask、比较函数(如大于、小于、等于、不等于、一直通过、一直不通过等)。

比较的结果可以这样表示: (ref & readMask) Cmp (stencilBufferValue & readMask)

如果结果为true,则通过模板测试,可以继续执行后面的渲染管线。

如果结果为false,则不通过模板测试,那么丢弃这个像素。

模板缓存写入

当开启模板缓存写入时,如果模板测试不通过,还要执行深度测试。根据模板测试和深度测试的结果决定如何写入深度缓存。

写入深度缓存需要设置

  • 模板测试失败的操作函数
  • 模板测试成功但深度测试失败的操作函数
  • 模板测试失败、深度测试失败的操作函数
  • 写入模板缓存的掩码writeMask

操作函数大概有:

  • keep:保留stencil buffer的内容
  • zero:将0写入stencilbuffer
  • replace:将参考值ref写入深度缓存
  • IncrSat:stencilBufferValue加1,如果大于255,则保留为255
  • DecrSat:stencilBufferValue减1,如果小于0,则保留为0
  • Invert:将stencilBufferValue按位取反
  • 其他的查阅图形API即可。

硬件遮挡查询#

硬件遮挡查询指的是GPU遮挡查询。它是在向GPU发起一个遮挡查询的命令,再渲染物体(不写入深度缓存和颜色缓存),等待查询结果返回,如果查询结果为渲染的像素数大于0表示物体没有被完全遮挡,应该被渲染(之后再渲染这个物体);否则就是被完全遮挡,跳过它的渲染即可。

D3D11的query文档:ID3D11Query (d3d11.h) - Win32 apps

用法见下面的代码。

D3D11_QUERY_DESC queryDesc;
queryDesc.Query = D3D11_QUERY_OCCLUSION;
queryDesc.MiscFlags = 0;
ID3D11Query * pQuery;
pDevice->CreateQuery(&queryDesc, &pQuery);
pDeviceContext->Begin(pQuery);
// draw vertex buffer.
pDeviceContext->End(pQuery);
UINT64 queryData; // This data type is different depending on the query type
while( S_OK != pDeviceContext->GetData(pQuery, &queryData, sizeof(UINT64), 0) )
{
   // if queryData > 0, is not occluded.
}

ID3D11Query (d3d11.h) - Win32 apps

最简单的使用流程是:

  1. 初始化一个遮挡查询。
  2. 关闭颜色和深度写入。
  3. 渲染物体或其近似的形状(一般为包围盒),GPU统计通过深度测试的片元数目。
  4. 结束遮挡查询。
  5. 获取查询的结果也就是物体的可见像素数目,如果像素数超过阈值(一般为0),则渲染该物体。

从代码里可以看到,这样的写法会有两个问题:

  • 增加了drawcall
  • CPU获取查询结果时是等待状态,非常浪费性能。

为了优化这两个引发的问题,现有的方案有Hierarchical Depth-Buffer Occlusion Culling、.Soft Occlusion Culling,Gpu Driven Pipeline、Precomputed Visibility,这些之后专门写文章介绍。

Geometry Shader#

VertexShader stage和PixelShader stage之间有个Geometry Shader的阶段。Geometry Shader stage的输入是VS的输出,geometry shader可以在变换顶点,也可以用这个顶点生成更多的顶点。

它常被用来做GPU粒子系统,点的扩散(如像素风游戏我的世界)。

LearnOpenGL——Geometry Shader 里介绍了详细的用法。

#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;
in VS_OUT {
vec3 color;
} gs_in[];
out vec3 fColor;
void build_house(vec4 position)
{
    fColor = gs_in[0].color; // gs_in[0] since there's only one input vertex
    gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1:bottom-left
    EmitVertex();
    gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0); // 2:bottom-right
    EmitVertex();
    gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3:top-left
    EmitVertex();
    gl_Position = position + vec4( 0.2, 0.2, 0.0, 0.0); // 4:top-right
    EmitVertex();
    gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0); // 5:top
    fColor = vec3(1.0, 1.0, 1.0);
    EmitVertex();
    EndPrimitive();
}
void main() {
    build_house(gl_in[0].gl_Position);
}

Tessellation Shader#

当我们近距离观察一个模型的时候,我们希望能看到更多的细节,而离得较远的时候可以使用包含较少的细节,这样可以省一部分的性能。

一种方法是用LOD,让艺术家制作高中低精度的模型,根据距离选择合适精度的模型,但是会消耗更多的资源,视觉上的连贯性也无法得到保证。

另外一种就是曲面细分,从一个低精度模型,在GPU上细分更多的三角面。

曲面细分是位于VertexShader stage和Pixel Shader stage中间。

完整的曲面细分分为三个部分:

  • Tessellation Control Shader
  • Primitive Generator
  • Tessellation Evaluation Shader

TCS(Tessellation Control Shader)#

TCS基于一组控制点,定义了一个geometry的表面。这个表面由多项式定义。可以通过移动一个控制点改变曲面的形状,这个贝塞尔曲线非常像。一组控制点通常叫做Patch,下面这个图里的表面就是一个拥有16个控制点的patch。

TCS接收一个patch作为输入,并输出一个新的patch。开发者可以在shader里对这些控制点做一些变换或者增删控制点。TCS还生成了一组叫做曲面细分级别(Tesselation Levels,TLs)的数据。决定了细分的程度——patch由多少三角形组成。例如,如果光栅化的三角形覆盖的像素小于100,可以让TLs为3,在101到500之间让TLs为7,更多的像素则设置TLs为12.5。

PG(Primitive Generator)#

PG这个阶段是真正执行划分的。但是PG并不访问TCS输出的patch,而是拿到TLs,并划分成一个Domain。Demain可以使一个单位化的2D正方形,也可以使一个基于质心坐标定义的三角形。

我们以Triangle Domain举例,根据三个顶点,由UVW这三个变量来表示三角形内的点,三角形的重心坐标UVW是(1/3, 1/3, 1/3)。

PG根据TLs的值生成三角形内的一系列点,每个点都是由重心坐标表示的。开发者可以选择PG的输出为点或三角形。如果是点,PG会发送这些点至下个阶段执行光栅化。如果选择三角形,PG会连接所有的点,也就是说三角形的表面被细分成更小的三角形。

总结来说,PG根据TLs的值来决定输出由UVW坐标表示的点的数量。

TES(Tessellation Evaluation Shader)#

TES接受TCS的输出的patch和PG输出的UVW坐标。根据UVW坐标生成一个点,再根据patch决定这个点的位置、法线等顶点信息。PG等TES处理完一个三角形的三个UVW坐标后,将生成的顶点提交到下个阶段用于光栅化。

所以,TES的主要目的是在PG生成的Domain区域应用表面公式,生成新的顶点,并由PG组成三角形,发送到渲染管线的下个阶段。

总结整个渲染管线的过程:

  1. VS作用于patch内的所有点。且patch内是包含了控制点的。
  2. TCS处理VS的输出,并生成一个patch和TLs,将他们发送到下个阶段。
  3. PG根据TCS的输出的TLS,生成一系列由UVW坐标表示的点。
  4. TES根据TCS输出的patch和PG输出的点,生成新的顶点。
  5. PG根据TES配置的片元类型(三角面/Quad/线),将TES生成的顶点组装发送到渲染管线的下个的阶段——Geometry Shader或光栅化。

Instance#

当一个模型需要绘制非常多次,区别只是空间变换不同。如草地中的草和花,森林中的树木。如果每一个个体都执行一次DrawCall,那将是巨量的开销。Gpu提供了instance解决一次绘制大量模型的方案。

下面这个图里用instance技术渲染了100000个小行星,每个小行星有576个顶点,大概有5700万个定点,但帧率没有出现丝毫下降。

使用instance技术,需要把同一种模型的空间变换组织到一起,建立一个transform的数组。

可以将这个tranform数组作为uniform传递给vs,并通过glDrawArraysInstanced渲染模型。我们需要在vs中用到opengl内置的gl_InstanceID。gl_InstanceID的初始值为0,每个实例渲染时都会加1,最后的值是tranform数组的长度减1。可以通过gl_InstanceID索引tranform的uniform数组,就可以利用索引到的transform将顶点变换到相应的位置上。

这种方法有一个缺点,uniform的数量是有限制的,尤其是在一些低端移动设备上只有256,无法满足我们一次渲染大量同样物体的需求。

当然还有另外一种方式——用实例化数组(instance array)。它实际上就是一个transform的array buffer。

GLuint instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);

通过设置instance array顶点属性告诉GPU这个instance array的组成。另外一个需要设置的是属性除数(attribute divisor)。它的默认值是0,根据这个值,GPU渲染下一个实例时决定如何更新顶点中的内容。当attribute divisor为0时,保持不变;为1时,渲染下一个实例就更新顶点内容;为2时,每两个实例更新一次顶点内容。通过将attribute divisor设置为1,可以达到我们想要的每个实例使用不同tranform的目的。

glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1);

参考#

[1] learnopengl.com/Advanced-Op…

[2] www.cs.cornell.edu/courses/cs4…

[3] ogldev.org/www/tutoria…

[4] 实例化 - LearnOpenGL-CN

posted @   silence394  阅读(0)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示