Chapter 40: Photon Mapping
Photon Mapping 解决的核心问题,是把从光源出发的能量分布先记录下来,再在相机渲染阶段用这些记录估计间接光。读完本章后,读者应能追踪一条光子从灯光发射、穿过玻璃、落到漫反射表面、写入 photon map、再被 camera gather 读取的完整路径,并能判断焦散模糊、斑块噪声、内存增长和动态场景更新成本分别来自哪一段。
本章使用一个贯穿场景:一盏小面积强光照向玻璃球,玻璃球放在粗糙桌面上,相机观察桌面上的 caustics 高亮纹理。这个场景里,困难光路通常是 Light → Specular → Diffuse → Camera,也就是光先经过折射或镜面反射,再落到漫反射表面,最后被相机看到。普通 path tracing 从相机端随机采样漫反射方向时,很难命中经过玻璃后连接到小光源的方向,所以画面容易出现火点、缺失焦散或长时间噪声。
Photon Mapping 的工程价值来自两件事。第一,它把光源端路径作为可复用数据写入空间结构,让一次光子追踪服务许多相机采样点。第二,它用 density estimation 在着色点附近聚合光子能量,把难命中的路径转换成局部密度查询。Jensen 1996 年的 Global Illumination using Photon Maps 给出了经典两阶段结构,PBRT v3 的 Stochastic Progressive Photon Mapping 则把 photon map 放入路径空间与 progressive 更新语境中解释。
Photon Mapping 的代价同样明确。它引入半径、近邻数量、kernel 权重和存储上限这些参数,因此结果通常带有密度估计偏差;光子越少或半径越大,焦散越容易变厚;半径越小或光子分布越稀,画面越容易出现斑点。工程实现的重点,就是把光子发射、存储、查询、gather 和调试可视化拆成稳定管线,并用同一组证据判断画面问题来自采样、数据结构、材质交互还是估计参数。
40.1 Photon Mapping 算法原理与双阶段结构
Photon Mapping 的最小工作定义是:从光源端发射带通量的粒子样本,沿 BSDF 在场景中传播,在合适的表面交互点写入光子记录;随后从相机端发射射线,在可见着色点附近查询这些记录,用局部密度估计间接入射辐照度或出射辐射亮度。这里的 photon 是算法样本,记录的是一小份光通量、入射方向、位置和路径类型;它服务于渲染估计,和物理学中的单个真实光子数量概念保持区分。
贯穿场景中的玻璃球焦散可以按两阶段阅读。第一阶段从小面积光源发射许多 photon,光子进入玻璃球后发生折射,再落到桌面某个区域;桌面是漫反射表面,算法把位置、入射方向、剩余通量和路径标记写入 caustic photon map。第二阶段从相机射线找到桌面可见点,在这个点周围查询若干邻近光子,把这些光子的通量除以查询面积,再乘以桌面 BRDF,得到相机方向上的间接贡献。
这个流程可以用一张图定位数据路径。图中的 photon tracing 只处理光源端传播,camera gather 只处理可见点估计;两者通过 photon map 这个空间数据结构连接。
第一阶段的关键输入是光源采样策略、场景加速结构、材质 BSDF 和终止策略。每个 photon 至少需要携带四类数据:位置 position,入射方向 incidentDir,光通量 flux,以及路径类型标记。路径类型决定这个 photon 属于 caustic map、global map,还是后续调试使用的临时 map。玻璃球场景中,从光源出发后经过 specular 或 transmission 事件再命中 diffuse 表面的 photon,应进入 caustic map;经过漫反射多次反弹后记录的 photon,更适合进入 global map。
第二阶段的关键输入是相机可见点、表面法线、出射方向、材质 BRDF、查询半径或近邻数量。对桌面上的一个可见点 ,若在半径 内找到若干 photon,最简单的估计可以理解为:把这些 photon 的通量求和,除以圆盘面积 ,再按 BRDF 和入射方向权重换成相机方向的出射 radiance。实际实现会加入 kernel 权重、法线一致性检查、路径类型过滤和材质过滤,但主干仍是“局部能量密度 → 着色贡献”。
Photon Mapping 的双阶段结构带来一个稳定判断:第一阶段决定光子分布是否覆盖了真实高能量区域,第二阶段决定这些离散光子如何被平滑成像素贡献。焦散完全缺失时,优先检查 photon emission、specular path 标记和 caustic map 写入;焦散存在但过厚时,优先检查 gather 半径、近邻数量和 kernel;焦散边缘出现随机斑点时,优先检查 photon 数量、局部密度和 progressive 收敛策略。
最小 photon 记录可以写成下面的伪代码。示例展示的是数据结构边界,省略了场景求交、BSDF 采样和光源 PDF 的细节。
struct Photon {
Vec3 position;
Vec3 incidentDir;
Spectrum flux;
PhotonFlags flags;
};
struct GatherQuery {
Vec3 position;
Vec3 normal;
Vec3 outgoingDir;
float radius;
int maxPhotons;
};
这段结构把 Photon Mapping 的工程对象固定下来。position 决定空间索引;incidentDir 决定 BRDF 评估方向;flux 决定能量贡献;flags 决定该 photon 是否参与 caustic、global 或 debug 查询。GatherQuery 则把一次相机端估计所需的局部条件集中起来,后续调试时可以直接记录半径、命中数量和最终能量。
Photon Mapping 的数学近似可以用一个工程形式表达。设着色点为 ,出射方向为 ,半径内 photon 集合为 ,单个 photon 的入射方向为 ,通量为 ,材质 BRDF 为 ,则一个基础估计写作 。这个式子只说明数据路径:空间半径提供面积归一化,photon flux 提供入射能量,BRDF 把入射能量转成相机方向出射值。
这个估计的边界来自局部平滑假设。算法把着色点附近的 photon 当成该点入射光分布的近似,因此半径越大,估计越平滑,细节越容易被抹宽;半径越小,局部变化保留更多,稀疏区域的方差更明显。对玻璃球焦散这种高频光照结构,半径和 photon 密度直接决定边缘锐度。
40.2 光子存储数据结构与查询机制
Photon map 的数据结构要回答一个具体问题:给定相机着色点,如何快速找到附近的 photon,并且让返回集合的空间范围、数量和材质条件可控。这个问题处在算法性能和画质之间。查询越快,camera pass 的每个 shading point 成本越低;返回集合越稳定,density estimation 的噪声越低;空间结构越紧凑,内存和构建时间越可控。
经典 photon map 常用 kd-tree,因为 photon 是三维空间点,kd-tree 能按空间轴递归划分,把最近邻和半径查询从线性扫描降到更可接受的范围。对离线渲染器,构建一次 kd-tree 后在大量 camera samples 中复用,适合静态场景或光照不频繁变化的场景。玻璃球焦散场景中,如果桌面上存了数百万个 caustic photons,相机每个 hit point 只需要在局部查几十到几百个近邻,kd-tree 可以显著减少无关 photon 访问。
Hash grid 的优势在于构建与并行更新更直接。空间被划分成固定大小或层级网格,每个 photon 根据位置落入 cell,查询时只访问着色点附近的 cell。这个结构适合 GPU 原型、动态 photon 更新或半径相对固定的 caustics pass。它的代价是 cell size 直接影响查询质量:cell 太大时无关 photon 增多,cell 太小时邻域跨越多个 cell,查询实现和内存链表管理更复杂。
半径查询和 k-nearest 查询控制的是估计方式。固定半径查询先给出物理空间范围,再收集范围内所有 photon;它让焦散尺度更可控,适合调试半径对画面锐度的影响。k-nearest 查询先要求固定数量,再用第 k 个 photon 的距离决定局部支持范围;它能在稀疏区域自动扩大半径,在密集区域自动收窄半径。玻璃球焦散的亮斑中心通常 photon 密集,k-nearest 会得到较小半径;焦散边缘 photon 稀疏,k-nearest 会扩大半径,边缘因此更平滑。
下面的表格把三组工程选择放在同一维度比较,便于实现时定位取舍。
| 选择对象 | 主要输入 | 输出 | 适用场景 | 主要风险 |
|---|---|---|---|---|
| kd-tree | photon position | 近邻集合或半径集合 | 静态 photon map、离线 gather | 构建成本和指针访问开销 |
| hash grid | photon position、cell size | 邻近 cell 中的候选集合 | GPU 原型、固定半径查询、动态更新 | cell size 绑定画质和性能 |
| 固定半径 | 着色点、半径 | 半径内所有 photon | 控制焦散物理尺度、调试 blur | 稀疏区域样本数波动 |
| k-nearest | 着色点、近邻数量 | 固定数量 photon 与自适应半径 | 局部密度变化大 | 高亮边缘被自适应半径抹宽 |
| kernel 权重 | 距离、半径、kernel 类型 | 加权通量和 | 提高平滑质量 | 参数改变会影响亮度感知 |
查询机制还需要处理几何和材质边界。着色点附近的 photon 在三维距离上很近,仍可能位于另一侧墙面、桌面背面或不同材质区域。稳定实现会使用法线一致性、平面距离、材质标记和路径类型过滤。对桌面焦散,若查询半径穿过桌面边缘或薄几何,来自背面的 photon 会污染结果;如果 caustic photon 和 global photon 混用,焦散高频结构会被漫反射间接光稀释。
一次 gather 查询可以按下面顺序执行。第一,空间结构返回候选 photon;第二,按法线夹角、路径类型和半径过滤;第三,按 kernel 计算权重;第四,把 photon flux 经 BRDF 转换为出射贡献;第五,记录命中数量、最大距离和能量和,用于 debug visualization。这个顺序使查询既服务画面,也服务诊断。
Spectrum estimatePhotonContribution(const GatherQuery& query, const Material& material) {
PhotonSet photons = photonMap.findNearby(query.position, query.radius, query.maxPhotons);
Spectrum sum = Spectrum(0.0f);
for (const Photon& photon : photons) {
if (!isCompatible(photon, query.normal, material)) {
continue;
}
float weight = kernelWeight(distance(query.position, photon.position), query.radius);
Spectrum brdf = material.evaluateBrdf(-photon.incidentDir, query.outgoingDir, query.normal);
sum += weight * brdf * photon.flux;
}
return sum / areaOfDisk(query.radius);
}
这段伪代码的关键点在过滤和归一化。isCompatible 保护几何与材质边界;kernelWeight 决定距离越远的 photon 如何衰减;areaOfDisk 把局部通量转换成面积密度。若实现使用 k-nearest,query.radius 可以替换为第 k 个 photon 的距离;若使用 anisotropic kernel,还需要用切线空间或局部协方差描述查询区域。
数据结构调试应直接输出可观察图层。第一张图层显示 photon hit positions,用点云颜色区分 caustic、global 和 shadow。第二张图层显示每个 camera hit 的 gathered photon count。第三张图层显示 gather radius 或第 k 近邻距离。第四张图层显示 photon contribution heatmap。四张图层连起来,可以把“焦散不好看”拆成光子没有落到正确位置、查询没有找到足够光子、半径参数过大、或者 BRDF 转换出了能量问题。
40.3 构建 Photon Map 管线
构建 Photon Map 管线时,目标是让每个阶段都能单独检查输入、输出和失败症状。贯穿场景可以拆成五段:light photon emission、surface interaction、caustic/global map 写入、gather pass、debug visualization。每段的输出都应能落到 buffer、map、统计计数或图像 overlay 上,否则画面问题会在最后像素阶段混在一起。
Light photon emission 的任务是从光源按功率分布采样 photon 的初始位置、方向和通量。对点光源或小面积光源,初始方向分布直接决定光子是否有机会穿过玻璃球。简单均匀球面发射会把大量 photon 投向无关区域;面向焦散调试时,可以在保持权重正确的前提下,把 emission 方向限制到玻璃球包围体所在的立体角,再用对应 PDF 修正 flux。这样做改变的是采样效率,物理能量仍由光源功率和 PDF 决定。
Surface interaction 的任务是沿场景 BVH 追踪 photon,并按 BSDF 决定反射、折射、漫反射或吸收。每次交互都应更新 photon throughput,也就是 photon 当前携带的剩余通量。Russian roulette 可以在路径较长或能量较低时终止追踪,同时用存活概率修正权重。玻璃球焦散场景中,specular transmission 事件需要保留路径标记,后续命中桌面漫反射面时才能写入 caustic map。
Caustic map 和 global map 的拆分能让 gather 更可控。Caustic map 存储经过 specular 或 glossy 事件后命中 diffuse 表面的 photon,服务高频焦散。Global map 存储更普通的多次漫反射间接光,服务低频 color bleeding 和柔和间接照明。两者使用不同 photon 数量、查询半径和 filter 参数。对桌面焦散,caustic map 应使用较密集光子和较小半径;global map 可以使用更平滑的估计,因为漫反射间接光通常变化较慢。
构建流程可以用以下伪代码组织。这段伪代码只固定各阶段的责任边界,完整渲染器还需要补齐求交、采样器、材质系统和资源管理。
void buildPhotonMaps(const Scene& scene, int photonCount) {
PhotonBuffer causticPhotons;
PhotonBuffer globalPhotons;
for (int i = 0; i < photonCount; ++i) {
PhotonRay ray = samplePhotonFromLights(scene.lights);
Spectrum throughput = ray.initialFlux;
PathFlags flags = PathFlags::None;
for (int bounce = 0; bounce < MaxPhotonBounces; ++bounce) {
SurfaceHit hit = scene.intersect(ray);
if (!hit.valid) {
break;
}
if (hit.material.isDiffuse()) {
Photon photon{hit.position, -ray.direction, throughput, flags};
if (flags.hasSpecularEvent()) {
causticPhotons.push(photon);
} else {
globalPhotons.push(photon);
}
}
BsdfSample sample = hit.material.sampleBsdf(-ray.direction, hit.normal);
if (!sample.valid) {
break;
}
flags.update(sample.eventType);
throughput *= sample.weight;
ray = PhotonRay(hit.offsetPosition(sample.direction), sample.direction, throughput);
if (shouldTerminate(throughput, bounce)) {
break;
}
}
}
causticMap.build(causticPhotons);
globalMap.build(globalPhotons);
}
这段代码把三类错误暴露出来。若 flags.hasSpecularEvent() 更新错误,玻璃球产生的 photon 会进入 global map,caustics gather 会缺少应有数据。若 sample.weight 没有包含 BSDF、cosine 和 PDF 关系,photon flux 会系统性偏亮或偏暗。若 offsetPosition 处理粗糙,photon 可能在玻璃或桌面表面自相交,形成异常密集点或路径提前终止。
Gather pass 的职责是把 photon map 接入 camera rendering。相机射线命中桌面后,直接光、镜面反射和折射仍可由常规 ray tracing 处理;caustic 和部分 diffuse indirect 由 photon map 提供。这个混合方式让 Photon Mapping 服务它擅长的困难光路,同时让直接光阴影保持清晰。工程上可以把最终像素拆成 direct + specular + causticGather + globalGather,每一项单独输出 debug buffer。
Debug visualization 是 Photon Mapping 管线的必备阶段。它至少应包含六个统计量:发射 photon 总数、命中 diffuse 的 photon 数、写入 caustic map 的 photon 数、写入 global map 的 photon 数、每个 shading point 的 gather count、每个 shading point 的估计能量。玻璃球焦散场景中,如果 caustic map 写入数很低,问题在 emission 或 specular path;如果写入数足够但 gather count 很低,问题在空间查询或半径;如果 gather count 足够但能量异常,问题在 flux、BRDF 或面积归一化。
下面的管线检查顺序可复用到其它 Photon Mapping 实现中。先渲染 photon hit point cloud,确认光子落在预期区域;再查看 caustic/global 分类,确认路径标记正确;再关闭直接光,仅显示 caustic gather,确认贡献来自 photon map;再调半径和 photon count,观察焦散锐度与斑点变化;最后恢复完整 lighting,检查 caustic 是否与直接阴影、玻璃折射和桌面材质一致。
这个顺序的价值在于分层验证。Photon Mapping 的画面错误通常同时来自采样、路径状态、空间查询和材质评估的组合。管线拆开后,每一段都有明确证据:点云位置证明 emission 和 tracing,map 分类证明 path flags,gather count 证明查询,contribution heatmap 证明密度估计,最终 beauty pass 证明组合结果。
40.4 与 Path Tracing 的比较优势
Photon Mapping 和 Path Tracing 的对比应放在光路采样效率上。Path Tracing 从相机端逐步扩展路径,优势是实现统一、样本叠加自然、内存增长主要来自采样次数和路径状态。Photon Mapping 从光源端预先生成可复用样本,优势是对光源端容易发现、相机端难命中的路径更有效,例如小光源经过玻璃或金属聚焦到漫反射表面的 caustics。
玻璃球桌面场景给出最直接的差异。相机射线命中桌面后,若从桌面漫反射方向随机采样,很大概率无法刚好选择一个经过玻璃折射后连到小光源的方向。Path Tracing 可以通过更多样本、双向方法、路径引导或 MLT 改善,但普通单向 path tracing 在这种 SDS 型困难路径上会产生高方差。Photon Mapping 从光源出发,光子穿过玻璃后自然落到桌面,再由密度估计提供局部贡献,因此 caustics 更容易成形。
比较优势可以按同一组维度整理。
| 维度 | Photon Mapping | Path Tracing |
|---|---|---|
| 困难焦散 | 光源端 photon 能主动覆盖 specular 后的 diffuse 区域 | 相机端随机方向命中概率低 |
| 小光源间接光 | 通过 light emission 集中采样提高 photon 密度 | 需要直接采样、MIS 或大量样本支撑 |
| diffuse GI | global map 可复用,但低频区域常用较大半径 | 噪声随 samples per pixel 收敛 |
| 内存成本 | 存储 photon map,数量上限受内存影响 | 主要增加计算时间,增量存储较少 |
| 偏差特征 | 密度估计带来 blur 和 bias | 无偏估计语境下主要表现为方差 |
| 动态场景 | 光源、几何或材质变化可能要求重建 photon map | 每帧重新采样路径,结构更新相对直接 |
| 调试入口 | photon 点云、半径、近邻数、贡献热力图 | sample path、variance、firefly、denoiser 辅助量 |
这个对比应回到目标效果和场景更新方式。当目标是电影级离线 caustics、透明材质聚光、局部高能量纹理时,Photon Mapping 的 light-side reuse 很有价值。当目标是动态镜头、复杂材质统一处理、分布式路径效果和生产管线一致性时,Path Tracing 的统一估计模型更容易维护。很多系统把两者组合:直接光和常规间接光用 path tracing,焦散分量用 photon mapping 或 photon guiding。
Photon Mapping 的 memory-bias 取舍需要被正面接受。光子存储越多,局部估计可使用更小半径,焦散越锐利;存储有限时,半径必须增大以获得稳定样本数,焦散就会变厚。Progressive Photon Mapping 和 Stochastic Progressive Photon Mapping 通过多轮迭代逐步收缩半径、累计通量,缓解一次性存储大量 photon 的压力。它们把“存满一个巨大 photon map”的问题转成“多轮 photon pass 累积并更新估计”的问题。
Path Tracing 的 firefly 和 Photon Mapping 的 blotch 是不同症状。firefly 通常是少数高贡献路径被随机采到后形成孤立亮点;blotch 通常是 photon 密度不足、半径过大或局部估计不稳定形成的斑块。玻璃球焦散测试中,如果普通 path tracing 画面出现少数极亮噪点,说明困难路径偶尔被采到;如果 Photon Mapping 画面形成大片浑浊亮斑,说明 photon gather 半径或核估计正在过度平滑。
动态场景适配是 Photon Mapping 的工程边界。只移动相机时,静态 photon map 可以复用,camera gather 重新执行即可。移动玻璃球、桌面、光源或改变材质时,photon hit distribution 已经失效,需要重新发射和构建。实时渲染里,Photon Mapping 常被压缩成局部 caustics pass、screen-space gather、light-space buffer 或 probe/radiance cache 方案。它们保留“从光源端记录能量分布”的思想,但牺牲部分物理完整性换取帧率和可控内存。
因此,选择 Photon Mapping 的判断顺序应是:先判断目标效果是否包含 light-specular-diffuse 的困难路径;再判断场景是否允许预计算或局部重建 photon map;再判断内存是否能容纳目标光子密度;再用 half radius、double photons、change kernel 这类实验观察焦散是否按预期变锐、变稳或变暗。若这些实验仍然无法解释画面变化,问题往往在 photon flux 权重、BSDF 事件分类或材质能量守恒上。
40.5 利用 Photon Mapping 实现 caustics 效果
Caustics 是 Photon Mapping 最能体现价值的目标效果。它来自光线被玻璃、水、金属或其它 specular/glossy 表面改变方向后,在某个接收面上形成局部能量聚集。贯穿场景中,小面积光源发出的光经过玻璃球折射,许多路径集中到桌面弯曲区域,camera ray 命中桌面时看到亮纹。这个亮纹的形状由折射几何决定,亮度由光源功率、透射损耗、photon density 和 BRDF 共同决定。
实现 caustics 时,第一步是单独构建 caustic photon map。路径标记规则要清楚:photon 从 light 发射后,只要经历过 specular reflection、specular transmission 或足够窄的 glossy event,后续首次命中 diffuse 接收面时就可以写入 caustic map。若 photon 已经在漫反射表面散射多次,它更适合进入 global map。这个规则让 caustics map 专注高频聚焦能量,减少普通间接光对焦散纹理的稀释。
第二步是让 photon emission 对准可能产生 caustics 的物体。玻璃球占据的立体角通常远小于整个场景,完全随机发射会浪费大量 photon。工程实现可以对 caustic 生成物体建立 light-space target bounds,从光源向这些 bounds 采样方向,同时记录采样 PDF,用它修正 photon flux。这样既提高玻璃球附近的 photon 密度,又保持能量估计可解释。
第三步是把接收面上的 photon 点云可视化。调试时先隐藏最终 shading,只显示落在桌面上的 caustic photons。正确点云应该沿焦散纹理聚集,亮区 photon 密集,暗区 photon 稀少。若点云散布在整个桌面,说明光子穿过玻璃后的方向分布没有形成聚焦;若点云集中在玻璃内部或背面,说明折射、法线方向或表面偏移存在错误;若点云完全缺失,说明路径分类、材质透射或光源采样入口有问题。
第四步是调整 gather 半径和 photon 数量。半径控制焦散纹理宽度,photon 数量控制局部估计稳定性。稳定实验可以固定光源和材质,先把 photon 数量提高到较大值,观察小半径下的形状是否正确;再逐步降低 photon 数量,观察斑点如何出现;最后固定 photon 数量,调整半径,观察纹理从锐利到厚重的变化。这个实验能把“形状错误”和“估计噪声”分开。
第五步是检查能量归一化。caustics 变亮时,常见原因是 photon flux 没有除以发射 photon 数、光源采样 PDF 未进入权重、折射事件没有乘透射项、或者 gather area 使用错误。caustics 变暗时,常见原因是 Russian roulette 没有权重补偿、路径标记没有写入 caustic map、BRDF 评估方向反了,或者 query 过滤过严。最可靠的调试方式是把每个 photon 的 flux 以 false color 显示,并在 gather 处输出 sumFlux、area、brdfFactor 和最终贡献。
可以把 caustics 实现压成以下检查表。每一项都对应一个可观察结果,方便在离线渲染器、教学 renderer 或 GPU prototype 中复用。
| 检查对象 | 正常证据 | 失败症状 | 修正方向 |
|---|---|---|---|
| 光源发射 | photon ray 指向玻璃球所在区域 | 大量 photon 离开场景 | 检查 emission 方向 PDF 与 target bounds |
| 折射交互 | photon 穿过玻璃后方向连续 | 点云卡在表面或内部 | 检查法线朝向、IOR、ray offset |
| 路径标记 | caustic map 数量随玻璃球存在而增加 | caustic map 接近空 | 检查 specular/transmission flag |
| 接收面写入 | 桌面点云沿焦散形状聚集 | 点云随机散布 | 检查 BSDF 采样和几何求交 |
| gather 半径 | 半径缩小后边缘更锐 | 半径变化与画面无关 | 检查查询结构和半径单位 |
| photon 数量 | 数量增加后斑点减少 | 增加数量仍然异常 | 检查 flux 权重和 map 分类 |
| 能量输出 | contribution heatmap 与点云密度一致 | 局部爆亮或全局偏暗 | 检查面积归一化、BRDF、PDF |
玻璃和金属焦散的差异在路径事件上。玻璃球主要依赖 specular transmission,入射光经过折射后形成聚焦;金属弯曲面主要依赖 specular reflection,反射方向集中到接收面。两者都可以进入 caustic map,但材质事件、法线方向和 Fresnel 权重不同。实现时应把 event type 写进 photon flags,调试时分别显示 reflection-caustic 和 transmission-caustic,以免两个效果混合后难以定位问题。
焦散还需要和最终材质组合。桌面如果是纯 Lambertian,caustic gather 直接乘漫反射 BRDF 后进入出射 radiance;桌面如果有 rough glossy 分量,caustic 的相机方向响应会随 BRDF lobe 变化。这里要区分 photon map 提供的是入射能量分布,材质仍然决定相机看到多少。把 caustic contribution 直接加到像素颜色而跳过 BRDF,会让接收面材质失去控制,也会破坏能量关系。
最近一些研究继续围绕 photon density 和 caustics 质量展开,例如 Deep Photon Mapping 用学习到的 kernel 改善粒子聚合,Online Photon Guiding with 3D Gaussians for Caustics Rendering 用引导方法提升 caustic photon 密度。这些方向说明 Photon Mapping 的核心困难仍然集中在两处:如何把更多光子投向有贡献的区域,如何把局部 photon 分布稳定转换成可见 radiance。工程实现本章的经典版本时,也应围绕这两处建立调试证据。
最终判断 caustics 是否合格,应超过“亮斑是否出现”这一层。应同时检查四个层级:光子点云是否落在接收面正确区域,gather 半径是否解释焦散锐度,flux 归一化是否解释亮度,材质 BRDF 是否解释相机方向上的可见结果。四个层级全部闭合时,Photon Mapping 的 caustics 才从“看起来有光斑”变成可复盘、可调参、可迁移的渲染管线。
最小自检任务
给定一个教学 renderer 场景:小面积白色光源照射玻璃球,玻璃球下方是灰色漫反射桌面,相机从斜上方观察桌面。你已经实现 photon emission、折射、caustic photon map、kd-tree 查询和 camera gather,但最终画面中桌面焦散非常模糊,并且亮度比预期低。请按本章方法列出排查顺序,说明每一步要观察的管线阶段、资源或调试图层,以及不同观察结果分别指向什么问题。
答案要点
先单独显示 caustic photon point cloud,确认 photon 是否穿过玻璃球并落在桌面焦散区域。若点云没有聚集到桌面,问题在 light emission、折射事件、法线方向、IOR 或 ray offset;若点云正确聚集,说明第一阶段光子追踪主路径成立。
再检查 caustic map 写入数量和路径标记。若玻璃球存在时 caustic map 数量很低,说明 specular/transmission flag 没有正确保留,或者 diffuse 接收面写入规则过严;若数量充足,继续检查查询阶段。
随后输出每个 camera hit 的 gather count、实际查询半径和第 k 近邻距离。若 gather count 足够但半径很大,模糊来自密度估计支持域过宽;若半径缩小后出现强斑点,说明 photon 数量或 emission 引导不足;若半径变化对画面几乎没有影响,说明 kd-tree 查询参数、单位或 shader 读取路径可能没有生效。
接着检查能量路径。输出 sumFlux、area、brdfFactor 和最终 caustic contribution。亮度偏低通常来自 photon flux 归一化错误、光源 PDF 权重过小、Russian roulette 没有存活权重补偿、BRDF 方向评估错误或 query 过滤删除了有效 photon。若 sumFlux 正常但最终贡献偏低,应重点看 BRDF、法线和面积归一化。
最后恢复完整 beauty pass,分别打开 direct、specular、causticGather 和 globalGather。若 caustic 单独正确但合成后偏弱,问题在 tone mapping、曝光、材质混合或 pass 权重;若 caustic 单独已经模糊偏暗,问题仍在 photon density、query radius、kernel 或 flux 权重。完整排查顺序应从 photon 点云到 map 分类,再到 gather 统计,再到能量分解,最后进入最终合成。
本章知识点总结
- 双阶段结构:Photon Mapping 先从光源端追踪并存储 photon,再从相机端查询 photon map 完成局部密度估计。
- 贯穿光路:玻璃球焦散的关键路径是光源经过 specular 或 transmission 事件后落到 diffuse 接收面,再被相机看到。
- Photon 记录:一个 photon 至少需要位置、入射方向、通量和路径标记,才能支持空间查询、BRDF 评估和 map 分类。
- 密度估计:Gather pass 把着色点附近的 photon flux 转成面积密度,再通过材质 BRDF 得到相机方向贡献。
- 半径取舍:查询半径越大结果越平滑,焦散细节越厚;查询半径越小细节越锐,稀疏区域越容易出现斑点。
- 空间结构:kd-tree 适合静态 photon map 的近邻查询,hash grid 适合固定半径、并行构建和动态原型。
- Map 拆分:Caustic map 服务高频聚焦能量,global map 服务低频漫反射间接光,二者应使用不同查询参数。
- 管线证据:Photon hit point cloud、map 分类、gather count、radius heatmap 和 contribution heatmap 能把画面问题拆回具体阶段。
- 路径标记:Specular 和 transmission 事件需要写入 flags,否则焦散 photon 可能进入错误 map 或被 gather 阶段过滤。
- 能量归一化:Photon flux 必须和光源功率、采样 PDF、路径 throughput、Russian roulette 和 gather area 保持一致。
- 对比优势:Photon Mapping 对 light-specular-diffuse 的困难焦散路径更有效,Path Tracing 在统一采样和动态更新上更直接。
- 内存边界:经典 photon map 受存储数量限制,progressive 方法通过多轮发射和半径更新缓解一次性存储压力。
- 动态边界:相机移动可以复用静态 photon map,光源、几何或材质变化通常要求重新发射和重建。
- Caustics 判断:合格焦散需要点云位置、查询半径、通量归一化和材质响应四个层级同时闭合。