直接把结论放在前面:如果你已经有至少一门图形 API 的基础,每周能投入 10 到 15 小时,那么用 6 个月从零手搓出 Lumen 的第一帧画面,是可行的。Lumen 是 Unreal Engine 5 中的实时全局光照与反射系统,它不依赖硬件光追,而是通过屏幕空间追踪、网格距离场、软件光栅化追踪和 Surface Cache 的组合来得到间接光照。这篇文章要讲的是:不打开 UE 编辑器,从空窗口开始,自己搭一个实时渲染器,逐步实现 Lumen 的简化版本,并且亲眼看到第一帧。
读到这里你应该已经明白,这个任务的难度不在“打开引擎开关”,而在于你如何把渲染循环、场景表示、光照缓存和追踪算法串起来。文章适合三类人:学过 OpenGL/Vulkan 但没做过完整渲染器的人;想在简历里放一个高质量图形学项目的人;以及被“Lumen 很牛但不知道原理”困扰的开发者。如果你刚入门,连三角形都没画过,建议先花一个月补基础再开始这条路线。
接下来按四条线展开:先拆解 Lumen 到底在解决什么问题;再给出 6 个月的路线图;然后把关键算法的实现与验收标准列清楚;最后补充 API 选型、环境坑点和工程化习惯。
1. 手搓 Lumen 前,先理解这套方案到底在解决什么问题
1.1 Lumen 不是“实时光线追踪”这么简单
很多人把 Lumen 理解成“不用 RTX 的实时光追”,这个说法不准确。Lumen 本质上是一套混合全局光照方案,它根据不同的场景范围和表面类型,用不同精度的追踪策略,最后把结果合成到一起。
它至少包含几层内容:
- 屏幕空间追踪:从当前像素出发,在屏幕像素坐标里做光线步进,查找可见表面。这层适合处理短距离间接光、高光反射和接触阴影。
- 网格距离场:对场景网格生成有向距离场,用一个三维纹理记录每个位置到最近表面的带符号距离。光线追踪时可以用 Sphere Tracing 快速逼近命中点。
- 软件光栅化追踪:在距离场不够精确或场景规模较大时,用软件光栅化把目标的三角形投影到追踪空间中,再做射线与三角形求交。
- Radiance Cache / Surface Cache:把场景表面的光照信息按低分辨率缓存起来,采样时再按材质粗糙度做过滤和上采样。这样不需要每帧对每个像素做全分辨率路径追踪。
说实话,真实引擎里的 Lumen 还要处理很多工程细节,比如场景更新、LOD、多帧收敛、时序复用、降噪、多平台适配。但从学习角度,你必须先建立这个分层认知。如果一开始就把“全局光照”当成一个黑盒,后面遇到画面闪烁或颜色溢出不对时,你根本不知道是哪个环节出了问题。
1.2 第一帧画面到底应该长什么样
6 个月里说的“第一帧”,不是要求你复刻 UE5 编辑器里的高画质预览。你需要一个静态场景,里面有几个盒子、一块地面,甚至一个低面数角色模型都行。灯光只需要一个方向光和一个天空光,相机能自由转动。
这个第一帧的验收标准是什么?我的定义是:当你把光源颜色从白色改成暖黄色,房间里原本背光的那堵墙,会实时出现淡淡的黄色反光,而不是直接黑掉。这个现象在传统光栅化里需要烘焙 Lightmap,在 Lumen 类方案里应该能随光源变化实时更新。
如果你只实现了屏幕空间反射,看到金属地板上出现其他物体的倒影,那还算不上完成了第一帧。屏幕空间反射只是 Lumen 的一个子集。真正的第一帧至少要包含一次追踪命中和一次间接光照合成,哪怕分辨率只有一半,哪怕带点噪点,都算跨过了核心门槛。
1.3 OpenGL、Direct3D、Vulkan、Metal 怎么选
这四个 API 没有绝对好坏,关键是匹配你的目标。
- OpenGL:资料最多,窗口初始化最简单,适合先把算法思路跑通。缺点是状态管理比较散,做大规模异步计算和资源绑定时会比较别扭。
- Vulkan:如果你想做一个可以并行录制、显式控制同步、面向生产环境的渲染器,Vulkan 是更合理的长期选择。代价是学习曲线陡峭,验证层、同步、内存管理都会消耗时间。
- Direct3D 12:只面向 Windows 平台,调试工具好,PIX 很成熟,适合做 Windows 独占项目。但 API 细节多,资源状态转换和 Root Signature 都比较费神。
- Metal:只用 Apple 平台的话,Metal 是最舒服的,很多设计比 Vulkan 更顺手。缺点是不跨平台。
我给的建议很直接:如果这是你第一次完整做渲染器,第一版只选一个 API,不要想着“既学 Vulkan 又学 D3D12”。优先选你最容易在自己机器上调试的那个。OpenGL 能在最快时间内把性能瓶颈从“创建管线”转移到“算法逻辑”;Vulkan 则更适合你已经有光栅化基础、希望一步到位做工程化的情况。
1.4 为什么先跑通三角形,再谈 Lumen
手搓 Lumen 最大的风险不是算法难,而是你的渲染器连一个带深度缓冲的三角形都跑不流畅。我见过太多人跳过基础,直接去实现距离场和降噪,最后连调试画面都看不到,因为模型根本没渲染出来,也不知道算法输出对应的是哪个颜色通道。
所以建议第一个月不要碰任何 GI 算法。老老实实完成这些事:创建窗口、清空颜色、绘制三角形、写入深度、渲染到纹理、再把纹理贴到一个全屏四边形上。等你能够在多个 Render Target 之间随意切换,并且把深度值、法线值、命中颜色分别显示在屏幕上时,才真正具备做 Lumen 的基础。
这一步看起来枯燥,但它能帮你提前暴露一堆环境问题:驱动版本、窗口系统、着色器编译、矩阵方向、纹理格式、深度精度。这些问题如果堆到后面,会跟 GI 算法本身的 Bug 混在一起,极难排查。
2. 六个月路线图:从空窗口到 Lumen 第一帧
2.1 第 1 到第 2 个月:搭好渲染框架和光栅化基础
前 8 周的任务不需要和 Lumen 直接相关,但每一件事都是在给后面铺路。
第一周做 API 初始化和窗口循环。如果是 OpenGL,用 GLFW 或 SDL 创建窗口,加载 GLAD 扩展;如果是 Vulkan,先用最简单的验证层跑出第一个清屏帧。Vulkan 的验证层会在这一周消耗大量时间,但一旦跑通,后面 debug 会舒服很多。
第二周做顶点缓冲、索引缓冲、MVP 矩阵和深度测试。这里有两个点最容易踩坑。第一个是矩阵方向:比如glUniformMatrix4fv传入矩阵时,OpenGL 默认按列主序读取,如果从 glm 拿到矩阵直接传,需要把 transpose 标志设为 GL_FALSE;如果你手动构造数组,必须确认存储顺序是列优先。第二个是坐标系的差异:OpenGL 默认 NDC 的 Z 范围是 -1 到 1,Vulkan 是 0 到 1,D3D 也是 0 到 1。如果在深度比较时总觉得“反了”,先检查自己的投影矩阵用的是哪个约定。
第三周到第四周,把场景渲染到纹理。颜色纹理、法线纹理、深度纹理都分别准备一张,然后在全屏四边形上做调试。这个阶段的目标不是好看,而是熟练。你要能做到:切换输出目标,查看任意 Render Target,不重新编译工程。
第 5 到第 8 周,加入一个简单的材质结构,例如 Albedo、法线、粗糙度、金属度,并把它们渲染到 GBuffer 中。Lumen 的所有追踪都需要知道命中点处“这面墙是什么颜色、反射有多强”。你没有 GBuffer 也可以做,但后面调试时会非常痛苦。
2.2 第 3 个月:实现屏幕空间追踪和 Hi-Z 加速
第 3 个月是画面视觉产出最明显的阶段。屏幕空间追踪的核心思路并不复杂:从某个像素出发,沿着追踪方向在屏幕像素坐标上逐步推进,每一步都拿当前深度和深度缓冲做比较,如果深度差低于阈值,就认为光线击中了表面。
第一个版本不要追求性能。先固定步长,比如每步前进 0.01 个屏幕空间单位,走 32 步。这个版本跑通后,你会在镜面材质上看到扭曲的倒影。接着再引入两个关键优化:
- 深度范围裁剪:步进前先计算起点和终点在深度方向上的范围,超出深度的步数直接跳过,能减少无效采样。
- Hi-Z 层次深度缓冲:先查低分辨率 mipmap 层的深度做粗步进,命中后再到高分辨率层精化。这能把平均步进次数从几十次降低到几次。
任务收尾时,你应该有一个 Debug 视图:用绿色显示命中的像素,用红色显示未命中。然后你转动相机,确认反射轮廓会跟随场景真实几何关系变化。如果轮廓错位,先检查追踪方向是否乘了正确的逆矩阵;如果全屏都是噪点,先减小步长再看看命中阈值。
2.3 第 4 个月:网格距离场和屏幕外追踪
屏幕空间追踪有个天然缺陷:物体一旦在屏幕外、被遮挡,或者对着镜头背面,屏幕空间里根本没有信息可以命。这个时候就要靠网格距离场来做更大范围的追踪。
简化版实现可以这样规划:
- 先对单个网格离线生成 SDF 纹理,分辨率从 64 立方起,不要一上来就 256 立方。
- 把 SDF 数据存成纹理,采样格式用 R16F 或 R8_SNORM,降低内存和带宽消耗。
- 从光线起点开始做 Sphere Tracing:每次前进的距离等于当前采样到的 SDF 值,当距离小于一个阈值时认为命中。
这里有个容易被忽略的问题:SDF 纹理的滤波方式。如果你默认开了三线性过滤,在物体边界处可能过度平滑;如果你完全关掉滤波,又会出现明显的体素感。建议先开三线性,再单独做一次命中距离的阈值判断,避免把远离表面的假命中混进来。
第 4 个月结束前,做一个小场景验证:把一个红色物体放在屏幕外,把一个灰色物体放在屏幕内,让灰色物体的背光面能接收到一点红色。这个效果如果出现了,说明你的距离场追踪已经能覆盖屏幕外区域。
2.4 第 5 到第 6 个月:Radiance Cache 与最终合成
距离场追踪能告诉你“光线在哪里命中”,但它本身并不给你光照值。还差一层:命中点表面缓存的光照信息。
Lumen 的简化版可以做成 Surface Cache:
- 把场景中主要表面拆成若干区域,每个区域保存低分辨率的光照结果;
- 间接光照采样时,先查屏幕空间追踪,再查 Distance Field 追踪;
- 查不到就用 Surface Cache 的低频光照结果做兜底;
- 采样方向根据材质粗糙度做过滤,粗糙度越高,采样范围越大,越不需要高频细节。
第 5 个月的核心任务是实现一次漫反射 GI 合成。先把方向光、直接光渲染出来,然后对每个像素发射一次屏幕空间追踪;命中就取命中点颜色做间接光,未命中就查 SDF 或 Cache。最后把这一路结果加到直接光上。
第 6 个月的重点已经不是算法本身,而是把整套流程串稳。你需要统一的参数面板、Debug 视图、帧耗时统计和日志输出。到月底,你手里应该有一个可以转相机、改光源颜色、改追踪步长、改 Cache 更新频率的完整渲染器。即使画面还有噪点,优先级也比“再加一个高级降噪”更高。
3. 关键算法怎么一步步落地并验证
3.1 屏幕空间追踪的最小实现示例
下面这段是 GLSL 风格伪代码,不是可直接编译的完整 shader,但能表达核心流程:
vec3 startScreen = ProjectAndDivide(worldOrigin); vec3 endScreen = ProjectAndDivide(worldTarget); vec3 dir = normalize(endScreen - startScreen); vec3 samplePos = startScreen; for (int i = 0; i < maxSteps; ++i) { samplePos += dir * stepLength; if (samplePos.x < 0.0 || samplePos.x > 1.0) break; if (samplePos.y < 0.0 || samplePos.y > 1.0) break; if (samplePos.z < 0.0 || samplePos.z > 1.0) break; float depthAtPixel = texture(depthTexture, samplePos.xy).r; float depthDiff = depthAtPixel - samplePos.z; if (depthDiff < hitThickness && depthDiff > -hitThickness) { return samplePos.xy; } } return vec2(-1.0);注意几个关键点。ProjectAndDivide必须把世界坐标转换到屏幕坐标,并且做透视除法;深浅比较要同时考虑正负阈值,否则容易漏掉薄物体;最大步数和步长一开始用固定值,先看结果,再谈加速。
判断标准也很简单:场景里放两个立方体,一个红色,一个灰色,灰色表面要能看到红色物体的模糊轮廓。如果反射方向完全错了,百分之八十是矩阵或方向向量的问题,不是算法的问题。
3.2 简化版距离场怎么生成和追踪
个人建议不要从零写一个高并发体素化器,学习阶段先吃现成的库或工具。你真正需要理解的是查询和追踪流程:
float dist = SampleSDF(instanceTransform, worldPos); if (dist < hitDistanceThreshold) { // 命中表面 }SDF 的精度和内存是有取舍的。64 立方的纹理非常省,但靠近表面时误差明显;128 立方在单物体上效果不错,但在一个复杂场景里会占用大量显存。如果你跑大型场景,考虑用 Clipmap 或者只对静态物体生成 SDF,动态物体继续用屏幕空间追踪。
调试距离场时,我推荐把光线步进中“最终命中的距离”输出成颜色:距离越小越接近表面,颜色越亮;如果整个屏幕都是亮色,说明阈值设置得太松,很多假命中。调阈值时不要一次改太多,从 0.01 到 0.05 的量级开始测试。
3.3 Surface Cache 为什么能省下大量计算
Surface Cache 的思路是用低分辨率缓存近似高频光照。你不必对每个像素都重新追踪一遍,可以每隔几帧更新一次 Cache,然后在上采样时用双线性插值恢复画面。
按粗糙度做三级策略比较实用:
| 材质类型 | 追踪策略 | 更新频率 | 视觉特征 |
|---|---|---|---|
| 光滑镜面 | 屏幕空间追踪,高分辨率 | 每帧 | 反射轮廓清晰 |
| 粗糙表面 | Distance Field 或 SDF | 每帧或每隔一帧 | 有颜色渗出但模糊 |
| 纯漫反射 | Surface Cache 低频查询 | 每 4 到 8 帧 | 整体亮度稳定,细节少 |
实际写代码时,不要一上来就做“多级 Cache + 时序复用”。先把 Surface Cache 做成一整张低分辨率纹理,比如 256x144,每帧整张更新一次。跑通后再按更新频率切分。这样你能更容易判断画面闪烁是 Cache 更新频率太低,还是追踪本身噪声太大。
3.4 每个阶段的验收标准和调试手段
阶段的完成标准必须具体到“你能看到什么”,而不是“代码编译通过”。
| 阶段 | 完成标准 | 主要调试手段 |
|---|---|---|
| 渲染框架 | 能渲染带深度场景,能切换多个颜色输出 | 把法线、深度分别显示成颜色 |
| 屏幕空间追踪 | 反射轮廓正确跟随相机 | 绿色标记命中,红色标记未命中 |
| 距离场追踪 | 屏幕外物体能贡献间接光 | 显示步进次数,显示最终 SDF 距离 |
| Surface Cache | 光照变化时背光面颜色能跟着变 | 单独显示 Cache 低分辨率结果 |
| 最终合成 | 直接光和间接光叠加后画面不闪烁 | 分别显示 Direct 和 Indirect 通道 |
排查时记住一个原则:先降低变量。所有参数全部用最保守的默认值,调一个,看一个,不要同时改步长、阈值、分辨率和更新频率。一次改太多,出问题你根本不知道是哪个参数导致的。
4. 图形 API 选型、环境问题和典型坑点
4.1 OpenGL:算法验证快,但要注意现代特性
OpenGL 做实时 GI 原型完全没有问题。它虽然年老,但 compute shader、SSBO、MRT、Framebuffer 这些能力都够用。我甚至会建议第一版先用 OpenGL,因为在 OpenGL 里把一颗三角形的矩阵改对,比在 Vulkan 里把一个 pipeline 的 layout 配好要快得多。
热搜词里提到的glUniformMatrix4fv就是典型例子。它本身用法很简单,但方向感很容易错。OpenGL 约定矩阵存储为列主序,当你在 shader 里写mat4 mvp然后gl_Position = mvp * pos时,CPU 端传入数组的顺序必须是列优先。如果画面出现左右翻转、前后遮挡混乱,先打印 MVP 矩阵看看。
另一个小问题是glLineWidth。很多驱动只支持 1 像素宽的线段,你在做 Debug 线框时如果发现线段粗细没变化,不用怀疑代码,去驱动文档确认限制。要画粗线,可以用三角形带,或者用几何着色器把线段扩张成四边形。
4.2 WSL、Ubuntu 和 OpenGL 软件渲染的问题
WSL2 里跑图形程序很容易踩同一个坑:nvidia-smi能看到 GPU,但你的 OpenGL 上下文实际用的是 CPU 软件模拟,也就是 llvmpipe。这会导致所有渲染都极慢,你误以为自己的算法复杂度太高,其实根本没有使用 GPU。
快速检查方法是在终端里看渲染器名称:
glxinfo | grep "OpenGL renderer"如果输出是llvmpipe,说明当前上下文是软件渲染。可能的原因包括:WSLg 默认的 OpenGL 映射层不完整、环境变量LIBGL_ALWAYS_SOFTWARE被设置、X11 转发时缺少 GLX 扩展。建议优先启用原生 Vulkan 或确认宿主机的 DX12 映射层,并在 WSL 里安装完整 Mesa 驱动。
但说实话,如果你要跑复杂的实时全局光照,我建议不要在 WSL 里做核心开发。WSL 更适合做编译、脚本、日志分析和模型转换。图形调试还是在 Windows 宿主环境或原生 Linux 桌面上更顺手。省下的时间足够多写一个 Pass。
4.3 Vulkan:先单线程跑通,再谈异步和多队列
Vulkan 最大的门槛不是难,而是繁琐。验证层、render pass、pipeline barrier、descriptor set 这些概念叠加在一起,很多人还没开始写 GI 就放弃了。
我的建议是:前两周完全不要碰多线程。先在单线程里以最简单的方式渲染一帧:一个 command buffer、一个 render pass、一个 descriptor set。当你能把三角形渲染出来,并且验证层没有报错后,再逐渐引入 swapchain 重建、per-frame descriptor 同步和异步 compute。
Vulkan 里最常见的报错和排查顺序可以从这几条入手:
- 验证层报 image layout 错误:先看当前 pass 是否声明了正确的 initialLayout 和 finalLayout;
- Descriptor 数据不对:先检查 shader 里的 binding 序号和 CPP 里的 descriptor set 是否一一对应;
- 画面闪屏或黑屏:先查同步,不要直接怀疑算法;
- 性能突然下降:可能是你没有把 compute workgroup 个数设对,或者在主循环里创建了太多对象。
如果觉得直接写 Vulkan 成本太高,也可以先用一个带封装的教学框架打好底子。但注意,封装框架只解决“API 繁琐”的问题,不解决“算法理解”的问题。最终你还是要能回答出每一帧里,哪些资源在哪个阶段被谁读写。
4.4 Direct3D 12 和 Metal 的取舍
如果项目只面向 Windows,D3D12 的调试工具非常强。PIX 能直接抓 GPU 时序、资源状态和着色器信息,这对全局光照这类复杂渲染器来说很有价值。缺点是 D3D12 的资源状态转换、Root Signature、Descriptor Heap 都不简单,前期的工程负担比 Vulkan 还重。
Metal 在 Apple 生态里是唯一合理选择,学习曲线相对平滑。但如果你之前只写过 OpenGL,首次切换到 Metal 时要注意 Z 范围、纹理坐标原点、资源同步和参数缓冲的差异。Metal 的 GPU Frame Capture 非常好用,能直观看到每个 Pass 的输出。
我见过不少人在 6 个月里想同时学两个 API,结果两边都只跑了个三角形。如果你的目标是做 Lumen 第一帧,API 只是工具,不是学习主题。选一个自己机器上最顺手的,把时间花在追踪算法和缓存设计上。
4.5 环境检查清单:跑任何 Demo 前先确认这几项
如果 Demo 启动失败或渲染异常,先按顺序检查外部条件,不要急着改 shader。
- 驱动版本和设备信息:用
vulkaninfo --summary或glxinfo -B查看; - 编译链是否完整:确认 shader 编译日志没有任何错误;
- 窗口大小和交换链格式:确认初始化参数和渲染目标分辨率一致;
- 纹理和 Buffer 内存是否足够:大场景可以先降分辨率;
- 权限、目录、输入文件是否就位:加载模型失败时,绝大多数问题出在路径;
- 多 GPU 环境:确认实际使用的显卡,不是集显或转译层。
检查完后,再去看算法参数。很多“模型加载失败”根本不是模型文件坏了,而是相对路径在工作目录里没配好;很多“画面全黑”不是 GI 没实现,而是 framebuffer 没有绑定到正确 attachment。
5. 从 Demo 到能持续开发的工程习惯
5.1 资源生命周期要尽早规划
实时渲染器里最恶心的问题就是资源泄漏和生命周期混乱。常见场景是:你初始化时创建了 framebuffer,每帧渲染时又开始创建同一个纹理;或者窗口大小变化后,没有重建交换链和 framebuffer,导致画面拉伸、黑屏、报错。
建议从一开始就维护一张资源表,至少包含类型、创建时间、复用处、销毁条件:
| 资源类型 | 创建时机 | 主要用途 | 容易犯的错 |
|---|---|---|---|
| 交换链 | 初始化时 | 呈现画面 | 窗口 resize 后没重建 |
| Render Target | 初始化时 | 颜色/法线/深度输出 | 跟窗口分辨率绑定,导致拉伸 |
| SDF 纹理 | 模型加载时 | 距离场追踪 | 动态物体更新没暂停 |
| Surface Cache | 初始化时 | 间接光缓存 | 低分辨率更新但忘了上采样 |
不要等到功能写完再做资源管理。我见过很多项目第 5 个月开始稳定崩溃,原因不是算法,而是每帧创建了太多 descriptor 或 buffer,驱动内存被吃满。这个问题越早建模越容易解决。
5.2 帧耗时分析要从全屏 Pass 数量和分辨率入手
实时 GI 渲染器最常见的性能问题是“慢”和“卡”。慢通常是每次追踪步数太多或全屏 Pass 太多;卡通常是同步、资源等待或某个高频更新操作引起。
推荐用分层计时的方式定位:
- CPU 端:场景更新、CB 上传、command 录制耗时;
- GPU 端:光栅化、屏幕空间追踪、SDF 追踪、Cache 更新、上采样各占多少毫秒;
- 每帧总耗时:在当前窗口分辨率下是 16ms 还是 33ms,直接决定体验。
一个非常有用的经验是:先用半分辨率跑屏幕空间追踪和 SDF 追踪,再上采样到全分辨率。4K 下每多一个全屏 Pass,带宽压力都很明显;半分辨率 + 双线性上采样,在视觉上完全可接受,尤其是粗糙表面上的间接光,本来就不该有高频细节。
性能工具方面,RenderDoc、Nsight、PIX、Frame Capture 都可以用。但初学者最容易犯的错是:每遇到性能问题就打开工具看一把,然后不知道看什么。我的建议是先看“全屏 Pass 数量”和“单 Pass 内平均步数”,这两项占性能问题的七成。
5.3 常见报错和排查顺序
针对手搓 Lumen 这个主题,我整理了一套排查顺序:
- 画面全黑或花屏:先查 framebuffer、渲染目标绑定、shader 编译日志;再查是否真的渲染了场景,最后看资源状态和格式是否匹配。
- 反射错位:先查视角方向到屏幕方向的矩阵,再查深度采样时是否做了透视除法,最后看步长和方向是否归一化。
- 颜色溢出不对:先查命中点颜色采样顺序,再查命中点的 uv 是否有偏移,最后看间接光要不要乘材质颜色。
- 距离场追踪闪烁:先查 SDF 纹理格式和滤波方式,再查阈值,最后降低更新频率。
- 画面整体偏暗:先看直接光是否正常,再看间接光是否只有一次反弹,不要急着加到多次反弹。
- 在 WSL 下很慢:先确认是不是 llvmpipe,再检查资源传输是否走 PCIe,尽量避免频繁的 CPU-GPU 同步。
这套顺序的核心逻辑是“由外到内”:先排除环境、帧缓冲、资源状态这些容易背锅的东西,再进入算法内部。
5.4 如果 6 个月跑不完完整方案怎么办
到第 5 个月,你很可能发现 Radiance Cache 的调参难度超出预期。不要硬扛,启用降级方案完全合理:
- 不做动态 Cache,先烘焙一帧到 Cache,然后只在你手动按下按键时刷新;
- 不做多个物体的 SDF 合成,先只对场景里最大的静态物体生成 SDF;
- 不做间接镜面反射,只做漫反射 GI;
- 不做时序降噪,用一个 3x3 空间模糊代替,视觉上能去掉大部分点状闪烁;
- 不做多分辨率策略,直接全部半分辨率跑,先保证实时交互,再谈画质。
这个降级过程和做产品一样:先有最小可用版本,再逐步增加复杂度。Lumen 在真实引擎里也是由很多团队花大量时间打磨出来的,一个人 6 个月能把简化版跑通,已经非常能说明你对图形学基础的理解。关键是要清楚知道自己的方案在哪个环节做了简化,简化后视觉上少了什么,后续要往哪个方向补。
5.5 我认为 6 个月最值得注意的三件事
第一,不要追求“和 UE5 一模一样”。你只需要让光源颜色改动后,背光墙面出现实时颜色反光,这个里程碑比任何花哨参数都重要。只要这个闭环成立,Lumen 的核心逻辑你就已经摸到了。
第二,调试视图比最终画面更重要。整个项目里,真正帮你理解 Lumen 的往往是那些“绿色命中、红色未命中”的 Debug 画面,而不是最终合成美化后的结果。建议把 Debug 输出做成可切换模式,保留到最后一刻。
第三,坚持每个阶段都留一个可演示的版本。第 2 个月结束时有深度缓冲场景,第 3 个月结束时有屏幕空间反射,第 4 个月结束时有距离场追踪,第 5 个月结束时有间接光,第 6 个月则有完整链路。每个版本都能单独拿出来演示,这比一直在同一份代码上推翻重写要靠谱得多。
最后说一句经验之谈。我见过太多人刚开始就想着把 Lumen 的 Surface Cache、屏幕空间追踪、距离场、软件光栅化全部做出来,结果在第二个月因为管线问题卡住。更务实的方式是:先让一个光源照亮一个带深度缓冲的场景,再做屏幕空间追踪,再做距离场,最后把缓存和合成串起来。当你真的把光源颜色从白色改成暖黄色,看到隔壁墙面从黑色慢慢泛出黄光的那一刻,这 6 个月的所有投入就都值了。