什么是顺序无关渲染
在 3D 渲染中,物体的渲染是按一定的顺序渲染的,这也就可能导致半透明的物体先于不透明的物体渲染,结果就是可能出现半透明物体后的物体由于深度遮挡而没有渲染出来。对于这种情况通常会先渲染所有的不透明物体再渲染半透明物体或者按深度进行排序来解决。但这样仍然无法解决半透明物体之间的透明效果渲染错误问题,特别是物体之间存在交叉无法通过简单的排序来解决。于是就有一些用专门来解决半透明物体渲染算法,OIT 算法即 Order Independent Transparency (顺序无关的半透明渲染)。Depth Peeling 是众多 OIT 算法里可以得到精确 blending 结果的一个,在非游戏的 3d 应用场景中应该还是很有价值的。
两个交叉半透明四边形(未使用 OIT 渲染)
两个交叉半透明四边形(使用 OIT 渲染)
Single Depth Peeling 原理
Single Depth Peeling 顾名思义,就是通过多次绘制,每次绘制剥离离相机最靠近的一层,像剥洋葱一样层层剥开,按顺序混合就得到了精确的混合结果。既然有 Single Depth Peeling,还有一种优化版本就是Dual Depth Peeling,从前后两个方向剥离,不在本次讨论的范围,有兴趣可以参考链接论文。
深度剥离是一种对深度值进行排序的技术。它的原理比较直观,通常的深度检测是将场景中 Z 值最小的像素输出到屏幕上,就是里相机最近的像素。如此一来就一定有离相机第二近的点,第三近的点·····。通过多次渲染的方法,第一次正常渲染,将深度值存入纹理就得到来离相机最近像素的深度和颜色。第二遍渲染时,把每个像素的深度与上次的深度值做比较,凡是小于上次深度值的都通过测试,在加上 FBO 深度测试的最小值功能就能得到下一个最小的深度值与颜色值,以此类推即可。
缺点
需要剥离 N 次才能完成,就需要 N 个 Pass,N 是深度复杂度。因此性能是严重的瓶颈,另外如何确定 N 也是个问题。
具体流程
1 、创建两对颜色纹理和两对 GL_FLOAT 类型的深度纹理用来 pingpong 。
2 、clear 深度纹理为 0,关闭 OpenGL 混合
2 、正常渲染,大于深度纹理上的值都可以通过测试,加上深度缓冲测试的最小深度值就可以得到离相机最近的深度与颜色值。将颜色结果与颜色纹理中的颜色做混合,深度写入深度纹理。
3 、使用上次得到的颜色与深度作为输入纹理重复 2 的操作,直到剥离完成。
如何从前向后混合颜色
从前向后直接混合明显是错误的,但是我们可以根据混合算法推导出反向混合的算法,具体推导可以参考 Dual Peth Peeling 的 paper 。具体混合算法为:
glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_DST_ALPHA, GL_ONE, GL_ZERO, GL_ONE_MINUS_SRC_ALPHA);
如何确定 N
我们无法确定需要剥离多少次,因为不同的渲染目标的深度复杂度是不同的。目前来说最好的方法是采用遮挡查询的方式来检测是否剥离完成。但这种方式需要 GPU 同步,也会带来严重的性能问题。方式如下
GLuint queryId;
glBeginQuery(GL_SAMPLES_PASSED, queryId);
//depth peeling
glEndQuery(GL_SAMPLES_PASSED);
GLuint queryReady = GL_FALSE;
glGetQueryObjectuiv(queryId, GL_QUERY_RESULT_AVAILABLE, &queryReady);
GLuint samples = 0;
glGetQueryObjectuiv(mOITQueryId, GL_QUERY_RESULT, &samples);
samples 为 0 时就剥离完成了,不能 0 则继续剥离。
实际应用中值得注意的地方
由于深度精度问题可能会造成交叉的地方有接缝,具体做法如下:
1 、深度缓冲及纹理使用 GL_FLOAT 类型增加精度。
2 、纹理需要使用高精度的纹理 precision highp sampler2D;
3 、离摄像机过于仍然会由于精度不足而出现接缝,这时就需要动态调整摄像机远近平面来提升精度
4 、优化遮挡查询中的同步操作
5 、避免遮挡查询出现死循环
优化方向
1 、本文采用从前向后剥离,在细节要求不高的情况下可以固定 N,忽略后续的剥离影响不大。
2 、使用 Dpeth Peeling 的优化版本 Dual Dpeth Peeling
3 、使用高版本才能支持的 per pixel linked list 方法