How stencil buffer work in graphics pipeline

0x1 总体介绍

下图是Stencil Test在在post fragment pipeline中的位置。
post_shader_fragment_pipeline

Stencil Test的作用可以把绘制限定在特定的区域,这点和scissor test类似,但是stencil test支持不规则的区域,而scissor test做不到这一点。

Stencil Test是通过stencil buffer中记录的模板信息来完成的,stencil buffer中保存着每个位置的fragment对应的mask值,当graphics pipeline执行stencil test的时候,把通过api设定的模板参考值与相应位置的fragment对应的mask值进行比较,如果不符合条件则被丢弃,反之则进行绘制。

Stencil Test的使用可以看成两步操作,第一步是生成stencil buffer,通过渲染几何体并指定stencil buffer的更新方式来实现。第二步是使用stencil buffer中的内容来控制最终颜色缓存区的渲染。

0x2 从GPU硬件的角度理解Stencil test

下面介绍Stencil test是如何在gpu hardware pipeline中执行的。

下图是broadcom v3d system block图。

v3d是tile buffer rendering的gpu,其渲染分成两个阶段,第一阶段是计算出每个tile中有哪些triangle需要绘制,第二阶段是遍历所有的tile,对每个tile执行真正的绘制工作。

如下图所示,stencil buffer是gpu hardware中tile buffer的一部分,这是gpu硬件中单独的一块buffer,用来保存当前渲染过程中使用到的stencil值。

在每个tile的绘制过程中,都需要根据tile坐标取得stencil buffer中对应的stencil mask, 然后执行stencil test, 执行完了以后,如果必要,还需要更新stencil buffer中对应的stencil值。

v3d system block

0x3 Stencil test测试程序

这个Stencil test测试程序分成两步操作。

第一步只是生成stencil buffer的内容,该过程不输出color,只更新stencil buffer的内容。

更新stencil buffer的策略通过下面的api来控制。
glStencilOp(GL_KEEP, GL_INCR, GL_INCR);

第一个参数是stencil test fail的时候更新stencil buffer的操作,这里的GL_KEEP表明stencil test fail时不对stencil buffer进行更新。

第二个参数表明stencil test pass但depth test fail的时候更新stencil buffer的操作,这里的GL_INCR表明此时对stencil buffer的相应位加1.

第三个参数表明stencil test pass而且depth test pass的时候更新stencil buffer的操作,这里的GL_INCR表明此时对stencil buffer的相应位加1.

以上的设置表明只要stencil test pass的话就对stencil buffer的相应位加1,不管depth test是pass或者fail.

绘制triangle1的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, 0, 0);
glStencilOp(GL_KEEP, GL_INCR, GL_INCR);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glUseProgram(gProgram);
// draw triangle 1
glVertexAttribPointer(gfPositionHandle, 3, GL_FLOAT, GL_FALSE,
TRIANGLE_VERTICES_DATA_STRIDE_BYTES, gTriangleVerticesData);
glEnableVertexAttribArray(gfPositionHandle);
glUniform4fv(gfColor, 1, gColor);
glDrawArrays(GL_TRIANGLES, 0, 3);

绘制triangle2的代码

1
2
3
4
5
6
7
8
9
// draw triangl2
glVertexAttribPointer(gfPositionHandle, 3, GL_FLOAT, GL_FALSE,
TRIANGLE_VERTICES_DATA_STRIDE_BYTES, gTriangleVerticesData1);
glEnableVertexAttribArray(gfPositionHandle);
glUniform4fv(gfColor, 1, gColor1);
glDrawArrays(GL_TRIANGLES, 0, 3);

第二步是根据stencil buffer中的内容为mask来绘制triangle1和triangle2的重叠区域,这个时候发送给opengles的绘制区域可以设置成整个屏幕的大小,但是只有相应的位符合stencil buffer中的条件才能使stencil test通过,这个时候才能输出color。

这个控制相应的位符合stencil buffer中条件的代码如下所示,也就是说stencil buffer相应的位的值需要为0x02,也就是说这个点绘制过两次(triangle1和triangle2都绘制过)。

glStencilFunc(GL_NOTEQUAL, 0, 0xf8 | 0x02);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
glDisable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
glVertexAttribPointer(gfPositionHandle, 3, GL_FLOAT, GL_FALSE,
TRIANGLE_VERTICES_DATA_STRIDE_BYTES, gTriangleVerticesData2);
glEnableVertexAttribArray(gfPositionHandle);
glUniform4fv(gfColor, 1, gColor2);
// draw the area with blue color
glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_FALSE);
glStencilFunc(GL_NOTEQUAL, 0, 0xf8 | 0x02);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

0x4 程序运行效果

triangle1单独绘制的效果
draw_triangle1

triangle2单独绘制的效果
draw_triangle2

triangle1和triangle2同时绘制的效果
draw_triangle1_triangle2

triangle1和triangle2同时绘制但不更新color buffer, 然后根据stencil buffer绘制triangle1和triangle2相交区域的效果
drawing_with_stencil_buffer