这篇文章总结我在一个 Unity 客户端项目里实现并迭代路径追踪器的完整过程:
基于 URP ScriptableRenderPass + HLSL Compute Shader + SDF 场景表示,从"能跑"走到"可持续优化"。
一、项目目标
我想在 Unity 里做一个可控、可扩展的离线路径追踪风格渲染流程,目标是:
- 支持基础材质:漫反射 / 金属 / 电介质(玻璃)
- 支持发光体直接采样与多次反弹
- 支持时间累积降噪
- 在工程上可调参、可回退、可分阶段优化
二、整体架构(CPU + GPU)
1) CPU 侧(C# / URP Pass)
核心职责:
- 扫描场景对象(Collider + MeshFilter)并构建
SdfShape列表 - 把几何和材质参数上传到 GPU(
ComputeBuffer) - 管理历史纹理、分辨率缩放、相机变化重置
- 调度 Compute Shader 内核(单核 or 分阶段)
2) GPU 侧(Compute Shader)
核心职责:
- 射线生成(像素抖动多样本)
- SDF Ray March 求交 + 法线估计
- BSDF 采样与路径积分(含俄罗斯轮盘赌)
- 发光体直接采样 + MIS 权重
- 当前帧结果与历史样本融合
三、渲染主流程(按帧)
下面是精简后的伪代码:
pseudo
function ExecuteFrame():
if needRebuildSdfBuffer():
shapes = collectSceneShapes()
upload(SDFShapes, shapes)
emissiveIndices = []
for i in range(shapes.count):
if shapes[i].isQuadLike and shapes[i].emission > eps:
emissiveIndices.add(i)
upload(EmissiveIndices, emissiveIndices)
setupRenderTargets(traceResolutionScale)
if cameraMoved:
resetHistorySamples()
dispatch(CSPathTraceStage) // 只做本帧采样
dispatch(CSComposeStage) // 历史融合并输出
copyCurrentToHistory()
blitToCameraTarget()
四、路径追踪核心(GPU)
路径积分伪代码:
pseudo
function TracePath(ray):
radiance = 0
throughput = 1
for bounce in [0..maxBounces):
hit = rayMarchSDF(ray)
if !hit:
break
mat = fetchMaterial(hit.shape)
if mat.emissive:
radiance += throughput * mat.emissionColor
break
if mat.isMetal:
wi = reflect(ray.dir, hit.normal)
throughput *= fresnelSchlick(...)
else if mat.isDielectric:
wi = reflect_or_refract_by_fresnel(...)
else:
radiance += sampleDirectLightMIS(hit, throughput)
wi = cosineHemisphereSample(hit.normal)
throughput *= mat.albedo
if bounce >= rrStart:
if random() > survivalProb(throughput):
break
throughput /= survivalProb
ray = offsetRay(hit.pos, wi, normalBias)
return clampHuePreserve(radiance)
五、关键技术点
- SDF 几何表示:sphere / box / quad 统一距离场,便于在 Compute 中做统一求交
- Ray Marching:按距离步进,命中阈值 + 偏移避免自相交
- 材质模型 :
- 漫反射:余弦半球采样
- 金属:反射 + Schlick Fresnel
- 电介质:折射/反射概率 + 全反射判断
- 直接光采样:对发光面片采样点,计算面积 PDF
- MIS(Power Heuristic):平衡"光源采样"和"BSDF采样"贡献
- 时间累积 :
color + sampleCount方式做稳定融合 - 颜色稳定性:用"亮度保持色相"的 clamp,避免 RGB 白化/偏色
六、这次优化里最有效的一步
发光索引缓存(CPU 预构建 + GPU 直接索引)
之前直接光采样每次都要在 shader 里遍历 _SDFCount 做发光体筛选,热点非常明显。
优化后流程变成:
- CPU 构建 SDF 时顺便筛发光体索引
- 上传
EmissiveIndices+_EmissiveCount - GPU 直接
chosen = EmissiveIndices[pick]取样本对象
伪代码:
pseudo
// CPU
emissiveIndices = filter(shapes, shape => shape.type is emissiveQuad && shape.emission > eps)
upload(EmissiveIndices, emissiveIndices)
// GPU
if _EmissiveCount == 0: return 0
pick = randomInt(0, _EmissiveCount-1)
lightIndex = EmissiveIndices[pick]
sample(lightIndex)
优点:
- 降低 shader 中重复遍历
- 提高直接光阶段吞吐
- 结构清晰,后续可扩展到多类光源索引
七、技术栈清单
- 引擎与渲染管线:Unity + URP
- CPU 侧:C#、ScriptableRendererFeature、ScriptableRenderPass、CommandBuffer
- GPU 侧:HLSL Compute Shader(多 kernel 分阶段)
- 数据结构:ComputeBuffer(结构化缓冲)、RenderTexture(ARGBFloat)
- 算法:SDF Ray Marching、Monte Carlo Path Tracing、MIS、Russian Roulette、Temporal Accumulation
- 工程策略:相机变化检测、静态/动态 SDF 重建策略、分辨率缩放、更新帧间隔控制
八、后续优化路线
- Step 1(推荐下一步):自适应采样(方差驱动,低噪像素降采样)
- Step 2(大头):SDF 空间加速结构(网格分桶/BVH)降低全量遍历复杂度
- Step 3(画质稳定):重投影历史融合(motion vector + neighborhood clamp)