使用 Metal 构建实时神经网络渲染管线
Build real-time neural rendering pipelines with Metal
2026年6月10日
一句话判断
✅ Metal 4 把神经网络从“离线后处理”彻底拉进了“实时渲染管线”的腹地,能在 Shader 里直接跑 TensorOps 是真正的杀手锏。
这场 Session 讲了什么
以前在实时渲染里加 AI,基本都是“外挂”模式:GPU 渲染完,把图倒腾到 ANE (Apple Neural Engine) 跑个模型,再拿回来做后处理。这中间的内存拷贝和同步开销,让“实时”两个字大打折扣。现在 Metal 4 直接把 ML 塞进了图形管线的最深处。
这次 Apple 给了三个层级的武器。最外层是 MetalFX Denoising,专门解决 1 spp (每像素采样数) 路径追踪的噪点问题,属于开箱即用的黑盒。中间层是 ML Command Encoder,让你把离线训好的 Tone Mapper (色调映射器) 导出成 MPS Graph,直接在 Command Buffer 里和渲染 Pass 无缝调度。最狠的是最内层:TensorOps API。它允许你在 Metal Shader 里直接构建和运行小型 MLP (多层感知机),甚至利用 M5 和 A19 Pro 芯片上的 Neural Accelerator (神经网络加速器) 做每帧在线微调。
这对做 3D 引擎、AR 渲染和高级视觉特效的团队影响最大。渲染管线的范式正在从“纯解析计算”向“神经推断”转移,以前靠复杂数学公式和庞大缓存算的光照探针,现在可能只需要 Shader 里跑个几 KB 的微型神经网络。
值得深挖的点
TensorOps 与 Shader 内联神经网络
Apple 推出 TensorOps API 和 Cooperative Tensors (协作张量),是为了解决图形与 AI 之间最后一道数据墙。以前你要在渲染时用一个 ML 模型,必须走完整的 Command Encoder 切换,上下文开销巨大。现在,你可以直接在 Compute Shader 或 Render Shader 里调用 TensorOps,利用 GPU 内部的 Neural Accelerator 执行矩阵乘法。
Session 里展示的“在线训练天空可见度探针 (Sky Visibility Probe)”是个绝佳例子。传统做法是预计算光照探针,场景一变就穿帮或者需要漫长的重算。用 TensorOps,你可以每帧根据当前场景动态微调一个极小的 MLP。这种设计的 Trade-off 很明显:你跑不了 LLM 或庞大的视觉模型,只能塞下几层全连接层;但换来的是零数据搬运延迟,ML 推理和图形渲染在同一个 Pass 里完美融合。这才是真正的“神经渲染”。
MetalFX Denoising 的“喂饭”哲学
MetalFX 降噪不是什么新魔法,但 Session 里 Maxon Redshift Live 团队分享的“喂饭”细节非常硬核。降噪器是典型的 Garbage In, Garbage Out (垃圾进,垃圾出)。Apple 反复强调 Auxiliary Inputs (辅助输入) 的纯净度,特别是 Diffuse Albedo (漫反射反照率)。
最容易被坑的是 Motion Vectors (运动向量)。为了做 TAA (时间抗锯齿) 或超分,相机矩阵通常会加 Jitter (子像素抖动)。如果你直接把带 Jitter 的投影矩阵算出来的屏幕空间位移传给 MetalFX,降噪器会以为画面在疯狂抖动,导致边缘出现严重的 Shimmering (闪烁)。必须像官方代码那样,手动把当前帧和上一帧的 Jitter Delta 减掉,喂给 MetalFX 绝对干净的物理运动向量。
代码片段
1. 计算去 Jitter 的 Camera-only Motion Vectors
场景:给 MetalFX 提供干净的运动向量,防止画面移动时边缘闪烁。
#include <metal_stdlib>
using namespace metal;
// 计算仅包含相机运动的运动向量
float4 clipCurrent = viewProjCurrent * float4(worldPos, 1.0);
float2 ndcCurrent = clipCurrent.xy / clipCurrent.w;
float4 clipPrevious = viewProjPrevious * float4(worldPos, 1.0);
float2 ndcPrevious = clipPrevious.xy / clipPrevious.w;
// 基础位移差
float2 motion = ndcPrevious - ndcCurrent;
// 获取当前帧和上一帧的子像素抖动偏移量
float2 jitterCurrent = getJitter(frameIndex);
float2 jitterPrevious = getJitter(frameIndexPrevious);
// 关键:减去抖动差值,得到纯粹的物理运动向量
motion -= jitterPrevious - jitterCurrent;
坑:如果是带骨骼动画或变形的物体,这套只算相机运动的代码会失效,必须存储上一帧的世界坐标或进行两次蒙皮计算。
2. 在 Shader 中使用 TensorOps 评估微型 MLP (概念演示)
场景:在 Compute Shader 中直接运行一个小型神经网络,比如计算环境光遮蔽或光照探针,避免 CPU/GPU 数据同步。
#include <metal_stdlib>
#include <metal_tensor> // 引入 TensorOps 头文件
using namespace metal;
kernel void evaluate_inline_mlp(
texture2d<float, access::read> inTexture [[texture(0)]],
texture2d<float, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]])
{
// 读取输入特征
float4 inputFeatures = inTexture.read(gid);
// 使用 TensorOps 将数据打包为张量
tensor<float> inputTensor = make_tensor(inputFeatures.xyz);
// 直接在 Shader 内调用硬件神经网络加速器执行矩阵乘法
// 这里省去了传统的 Command Encoder 切换开销
tensor<float> hidden = tensor_ops::matmul(inputTensor, weights1_tensor);
hidden = tensor_ops::add(hidden, biases1_tensor);
hidden = tensor_ops::relu(hidden);
// 输出结果写回纹理
float3 result = tensor_ops::extract<float3>(hidden);
outTexture.write(float4(result, 1.0), gid);
}
坑:TensorOps 目前只支持 M5 和 A19 Pro 及以上芯片的 Neural Accelerator,老设备需要写 Fallback 逻辑,且网络规模受限于 Shader 寄存器和共享内存。
最佳实践
- 已有项目的迁移策略:别一上来就搞 TensorOps。第一步先把 MetalFX Denoising 接进你的路径追踪或光栅化管线,把 1 spp 或低分辨率渲染的画质拉上来。重点检查你的 Albedo 和 Motion Vectors 够不够干净。第二步,把那些又长又臭的后处理 Chain (比如复杂的 Tone Mapping 和 Color Grading) 用 ML Command Encoder 替换成一个离线训练好的 MPS Graph 模型。
- 新项目的采用建议:如果你的场景包含大量动态光照或需要实时适应的环境探针,直接尝试 TensorOps。用 PyTorch 训练一个极小的 MLP,导出权重,在 Shader 里做在线推断。
- 实战避坑:处理玻璃和镜子时,MetalFX 容易被反射/折射搞晕。使用 Primary Surface Replacement (主表面替换) 技术,用菲涅尔项把反射和折射的颜色混合进 Diffuse Albedo 里,骗过降噪器,能得到锐利得多的反射效果。
还有什么值得关注
- M5/A19 Pro 硬件绑定:TensorOps 强依赖新一代 Apple Silicon 的 Neural Accelerator,这意味着在旧设备上你需要准备纯 Compute 的 Fallback 方案。
- Denoiser Strength Mask (降噪强度遮罩):对于天空、粒子或体积雾这种本来就没噪点或者不需要降噪的区域,可以通过传入 Mask 让 MetalFX 跳过计算,白嫖一点性能。
- Redshift Live 的背书:Maxon 这种工业级渲染器在 Cinema 4D 的实时视口里全面采用了这套方案,证明这套 API 已经具备生产级 (Production-ready) 的稳定性,不再是实验室玩具。