Rain Rust 的 2D 光追渲染管线
摘要:《Rain Rust 的 2D 光追渲染管线》该文介绍了一个基于JFA算法的2D光线追踪渲染管线,包含光源绘制、自发光物体标记和跳转洪水算法(JFA)等核心阶段,实现了动态光影效果,在RTX 5070ti和M4 GPU上均能达到120FPS。
前言
Rain Rust 是作者的毕业设计作品代号, 以下也以Rain Rust作为作品的名称.
Rain Rust 是一款以精确动作、解密、探索为主要玩法的2d平台跳跃游戏.
游戏中玩家将探索一个被弃用的神秘工厂, 其中存在着各式由一种特殊物质“耦合蜜”驱动的机械造物, 而玩家可以利用自身的能力去控制它们运作.
而玩家的探索会将自己置身于一场更大的阴谋当中.
本项目开源在GitHub, 你可以点击这里进行访问, 欢迎留下一个小星星.
画面效果
你可以观看这个视频来了解画面效果.
由于平台原因, 此视频可能无法观看, 你也可以点击这个链接到笔者的bilibili账号中观看视频.
对于不想观看视频的读者, 也提供了一些游戏截图来了解效果





优点
- 这是光追
- 很cool的画面风格
- 完全动态的世界
缺点
- 这是光追
- jfa算法生成的sdf with true sdf间有偏差, 导致比如物体角落难以采样到光源
- [TODO] 在镜头移动时, 光场会剧烈抖动
- [TODO] 暂时不支持半透明物体
性能
测试环境1: 120 fps
-
系统: windows
-
GPU: RTX 5070ti desktop
-
分辨率: 4k
-
sample count: 256
-
resolution scale: 1
测试环境2: 120 fps
- 系统: macOS
- GPU: M4
- 分辨率: 1080p
- sample count: 128
- resolution scale: 1
管线概览
Pass Overview
这是此项目的Render grpah viewer.
其中绿框部分为 Rain Rust 的部分.

Stage 1: 绘制光源
- 绘制的所有原始光源, 用于生成光场图

Stage 2: 绘制自发光物体
绘制所有自发光物体, 用于标记哪些像素不需要收光场影响

Stage 3: JFA
使用 Stage 1 的Light Sorce Map 跑一遍JFA算法


Stage 4: JFA 生成 SDF
用 JFA 算法的结果生成 SDF

Stage 5: RayTracing 生成光场图
根据 SDF 以及 Light Source Map 来计算光场图

Stage 6: Composition
组合一系列计算结果, 生成最终结果

具体细节
管线假设
- 所有sprite的纹理都是自发光贴图
- 光线衰减并不遵从平方反比定律, 而是GTR (Generalized Trowbridge-Reitz)函数
管线切入点
- 在渲染完urp的所有半透明物体之后

为什么需要Stage 2
- 由于所有sprite的纹理都是自发光贴图
- 所以对于sprite不需要再受光照影响
- 因此在Stage 2中, 绘制了一张深度图, 用于Stage 6(合成)的深度测试
- 即如果是自发光像素, 直接过, 否则就查询光场进行着色
光场图缩放以优化性能
- 对于光场图, 我们完全可以降低分辨率来优化性能
- 而且这并不会对画面产生太大影响
JFA算法
可以参考笔者的这篇文章
这里直接引用原文:
Algorithm
首先我们有一张 的种子图
比如下图:
其中有颜色的地方为种子, 没有的则是’未定义’
随着算法迭代, 最终整张图的想读都会被’定义’
伪代码如下:
对于每个步长 , 执行一次 JFA
遍历处的每一个像素
对于每一个在$(x+i,y+j)$处的像素$q$ ( $i, j \in \{-k, 0, k\}$) 如果$p$未定义且$q$着色 将$p$的颜色更改为$q$的颜色 如果$p$着色且$q$着色 $p$的颜色使用 `min(dist(p,s),dist(q,s'))`, 其中, $s$ 和 $s'$ 分别是 $p$ 和 $q$ 的种子颜色
JFA to SDF
我们让JFA种子图中的每个像素存储当前位置的UV坐标,那么最终我们就得到了一张存储了离该像素最近的“物体像素”的 UV 坐标的纹理
那么, 轻易可以得到:
当然, 这里没有考虑屏幕比例
Ray Tracing 2D
将三维空间的路径追踪降低为二维, 只需要将原本的向球(或者半球)采样变成向圆采样即可
伪代码如:
for (float f = 0.; f < _Samples; f++)
{
const float t = f / _Samples * float(3.1415926 * 2.0);
result += Trace(i.uv, float2(cos(t), sin(t)) / _Aspect.xy);
}
而对于Trace()函数, 由于我们已经拥有了一张SDF, 我们使用Ray Marching 的方式计算结果
float3 Trace(const float2 uv, const float2 dir) // Ray Marching
{
float2 uvPos = uv; // 当前采样坐标
// 若起始点已在光源上, 直接返回颜色
const float4 color = tex2D(_ColorTex, uv).rgba;
if (color.a > 0)
return color.rgb / color.a;
// 步进
uvPos += dir * tex2D(_DistTex, uvPos).rr;
if (NotUVSpace(uvPos))
return _AmbientColor;
[unroll]
for (int n = 1; n < STEPS; n++)
{
const float4 color = tex2D(_ColorTex, uvPos).rgba;
if (color.a > 0)
{
// 使用 GTR 衰减
float attenuation = GTRAttenuation((uv - uvPos) * _Aspect.xy, _LightFalloffAlpha * color.a, _LightFalloffGamma);
return color.rgb * attenuation;
}
uvPos += dir * tex2D(_DistTex, uvPos).rr;
if (NotUVSpace(uvPos))
return _AmbientColor;
}
return _AmbientColor;
}
Composition

我们有以下输入:
- Urp绘制结果: 最底层的背景
- Light Map: 光场图
- Emissive: 自发光图
- Emissive Depth: 自发光深度图
我们的混合方案伪代码如下:
if (hasEmissive)
{
// 直接使用 Emissive 的结果 (与背景进行 Alpha 混合以保证透明物体正确渲染)
finalColor = lerp(background.rgb, receiver.rgb, receiver.a);
}
else
{
// 没有记录的地方: 混合光照结果和 main 的结果
#if defined(LIGHTING_BLEND_ADDITIVE)
finalColor = background.rgb + lighting.rgb;
#elif defined(LIGHTING_BLEND_ALPHABLEND)
finalColor = lerp(background.rgb, lighting.rgb, lighting.a);
#elif defined(LIGHTING_BLEND_MULTIPLY)
finalColor = background.rgb * lighting.rgb;
#elif defined(LIGHTING_BLEND_SCREEN)
finalColor = 1.0 - (1.0 - background.rgb) * (1.0 - lighting.rgb);
#elif defined(LIGHTING_BLEND_OVERLAY)
finalColor = (background.rgb < 0.5) ? (2.0 * background.rgb * lighting.rgb) : (1.0 - 2.0 * (1.0 - background.rgb) * (1.0 - lighting.rgb));
#else
finalColor = background.rgb + lighting.rgb;
#endif
}
return float4(finalColor, background.a);
时间复杂度
定义:
- : 光线采样数
- : 光场图宽度
- : 光场图高度
- : 迭代深度
- : 相交检测开销
与传统路径追踪对比:
| RainRust | Path Tracing | |
|---|---|---|
| 时间复杂度 | ||
| 迭代深度() | 1 | 次反弹 |
| 相交检测开销 | (SDF 纹理采样) | (BVH 等加速结构) 或 |
| 预处理开销 | JFA 生成 SDF () | 需要构建加速结构 (如 BVH) 其中: - 中值划分 (Median Split): - SAH 启发式 (Surface Area Heuristic): 到 - 线性 BVH (LBVH): - KD-Tree: 或 - 均匀网格 (Uniform Grid): |





