WWDC Quick Look 💓 By SwiftGGTeam
Explore hybrid rendering with Metal ray tracing

Explore hybrid rendering with Metal ray tracing

观看原视频

Highlight

Metal 混合渲染管线用光栅化做主渲染,光追做反射、阴影和环境光遮蔽,配合 tile memory 在单 render pass 内完成,避免 G-Buffer 写出到主内存。

核心内容

你的游戏已经有了完整的光栅化渲染管线。画面不错,但反射是假的(屏幕空间反射),阴影是硬边(shadow map),角落没有接触阴影。你想加入光追提升画质,但不想重写整个渲染器。

混合渲染就是这个问题的答案:光栅化做主,光追做辅。

详细内容

混合渲染管线架构

01:19

核心思路分三步:

  1. 光栅化生成 G-Buffer — 用传统方式渲染场景,输出位置、法线、材质信息
  2. 从 G-Buffer 发射光线 — 在 compute pass 或 fragment shader 中读取 G-Buffer,发射光线
  3. 合成最终画面 — 把光追结果和光栅化结果混合
Render Pass 1 (光栅化):
  - 渲染场景到 G-Buffer (position, normal, albedo)

Compute Pass (光追):
  - 读取 G-Buffer
  - 发射 shadow rays / reflection rays / AO rays
  - 输出 shadow mask / reflection color / AO factor

Render Pass 2 (合成):
  - 读取 G-Buffer 和光追结果
  - 计算光照并合成

06:32

关键点:

  • G-Buffer 只需要生成一次
  • 每种光追效果可以独立实现
  • 光追分辨率可以低于屏幕分辨率
  • 用 temporal accumulation 降噪

Ray-Traced Shadows

08:35

Shadow map 的问题是分辨率有限,近处锯齿明显,而且不支持软阴影。光追阴影直接从 G-Buffer 发射光线到光源,判断是否有遮挡。

kernel void rayTracedShadows(
    texture2d<float> gBufferPosition [[texture(0)]],
    texture2d<float> gBufferNormal [[texture(1)]],
    texture2d<float> shadowMask [[texture(2)]],
    uint2 gid [[thread_position_in_grid]]
) {
    float3 position = gBufferPosition.read(gid).xyz;
    float3 normal = gBufferNormal.read(gid).xyz;

    // 偏移起点避免自相交
    float3 origin = position + normal * 0.01;
    float3 direction = normalize(lightPosition - position);

    ray ray;
    ray.origin = origin;
    ray.direction = direction;
    ray.max_distance = distance(lightPosition, position);

    intersector<instancing> intersector;
    intersector.set_acceleration_structure(accelerationStructure);

    auto intersection = intersector.intersect(ray, accelerationStructure);

    // 如果有交点,说明被遮挡
    float shadow = (intersection.type == intersection_type::none) ? 1.0 : 0.0;
    shadowMask.write(shadow, gid);
}

11:54

关键点:

  • 从 G-Buffer 读取世界空间位置
  • 起点沿法线偏移避免自相交
  • max_distance 设为到光源的距离
  • 输出 shadow mask,合成时乘到光照上

软阴影可以通过向光源区域随机采样多个点实现:

float shadow = 0.0;
for (int i = 0; i < sampleCount; i++) {
    float3 lightSample = lightPosition + randomOffset(i);
    float3 direction = normalize(lightSample - position);
    ray.direction = direction;
    ray.max_distance = distance(lightSample, position);

    auto intersection = intersector.intersect(ray, accelerationStructure);
    shadow += (intersection.type == intersection_type::none) ? 1.0 : 0.0;
}
shadow /= sampleCount;

Ray-Traced Ambient Occlusion

13:30

Screen Space Ambient Occlusion(SSAO)在屏幕边缘和遮挡物后方会失效。光追 AO 从 G-Buffer 向半球方向发射短距离光线,统计被遮挡的比例。

kernel void rayTracedAO(
    texture2d<float> gBufferPosition [[texture(0)]],
    texture2d<float> gBufferNormal [[texture(1)]],
    texture2d<float> aoTexture [[texture(2)]],
    uint2 gid [[thread_position_in_grid]]
) {
    float3 position = gBufferPosition.read(gid).xyz;
    float3 normal = gBufferNormal.read(gid).xyz;

    float ao = 0.0;
    for (int i = 0; i < sampleCount; i++) {
        float3 sampleDir = sampleHemisphere(normal, i);

        ray ray;
        ray.origin = position + normal * 0.01;
        ray.direction = sampleDir;
        ray.max_distance = aoRadius; // 短距离,比如 1.0 米

        auto intersection = intersector.intersect(ray, accelerationStructure);
        ao += (intersection.type != intersection_type::none) ? 1.0 : 0.0;
    }
    ao = 1.0 - (ao / sampleCount);
    aoTexture.write(ao, gid);
}

15:07

关键点:

  • 采样方向限制在法线半球内
  • 光线距离短(AO 只关心附近遮挡)
  • 结果可以模糊降噪
  • 每像素 4-8 条光线就能有不错的效果

Tile Memory 优化

17:20

在 Apple GPU 上,混合渲染可以利用 tile memory 避免写出 G-Buffer。

// 光栅化 pass:G-Buffer 存到 tile memory
fragment GBufferOutput gBufferFragment(...) {
    GBufferOutput output;
    output.position = float4(worldPosition, 1.0);
    output.normal = float4(normal, 0.0);
    output.albedo = albedo;
    return output;
}

// 在同一个 render pass 的 tile shader 中做光追
[[kernel]] void tileShader(
    texture2d<float> positionTexture [[texture(0)]], // tile memory
    texture2d<float> normalTexture [[texture(1)]],   // tile memory
    ...
) {
    // 直接从 tile memory 读取 G-Buffer,发射光线
    // 结果写回 tile memory
}

18:45

关键点:

  • tile shader 在 tile memory 中执行
  • G-Buffer 不需要写出到主内存
  • 光追结果直接用于合成
  • 大幅减少内存带宽

降噪和时域累积

20:30

每像素只发射少量光线(1-4 条)会产生噪声。解决方案是时域累积:把当前帧的结果和历史帧混合。

// 当前帧的光追结果
float4 currentResult = traceRays(...);

// 重投影到上一帧的屏幕坐标
float2 prevUV = reproject(currentPosition, prevViewProjMatrix);

// 读取历史帧结果
float4 historyResult = historyTexture.sample(sampler, prevUV);

// 混合(指数移动平均)
float blendFactor = 0.9; // 90% 历史 + 10% 当前
float4 denoisedResult = lerp(currentResult, historyResult, blendFactor);

22:15

关键点:

  • 重投影需要上一帧的 view-projection 矩阵
  • blend factor 可以自适应调整(运动快时降低)
  • 需要拒绝无效的历史样本(遮挡变化时)
  • 配合 spatial filter 进一步降噪

核心启发

  1. 从 shadow map 迁移到光追阴影。用 G-Buffer 位置发射光线到光源,支持任意精度和软阴影。入口 API:intersector.intersect(ray, accelerationStructure)

  2. 用光追 AO 替代 SSAO。在角落和接触面获得更准确的遮挡信息。入口 API:半球采样 + 短距离光线求交。

  3. 在 Apple GPU 上用 tile shader 做混合渲染。G-Buffer 留在 tile memory,光追和合成在同一个 render pass 完成。入口 API:[[kernel]] tileShader + tile memory texture。

  4. 光追结果用时域累积降噪。每像素少量光线 + 历史帧混合 = 平滑结果。入口 API:重投影 + 指数移动平均。

  5. 逐步添加光追效果。先加阴影,再加 AO,最后加反射。每个效果独立实现,不影响现有管线。入口 API:独立的 compute pass 或 tile shader。

关联 Session

评论

GitHub Issues · utterances