先聊个现象:把一束阳光透过装满水的玻璃杯投到桌面上,墙上会出现一道弯弯的亮纹,水里还会泛起一缕缕打转的光丝。又比如游泳池底那张不断晃动的光网,或者一枚玻璃戒指在纸上投出的一圈圈高亮光斑。这些亮得出奇、边界清晰又带着明显聚集效果的光影,就是焦散(Caustics)。
做渲染的朋友对这个词应该不陌生,尤其当你第一次用路径追踪渲染玻璃器皿,兴致勃勃地把采样数拉到5000,结果桌面上的高光还是布满彩噪,怎么调都压不下去——那一刻你才算真正认识了焦散。它不是什么边缘特效,也不是后处理能糊弄过去的装饰元素,它是光的真实输运结果。正因如此,它成了离线渲染器里最考验算法功底的一类效果,也成了实时光栅化渲染几乎绕不开的难题。
这篇文章我会从物理本质、主流算法、实际案例到排查心得,把这几年我在项目里和焦散“搏斗”的经验完整摊开。适合刚上手Blender、Mitsuba等离线渲染工具的新手,也适合准备用实时方案模拟焦散效果的技术美术。你不需要一上来就懂辐射度量学,但我会尽量把原理讲得足够清楚,让你踩坑之后不是瞎调参数,而是能推导出问题出在哪一环。
1. 焦散到底是什么:一个常见又难搞的光学现象
1.1 焦散的物理本质
先别急着开渲染器,我们把“焦散”这个词拆开。它对应的英文Caustics来源于希腊语“καυστός”,意思是“燃烧的”。这个命名不是没来由的——早在古希腊,阿里斯托芬的剧作里就提到用装水的玻璃球聚光生火。你用手电筒照一个装满水的球形烧瓶,就能看到光线被“拧”成一束,落在纸上形成极亮的点,温度足够让纸冒烟。这就是聚焦。
焦散的物理本质,其实就是光的能量在传播过程中被折射或镜面反射重新分配,形成局部能量密度剧烈升高的区域。直白点说,光本来是均匀地往前跑的,结果穿过一个透明球体后,不同位置的光线被弯折到几乎同一个方向,在这个方向上能量密度骤然拉升,视觉上就出现一条细亮的线或一片亮斑。
这跟普通的反射镜面高光不一样。镜面高光是光线直接从光源打到光滑表面、以等于入射角的角度弹进眼睛形成的,它在观察者位置不变时通常是一个清晰的小点。而焦散是光线经过至少一次折射或反射之后再打到某个漫反射面上,形成的光斑位置和形状取决于透明体的曲率、折射率和光源形状。更关键的是,焦散的能量密度可以比入射光高出好几个量级,这也解释了为什么同样一盏灯照在普通白墙上只有灰白一片,一旦中间隔了一个玻璃球,桌面就可能出现刺眼的亮纹。
如果把这个问题用渲染术语表达:焦散是辐射度(Radiance)在空间上的聚焦现象。它天然带有高频特征——光斑的亮度分布非常锐利,尺度很小、对比度极高。这种高频特征正是蒙特卡洛渲染器最害怕的东西。
1.2 为什么说焦散是渲染器的“硬骨头”
要理解焦散为什么难渲染,得先知道路径追踪是怎么工作的。
路径追踪的原理是从摄像机出发,向每个像素发射大量光线,光线在场景里弹射,每撞到一个表面就根据该表面的BSDF采样一个新的方向,直到命中光源。这个过程等价于在“光传输路径空间”里做蒙特卡洛采样,把所有样本的亮度加权平均得到像素颜色。
问题在于,焦散路径的结构很特殊:光源 → 镜面/透明体折射面 → 漫反射面 → 摄像机。在这种路径里,光必须在玻璃表面发生一次“定向折射”,然后恰好打到摄像机对着的那片漫反射区域。如果你从摄像机出发随机采样方向,想精确命中那条“折射后恰好指向光源”的方向,概率极低。
打一个比方:你站在操场上,闭着眼睛朝四面八方扔飞镖,要扔中远处看台上某个固定座位,这概率基本是零。传统的路径追踪就是在干这件事——它在每块玻璃表面随机选折射或反射方向,要恰好连到光源上的概率太低,于是焦散区域全是黑色噪点,只有把采样数拉到几万次,噪点才勉强收敛。
正是因为这种“概率性盲选”的低效性,焦散成为了经典路径追踪的短板。当然,现代渲染器已经用各种手段缓解这个问题,比如双向路径追踪、光子映射、MLT,后面我们会逐一展开。但要记住一个总原则:焦散的难度不在“光多不多”,而在“找到那条正确的光路难不难”。
1.3 焦散的分类:反射焦散与折射焦散
在动手渲染之前,先建立两个基本分类,后面排查问题会非常有用。
第一类是反射焦散(Reflective Caustics)。光线经过镜面反射后聚集在某个平面上。最经典的例子是一个凹面镜,平行光射上去会被反射到焦点附近,形成一片亮斑。汽车后视镜、不锈钢碗的内壁、电镀金属装饰件附近的亮纹,都是反射焦散。
第二类是折射焦散(Refractive Caustics)。光线穿过透明介质,因为折射率变化而弯曲,聚集在某处形成光斑。这是最常见也最出效果的一类——玻璃球、水晶吊坠、水面波纹、一玻璃杯水在阳光下投影,全属于这一类。水的波纹会让焦散形成不断移动的网状光斑,因为水面每个微小曲面都在动态改变光线的偏折方向。
真实场景里经常两种焦散同时出现。一个装了水的玻璃杯,杯壁外表面产生反射焦散,杯内水面和杯体产生折射焦散,两套光斑叠加在一起,才形成了那幅让人着迷的画面。渲染时,如果你只开了折射焦散而忽略了反射焦散,或者反过来,结果都会显得“差一口气”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 渲染焦散的主流技术路线:从光子映射到路径追踪
2.1 光子映射:专门为焦散而生的方案
在路径追踪还没成为绝对主流的年代,渲染焦散的第一利器是光子映射(Photon Mapping),由Henrik Wann Jensen在1996年提出。它的思路和路径追踪正好相反——不从摄像机出发,而是从光源出发。
光子映射分两阶段。第一阶段,从光源发射大量“光子”,让它们在场景里四处弹射。每撞到一个表面,要么被吸收,要么被反射/折射,一直追到能量耗尽或达到最大弹射次数。所有光子的落点被记录在一个光子图(Photon Map)里。
第二阶段,用传统的路径追踪把摄像机视图渲染出来,但在着色时,需要额外查询光子图:对于一个已知着色点,把它附近的N个光子找出来,用这些光子的能量密度估算该点的入射辐射度。光子密集的区域,亮度自然就高,焦散也就自然显现。
为什么要单独提它?因为光子映射处理焦散有个独门优势:光子从光源出发,沿着玻璃折射的方向走,它们天生就在“正确的光路”上。高概率事件,样本自然多,焦散区域的光子密度天然比普通漫反射区域高,渲染结果也就干净得多。这跟路径追踪“用概率硬撞”的思路完全相反。
当然,光子映射也有自己的问题。第一是内存开销——几百万个光子存在八叉树或KD树里,内存压力不小。第二是有偏性,实际渲染会用一个固定半径去统计附近光子,半径大了焦散糊成一片,半径小了又会出现一大堆亮斑“火斑”(Fireflies)。这也是为什么后来出现了渐进光子映射(PPM)和随机渐进光子映射(SPPM),它们不断缩小统计半径,在迭代中逼近无偏结果。Blender Cyclos和V-Ray里提供的光子焦散算法,多少都继承了这个思想。
2.2 路径追踪、BDPT 与 MMLT
现在的主流离线渲染器默认的其实是基于路径追踪的系列算法,它们也在不断进化。先说普通路径追踪(PT)。
普通PT对焦散的低效主要发生在“漫反射面 → 折射体 → 光源”这段路径上。BDPT(双向路径追踪)在这个方向上有明显改进。BDPT同时从光源和摄像机各生成一条子路径,然后尝试把两条子路径的端点连接起来,构成一条完整的光传输路径。这样一来,光源路径里的折射部分已经先于连接过程被采样出来了。连接时只要把摄像机子路径的最后一个顶点和光源子路径的第一个顶点连上,就能得到一条完整的折射焦散路径。
听起来BDPT能完美解决焦散?实际上并没有。BDPT的顶点连接过程依然需要一定概率才能成立。对比较简单的场景——一个玻璃球、一个地面、一个点光源——BDPT能有不错的效率。但换成复杂的半透明水杯、多层折射、焦散多次弹射的场景,BDPT的连接策略会变得极其复杂,效率反而可能低于纯PT,因为它要把大量时间浪费在连接失败的路径上。
MMLT(Metropolis Light Transport,大都市光输运)是另一条路线。它基于马尔可夫链的思想——既然“正确光路”的出现概率极低,那我找到一条有效的采样路径后,就在它附近做微小扰动,探索附近的“高贡献路径”空间。焦散路径是一簇一簇的,光斑附近一定存在大量对像素亮度贡献高的相似路径。MLT在初始路径之外不断变异出新的路径,能极大提升焦散这类低概率高贡献路径的采样效率。
代价是MLT容易引入相关性,导致同一帧图像局部区域出现颗粒状的相关性噪声,而且参数调起来更麻烦。它一般作为最终渲染的备选方案,而不是默认方案。
2.3 实时渲染里的焦散怎么解决
实时渲染没法直接跑光子映射或MLT这种重计算方案,所以业内更多走“近似”和“预处理”路线。
最实用的一套是屏幕空间焦散(Screen-Space Caustics)。原理很简单——先渲染一张从光源视角看的深度图,拿到场景的几何信息;再对每个像素,根据附近深度变化估算曲率,判断光线是否被折射汇聚。把汇聚强度当成焦散亮度叠加到最终画面里。这种办法速度快,适合水底光斑、波纹焦散这类效果,但没法处理反射焦散和多次折射,因为屏幕空间本身只对单层表面有效。
另一条路线是光子映射的GPU化。CryEngine、Unity的HDRP等引擎都做过把光子追踪放到Compute Shader里跑的方案。大致逻辑是:在GPU上发射光子,把光子位置、方向、能量写到缓冲区;渲染时再对光子图做密度估计。优点是可以离线渲染器画质看齐,缺点是场景复杂度和光子数量受GPU带宽限制,通常只能维持几十万到一两百万个光子,对大面积焦散还算够,对精度要求极高的特写就吃力了。
还有一条更取巧的路:烘焙。如果焦散的位置基本固定,比如一个静态玻璃摆件配一盏固定灯,就可以离线烘焙一张光照贴图,把焦散信息直接写到贴图里运行时读取。这种方法成本最低、画质最高,是很多手游和建筑可视化的首选。代价是不能有动态光源,物体一动焦散就“破功”。
3. 实操案例:在 Cycles 里一步步复现经典焦散
3.1 场景搭建与灯光参数
下面进入实操。我用Blender + Cycles做一个最经典的焦散案例:一个玻璃球放在粗糙桌面上,上方一盏小面积聚光灯照射,桌面出现明亮折射焦散和一道柔和的反射焦散。
为什么要用Cycles而不是Eevee?Cycles是光线追踪渲染器,光子映射和路径追踪的算法都在内核里,不需要额外插件就能出焦散。Eevee是光栅化渲染器,默认不支持真正的焦散,硬要出效果得用屏幕空间焦散插件或者烘焙,不是这个案例的重点。
打开Blender,新建场景后删除默认立方体。添加一个平面(Shift+A → Mesh → Plane)作为桌面,再添加一个UV球体(Shift+A → Mesh → UV Sphere),把球体放在平面正上方约0.1米处,刚好让它微微接触平面但不穿模。
灯光方面,加一盏聚光灯(Spot Light),放在球体斜上方45度角的位置,距离约2米。关键参数是灯光尺寸(Radius)——这直接决定焦散的锐利程度。默认的0.1米半径会让焦散糊成一团,我建议调到0.02米到0.05米之间,模拟一个接近点光源的聚光效果。
关于灯光的能量,我踩过不少坑:Cycles的物理单位是辐射度,聚光灯默认能量是100W,对两米距离的场景其实偏弱。把灯光能量提升到300W到500W比较合适。如果后面渲染时桌面依然偏暗,优先加灯光能量而不是盲目调曝光。
3.2 材质的“透明”与“折射”细节
现在处理材质。这里是最容易出问题的地方。
先给平面一个Principle BSDF材质。把粗糙度(Roughness)调到0.3左右,别用0.0的完全镜面,因为完全镜面会把焦散变成一道难以看清的细线,并且产生大量尖锐高光。漫反射颜色随便选个浅灰即可,不要太暗,否则焦散亮度会被整体拉低。
再给球体一个Principled BSDF。这材质是Cycles里做玻璃最顺手的着色器。把“透射(Transmission)”拉到1.0,粗糙度设为0(或者0.01,用来模拟轻微磨砂玻璃),IOR(折射率)设为1.45——这是普通玻璃的常见折射率。
要注意,有些教程上来就加一个Glass BSDF节点替换掉Principled BSDF,这也能做,但Principled BSDF在Cycles里更常用,而且后续调色散、吸收都更方便。
设置完透射后,打开材质面板里的“Glass”相关选项。Cycles的玻璃材质有一个关键开关:材质属性栏里“Settings”底下有个“Screen Space Refraction”,它只在Eevee里有效,Cycles里不用管。Cycles里真正影响焦散的是“Caustics”——它在“渲染属性 → Light Paths”面板里,有“Reflective Caustics”和“Refractive Caustics”两个勾选项。默认都是开启的。如果你发现玻璃球完全不产生焦散,第一步先检查这两个开关是否被关掉了。
3.3 采样与噪点控制的关键参数
材质和灯光就绪后,先别急着上大采样,用200采样数试渲染一版。这时你会看到玻璃球下方的桌面有隐约的亮纹,但噪点很重。这就是焦散的典型状态。
Cycles的采样器参数里,想逼出干净的焦散,我建议按这个顺序调整:
第一,把渲染器的最大采样(Samples)提到1000到2000。焦散是高频光,低采样下根本收敛不了。
第二,打开“去噪(Denoising)”。Cycles 3.0以后默认用OptiX去噪(N卡)或OpenImageDenoise(CPU),效果非常好。但注意,去噪要勾选“渲染”和“视图”两个选项,否则你渲染了动画或高采样图,去噪结果不会直接体现在输出里。
第三,调整“自适应采样(Adaptive Sampling)”的阈值。Cycles会自动检测噪点不严重的区域,减少采样数。建议把噪点阈值(Noise Threshold)从默认的0.01降到0.005。焦散区域的对比度极高,自适应采样容易误以为“已经够干净”而提前停止,导致焦散区域依然油乎乎的。
第四,如果你发现焦散附近还是有类似白噪点的高频颗粒,多半是“火斑(Fireflies)”。在性能面板里把“光阈值(Light Threshold)”从默认的0.01降到0.001,可以强制截断那些异常高亮路径。代价是略微降低物理准确性,但视觉上干净得多。
总之,Cycles里焦散出得干不干净,采样数从来不是唯一变量,灯光尺寸、去噪设置、自适应阈值、光阈值四者强关联。我通常的基准配置:采样2000、自适应噪点阈值0.005、光阈值0.001、开启去噪,然后根据测试图的噪点情况微调。
4. 常见问题与排查实录:焦散翻车现场汇总
4.1 焦散“出不来”的几大典型原因
“我设置了玻璃材质,为什么桌面根本看不到焦散?”这是我在群里被问过最多的问题。根据我自己的调试经验,90%的情况都出在下面三个环节:
第一,渲染器Light Paths里的“Refractive Caustics”被关了。很多人为了提速,误把这个选项理解为“不需要的折射效果”,结果玻璃球变成了一个透明的“隐形球”,桌面干干净净。如果你用的是Cycles,检查渲染属性 → Light Paths → Caustics区域,把“Refractive Caustics”勾上。
第二,灯光的半径太大。这非常隐蔽——灯光半径设置成0.5米以上时,光线从无数个方向射向玻璃,折射后能量被摊薄,焦散被糊成一片极其微弱的漫光。你可能会以为“焦散不见了”,但其实是它太软太弱,被环境光淹没了。把灯光半径压到0.05米以内,焦散立刻“勾”出来。
第三,玻璃材质透明度设置不对。很多人用Principled BSDF,但忘了把“混合模式(Blend Mode)”设置成“Alpha Hashed”或者用透明BSDF来模拟玻璃。实际上如果你只把Base Color的Alpha调低,那是标准透明度,不产生折射,焦散自然出不来。记住:能被焦散的光路依赖真实的折射,而折射需要“透射(Transmission)=1.0”,不是“Alpha通道透明”。
还有一个容易忽略的点:如果桌面上叠加了碰撞体、平面没封口、玻璃球内部有额外的网格面,都会产生莫名其妙的阴影层。Cycles里一个模型如果是开放面,光线在进入和离开物体时可能出现二次折射、能量丢失,表现就是焦散特别暗。复杂模型可以用“固体+厚度”的方式建模,但干净的测试场景里,一个完整球体就好,千万别自作聪明地在里面塞一个空心壳。
4.2 噪点、火斑与能量偏暗的处理
焦散出来了,但亮斑周围全是彩噪,这又是另一片战场。
先分清楚两种噪点:一种是低频的“糊噪”,整个画面像蒙了一层沙子;另一种是高频的“星状噪点”,一个像素特别亮、四周漆黑。前者通常来自采样不足——把采样提到2000以上,糊噪会先消失。后者基本是火斑(Fireflies),由某些极端高能路径导致。
火斑的根治方法除了前面说的光阈值,还有一个技巧:在渲染属性 → 色调映射里打开“曝光(Exposure)”,同时把“色彩管理(Color Management)”的“查看变换(View Transform)”调为“Filmic”或“AgX”。这两个色彩空间会把高动态范围的极亮值压回可见范围,火斑的视觉冲击力大幅下降。我自己测试过,同一张图,从Standard换成Filmic,火斑数量看起来能少一半以上,而焦散的亮度层次反而更舒服。
能量偏暗的问题则跟玻璃材质的光路次数有关。光线在玻璃球里进出,至少需要两次透射事件。如果材质的最大反弹次数(Max Bounces)里的“传输(Transmission)”次数被限制为1,光线只穿过玻璃一次就终止了,焦散能量直接丢失一半。建议把渲染属性 → Light Paths → Max Bounces → Transmission设为4到6。我的习惯是直接设置总反弹为12,透明反弹为8,让光线有足够的空间在场景里来回折腾。
另外提醒一下:玻璃IOR如果设置成1.0(等同于空气),光线不弯曲,焦散也绝对出不来。这个错误比较新手向,但排查起来极容易忽略——尤其当你在某个预设材质基础上只改了颜色,没留意IOR被默认成1.0。
4.3 性能取舍:焦散值得花的渲染成本
最后聊一个很务实的话题:焦散到底值不值得为它付出高昂的渲染时间?
我的个人结论是:值得,但要分用途。如果你做的是产品渲染、静物广告,玻璃杯、水晶、水面特写是画面主角,那么焦散几乎是刚需——没有焦散的玻璃杯看起来像塑料,没有焦散的水面像一块刷了高光贴图的蓝色瓷砖。这种场景,我宁愿把采样拉到3000,也要把焦散渲干净。
但如果你做的是室内全景、建筑漫游,场景里有一百个窗户和玻璃幕墙,每个都在制造锐利焦散,那就没必要全开。这类场景里焦散是背景氛围的一部分,过度追求焦散的物理正确反而会拖垮交付时间。我的做法是:大场景关掉全局焦散,只针对距离摄像机最近的1到2个重点玻璃物件开高采样,其他远处玻璃用“模拟焦散”的纹理贴图或者简单的光晕处理。
实时渲染项目更不用纠结。Unity和UE里的屏幕空间焦散,虽然物理上不严格,但观众的眼睛更在意“有没有水面光网”“玻璃杯边缘那道光是否自然”。用纹理动画模拟水面焦散,用投影贴图模拟玻璃杯焦散,视觉效果已经有九成像了。剩下那一成物理精度,是离线渲染的领域,别在实时项目里烧GPU。
4.4 动画焦散与动态水面的额外坑
如果你做的是水面波纹扭动的动画,焦散的动态特性会增加一个额外的维度,这里单独列一节,因为它的坑和静态焦散完全不同。
静态场景里,焦散光斑出现的位置基本固定,你可以用足够多的采样慢慢收敛。但水面动画中,每一帧的折射方向都在变化,焦散的光斑每一帧都在快速游走。这意味着采样必须逐帧重新分布,上一帧积累的光子信息对下一帧几乎无帮助。如果你用光子映射,水面的每一帧都必须重新发射光子,计算量成倍翻。
而且,动态水面有“时间性闪烁”问题:即使单帧画面看着还行,连续播放时焦散位置跳动迅速,就会产生高频率的闪烁感,看起来像整片水在“发癫”。这个现象在Cycles里尤其明显,因为焦散本身是高频信号,帧间采样噪声会直接转化为视觉闪烁。
我的建议是,动画项目中给焦散单独烘焙通道,或者尽量用更大的灯光半径去软化焦散——软化后的焦散运动更平滑,闪烁感会明显下降。一定要尖锐焦散的话,那就走离线渲染加多帧复用的路线,而不是指望单帧硬怼。
最后再分享两个小技巧
焦散调多了之后,我发现一个非常实用的习惯:在测试阶段先别开去噪。去噪会掩盖真实的噪点分布,让你误以为“采样够了”,结果一关去噪,原形毕露。我一般是先关去噪、采样500,用一张带噪的测试图判断焦散轮廓和位置是否正确,确认构图没毛病后再去噪做高采样出图。这比反复开着去噪试参数高效得多。
另一个技巧是,如果桌面颜色太黑导致焦散不明显,可以临时把桌面材质调成亮灰色,确认焦散出现后,再调回最终颜色。这样能帮你分辨到底是“没焦散”还是“焦散太暗看不出来”。这两个问题虽然最终表现接近,但修起来的路径完全不同。
焦散这个东西,乍看是渲染器里的一个小功能,细挖下来,却贯穿了从蒙特卡洛采样到光学物理再到工程性能优化的整条链路。希望这篇文章能把你带进这个链路里,少走几年弯路。
