用渲染排序解决 RealityKit 中的透明物体嵌套问题
半透明物体嵌套问题
RealityKit 和其他游戏引擎类似,对半透明物体有一套单独的处理流程,在一个透明物体单独出现的情况下,是没有问题的。但是当多个透明物体聚集起来时,尤其是嵌套在一起时,就可能会出现问题。
比如苹果的 Demo ,SwiftSplash 中就出现了这个问题:半透明的鱼缸,里面有半透明的水,水里鱼的护目镜也是半透明的。当旋转或移动时,里面的水和护目镜会在特殊角度消失。
简化示例
为了更方便演示,我们在 Reality Composer Pro 中创建一个更简单的示例进行效果演示。在场景中创建三个半透明球体,从大到小嵌套在一起,最外层是最大的白色球体,中间是红色球体,最里面是蓝色球体,为了效果更明显我们让它们沿水平方向稍微偏移一些,呈现偏心的状态:
这样,当我们在水平方向旋转时,就会看到有时里面的球体会消失,接着旋转又会重新出现:
这就是透明物体的渲染顺序导致的异常。
透明物体渲染顺序
在真实的世界中,两个透明物体叠加时,前后顺序不同,得到的颜色也不同,这也就是为什么渲染引擎在处理透明物体时需要排序。
公式为:
color = foreground.alpha * foreground.color + (1 - foreground.alpha) * background.color
出于性能考虑,RealityKit 针对透明物体,采用了按透明物体坐标原点(一般是中心点)排序,然后从后向前渲染的策略。
但是要得到正确的颜色,我们真正需要的是按实际的像素深度,从后向前渲染才行。幸运的情况下,当我们从某个角度观察时,恰好像素深度和中心距离顺序是一样的,于是就得到了正确的结果。
但是当我们换个角度观察,实际像素深度和中心距离顺序不一致。我们先渲染了白色球体,因为它的中心距离最远,但实际上由于半径最大,它的表面实际离的最近。
这样造成的后果就是,引擎已经将白色球体和真实的背景进行了混合,原本期望下一个颜色的深度更小,就可以正常按公式叠加,但实际接下来要渲染的却是红色球体,它的表面深度更大,这样就无法正常按公式叠加,默认情况下,引擎就会丢弃后来的像素,于是红色球体就看不到了。
那么有没有办法修改渲染顺序呢?或者修改颜色叠加公式呢?答案就是:能! ModelSortingGroup 就是用来干这个的。
ModelSortingGroup
ModelSortingGroup 既可以在 Xcode 中通过代码创建并设置顺序,也可以在 Reality Composer Pro 中添加并拖拽设置顺序。
在 Reality Composer Pro 中使用 ModelSortingGroup
点击左下角的 “+” 创建一个 ModelSortingGroup,然后先选中它,再将要排序的物体拖拽到右侧出现的面板中,就完成了排序。
然后,我们可以旋转移动场景,三个球体的渲染都是正确的。
注意:Reality Composer Pro 有时会出现 bug:当你删除 ModelSortingGroup 后,它的效果仍然没有消失,需要重启 Reality Composer Pro 才会正常显示默认效果。
通过代码使用 ModelSortingGroup
注意,在代码中使用时,名字叫:ModelSortGroup。我们可以直接通过代码创建一个 Group,然后通过给要排序的 ModelEntity 添加 ModelSortGroupComponent
来实现排序。在 ModelSortGroupComponent
中我们可以指定属于哪个 Group,以及排第几。
fileprivate func setEntityDrawOrder(_ entity: Entity, _ sortOrder: Int32, _ sortGroup: ModelSortGroup) {
entity.forEachDescendant(withComponent: ModelComponent.self) { modelEntity, model in
logger.info("Setting sort order of \(sortOrder) of \(entity.name), child entity: \(modelEntity.name)")
let component = ModelSortGroupComponent(group: sortGroup, order: sortOrder)
modelEntity.components.set(component)
}
}
/// Manually specifies sort ordering for the transparent start piece meshes.
func handleStartPieceTransparency(_ startPiece: Entity) {
let group = ModelSortGroup()
// Opaque fish parts.
if let entity = startPiece.findEntity(named: fishIdleAnimModelName) {
setEntityDrawOrder(entity, 1, group)
}
if let entity = startPiece.findEntity(named: fishRideAnimModelName) {
setEntityDrawOrder(entity, 2, group)
}
}
那如果 USDZ 文件中已经有了 ModelSortingGroup,我们如何用代码获取呢?我们可以直接遍历查找 ModelSortGroupComponent
,也可以遍历所有带有 ModelComponent
的 Entity,查看有没有同时带有 ModelSortGroupComponent
,因为它们需要同时出现才能起效。
extension Entity {
var descendentsWithModelComponent: [Entity] {
var descendents = [Entity]()
for child in children {
if child.components[ModelComponent.self] != nil {
descendents.append(child)
}
descendents.append(contentsOf: child.descendentsWithModelComponent)
}
return descendents
}
}
// 找到带有 ModelSortGroupComponent 的 ModelEntity
let entityWithSortComponent = balls.descendentsWithModelComponent.first { entity in
entity.components[ModelSortGroupComponent.self] != nil
}
// 从 ModelSortGroupComponent 中获取所属的 Group
if let sortComponent = entityWithSortComponent?.components[ModelSortGroupComponent.self] {
let group = sortComponent.group
.....
}
其他事项
当我们使用 ModelSortingGroup 后,三个球体嵌套的问题已经解决了。但现实中,透明物体的形状可能会很复杂,它们有可能互相交错在一起,仍然会出现渲染问题。这时我们怎么办?
还有,ModelSortingGroup 中的 Depth Pass 选项,作用又是什么呢?
这些问题,我们留到下篇进行讲解,敬请期待!