几年前的一次项目评审会上,美术组指着背光面渲染结果说“死黑,没法看”。主程随口回了句“把漫反射改成半兰伯特不就行了”,旁边一个图形方向的同学立刻反对:“写实管线,PBR不能瞎改。”主程也来了劲:“那Disney diffuse不也在漫反射里加了掠射角增强吗?跟半兰伯特不就是换皮?凭什么它是PBR?”会议室安静了一秒,然后所有人都陷入了同一个问题——半兰伯特算hack,Disney diffuse算PBR,到底靠什么区分?
当时我没能一句话回答。事后我把两个模型翻出来重新推了一遍,这问题其实可以收敛成非常具体的几条判据。这篇文章就是那套思考过程的完整记录,包含公式拆解、工程落地的取舍,以及我踩过的一些坑。
1. 半兰伯特到底做了什么,为什么被叫hack
1.1 先看当年它是从哪里来的
半兰伯特,也就是Half Lambert,第一次被大众知道是因为Valve在《半条命2》里大量使用。2004年那会儿的实时渲染没有现在这么多间接光方案,静态场景还能烘Lightmap,动态角色就麻烦了——环境光很弱、也没有像样的AO,背光面直接黑死。美术想尽办法调灯位、加补光,效果都不稳定。Valve的做法很简单粗暴:把漫反射系数从NdotL换成了NdotL乘以0.5再加0.5。这样哪怕法线完全背对光源,漫反射亮度也还有原来的50%,不会黑到底,人物轮廓在任何角度下都能看清。
这个改动在当年是很有针对性的:解决“动态物体背光面死黑”这一个具体问题。它没有试图去解释真实世界为什么暗部还有光,就是单纯从画面出发,让不该黑的地方不黑。换句话说,这是一个非常典型的、由项目痛点催生出来的临时方案。
1.2 公式层面它干了什么
兰伯特漫反射的经典形式是:
C = albedo * max(0, NdotL)
半兰伯特改成:
C = albedo * (NdotL * 0.5 + 0.5)
关键就在这里。max(0, NdotL)是硬截断,NdotL为负值就输出0,黑色就是黑色。半兰伯特把NdotL从[-1, 1]整体映射到了[0, 1],等于把负数也翻折进可见范围,而且最小值不是0,是0.5。这意味着什么?意味着半兰伯特明确保证“任何一点在任何光照条件下都不会低于最大亮度的50%”——这纯粹是动态范围压缩,跟物理一点关系都没有。
你可能会想:那Disney diffuse不也在NdotL接近0的时候把亮度抬上去了吗?对,形状上确实有点像,但两者的出发点完全不同,这正是后面要展开的核心。
1.3 为什么它是个hack而不是模型
业界说某某是hack,通常不是贬义词,而是说它满足三个特征:第一,没有理论根基,没有人能解释它为什么是0.5而不是0.4或0.6,纯粹是调出来的;第二,没有可推广性,换一个光照环境、换一种材质,它不会自动调整,永远是一套固定映射;第三,没有可验证性,你没法拿真实测量数据来对照说它对还是错,只能说“这个画面看起来还可以”。
半兰伯特完美命中这三点。它就是Valve为《半条命2》当时的引擎能力打的一个补丁。后来很多卡通渲染、风格化渲染沿用到现在,因为它的效果确实稳定,但那依然是补丁的用法,不是模型的用法。
2. Disney diffuse凭什么被认为是PBR
2.1 Burley那篇论文到底做了什么事
2012年SIGGRAPH上,迪士尼的Brent Burley发表了那篇著名的《Physically Based Shading at Disney》。这篇论文的核心思路不是发明一个炫酷的公式,而是先大量测量真实材质的双向反射分布函数,包括织物、车漆、石膏、皮肤之类的漫反射部分。他们发现了一个很重要、但在计算机图形学里经常被忽略的现象:真实材质的漫反射在掠射角方向会明显变亮,而且表面越粗糙,这种“掠射角提亮”越明显。
这个现象用大白话讲就是:你拿手电筒以很斜的角度去照一张白纸,会发现反射光里其实混着不少散射光,纸面并没有像兰伯特模型预测的那么平。兰伯特模型假设漫反射在所有观察角度上都是均匀的,但真实世界的漫反射是有方向性的。Burley他们做的事情,就是用一个可调参数的函数去拟合这批测量数据,为的是让渲染结果更接近真实测量。
2.2 Disney diffuse的公式拆解
Disney diffuse在论文里的形式是:
f_d = baseColor / PI * (1 + (F_D90 - 1) * (1 - NdotL)^5) * (1 + (F_D90 - 1) * (1 - NdotV)^5)
其中F_D90 = 0.5 + 2 * roughness * LdotH^2。
这个公式怎么读?baseColor除以PI是兰伯特的能量项,保证半球积分不超能量。后面两个5次幂项,一个是跟入射方向有关的“光散射抬升”,一个是跟视线方向有关的“视角散射抬升”。5次幂的作用是让抬升集中在接近掠射角的一个窄区间里,正面方向几乎不受影响。F_D90决定抬升幅度,粗糙度越高、F_D90越大,掠射角处的漫反射就提得越多。
这跟半兰伯特那种“把NdotL整体映射到0.5以上”的做法有本质区别:半兰伯特是全局拉亮,Disney diffuse只在接近掠射角的地方有局部变化,而且这个变化是跟粗糙度参数联动、有物理测量作依据的。它不是简单地调亮暗部,而是对真实散射机制做了拟合。
2.3 它在PBR框架里的位置
PBR不是某一个公式,而是一整套设计约束。业界公认PBR渲染至少要满足这么几个点:能量守恒、参数语义对应真实物理属性、模型有可验证的物理依据。
Disney diffuse在这三点上都很硬。能量方面,前面乘了1/PI,整体反射能量不会超过入射能量。参数方面,roughness直接对应微表面统计特性,粗糙度变化会让掠射角响应跟着变,这是有物理映射的。可验证方面,它本身就是从测量数据里拟合出来的,你可以拿它的预测曲线去跟实测数据对比,误差是可量化的。只要这些约束成立,哪怕公式从头到尾都是拟合出来的,它在方法论上也属于PBR家族。
另外还有一点值得说:Disney diffuse里那个掠射角提亮,在物理上叫retro-reflection,也就是逆反射,方向性背向散射。最典型的例子是月球表面,你在满月时正对月亮的方向看,它反而最亮,因为月球风化层土壤对直射方向有强烈的逆反射。粗糙表面之所以会有逆反射,是因为微表面之间互相遮挡、多次散射,导致光线倾向于沿入射方向弹回。这给了F_D90一个非常清晰的物理叙事:粗糙度高的表面,微表面间多次散射越强,逆反射越明显。
3. 区分的依据:四个可以直接用来判断的维度
3.1 判断一个漫反射改动是hack还是PBR,先问四个问题
第一,它的设计动机是什么?是为了解决某个具体项目的画面缺陷,还是为了拟合某种可观测的物理现象?半兰伯特属于前者,Disney diffuse属于后者。这是最根本的区分。
第二,它的参数有没有物理对应?半兰伯特里的0.5是拍脑袋拍的,没有任何参数会随材质变化而响应;Disney diffuse里的roughness是有明确物理含义的,粗糙度一变,掠射角亮度变化曲线就会跟着变。
第三,它的能量从哪里来?半兰伯特把最低亮度抬到50%,这部分多出来的能量没有任何来源解释,直接违反了能量守恒的分析前提;Disney diffuse的掠射角抬升,可以用微表面多次散射和逆反射来解释,能量来源在机理上是有交代的。
第四,它能不能被验证?半兰伯特没法验证,因为从物理上它什么都没声称。Disney diffuse做出来的时候就是跟着测量曲线走的,你可以拿它去拟合新的实测数据看拟合质量,这是一个可证伪的过程。
这四个问题,我用一句话总结过很多次:在渲染里,凡是需要回答“这个亮是从哪来的”却答不上来的改动,一律按hack处理;凡是能从机理上解释、并且有实测数据支撑的改动,才有资格进PBR。
3.2 半兰伯特与Disney diffuse的直观对照
| 判断维度 | 半兰伯特 | Disney diffuse |
|---|---|---|
| 设计动机 | 解决背光面死黑 | 拟合真实漫反射测量数据 |
| 物理参数 | 无,固定0.5偏移 | roughness、LdotH参与控制 |
| 能量来源 | 无解释,凭空注入 | 微表面多次散射、逆反射 |
| 适用范围 | 所有表面都一样 | 随粗糙度变化而不同 |
| 可验证性 | 无法与实测对比 | 可拟合测量曲线,误差可量化 |
| 在PBR框架内的地位 | 补丁 | 模型 |
这张表是帮团队里新人最快建立判断力用的。你拿任何一个“看起来有点怪的漫反射改法”,放进这个表里过一遍,基本都能得出靠谱结论,不用纠结术语。
3.3 拿这套判据再看几个容易混淆的东西
这个方法还能帮你认清其他几个常见渲染技巧的身位。
比如Wrap Lighting,它本质上也是把NdotL负值映射到正值,让暗部亮起来。它跟半兰伯特是同一类操作,变量名不同而已。而像“环境光遮蔽”,虽然有“遮蔽”这种看似物理的词汇,但它在PBR流程里是用来补偿间接光遮挡的,本身不是对测量数据的拟合,它的定位更接近一种数据辅助手段,不是反射模型的一部分。
再比如Gamma校正,它改的是显示编码空间,不是光照模型,不在这个讨论范畴里。但如果你拿判据硬套,会发现Gamma校正是有物理依据的——显示器的非线性是客观存在,所以它不是hack,它是工程正确的做法。
这套判据最大的价值不是让你给某个技术贴标签,而是逼你在动手改渲染之前想清楚:我这个改动是在解决视觉问题,还是在拟合物理问题。这两种动机会导致完全不同的技术路线。
4. 工程落地:什么时候用半兰伯特,什么时候用Disney diffuse
4.1 风格化渲染里,半兰伯特依然是利器
半兰伯特从来不是该被消灭的东西。在我的项目经验里,卡通渲染、二次元角色渲染、部分低多边风格游戏,半兰伯特几乎是标配。因为它带来的“柔和暗部”是风格化里很有用的画面语言,可以让角色在任何场景里都保持清晰的轮廓,避免暗部脏成一团。
但要注意,风格化里用半兰伯特通常不是单独用的。我一般会让美术在材质参数里再乘一个自发光系数,或者叠加一层非物理的rim light,把暗部亮度和轮廓光拆成两个独立通道来调整。半兰伯特负责提供一个稳定的底亮度,rim light负责塑造形体,两者加起来才是完整的风格化效果。
具体实现的时候,半兰伯特的系数最好不要写死。我给美术暴露两个参数:一个offset,一个scale,材质里写成:
C = albedo * (NdotL * scale + offset)
这样美术可以在材质实例里微调。默认值我还是保持Valve当年的0.5和0.5,毕竟经过十几年验证,这两个数是风格化里最不容易翻车的起点。
4.2 写实管线里,漫反射默认选Disney diffuse
一旦目标是写实渲染,我的默认选择就是Disney diffuse,而且不建议改公式。有人觉得它跟半兰伯特看起来差不多,就在写实管线里把漫反射简化成半兰伯特加个roughness映射,结果通常很惨。
举个我实际见过的案例:某项目为了省性能,把漫反射换成了半兰伯特,然后在roughness高的地方乘一个系数“模拟”掠射角提亮。视觉上确实在掠射角变亮了,但材质之间的差异感没了。金属表面和非金属表面、光滑塑料和粗糙砂石,在掠射角区域全变成同一种亮度趋势,美术没法通过roughness制造材料区分度。最终项目还是改回了Disney diffuse,性能预算从Shader指令数上抠,不再打漫反射模型的主意。
写实PBR的场景里,漫反射模型的地位不只是“出一层颜色”,它跟高光项、微表面分布、可见性项是耦合的。你单独改掉漫反射那一项,整体能量平衡就破了。半兰伯特的50%最低亮度会直接抹掉光源角度变化带来的体积感,尤其是法线贴图里那些NdotL接近0的细节,会一起被压平,这比性能损失更致命。
4.3 Disney diffuse在实时引擎里的落地结构
Unreal Engine里的漫反射不是完整的Burley公式,而是一个基于相同思路的简化版。常驻Shader里会看到类似这样的结构:
float DisneyDiffuse(float NdotV, float NdotL, float LdotH, float roughness) { float FD90 = 0.5 + 2.0 * roughness * LdotH * LdotH; float lightScatter = 1.0 + (FD90 - 1.0) * pow(1.0 - NdotL, 5.0); float viewScatter = 1.0 + (FD90 - 1.0) * pow(1.0 - NdotV, 5.0); return lightScatter * viewScatter; }这里LdotH平方进入F_D90,是在用微表面半程向量来近似“掠射角增强的强度”。粗糙度低时,F_D90接近0.5,5次幂衰减后整体漫反射接近兰伯特;粗糙度高时,F_D90变大,掠射角的逆反射特征明显强化。这个结构在实时渲染里性能开销很小,几个pow加上一次乘法,但在材质区分度上的回报非常高。
我在用这个函数的时候有几个习惯:一,roughness传入前会先做一次平方处理,因为美术给的roughness参数是线性感知的,直接传给F_D90会让掠射角变化过于集中在低粗糙度区间。二,NdotV和NdotL都clamp到0到1之间,避免负数破坏5次幂的曲线形状。三,这个漫反射输出后不会单独再乘一个环境光项,间接光由IBL单独走一套卷积积分,避免重复计算。
4.4 一个经常在项目里出现的陷阱
写实管线里最常见的错误是把半兰伯特当作“暗部补偿工具”挂到PBR漫反射上。美术反馈说角色背光面太黑,主程图省事直接把半兰伯特的0.5偏移加到PBR的diffuse上。短期看画面确实亮了,但带来的问题一大堆:所有的暗部对比度被压缩,材质在暗部的区分度消失,法线贴图的立体感下降,而且没有任何参数能让美术针对特定材质恢复暗部细节。
正确的做法是先排查暗部死黑的来源。如果是间接光照不够,应该去调IBL强度、加环境光遮蔽补偿、检查曝光曲线;如果是材质本身的问题,应该去调albedo明度或粗糙度。直接用半兰伯特改漫反射其实是把光照问题伪装成了材质问题,后面一定会还债。
5. 我在实际项目里的排查笔记与经验补充
5.1 数字上怎么快速验证漫反射模型是否“物理”
有时候不需要全流程跑起来才能判断改法有没有问题,几个数字就能验出来。
把normal固定成垂直于光源方向,也就是NdotL等于0,先看模型输出。兰伯特的输出是0,半兰伯特的输出是0.5倍albedo,Disney diffuse在roughness不同时输出在一个区间内变化。这个数字一摆出来,就能看出半兰伯特的输出跟材质完全无关,而Disney diffuse的同一位置会随roughness产生合理波动。
再看掠射角,设NdotL等于0.1,NdotV也是0.1,roughness从0.2调到0.9,Disney diffuse的输出会有明显上扬,而半兰伯特纹丝不动。这两个测试不需要引擎,写个小脚本跑一遍就能确认模型行为。我每次在项目里评估一个新的漫反射模型,都先跑这两组数字。
5.2 序列帧测试比静态帧更能暴露问题
半兰伯特在静态帧里往往很讨好,因为暗部柔和、对比度低。但要判断一个漫反射模型能不能用于写实项目,一定要做动态序列测试:让光源绕着物体转一圈,或者让物体在固定光下转一圈,盯着NdotL从0附近扫过的那段帧。
半兰伯特在光源角度变化时,亮度变化会很“平”,因为映射把变化斜率砍了一半,物体滚动时体积感的起伏变弱了。Disney diffuse在掠射角附近会产生明显的亮度抬升,但这种抬升是局部的,物体转过去之后暗部还是暗部,动态范围没有过度压缩,整个旋转过程中材质的体积感是连贯的。
我见过不止一次项目在静态截图评审里通过了半兰伯特方案,到了动态演示阶段被推翻重做。原因就是静态帧里那张图“看起来很干净”,转起来才发现角色像个纸片。所以现在我对任何漫反射改动都坚持要求序列帧验收。
5.3 在PBR和风格化之间切换时,我留了一条经验线
如果你做的是那种“不同角色共用一套渲染器,但有些写实有些卡通”的项目,会有更实际的困扰:同一个材质节点里,写实角色和卡通角色都要用。我现在的做法是提供两个独立的漫反射模式分支,卡通材质走半兰伯特加自定参数,写实材质走Disney diffuse,两个分支之间不做任何插值。
有人试过做一个“PBR到卡通风格的连续过渡滑块”,把漫反射从Disney diffuse插值到半兰伯特,听起来优雅,实际效果很糟糕,在中段位置既不像物理也不像风格,美术根本没法在中间值上做材质。风格和物理之间的过渡不是线性插值能解决的,它们属于两个不同的叙事体系。
这个项目经验让我后来形成了一个口头标准:凡涉及漫反射的改动,都要先回答“这个亮是从哪来的”。答不上来的按hack处理,答得上来但跟项目目标不符的,也要按hack处理。hack不是脏话,它是工程判断的工具词。半兰伯特是hack,这没什么好丢人的,迪士尼那套diffuse也是拟合出来的,它之所以能叫PBR,不是因为公式更复杂,而是因为整个方法论的底座是实测数据和能量守恒。
下次再有人问我“这俩到底怎么区分”,我就直接甩这四个问题过去:动机是什么,参数跟物理有没有对应,能量从哪来,能不能拿实测验证。这比争论“谁的公式更物理”靠谱得多。