Highlight
RealityKit 2 开放了自定义 Metal shader、后处理效果和动态网格 API,让 AR 应用的视觉表现力从”逼真”升级到”风格化”。
核心内容
RealityKit 1 的渲染管线是封闭的。开发者可以调整材质参数,但无法干预顶点变换和片元着色。这限制了创意表达。
RealityKit 2 打开了三个渲染扩展点:Geometry Modifier(顶点动画)、Surface Shader(自定义表面着色)、Post Process(全屏后处理)。同时新增了动态网格 API,允许在运行时创建和修改几何体。
Session 继续用 underwater demo 展示这些能力:海草随水流摆动(Geometry Modifier)、章鱼变色过渡(Surface Shader)、水下深度雾(Compute Post Process)、螺旋特效环绕潜水员(Dynamic Mesh)。
详细内容
Geometry Modifier:顶点动画
Geometry Modifier 是在 GPU 上执行的 Metal 程序,每帧修改模型的顶点位置。适合环境动画、变形、粒子系统和公告板效果。
#include <RealityKit/RealityKit.h>
[[visible]]
void seaweedGeometry(realitykit::geometry_parameters params)
{
float spatialScale = 8.0;
float amplitude = 0.05;
float3 worldPos = params.geometry().world_position();
float3 modelPos = params.geometry().model_position();
float phaseOffset = 3.0 * dot(worldPos, float3(1.0, 0.5, 0.7));
float time = 0.1 * params.uniforms().time() + phaseOffset;
float3 maxOffset = float3(
sin(spatialScale * 1.1 * (worldPos.x + time)),
sin(spatialScale * 1.2 * (worldPos.y + time)),
sin(spatialScale * 1.2 * (worldPos.z + time))
);
float3 offset = maxOffset * amplitude * max(0.0, modelPos.y);
params.geometry().set_model_position_offset(offset);
}
关键点:
- 函数标记
[[visible]],接收realitykit::geometry_parameters(04:52) params.uniforms().time()获取当前时间params.geometry().world_position()获取顶点世界坐标,用于空间一致的动画params.geometry().model_position()获取模型局部坐标,用于控制动画强度(如底部固定)set_model_position_offset()设置顶点偏移量
func assignSeaweedShader(to seaweed: ModelEntity) {
let library = MTLCreateSystemDefaultDevice()!.makeDefaultLibrary()!
let geometryModifier = CustomMaterial.GeometryModifier(
named: "seaweedGeometry",
in: library
)
seaweed.model!.materials = seaweed.model!.materials.map { baseMaterial in
try! CustomMaterial(from: baseMaterial, geometryModifier: geometryModifier)
}
}
关键点:
- 从默认 Metal library 加载 shader(05:43)
CustomMaterial(from:geometryModifier:)继承原有材质属性,只添加几何修改器- 每个材质都需要单独转换
Surface Shader:自定义表面外观
Surface Shader 在片元阶段执行,定义每个可见像素的外观。可以修改颜色、法线、粗糙度、金属度等表面属性。
演示中的章鱼在两个外观间过渡:紫色和红色。过渡效果使用一张三通道 mask 纹理控制。
[[visible]]
void octopusSurface(realitykit::surface_parameters params)
{
constexpr sampler bilinear(filter::linear);
auto tex = params.textures();
auto surface = params.surface();
auto material = params.material_constants();
// USD 纹理的 Y 轴需要翻转
float2 uv = params.geometry().uv0();
uv.y = 1.0 - uv.y;
// 采样 mask 纹理(噪声 + 渐变 + 遮罩)
half3 mask = tex.custom().sample(bilinear, uv).rgb;
half blend, colorBlend;
transitionBlend(params.uniforms().time(), mask, blend, colorBlend);
// 采样两种颜色纹理并混合
half3 baseColor1 = tex.base_color().sample(bilinear, uv).rgb;
half3 baseColor2 = tex.emissive_color().sample(bilinear, uv).rgb;
half3 blendedColor = mix(baseColor1, baseColor2, colorBlend);
blendedColor *= half3(material.base_color_tint());
surface.set_base_color(blendedColor);
// 法线贴图
half3 texNormal = tex.normal().sample(bilinear, uv).rgb;
half3 normal = realitykit::unpack_normal(texNormal);
surface.set_normal(float3(normal));
// 材质属性
half roughness = tex.roughness().sample(bilinear, uv).r;
half metallic = tex.metallic().sample(bilinear, uv).r;
half ao = tex.ambient_occlusion().sample(bilinear, uv).r;
roughness *= material.roughness_scale();
roughness *= (1 + blend); // 红色状态更粗糙
metallic *= material.metallic_scale();
surface.set_roughness(roughness);
surface.set_metallic(metallic);
surface.set_ambient_occlusion(ao);
}
关键点:
- Surface shader 接收
realitykit::surface_parameters(09:21) params.textures()访问材质纹理,包括base_color()、normal()、roughness()、custom()等params.material_constants()访问代码设置的材质参数- USD 纹理的 Y 坐标需要翻转(
uv.y = 1.0 - uv.y) realitykit::unpack_normal()解码法线贴图- 空 surface shader 会让模型呈灰色(09:30),开发者完全控制输出
func assignOctopusShader(to octopus: ModelEntity) {
let color2 = try! TextureResource.load(named: "Octopus/Octopus_bc2")
let mask = try! TextureResource.load(named: "Octopus/Octopus_mask")
let surfaceShader = CustomMaterial.SurfaceShader(named: "octopusSurface", in: library)
octopus.model!.materials = octopus.model!.materials.map { baseMaterial in
let material = try! CustomMaterial(from: baseMaterial, surfaceShader: surfaceShader)
material.emissiveColor.texture = .init(color2)
material.custom.texture = .init(mask)
return material
}
}
关键点:
CustomMaterial.SurfaceShader加载表面着色器(11:41)- 额外纹理赋值给
emissiveColor和custom槽位 - Geometry Modifier 和 Surface Shader 可以组合使用
Post Process:自定义后处理
后处理在每帧渲染完成后执行,输入是颜色纹理和深度缓冲,输出到目标颜色纹理。
Core Image 后处理
func initPostEffect(arView: ARView) {
arView.renderCallbacks.prepareWithDevice = { [weak self] device in
self?.prepareWithDevice(device)
}
arView.renderCallbacks.postProcess = { [weak self] context in
self?.postProcess(context)
}
}
func prepareWithDevice(_ device: MTLDevice) {
self.ciContext = CIContext(mtlDevice: device)
}
func postProcess(_ context: ARView.PostProcessContext) {
let sourceColor = CIImage(mtlTexture: context.sourceColorTexture)!
let thermal = CIFilter.thermal()
thermal.inputImage = sourceColor
let destination = CIRenderDestination(
mtlTexture: context.targetColorTexture,
commandBuffer: context.commandBuffer
)
destination.isFlipped = false
_ = try? self.ciContext?.startTask(
toRender: thermal.outputImage!,
to: destination
)
}
关键点:
prepareWithDevice在初始化时调用一次,适合创建纹理和管线(14:13)postProcess每帧调用context.sourceColorTexture是输入颜色,context.targetColorTexture是输出context.commandBuffer提供 Metal 命令缓冲- Core Image 提供数百种现成效果
Metal Performance Shaders:Bloom 效果
func postProcess(_ context: ARView.PostProcessContext) {
if self.bloomTexture == nil {
self.bloomTexture = self.makeTexture(matching: context.sourceColorTexture)
}
// 1. 阈值过滤:亮度低于 20% 的像素置零
let brightness = MPSImageThresholdToZero(
device: context.device,
thresholdValue: 0.2,
linearGrayColorTransform: nil
)
brightness.encode(
commandBuffer: context.commandBuffer,
sourceTexture: context.sourceColorTexture,
destinationTexture: bloomTexture!
)
// 2. 高斯模糊
let gaussianBlur = MPSImageGaussianBlur(device: context.device, sigma: 9.0)
gaussianBlur.encode(
commandBuffer: context.commandBuffer,
inPlaceTexture: &bloomTexture!
)
// 3. 叠加原图和 bloom
let add = MPSImageAdd(device: context.device)
add.encode(
commandBuffer: context.commandBuffer,
primaryTexture: context.sourceColorTexture,
secondaryTexture: bloomTexture!,
destinationTexture: context.targetColorTexture
)
}
关键点:
- MPS 提供高度优化的图像处理算子(16:15)
MPSImageThresholdToZero提取高亮区域MPSImageGaussianBlur模糊扩散光晕MPSImageAdd将 bloom 叠加回原图
SpriteKit 后处理
func prepareWithDevice(_ device: MTLDevice) {
self.skRenderer = SKRenderer(device: device)
self.skRenderer.scene = SKScene(fileNamed: "GameScene")
self.skRenderer.scene!.scaleMode = .aspectFill
self.skRenderer.scene!.backgroundColor = .clear
}
func postProcess(context: ARView.PostProcessContext) {
// 复制源纹理到目标
let blitEncoder = context.commandBuffer.makeBlitCommandEncoder()
blitEncoder?.copy(from: context.sourceColorTexture, to: context.targetColorTexture)
blitEncoder?.endEncoding()
// 更新 SpriteKit 场景
self.skRenderer.update(atTime: context.time)
// 在目标纹理上渲染 SpriteKit
let desc = MTLRenderPassDescriptor()
desc.colorAttachments[0].loadAction = .load
desc.colorAttachments[0].storeAction = .store
desc.colorAttachments[0].texture = context.targetColorTexture
self.skRenderer.render(
withViewport: CGRect(...),
commandBuffer: context.commandBuffer,
renderPassDescriptor: desc
)
}
关键点:
- SpriteKit 适合在 3D 场景上叠加 2D 效果(17:15)
- 背景设为透明,让 3D 内容透出
loadAction = .load保留已有内容(3D 渲染结果)
自定义 Compute Shader:深度雾
水下 demo 的深度雾需要结合 ARKit 的真实场景深度和 RealityKit 的虚拟内容深度。
typedef struct {
simd_float4x4 viewMatrixInverse;
simd_float4x4 viewMatrix;
simd_float2x2 arTransform;
simd_float2 arOffset;
float fogMaxDistance;
float fogMaxIntensity;
float fogExponent;
} DepthFogParams;
float linearizeDepth(float sampleDepth, float4x4 viewMatrix) {
float d = max(1e-5f, sampleDepth);
d = abs(-viewMatrix[3].z / d); // 反无限远 Z 投影
return d;
}
[[kernel]]
void depthFog(uint2 gid [[thread_position_in_grid]],
constant DepthFogParams& args [[buffer(0)]],
texture2d<half, access::sample> inColor [[texture(0)]],
texture2d<float, access::sample> inDepth [[texture(1)]],
texture2d<half, access::write> outColor [[texture(2)]],
depth2d<float, access::sample> arDepth [[texture(3)]])
{
const half4 fogColor = half4(0.5, 0.5, 0.5, 1.0);
float2 coords = float2(gid) / float2(inDepth.get_width(), inDepth.get_height());
float2 arDepthCoords = args.arTransform * coords + args.arOffset;
float realDepth = arDepth.sample(textureSampler, arDepthCoords);
float virtualDepth = linearizeDepth(inDepth.sample(textureSampler, coords)[0], args.viewMatrix);
float depth = min(virtualDepth, realDepth);
float fogAmount = saturate(depth / args.fogMaxDistance);
float fogBlend = pow(fogAmount, args.fogExponent) * args.fogMaxIntensity;
half4 nearColor = inColor.read(gid);
half4 color = mix(nearColor, fogColor, fogBlend);
outColor.write(color, gid);
}
关键点:
- ARKit
sceneDepth提供真实场景距离(米),分辨率较低(18:23) - RealityKit 深度缓冲使用 Reverse Infinite-Z,0 表示无限远,1 表示近平面(19:40)
- 线性化公式:
abs(-viewMatrix[3].z / sampledDepth)(20:01) - 取真实深度和虚拟深度的最小值,确保两者都正确雾化
- 用
saturate和幂函数控制雾的密度曲线
动态网格
网格检查
extension MeshResource.Contents {
func forEachVertex(_ callback: (SIMD3<Float>) -> Void) {
for instance in self.instances {
guard let model = self.models[instance.model] else { continue }
let instanceToModel = instance.transform
for part in model.parts {
for position in part.positions {
let vertex = instanceToModel * SIMD4<Float>(position, 1.0)
callback([vertex.x, vertex.y, vertex.z])
}
}
}
}
}
关键点:
MeshResource.Contents包含 instances 和 models(23:32)- Models 存储原始顶点数据,instances 引用 model 并附加变换
- Part 是按材质分组的几何体
- 遍历顶点时应用 instance transform 得到世界坐标
创建网格
extension MeshResource {
static func generateSpiral(
radiusAt: (Float)->Float,
radiusAtIndex: (Float)->Float,
thickness: Float,
height: Float,
revolutions: Int,
segmentsPerRevolution: Int
) -> MeshResource {
let totalSegments = revolutions * segmentsPerRevolution
var positions: [SIMD3<Float>] = []
var normals: [SIMD3<Float>] = []
var indices: [UInt32] = []
var uvs: [SIMD2<Float>] = []
for i in 0..<totalSegments {
let theta = Float(i) / Float(segmentsPerRevolution) * 2 * .pi
let t = Float(i) / Float(totalSegments)
let segmentY = t * height
if i > 0 {
let base = UInt32(positions.count - 2)
indices.append(contentsOf: [
base, base + 3, base + 1, // 第一个三角形
base, base + 2, base + 3 // 第二个三角形
])
}
let radialDirection = SIMD3<Float>(cos(theta), 0, sin(theta))
let radius = radiusAtIndex(t)
var position = radialDirection * radius
position.y = segmentY
positions.append(position)
positions.append(position + [0, thickness, 0])
normals.append(-radialDirection)
normals.append(-radialDirection)
uvs.append(.init(0.0, t))
uvs.append(.init(1.0, t))
}
var mesh = MeshDescriptor()
mesh.positions = .init(positions)
mesh.normals = .init(normals)
mesh.primitives = .triangles(indices)
mesh.textureCoordinates = .init(uvs)
return try! MeshResource.generate(from: [mesh])
}
}
关键点:
MeshDescriptor描述几何数据:positions、normals、primitives、textureCoordinates(26:37)MeshResource.generate(from:)调用 RealityKit 的网格处理器,自动优化- 处理器会合并重复顶点、三角化四边形/多边形、选择最高效的格式
- 法线和纹理坐标是可选的,处理器会自动生成法线
更新网格
if var contents = spiralEntity?.model?.mesh.contents {
contents.models = .init(contents.models.map { model in
var newModel = model
newModel.parts = .init(model.parts.map { part in
let start = min(self.allIndices.count, max(0, numIndices - stripeSize))
let end = max(0, min(self.allIndices.count, numIndices))
var newPart = part
newPart.triangleIndices = .init(self.allIndices[start..<end])
return newPart
})
return newModel
})
try? spiralEntity?.model?.mesh.replace(with: contents)
}
关键点:
- 频繁更新时,直接修改
MeshResource.Contents比重新生成更高效(28:17) mesh.replace(with:)用新内容替换现有网格- 注意:通过
MeshDescriptor创建的网格经过优化,拓扑结构可能改变 - 更新前需了解优化对网格拓扑的影响
核心启发
-
Geometry Modifier 实现环境动画:海草、树木、旗帜等物体的自然摆动,不需要骨骼动画,一个 sine wave shader 就能实现。用世界坐标作为输入确保相邻物体动画一致。
-
Surface Shader 实现风格化渲染:从写实到卡通、从金属到水晶,Surface Shader 让你完全控制每个像素的最终外观。mask 纹理 + 时间参数可以创造复杂的过渡效果。
-
后处理统一真实与虚拟:深度雾效果同时作用于真实场景(ARKit depth)和虚拟物体(RealityKit depth),让两者在视觉上融为一体。这是 AR 沉浸感的关键。
-
动态网格用于特效几何体:螺旋、粒子轨迹、生长动画等特效几何体不需要预置模型文件。用
MeshDescriptor程序化生成,用replace高效更新。 -
组合多种技术创造独特体验:Geometry Modifier + Surface Shader + Post Process + Dynamic Mesh 可以组合出无限可能。水下 demo 就是四种技术同时使用的范例。
关联 Session
- Dive into RealityKit 2 — RealityKit 2 的核心功能更新,包括 ECS、动画和角色控制器
- Create 3D models with Object Capture — 用照片创建 3D 模型,为自定义 shader 提供素材
- Render with Metal — Metal 渲染管线,为编写自定义 shader 打基础
- Explore ARKit 5 — ARKit 5 的深度和场景理解能力
评论
GitHub Issues · utterances