Skip to main content

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、查询半径或近邻数量。对桌面上的一个可见点 xx,若在半径 rr 内找到若干 photon,最简单的估计可以理解为:把这些 photon 的通量求和,除以圆盘面积 πr2\pi r^2,再按 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 的数学近似可以用一个工程形式表达。设着色点为 xx,出射方向为 ωo\omega_o,半径内 photon 集合为 Pr(x)P_r(x),单个 photon 的入射方向为 ωi\omega_i,通量为 Φi\Phi_i,材质 BRDF 为 frf_r,则一个基础估计写作 Lo(x,ωo)iPr(x)fr(x,ωi,ωo)Φi/(πr2)L_o(x, \omega_o) \approx \sum_{i \in P_r(x)} f_r(x, \omega_i, \omega_o) \Phi_i / (\pi r^2)。这个式子只说明数据路径:空间半径提供面积归一化,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-treephoton position近邻集合或半径集合静态 photon map、离线 gather构建成本和指针访问开销
hash gridphoton 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 MappingPath Tracing
困难焦散光源端 photon 能主动覆盖 specular 后的 diffuse 区域相机端随机方向命中概率低
小光源间接光通过 light emission 集中采样提高 photon 密度需要直接采样、MIS 或大量样本支撑
diffuse GIglobal 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 处输出 sumFluxareabrdfFactor 和最终贡献。

可以把 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 读取路径可能没有生效。

接着检查能量路径。输出 sumFluxareabrdfFactor 和最终 caustic contribution。亮度偏低通常来自 photon flux 归一化错误、光源 PDF 权重过小、Russian roulette 没有存活权重补偿、BRDF 方向评估错误或 query 过滤删除了有效 photon。若 sumFlux 正常但最终贡献偏低,应重点看 BRDF、法线和面积归一化。

最后恢复完整 beauty pass,分别打开 directspecularcausticGatherglobalGather。若 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 判断:合格焦散需要点云位置、查询半径、通量归一化和材质响应四个层级同时闭合。