用渲染排序解决 RealityKit 中的透明物体嵌套问题
透明物体交错
在本文的上篇中,我们通过使用 ModelSortingGroup 控制渲染顺序,解决了三个半透明球体嵌套的显示问题。但是,如果出现更加复杂的情况,仍然会有显示问题,比如克莱因瓶,如果里面装有水的话效果更加复杂:
我们还是用三个小球来演示,在交错的情况下,不指定排序的话,默认情况是下图这样,部分轮廓会随着视角变化消失和出现:
即使指定了物体的排序,只是避免了随着视角变化闪烁的情况,部分轮廓还是会“稳定”的看不见,比如下图中被蓝色球体挡住时,后面的物体轮廓消失了:
对静态模型进行分割处理
如果一个模型是相对固定的,比如克莱因瓶,就可以将内部的交错部分进行切割,让它成为单独的 Mesh,以便进行渲染排序,就可以得到更好的效果。
相信聪明的小伙伴已经想到了,干脆把透明物体分割成一千个或者一万个小零件,不就可以利用渲染引擎自带的方法,直接用中心点按距离排序处理了?
然而这是不可能的,原因:
- 首先多个物体,每帧运行排序算法,对性能消耗很大;
- 其次几千个小 Mesh 和一整个大的 Mesh 相比渲染效率也低很多;
- 最后这些小的 Mesh 边缘可能由于精度问题会出现各种闪烁与毛刺等现象;
- 最后的最后,极大的增加了模型制作的工作量;
所以一个复杂的透明模型,最多也就分成 3~5 块进行处理。分割之后,一般情况下我们还是需要用 ModelSortingGroup 进行手动排序,排序原则是:将最小的、最靠近内部的,排在最前面渲染;最大的、最靠近外部的,排在最后面渲染。
运动或动画中
那如果:
- 几个透明物体发生了运动,位置产生了变化
- 透明物体带有动画,改变了形状和大小
- 透明物体是来自用户上传或其他地方,根本不知道大小和形状
这时不管我们怎么改变的渲染顺序,对每一个物体来说,都可能是一部分在前,另一个部分在后,甚至还有一部分在中间。这时候又会出现后渲染的物体消失的情况,我们还是以三个透明球体做为示例,在有动画时很容易出现挡住其他物体轮廓的情况:
那有没有办法来控制不同部分(每个像素)的叠加效果呢?有,这就是下面要讲的内容:Depth Pass
使用 Depth Pass 近似模拟
前面我们讲过,真实世界的半透明颜色叠加公式是:
- color = foreground.alpha * foreground.color + (1foreground.alpha) * background.color
也就是根据每个像素的深度,分成前景色和背景色进行叠加。Depth Pass 选项,实际是在控制颜色混合策略。当然,改变颜色混合方式肯定会导致最终的颜色与真实世界不一致,但它可以帮我们节约性能开销,以及显示出物体轮廓。
ModelSortingGroup 中提供了三个 Depth Pass,来控制像素级颜色的混合策略,我们用伪代码来进行说明:
- Post Pass:按指定顺序叠加颜色,后渲染的强制按前景色进行混合,得到最终颜色
let objectList = 指定顺序
var colorInXY = 已有背景颜色
var depthInXY = 已有背景深度
for object in objectList {
for pixel in object {
//将 pixel 做为前景色进行处理,depthInXY 实际无用
colorInXY = pixel.alpha * pixel.color + (1 - pixel.alpha) * colorInXY
}
}
- Pre Pass:顺序无关,根据每个像素实际深度,近的像素强制遮盖后面的颜色,相当于按不透明物体处理
let objectList = 指定顺序或其他任意顺序,不影响结果
var colorInXY = 已有背景颜色
var depthInXY = 已有背景深度
for object in objectList {
for pixel in object {
//比较 depth,将 pixel 颜色直接覆盖
if pixel.depth < depthInXY {
colorInXY = pixel.color
depthInXY = pixel.depth
} else {
//丢弃不处理
}
}
}
- None:根据实际深度信息叠加颜色,如果离的近,就和背景色正常混合,如果离的远,就丢弃不处理
let objectList = 指定顺序
var colorInXY = 已有背景颜色
var depthInXY = 已有背景深度
for object in objectList {
for pixel in object {
//比较 depth,将 pixel 颜色根据透明度混合
if pixel.depth < depthInXY {
colorInXY = pixel.alpha * pixel.color + (1 - pixel.alpha) * colorInXY
depthInXY = pixel.depth
} else {
//丢弃不处理
}
}
}
三种混合策略最终导致的效果也不一样:
- Post Pass:所有物体轮廓都会显示出来,但最终的颜色可能不对,同时交错线消失。从视觉上,三个物体就像没有交错部分一样,颜色直接混合。
- Pre Pass:被挡住的部分轮廓不会显示,但交错处线条显示,最终的颜色没有混合效果。从视觉上,三个物体就像不透明物体一样,前面颜色挡住了后面。
- None:部分半透明轮廓会显示,交错处线条显示,最终颜色是正确的,但后渲染的物体部分轮廓丢失。从视觉上,三个物体有些角度和真实物体一样交错,有些角度像不透明物体一样,前面挡住了后面。
三种效果如下图,分别是 Post,Pre,None:
没有银弹,只能权衡选择
受限于渲染引擎的性能,我们只能对透明物体进行一些特殊处理,来模拟现实世界中的真实物体,但不论怎样,都没有完美的选择,最终只能在性能和效果之间权衡选择处理。
像其他的游戏引擎一样,RealityKit 中提供了 ModelSortingGroup 来控制渲染排序,提供了 Depth Pass 来处理颜色混合策略,让大家选择不同的近似策略,希望本文能帮助大家理解原理,更好做出选择。