visionOS 上的 Shader
visionOS 上使用 Shader 有两种方式:ShaderGraph 或 LowLevelTexture(LowLevelMesh) + ComputeShader
- Shader Graph:本质是 MaterialX 增加了特有的扩展,配合苹果自家的 RealityComposer Pro(以下简称 RCP),实现拖拽连线快速编写 Shader。
- Compute Shader:需要手工编写 Metal Shader 代码,复杂但更加自由。
而 Unity 中也有自己的 Shader Graph ,Unreal 中 蓝图 也可以对 Shader 拖拽连线,从这方面讲它们三个的操作具有很大的相似性,理论上可以很方便进行迁移。但 Unity 和 Unreal 是高度成熟的游戏引擎,各种材质效果和配套工具非常丰富,要在苹果原生 RealityKit 达到同样效果,需要手工迁移很多 Shader。虽然 Unity 的 PolySpatial 能自动将 Unity Shader Graph 转译为 visionOS 支持的 MaterialX 版 Shader Graph 材质以在模拟器和真机上运行,然而遗憾的是,当我们使用苹果原生开发时,PolySpatial 并不能帮上忙,所有效果还是要手工迁移。
在我的日常开发中,我经常要在 Unity 和 苹果原生之间切换,偶尔也会帮忙迁移一下 Unreal 到原生。在这个过程中经常面临的困难是:Unity 和 Unreal 中大量内置的 Shader 效果节点,在 RCP 中并没有内置,需要手工编写。于是我就经历了一系列痛苦的迁移过程,比如下图中这些复杂的连线都是手工迁移过来的:
RealityShaderExtension
相信我,经历过一次后,再也不想再重新经历第二遍编写与调试。所以我就把这些迁移好的节点整理了一下,形成开源项目 RealityShaderExtension,供其他开发者使用,可以方便地帮助大家完成从 Unity 和 Unreal 到苹果 visionOS 原生 Shader 的迁移。
RealityShaderExtension 复刻了来自 Unity 的 28 个 Shader Graph 节点和来自 Unreal 的 28 个 Blueprint 节点,此外还包含 20 多种颜色混合模式和 8 种颜色空间转换节点。
-
Unreal 节点参考文档:Unreal Engine Material Functions Reference 复刻了 28 个 Blueprint Shader 节点,示例效果如下:
-
Unity 节点参考文档:Unity Shader Graph Node Library 复刻了 28 个 Shader Graph 节点,示例效果如下:
-
颜色混合节点,Unity 参考文档:Unity Blend Node 和 Unreal 参考文档:Unreal Blend Functions 复刻 20 多种颜色混合模式和 8 种颜色空间转换节点,示例效果如下:
Shader Graph 调试
在手工迁移这些节点的过程,我遇到了非常多的 bug,总结下来有三类:
- 不小心写错的
- 误解(误用)了 RCP 中节点的参数
- RCP 自身的 bug
顺便来讲讲这些 bug 的应对方法。
a. 不小心写错的
最简单的当然是,花大量时间逐一检查对比节点和连线。当然我们还可以借助"假彩色图像"对输出值或中间值进行检验。
"假彩色图像"是指将输出值映射到[0, 1]区间,然后做为 RGB 值进行输出。
当我们在 Xcode 中编写代码时, RealityKit 自带的调试组件 ModelDebugOptionsComponent
就可以将 UV 和法线,显示为不同颜色。
在 RCP 中,右上角也有自带的 Debug Views 功能,可以将法线,UV ,粗糙度等用颜色显示出来。
除了自带方法之外,还可以手动转换为颜色进行显示,在项目的示例中,就有大量输出值被手动转换为 RGB 颜色表示出来,这样更加灵活。
b. 误用 RCP 中节点的参数
最常见的是,部分 RCP 中节点与 Unity 和 Unreal 中参数含义不同。
比如 RCP 中没有 lerp
函数,可以使用 Mix
来代替,但它们参数顺序是反过来的。还有 Step
函数,参数顺序也是反过来的:
c++
//GLSL
lerp(a, b, t) = b * t + a * (1-t)
step(edge, x)
//RCP
mix(F, B, m) = F * m + B * (1-m)
step(in, edge)
比如 RCP 中的条件选择节点 MTLSelect
,当条件为 true 时,会选择参数 B;条件为 false 时,选择参数 A,这点与常规判断不同,需要特别注意。
还有一些函数名称与常见的不同,比如其他平台常见的求导函数:ddx, ddy, fwidth,在 RCP 中则是名称的全称:
- ddx(dFdx):Screen-Space X Partial Derivative,屏幕空间 x 偏导数
- ddy(dFdy):Screen-Space Y Partial Derivative,屏幕空间 y 偏导数
- fwidth:Absolute Derivatives Sum,偏导数绝对值的和
c. RCP 的 bug
编写过程中,还会遇到 RCP 自身的一些 bug,有时不能自动更新效果,有时会给出错误的值。一般解决办法有:
- 重启 RCP
- 重新创建出错的 Node 并重新连线
- 选中出错的节点,点击输入输出面板后面的 Remov Override 按钮↩️
如果你在编写 Shader graph 过程中,遇到特别奇怪的问题:所有节点和连线都是正确的,但效果就是不对。那就需要查找一下,很可能就是某个节点出错了。删除再重新创建这个节点,或者点击 Remov Override 按钮↩️,重置输入输出可能就会恢复正常。
Instancing 技术
我在创建项目中的 Node Graph 时使用了 Instancing 。Instancing类似于单例,它可以节省 CPU 和内存成本,因为它只在内存中加载一个实例并重复使用它。但这样也造成了在使用时,有些许不方便,所以一般有 3 种方式来使用 RealityShaderExtension 中的节点。
- a. 直接复制粘贴所有 Node Graph,包含嵌套的。无需导入源文件,但需要手工复制嵌套的 Node。
- b. 将源文件放在项目中,右键创建 Instancing,再复制粘贴 Instancing 节点。适合大量重复使用
如果修改原始 Node Graph,所有 Instancing 的内容将会同步发生变化。
- c. 创建 Instancing,然后禁用 Instancing。无需导入源文件,相当于自动复制。
如果修改原始 Node Graph,禁用的实例化将不会发生变化,因为它们是不同的节点。
未来展望
在完成 RealityShaderExtension 中近百个节点的迁移工作后,我认为目前的 Shader Graph 使用门槛低适合入门,同时功能上基本完整,能够搭建出复杂效果。
不过 RCP 目前存在一些 bug,功能也需要进一步完善,比如:
- 增强与 Xcode 中代码的协作功能
- 增加对材质文件的双向索引功能,移动源文件不会影响引用了它的文件
最后,希望 RealityShaderExtension 对大家的开发有所帮助!