ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PBR与BRDF实战:用Cook-Torrance模型告别塑料感材质

PBR与BRDF实战:用Cook-Torrance模型告别塑料感材质 如果你写过一个实时渲染器八成遇到过这种尴尬同一个金属球换个环境就假得离谱高光要么灰蒙蒙一片要么亮成廉价塑料。我开始折腾渲染那会儿以为多调几个参数就能解决后来才明白真正该换的不是参数而是光照模型本身。这篇要聊的PBR基于物理的渲染、BRDF双向反射分布函数和Cook-Torrance模型就是我把金属球从“塑料感”救回来的那套方案。PBR不是某一个公式而是一整套让材质在不同光照下都能保持物理正确的思路BRDF负责描述表面怎么反射光Cook-Torrance则是这套思路里最常用的微表面模型。它适合正在写渲染器、搭材质系统或者调Shader调到怀疑人生的朋友。顺带说一句网上搜PBR有时会蹦出网络里的同名缩写含义完全不同这里聊的是图形学里的Physically Based Rendering。1. 项目整体设计与思路拆解1.1 PBR到底在解决什么问题传统Blinn-Phong模型里高光强度靠一个shininess硬撑材质在不同光照角度下的表现非常不稳定。你调好一盏灯下的效果换一盏灯、换一个环境金属就变成蜡、塑料变成陶瓷所有参数都要重新来一遍。PBR的出发点其实很朴素把材质参数做成“物理属性”而不是“针对某盏灯的特调数值”。这样同一个材质放到白天、夜晚、室内、室外表现都应该是统一且可信的。要做到这一点核心约束是能量守恒。传统模型常见的毛病是反射光能量超过入射光能量导致画面过曝、发灰。PBR要求反射光加上折射散射光的总能量不超过入射光能量。金属几乎没有次表面散射入射光绝大部分被反射非金属则有漫反射和透射。这一条约束直接决定了材质参数的语义金属度、粗糙度、基础色而不是高光系数、环境光系数。这也是我做材质系统时第一次感受到“参数有了物理含义”后的爽快感——美术同学问“这个值调大调小是什么意思”我能直接回答“这是表面有多粗糙”而不是“你试试看”。1.2 BRDF表面如何与光交互的“合同”BRDF全称Bidirectional Reflectance Distribution Function双向反射分布函数。它回答的问题是给定一束光从方向l打过来从视角方向v看过去这个表面上单位面积反射了多少光。它不是固定值而是四个变量的函数入射方向、出射方向、以及对应的两个球面角。用生活类比理解拿一张磨砂纸和一面镜子同一束光打上去镜子的反射光集中在一个方向磨砂纸则把光散得到处都是。BRDF就是描述这种“反射分布”的数学工具它只关心表面本身的性质跟具体光源强度无关。这也是为什么PBR能把“材质”和“光源”解耦——BRDF描述的是材质光源强度乘上去是最终辐照度。实际开发时写BRDF函数并不会直接处理四个角度的完整张量而是用半程向量h、法线n、入射方向l、视线方向v做点积运算简化到实时渲染能接受的范围。1.3 为什么最终选了Cook-Torrance模型BRDF是一个统称具体怎么算有一堆模型。早期有Lambert漫反射模型它假设表面把光均匀散射到所有方向适合纸张、墙面这类粗糙材质。但金属、车漆、塑料表面有清晰的镜面高光Lambert完全不够用。后来有人提出Phong模型和Blinn-Phong模型高光效果出来了但参数和物理量没什么对应关系。Cook-Torrance模型是Torrance和Sparrow在1967年微表面理论基础上发展的后来Cook和Torrance把它完善成计算机图形学可用的形式。我选择Cook-Torrance最关键的原因是它把镜面反射拆成了三个物理过程法线分布、几何遮蔽、菲涅尔反射。每个过程都有明确物理含义能独立调整、独立验证。相比之下Blinn-Phong只是用点积的幂次模拟高光带物理上说不通。Cook-Torrance模型配合GGX法线分布是当前实时渲染事实上的标准Unreal、Unity、Three.js都在用这套底层逻辑。选它意味着资料多、踩坑记录多、后续接IBL也顺滑。特性Blinn-PhongCook-Torrance (GGX)物理基础经验模型微表面理论能量守恒不保证显式保证参数含义shininess经验值roughness物理值金属表现塑料感强高光色受F0影响高光拖尾均匀衰减长拖尾更真实IBL扩展不方便可配合split-sum2. BRDF核心细节解析与实操要点2.1 BRDF的数学定义与物理约束正式定义里BRDF是反射辐亮度与入射辐照度的比值单位是1/sr球面度分之一。这个定义本身就包含了物理约束因为辐亮度和辐照度的比值有方向性所以BRDF天然要满足两个条件一是Helmholtz互易性也就是交换l和vBRDF值不变二是能量守恒对半球所有出射方向积分反射能量不能超过入射能量。实时渲染里我们不会真的对半球积分但能量守恒这条一定要落实在公式里。一个非常常见的问题是在漫反射项和镜面反射项之间没有做能量分配导致漫反射加镜面反射的总反射率超过1场景亮部死白一片。我在第一个PBR场景里就犯过这个错最后把漫反射系数kd和镜面反射系数ks加起来判断才发现kd ks 1的情况到处都有。正确做法是在计算完菲涅尔项后让漫反射系数乘上(1 - F)把反射不掉的能量留给漫反射。2.2 漫反射项与镜面反射项的拆分Cook-Torrance BRDF的完整形式是漫反射和镜面反射两部分的加权和f kd * f_lambert ks * f_cook_torrance其中f_lambert是Lambert漫反射项公式是baseColor / PI。这里的PI来自半球积分不是拍脑袋除的。方向光从某个方向打到表面漫反射均匀散射到半球上积分后会出现一个PI因子不除的话能量会整体偏亮。镜面反射项f_cook_torrance则是f_spec D(h) * F(v, h) * G(l, v, h) / (4 * (n dot l) * (n dot v))D、F、G三个字母是这套模型的灵魂。实践中我建议先把这三个项分开写成独立函数分别调试。光照结果不对时挨个把D、F、G输出成灰度图能快速看出来是哪个项算崩了。不要一上来就合在一起调不然出了问题根本不知道是粗糙度映射错了还是菲涅尔参数写反了。2.3 法线分布函数GGX是默认答案法线分布函数NDF描述的是微表面法线朝向半程向量h的概率密度分布。它直接决定高光的形状是锐利小点还是扩散大光斑。常用的有两个Beckmann分布数学上对应高斯分布高光衰减均匀但是拖尾太短金属看起来不够“润”。GGX分布Trowbridge-Reitz高光中心更亮拖尾更长更贴近真实材质测量数据。这也是业界主流选择。GGX公式如下D_GGX(h) alpha^2 / (PI * ((n dot h)^2 * (alpha^2 - 1) 1)^2)这里的alpha是粗糙度的映射值。UE4里直接把alpha roughness * roughness也就是对美术给出的粗糙度做了一次平方映射用来让参数感知更线性。我在项目里也沿用这个做法因为直接拿美术给的roughness作为alpha高光会偏弱过渡不自然。若你用的是roughness线性值建议也做一次平方映射效果差异非常明显。实现时注意分母内部的(alpha^2 - 1)当alpha接近0时这一项会让分母趋近(n dot h)^2D值极大金属高光会变成非常锐利的一个点这是对的。但极小的roughness会产生极大的高光峰值HDR场景里配合Bloom容易过曝需要控制对比度。2.4 几何遮蔽项与菲涅尔项细节决定成败几何遮蔽项G解决的是微表面互相遮挡的问题。当光线接近掠射角时微观凹凸会挡住入射或反射光线导致实际反射能量远低于NDF预测值。Smith模型把G拆成两个方向独立遮蔽的乘积G(l, v, h) G1(l) * G1(v)每个方向用Schlick-GGX近似G1(x) (n dot x) / ((n dot x) * (1 - k) k)这里k的取值很关键。直接光照下k alpha / 2IBL环境下k (alpha 1)^2 / 8。很多人在切IBL后高光突然过亮或过暗问题多半出在这里——他们没换k的计算方式。这个细节文档里不常写但我实测下来差别非常明显。菲涅尔项F描述的是不同入射角下反射率的变化。掠射角时所有材质都趋向于全反射这也是为什么从侧面看水面、玻璃会有刺眼反光。Schlick近似公式F F0 (1 - F0) * pow(1 - (v dot h), 5)其中F0是正入射时的反射率。非金属通常取vec3(0.04)金属直接用baseColor作为F0因为金属的反射光带有自身颜色。代码实现里可以用mix(vec3(0.04), baseColor, metallic)一步完成。注意pow中的底数不要用n dot v要用v dot h并且对底数做clamp到0到1之间否则会出现负数的幂导致闪点。3. Cook-Torrance模型实现过程3.1 搭建基础材质参数体系我习惯把所有材质参数收敛到四个核心值再加一个可选的环境光遮蔽baseColor基础色非金属时是漫反射颜色金属时是菲涅尔反射颜色。metallic金属度0到10代表非金属1代表金属。中间值很少见但在描述脏旧表面时很有用。roughness粗糙度0到10代表镜面光滑1代表粗糙。ao环境光遮蔽可选的遮蔽项只在间接光部分生效。这四个参数直接对应美术直觉不需要对着数值猜。实现时我建议把baseColor预乘sRGB到线性的转换。很多人会忽略这一点导致PBR材质在偏亮的HDR环境下整体偏暗或偏灰。线性空间是PBR的入场券在Gamma空间直接套GGX公式出来的高光形状和颜色都是错的。Shader侧我通常这样组织参数结构struct PBRMaterial { vec3 baseColor; float metallic; float roughness; float ao; };如果你是纯代码生成材质没有贴图建议给一个默认值集合baseColor vec3(0.8)、metallic 0.0、roughness 0.5、ao 1.0。这样至少能看到一个柔和的漫反射球体而不是刺眼的白色高光。3.2 光照计算主循环方向光部分先看最简单的情况单方向光。这是所有PBR照明的“骨架”后续加多个光源、加IBL都是在这个函数上扩充。vec3 calculateDirectLight(PBRMaterial mat, vec3 n, vec3 v) { vec3 lightDir normalize(u_lightPosition); vec3 lightColor u_lightColor; float lightIntensity u_lightIntensity; vec3 h normalize(v lightDir); float NdotL max(dot(n, lightDir), 0.0); float NdotV max(dot(n, v), 0.001); float NdotH max(dot(n, h), 0.0); float VdotH max(dot(v, h), 0.0); // 漫反射 vec3 diffuse mat.baseColor / PI; // 法线分布 GGX float alpha mat.roughness * mat.roughness; float alpha2 alpha * alpha; float D alpha2 / (PI * pow(NdotH * NdotH * (alpha2 - 1.0) 1.0, 2.0)); // 几何遮蔽 float k alpha / 2.0; float G1L NdotL / (NdotL * (1.0 - k) k); float G1V NdotV / (NdotV * (1.0 - k) k); float G G1L * G1V; // 菲涅尔 vec3 F0 mix(vec3(0.04), mat.baseColor, mat.metallic); vec3 F F0 (1.0 - F0) * pow(1.0 - VdotH, 5.0); // 镜面BRDF vec3 specular (D * F * G) / max(4.0 * NdotV * NdotL, 0.001); // 能量分配 vec3 kD (1.0 - F) * (1.0 - mat.metallic); return (kD * diffuse specular) * lightColor * lightIntensity * NdotL; }这段代码有几个关键点。第一NdotL被clamp到0避免背光面出现负光照第二分母里的max(0.001)是标准保底操作防止除零第三能量分配kD乘了(1 - metallic)金属没有漫反射第四F被用于分配漫反射能量因为菲涅尔反射掉的能量不能进入漫反射。我建议拿到代码后先测试三个极端参数metallic 0, roughness 0做镜面非金属metallic 1, roughness 0.3做金属metallic 0.5, roughness 0.8做脏旧表面。三个测试球的表现应该分别是“塑料亮斑”“金属高光”“哑光混合”如果三个看起来都像同一种材质大概率是能量分配或Gamma空间出了问题。3.3 参数选择与计算过程的底层逻辑为什么kd要乘(1 - F)而不是别的系数因为菲涅尔项已经告诉我们在当前观察角度下有多少能量被镜面反射了剩下的能量才可能进入漫反射。金属的F0是baseColor本身反射率极高漫反射能量自然趋近于零所以kd乘(1 - metallic)后直接归零。这是PBR参数“物理正确”的核心体现而不是人为凑出来的经验值。粗糙度映射这里我再多说一句。美术给的roughness往往不是物理学意义上的微表面粗糙度而是“感知粗糙度”。UE4把alpha roughness * roughness本质是把粗糙度感知范围拉升到更自然的区间。如果觉得高光太弱不要急着调灯先检查alpha映射是不是用了原始值。F0 vec3(0.04)这个常数也不是乱拍的。大多数非金属电介质的正入射反射率在4%左右。真实测量数据里水约0.02玻璃约0.04塑料约0.03到0.05。统一取0.04在实时渲染里误差可以接受省去每类材质单独查表的开销。点光源、聚光灯的实现和方向光类似只是要多算一个距离衰减。衰减公式我推荐attenuation 1.0 / (1.0 distance * distance)有些引擎用1.0 / (distance * distance)在近距离会数值爆炸需要额外clamp。用加1的版本更稳。距离衰减不要直接乘在BRDF上而是乘在光源辐射强度上严格来说光照度随距离平方衰减BRDF本身不关心光源距离。3.4 单位、HDR与色调映射PBR加完直接光以后很多人发现画面要么过曝要么发灰这时候八成是色彩空间和HDR问题。物理光照的数值范围是线性的方向光亮度动辄几十但普通显示器的亮度范围是0到1。如果不做色调映射直接输出亮部直接裁成白色。我的管线里有两个必做步骤HDR渲染、然后应用色调映射。比如用ACES Filmic Tonemappingvec3 acesFilmic(vec3 x) { float a 2.51; float b 0.03; float c 2.43; float d 0.59; float e 0.14; return clamp((x * (a * x b)) / (x * (c * x d) e), 0.0, 1.0); }如果场景偏暗先检查光照强度是不是太弱如果高光和暗部都发灰再检查Gamma校正是否做对了。通常我推荐最终颜色 pow(acesFilmic(线性颜色), 1.0 / 2.2)。但要注意pow到2.2的Gamma校正是在色调映射之后做顺序反了会让画面颜色发闷。4. 常见问题与排查技巧实录4.1 材质整体变暗或者过曝这是新手阶段最常踩的坑甚至我自己在第一个PBR场景里也折腾了快两天。现象是白色粗糙球体在单方向光下亮部发灰暗部死黑怎么调灯都没有用。排查顺序我固定如下先检查NdotL是否被正确clamp到0以上再检查分母4 * NdotV * NdotL是否用了max保底然后检查F0是否在非金属时给了0.04最后检查输出线性和Gamma。如果以上都没问题就把kd和ks分别输出成图看看哪个项吞掉了能量。经验上80%的过暗都是因为把baseColor当成了线性值而它实际还是sRGB。4.2 金属材质没有“金属感”金属感的核心在菲涅尔项。非金属的F0是固定的灰白色0.04金属的F0是baseColor。如果金属材质看起来像塑料多半是metallic没进F0的mix逻辑或者kd没有乘(1 - metallic)导致漫反射还在发光。金属不该有漫反射看到金属表面泛着漫反射白光那基本可以确定能量分配没做对。还有一个容易被忽略的细节金属对环境的反射依赖IBL。没有环境贴图时金属表面只有光源高光其他区域是黑的看起来像“黑铁”。这不算BUG是缺失了间接光。至少加一张简单的环境贴图金属感才会出来。4.3 粗糙度看起来不线性调整roughness时0到0.2之间变化剧烈0.5到1.0之间几乎看不出区别这是未做alpha映射的典型表现。加一行alpha roughness * roughness能立刻改善。如果做完映射还是觉得高光不够紧凑可以把高光的D项单独输出看到的是一个峰值尖锐、拖尾长的亮点那就是对的。如果看到均匀模糊的一团检查NdotH是否用了max(0)或者alpha有没有被错误地再次平方。4.4 常见问题速查表现象可能原因排查方向暗部死黑、过曝严重能量不守恒kd加ks超1检查kD/(1-F)/(1-metallic)金属像塑料metallic未影响F0或漫反射未归零检查mix(F0, baseColor, metallic)高光边界锯齿严重未做MSAA或分辨率不足硬件抗锯齿或TAA环境光下高光异常亮IBL的G项k值用了直接光公式切换k(alpha1)^2/8粗糙度调节不明显未做alpha平方映射alpharoughness*roughness带纹理材质发灰sRGB纹理未转线性采样后pow(color, 2.2)多光源时亮度翻倍没有归一化光源功率对每个光源单独计算并加权掠射角高光漂移F项里用了n dot v而非v dot h改为计算VdotH4.5 从方向光扩展到IBL环境光直接光搞定以后离一个“能看的PBR场景”还差环境光。IBL的做法是把环境贴图预先卷积成漫反射辐照图和镜面反射预滤波图再配合BRDF查找表LUT做split-sum近似。实时运行时用当前法线采样漫反射图用反射向量和粗糙度采样预滤波图再加上LUT查出来的菲涅尔项衰减。把IBL接进来后我才深刻感受到Cook-Torrance模型的好处同样的材质参数方向光只控制高光IBL负责填满金属的反射细节。二者共用的D、F、G计算方法完全一致只是G项的k值要按IBL规则切换。实践顺序上建议先实现IBL漫反射再实现镜面预滤波最后合并。每一步都能用Reflection Capture或者HDR全景图验证。这套模型在项目里稳定跑起来之后我最大的体会是PBR不是“加一个高光公式”就完事它是一整套从色彩空间、光照单位到材质语义的系统约束。初看觉得公式多真正实现完一遍再回头用Blinn-Phong反而觉得别别扭扭——参数没有物理含义调起来全靠猜。我的建议是如果你正卡在材质不真实的问题上别急着堆贴图先把手里的光照模型换成Cook-Torrance配合GGX和能量守恒材质表现会立刻上一个台阶。
返回列表