WWDC Quick Look 💓 By SwiftGGTeam
Explore advanced rendering with RealityKit 2

Explore advanced rendering with RealityKit 2

观看原视频

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)
  • 额外纹理赋值给 emissiveColorcustom 槽位
  • 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 创建的网格经过优化,拓扑结构可能改变
  • 更新前需了解优化对网格拓扑的影响

核心启发

  1. Geometry Modifier 实现环境动画:海草、树木、旗帜等物体的自然摆动,不需要骨骼动画,一个 sine wave shader 就能实现。用世界坐标作为输入确保相邻物体动画一致。

  2. Surface Shader 实现风格化渲染:从写实到卡通、从金属到水晶,Surface Shader 让你完全控制每个像素的最终外观。mask 纹理 + 时间参数可以创造复杂的过渡效果。

  3. 后处理统一真实与虚拟:深度雾效果同时作用于真实场景(ARKit depth)和虚拟物体(RealityKit depth),让两者在视觉上融为一体。这是 AR 沉浸感的关键。

  4. 动态网格用于特效几何体:螺旋、粒子轨迹、生长动画等特效几何体不需要预置模型文件。用 MeshDescriptor 程序化生成,用 replace 高效更新。

  5. 组合多种技术创造独特体验:Geometry Modifier + Surface Shader + Post Process + Dynamic Mesh 可以组合出无限可能。水下 demo 就是四种技术同时使用的范例。

关联 Session

评论

GitHub Issues · utterances