Unity PBR材质简单实现

发布时间:2026/10/14 3:17:10
Unity PBR材质简单实现 前言该文章主要用于作者本人学习留档内容存在纰漏酌情参考。简化渲染方程Kajiya 在 1986 年提出的渲染方程。什么是渲染方程渲染方程就是计算物体表面上的一个点朝你的眼睛方向发出了多少光。这是PBR的核心概念我们之后定制PBR材质主要依靠渲染方程的原理来。下面就是渲染方程。怎么理解其实就是朝你眼睛发射的光 物体自发光 物体受光反射的光渲染方程解析其中渲染方程式子中有一个概念可能比较陌生就是BRDFBRDF双向反射分布函数用来描述光从某个方向照到物体表面后会有多少光向另一个方向反射。可以把它理解为材质的“反光规则”。它主要取决于入射方向光从哪里来。观察方向你从哪里看。材质特性表面是粗糙、光滑还是金属等。这个后续再细说。总之后面这一堆中是单个光源本身的亮度光的强度。这个光通过不同的角度照射到物体表面时光在单位面积上的强度会发生一定衰减这个通过Lambert漫反射模型可以得知其原理也可以看下面的图了解。本来光拥有一份能量只在直径为dA的范围里存在斜照在表面上后一份能量被分到到直径为dA/Cosα的表面上。之前单位面积的光强度为1/dA现在变为了1/dA/Cosα。所以最后光的强度是入射时的Cosα倍。而Cosα可以由表面的法线n和入射光方向Wi的点积来表示使用点积的定义。因此在之后乘上一个上述两个式子主要考虑入射光的情况而没有考虑材质最终对出射光的影响。BRDF就补上了这部分限定了出射光经过材质的反射朝观察方向的出射光会有怎样的变化。因此乘上最后上述只计算出了一个光源的情况再把所有光源使用积分加起来就是渲染方程了。渲染方程简化假如我们不考虑物体自发光也不考虑多光源只单一光源。渲染方程可以简化为下面的形式。其实就是去掉了积分其他的参数只不过是原渲染方程换了个长相而已。这里有一个问题为什么式子前要乘一个π 。其实这个π主要是为了和Lambert漫反射模型进行配合在数学上约掉分母的去掉的话也无妨可以理解为在原有的灯光强度上额外添加的一个放大参数而已。怎么理解简化渲染方程里π和Lambert漫反射模型的关系首先我们要明白BRDF描述了一种光照反射的规则。那么Lambert漫反射模型作为最经典最简单的光照模型也应该有BRDF的。以下是Lambert漫反射模型的公式和简化的渲染方程对比就会发现其实就是π * BRDF这两项 1的渲染方程。那么也就是说Lambert漫反射模型的BRDF1/π。为什么是1/π还是见下图。首先作为漫反射模型其反射的光线应该均匀地向四周发射。可以想象一个半球形罩在这个拥有Lambert漫反射的平面上平面上点反射发出的光应该均匀地分散在这个半球体上。这个半球体的面积我们可以通过积分求得其中r设为1的话那就是2π假如发射出的能量是1份平均分的话那么半球体上一个单位面积所分得的能量应该是1/2π。然而这种平均分的前提是半球体上任何一个单位面积的光照角度都相同。因为不同角度看过去单位面积的光照强度会因为角度而发生变化。在渲染方程中已经体现了这种角度的思想。所以说BRDF就是为了这种不同角度的情况而产生的一种调节参数。不同角度的光照照进来后根据这个调节参数可以得到正确的单位面积所分得能量的值。我们不妨假设这个调节参数也就是BRDF为C最后就能得出CBRDF1/π这其实也是垂直正对着Lambert平面的半球体上一个单位面积所接收的能量的值根据角度不断增大半球体单位面积上的单位面积所接收的能量的值会从1/π转变为0。这些能量积分起来刚好是1。因此Lambert漫反射模型的BRDF就是1/π。这也就解释了为什么公式里BRDF前面带一个π了。主要是我们想把Lambert漫反射模型作为一个基准用这个π和BRDF相乘就变成1了。BRDF如上文所述BRDF其实就是一个参数可以适用于不同角度光照的最终计算。那么BRDF我们就可以随心所欲地自己定制了。之前的Lambert漫反射模型的BRDF 1/π,主要是描述漫反射的。我们还需要一个高光反射的BRDF才能实现比较真实的光照效果。Cook-Torrance 微表面镜面反射实现高光反射的BRDF中Cook-Torrance 微表面镜面反射是其中比较常见的范式。其公式如下其中分子上有DFG三个部分D、F、G项分别代表 法线分布项、微表面菲涅尔项、几何遮蔽项。Cook-Torrance 微表面镜面反射的理论基础建立在微表面理论上DFG三个项都和这个微表面理论有关。其分母负责把微表面的反射转换为相对于宏观表面的 BRDF。该值由辐射度量学得出不深究。微表面理论一般的着色模型假设着色的区域是一个平滑的表面表面的方向可以用一个单一的法线向量来定义来定义。但微表面理论认为粗糙的宏观表面是由大量完全平坦光滑的微表面组成的。所有这些微小表面都是光滑镜面反射的表面。因为这种模型认为着色区域的表面是由无数方向不同的小表面组成的只有唯一一种方向的小表面反射的光才可能反射到观察者的眼睛里。所以在 Microfacet 模型中着色区域并不能用一个法线向量来表示表面的方向只能用一个概率分布函数D来计算任意方向的微小表面在着色区域中存在的概率密度。如图所示这个平面上只有面向h的这些微表面射出的光线才能被观察者接收到。法线概率密度分布函数D比较常见的法线概率密度分布函数D有Blinn-Phong分布这个与经典 Blinn–Phong 高光使用了相同形式的幂函数但用途不同。这个公式怎么理解代表了物体表面半程向量以及真正的法线之间的夹角的大小。当αₚ0时1/π。概率密度分布函数呈现一条直线也就是说对于任意微表面方向出现的概率都是相等的这和Lambert漫反射模型的结论一致。当αₚ正无穷时会在完全重合时出现峰值趋近于正无穷。整个概率密度函数会越来越窄、中心越来越高说明微表面的法线方向越来越集中且趋向于真实法线方向。Beckmann分布这里的其实和Blinn-Phong分布中的没有本质区别m是微表面的法线h是半程向量。但是只有在微表面的法线和半程向量完全重合时才能被观察者观察到所以实际上二者没什么区别。Blinn-Phong分布和Beckmann分布的α功能恰好相反。GGX分布Trowbridge–Reitz一般在迪士尼规则下常把材质贴图中的 Roughness 视为感知粗糙度 r再定义理解这个公式重点看粗糙度如何改变分布。当小镜面与宏观表面完全平行时也就是说微表面法线和真实法线完全一致时当1时法线概率密度分布函数1/π。概率密度分布函数呈现一条直线也就是说对于任意微表面方向出现的概率都是相等的这和Lambert漫反射模型的结论一致。当趋近于0时法线概率密度分布函数趋近于无穷。也就是说物体表面几乎绝对光滑。GGX分布和Beckmann分布类似和Blinn-Phong相反。微表面菲涅尔项F微表面菲涅耳项F表示光照到一个小镜面上时有多大比例的光能被反射。下图把光照向粗糙表面时的反射方向画了出来用来辅助理解菲涅耳效应蓝色轮廓放大的粗糙表面有许多朝向不同的小斜面。橙色箭头入射光。绿色箭头反射光。由于各处小斜面的法线不同反射方向也不同。比如F0.04表示入射光能中约4% 被反射。其余部分进入材料随后可能传播、散射或被吸收。但是F并不是固定的。最重要的规律是反射比例与入射角有关。以玻璃或水面为例接近垂直看进去反射相对较弱更容易看到内部。沿着表面斜看反射通常更强更像镜子。也就是说微表面菲涅尔项F一定是一个和观测方向v以及微表面法线也就是半程向量h相关的函数。因此在公式内体现为。其实就是和之前一样表示光线角度的不同。实时渲染常用Schlick 近似来作为微表面菲涅尔项F其中F0是垂直入射时的反射率。也就是说观察者垂直观察物体时物体的反射率是F0。当观察者视线擦着小镜面观察时物体的反射率是1几乎全部反射观察者无法透过小镜面看到内部的情况。非金属的F0通常较低金属的F0通常较高而且由于金属表面只会发生反射/吸收不会发生次表面散射等现象因此金属的高光可以带有金属本身的颜色。几何遮蔽项G几何遮蔽项G描述朝向合适的小镜面中有多少既能被光照到又能把反射光送到眼睛而不被周围的微表面挡住。把粗糙表面想象成一片微小的山峰和山谷光路可能在两处被挡住入射遮挡Shadowing光还没照到目标小镜面就被旁边的凸起挡住了。出射遮蔽Masking光照到了小镜面但反射出去后又被旁边的凸起挡住无法到达眼睛。所以即使一个小镜面的朝向满足反射条件它也不一定能对你看到的高光产生贡献。G的常见算法是分别算“光进来有没有被挡”和“光出去有没有被挡”再把两者相乘。这两个分量都可以使用下面这个公式进行计算。G1代表一个G分量的意思首先分别对光照方向I和观察方向V进行计算。其中分子判断微表面有没有朝向对应方向光照方向或观察方向假如微表面没有朝向对应方向G1恒为0说明全部被遮挡。假如微表面朝向了对应方向G1分子为1其具体大小就开始取决于分母。具体的计算如下下方这个公式为Smith遮挡假设下推导出来的模型。其中有一个中间变量a其计算公式为这里的就是对应GGX里的。根据上述公式可以分别计算出光照方向和观察方向下对应的G1。假设我们认为光照进来和出去的遮挡无关可以直接相乘获得G2即完整的G项。但是正常情况下是有关的比如同一个微表面如果位置较高往往既容易被光照到也容易被眼睛看到如果位于低处两边都更容易被挡。两件事并不完全独立。那我们就可以用下面这个公式了,这个公式不需要单独算G1和G2而是算他们的DFG参数组装与代码编写直接使用f0的初始版本我们直接在UnityShader里根据我们上述所提到的内容进行代码编写。在不考虑物体自发光的情况下我们只需要获取材质的漫反射和高光反射即可而这两者我们刚刚都介绍了它们的BRDF漫反射模型我们就使用最简单的Lambert漫反射模型我们主要要计算出DFG所组成的高光反射的BRDF。首先我们先定义PI和两个数学函数用于简化之后公式里大量相乘的情况#define PI 3.1415926 float pow2(float x) { return x*x; } //这里为什么要用这种嵌套的写法主要是节省性能这样算只用进行3次乘法 float pow5(float x) { return pow2(pow2(x)) * x; }D首先我们先算D项D项是法线概率密度分布函数最终得到的是一个float标量。在计算D项时我们一般采用之前提到过的GGX分布其公式如下以下是代码//D float BRDF_D_GGX(float alpha, float ndh) { return (step(0,ndh) * alpha *alpha)/(PI * pow2(1pow2(ndh) * (pow2(alpha)-1))); }其中参数alpha就是粗糙度参数。ndh意味着n dot h就是公式里的n·m。m在这里指微表面法线在PBR中我们一般会把微表面法线和半程向量划等号因为只有和半程向量方向一致的微表面法线才能被观测到。step函数是unityshder自带的函数参数1大于等于参数2时返回1否则返回0。step(edge, value) { value edge ? 1.0 : 0.0 }其实就是套公式没什么好说的。FF项是微表面菲涅尔项其最后返回的数据类型是float3表示 RGB 三类光可以有不同的反射比例。例如铜对红光的反射比对蓝光更强因此反射呈现铜色。以下是代码参数中hdv意味着h dot v也就是微表面法线点乘视线方向F0是垂直入射时的反射率可以后续根据不同材质的相关表格查询。//F float3 BRDF_Fresnel(float hdv, float3 f0) { return f0 (1.0 - f0) * pow5(1.0 - saturate(hdv)); }刚刚为什么要事先定义pow5主要就是为了这里的性能考虑直接使用pow函数会×5次性能开销会比这样封装起来的大这样封装后我们只用×3次。这里的saturate函数是Unity自带函数其可以保证算出的结果非负把数值限制在[0, 1]范围内。等价于clamp(x, 0.0, 1.0)GG项是几何遮蔽项代码如下首先先写后续会用到视线方向和光线方向的lambda值的计算公式float BRDF_G_lambda(float nds, float alpha) { float a nds / (alpha * sqrt(1-pow2(nds))); return (-1 sqrt(1 1/pow2(a)))/2; }参数中中nds就是n dot s也就是法线和s方向的点积s后续包含视线方向和光线方向。alpha就是粗糙度参数和D项中一致。计算出Lambda之后开始计算整个几何遮蔽项float BRDF_G_Smith(float ndv, float ndl, float hdv, float hdl, float alpha) { return step(0,hdv) * step(0,hdl)/(1BRDF_G_lambda(ndv,alpha) BRDF_G_lambda(ndl,alpha)); }BRDF在DFG项都有相应计算函数后我们开始编写BRDF本身的计算函数由于我们D项使用GGX分布G项使用Smith分布我们这个BRDF就名为BRDF_SmithGGX。为什么Fresnel项不用加入到命名中因为Fresnel项的计算方式基本都是那一个没什么特色。但是D项和G项是可以选择的。//BRDF float3 BRDF_SmithGGX(float ndv, float ndl, float hdv, float hdl,float ndh,float alpha,float3 f0) { float3 dfg BRDF_D_GGX(alpha,ndh) * BRDF_Fresnel(hdv,f0) * BRDF_G_Smith(ndv,ndl,hdv,hdl,alpha); return dfg / (4*max(ndl,0)*max(ndv,0)); }参数方面就是把DFG三项计算以及公式分母计算要用的各个点积传进来同时传入alpha值粗糙度系数以及f0影响菲涅尔反射的垂直观察反射率。BRDF_VIS如上图所示在GGX和Smith函数同时使用时G项可以和BRDF的分母进行组合形成的结构可以近似为一个分子为0.5分母为线性插值的式子。我们可以使用这个结构的近似直接参与BRDF的运算可以得到类似的效果而且性能更好适合在手机平台上使用我们称为BRDF_Vis和BRDF具有完全相同的用法。//BRDF Vis float BRDF_Vis_Smith(float ndv, float ndl,float alpha) { return 0.5/lerp(2*abs(ndl) * abs(ndv), abs(ndl)abs(ndv),alpha); } float3 BRDF_SmithGGX_Vis(float ndv,float ndl,float hdv,float ndh,float alpha,float3 f0) { return BRDF_D_GGX(alpha,ndh) * BRDF_Fresnel(hdv,f0) * BRDF_Vis_Smith(ndv,ndl,alpha); }渲染方程由于刚刚获得了BRDF我们就可以根据渲染方程来写光照的逻辑了。我们暂时先只考虑我们刚刚算出来的镜面反射的BRDF值漫反射之后再考虑可以直接代入渲染方程得到下列函数//渲染方程 float3 LightModel(float3 lightColor,float3 f0,float alpha,float3 lightDirection,float3 viewDirection,float3 normal) { float ndv dot(normal,viewDirection); float ndl dot(normal,lightDirection); //这里判断的意思就是当光源或视线在法线的背面时直接返回0避免出现负数的情况。 //如果出现负数的情况那么在BRDF_SmithGGX中就会出现除以0的情况导致结果为NaN。 if (ndv 0.0 || ndl 0.0) return float3(0.0, 0.0, 0.0); //半程向量 float3 h normalize(lightDirection viewDirection); float hdv dot(h,viewDirection); float hdl dot(h,lightDirection); float ndh saturate(dot(normal, h)); return PI * BRDF_SmithGGX(ndv,ndl,hdv,hdl, ndh, alpha,f0) * lightColor * max(0,ndl); }其实要注意的点就是为什么中间要对ndv和ndl进行非负判断这是为了防止刚刚算BRDF时分母为0而无法正常计算。在片元着色器中输出struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 normal : TEXCOORD1; float3 worldPos : TEXCOORD2; }; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.normal UnityObjectToWorldNormal(v.normal); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag (v2f i) : SV_Target { float3 finalLighting float3(0,0,0); float3 worldNormal normalize(i.normal); float3 viewDirection normalize(_WorldSpaceCameraPos.xyz - i.worldPos); float3 lightDirection normalize(UnityWorldSpaceLightDir(i.worldPos)); float3 f0 (0.562, 0.565, 0.578); //粗糙度系数 float alpha 0.2; finalLighting LightModel(_LightColor0.rgb, f0, alpha,lightDirection, viewDirection, worldNormal); return float4(finalLighting, 1.0); }如上应该过程比较清晰不一一赘述。另外由于我们使用了unity自带的关于Light的内置变量需要引入一个相关的库#include Lighting.cginc然后说说f0的取值怎么确定。这两张是材质的f0参考表用来设置“光线垂直入射时表面反射多少光、反射什么颜色”。f0在表中可以通过Linear栏获知第一个表是非金属第二个表是金属。比如F00.04表示入射光直射材质表面时约4% 被反射可见非金属反射率比金属低一般都是折射走了。而且金属的f0是float3而不是非金属的单个float。这是因为常见非金属的镜面反射基本不带颜色因此通常用一个数同时表示 RGBfloat3 f0 float3(0.04, 0.04, 0.04);我们代码中所取的就是铁的f0.结果输出后如图所示还是效果很不错的。下面给出全部代码Shader MyHitStudy/SimplePBR { SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc #include Lighting.cginc #define PI 3.1415926 float pow2(float x) { return x*x; } //这里为什么要用这种嵌套的写法主要是节省性能这样算只用进行3次乘法 float pow5(float x) { return pow2(pow2(x)) * x; } //F float3 BRDF_Fresnel(float hdv, float3 f0) { return f0 (1.0 - f0) * pow5(1.0 - saturate(hdv)); } //D float BRDF_D_GGX(float alpha, float ndh) { return (step(0,ndh) * alpha *alpha)/(PI * pow2(1pow2(ndh) * (pow2(alpha)-1))); } //G float BRDF_G_lambda(float nds, float alpha) { float a nds / (alpha * sqrt(1-pow2(nds))); return (-1 sqrt(1 1/pow2(a)))/2; } float BRDF_G_Smith(float ndv, float ndl, float hdv, float hdl, float alpha) { return step(0,hdv) * step(0,hdl)/(1BRDF_G_lambda(ndv,alpha) BRDF_G_lambda(ndl,alpha)); } //BRDF float3 BRDF_SmithGGX(float ndv, float ndl, float hdv, float hdl,float ndh,float alpha,float3 f0) { float3 dfg BRDF_D_GGX(alpha,ndh) * BRDF_Fresnel(hdv,f0) * BRDF_G_Smith(ndv,ndl,hdv,hdl,alpha); return dfg / (4*max(ndl,0)*max(ndv,0)); } //BRDF Vis float BRDF_Vis_Smith(float ndv, float ndl,float alpha) { return 0.5/lerp(2*abs(ndl) * abs(ndv), abs(ndl)abs(ndv),alpha); } float3 BRDF_SmithGGX_Vis(float ndv,float ndl,float hdv,float ndh,float alpha,float3 f0) { return BRDF_D_GGX(alpha,ndh) * BRDF_Fresnel(hdv,f0) * BRDF_Vis_Smith(ndv,ndl,alpha); } //渲染方程 float3 LightModel(float3 lightColor,float3 f0,float alpha,float3 lightDirection,float3 viewDirection,float3 normal) { float ndv dot(normal,viewDirection); float ndl dot(normal,lightDirection); //这里判断的意思就是当光源或视线在法线的背面时直接返回0避免出现负数的情况。 //如果出现负数的情况那么在BRDF_SmithGGX中就会出现除以0的情况导致结果为NaN。 if (ndv 0.0 || ndl 0.0) return float3(0.0, 0.0, 0.0); //半程向量 float3 h normalize(lightDirection viewDirection); float hdv dot(h,viewDirection); float hdl dot(h,lightDirection); float ndh saturate(dot(normal, h)); return PI * BRDF_SmithGGX(ndv,ndl,hdv,hdl, ndh, alpha,f0) * lightColor * max(0,ndl); } struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 normal : TEXCOORD1; float3 worldPos : TEXCOORD2; }; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.normal UnityObjectToWorldNormal(v.normal); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag (v2f i) : SV_Target { float3 finalLighting float3(0,0,0); float3 worldNormal normalize(i.normal); float3 viewDirection normalize(_WorldSpaceCameraPos.xyz - i.worldPos); float3 lightDirection normalize(UnityWorldSpaceLightDir(i.worldPos)); float3 f0 (0.562, 0.565, 0.578); //粗糙度系数 float alpha 0.2; finalLighting LightModel(_LightColor0.rgb, f0, alpha,lightDirection, viewDirection, worldNormal); return float4(finalLighting, 1.0); } ENDCG } } }引入了Albedo、Metallic、漫反射的版本我们刚刚所实现的版本只涉及镜面反射这对于光滑的金属来说可以这么做但是对于非金属来说还需要加入漫反射。加入漫反射我们可以直接改变光照模型前文有提到过lambert漫反射的BRDF可以看作是1/PI因此我们可以直接修改为以下形式//渲染方程 float3 LightModel(float3 lightColor,float3 f0,float alpha,float3 lightDirection,float3 viewDirection,float3 normal,float3 albedo) { float ndv dot(normal,viewDirection); float ndl dot(normal,lightDirection); //这里判断的意思就是当光源或视线在法线的背面时直接返回0避免出现负数的情况。 //如果出现负数的情况那么在BRDF_SmithGGX中就会出现除以0的情况导致结果为NaN。 if (ndv 0.0 || ndl 0.0) return float3(0.0, 0.0, 0.0); //半程向量 float3 h normalize(lightDirection viewDirection); float hdv dot(h,viewDirection); float hdl dot(h,lightDirection); float ndh saturate(dot(normal, h)); //*******************此处发生修改********************** //return PI * BRDF_SmithGGX(ndv,ndl,hdv,hdl, ndh, alpha,f0) * lightColor * max(0,ndl); return PI * (((1/PI) BRDF_SmithGGX(ndv,ndl,hdv,hdl, ndh, alpha,f0)) * lightColor * max(0,ndl); }然而直接这样干会出现两个问题1、漫反射并没有颜色也不能灵活控制漫反射大小。2、漫反射的强度和镜面反射的强度彼此调节起来是独立的这有悖现实直接在镜面反射上叠加漫反射不符合能量守恒。漫反射反照率Albedo针对第一个问题我们引入了ρ替换1作为分子我们可以用漫反射反照率Albedo作为ρ将其置于lambert漫反射的分子。这样我们的lamber漫反射的BRDF就更新为如下形式。把这个代入到刚刚的光照模型中//渲染方程 float3 LightModel(float3 lightColor,float3 f0,float alpha,float3 lightDirection,float3 viewDirection,float3 normal,float3 albedo) { float ndv dot(normal,viewDirection); float ndl dot(normal,lightDirection); //这里判断的意思就是当光源或视线在法线的背面时直接返回0避免出现负数的情况。 //如果出现负数的情况那么在BRDF_SmithGGX中就会出现除以0的情况导致结果为NaN。 if (ndv 0.0 || ndl 0.0) return float3(0.0, 0.0, 0.0); //半程向量 float3 h normalize(lightDirection viewDirection); float hdv dot(h,viewDirection); float hdl dot(h,lightDirection); float ndh saturate(dot(normal, h)); //***************以下发生修改******************* return PI * (((albedo/PI) BRDF_SmithGGX(ndv,ndl,hdv,hdl, ndh, alpha,f0)) * lightColor * max(0,ndl); }我们再给albedo赋值为(1,1,1)很明显能看到金属感下降严重OK第一个问题解决了接着解决第二个问题漫反射的强度和镜面反射的强度彼此调节起来是独立的这有悖现实直接在镜面反射上叠加漫反射不符合能量守恒。漫反射和镜面反射各算各的没有协调两者使用的能量。例如提高F0后镜面反射增强了漫反射却完全不变。尤其当albedo接近白色时直接相加容易造成反射能量过多。因此我们需要给漫反射增加一个权重。利用 Fresnel降低漫反射的权重Fresnel 项 F 描述界面直接反射光线的比例。因此我们可以通过对计算F相对100%反射的剩余比例来作为漫反射的权重//渲染方程 float3 LightModel(float3 lightColor,float3 f0,float alpha,float3 lightDirection,float3 viewDirection,float3 normal,float3 albedo) { float ndv dot(normal,viewDirection); float ndl dot(normal,lightDirection); //这里判断的意思就是当光源或视线在法线的背面时直接返回0避免出现负数的情况。 //如果出现负数的情况那么在BRDF_SmithGGX中就会出现除以0的情况导致结果为NaN。 if (ndv 0.0 || ndl 0.0) return float3(0.0, 0.0, 0.0); //半程向量 float3 h normalize(lightDirection viewDirection); float hdv dot(h,viewDirection); float hdl dot(h,lightDirection); float ndh saturate(dot(normal, h)); //*************以下发生修改*************** return PI * ((1-BRDF_Fresnel(hdv,f0))*(albedo/PI) BRDF_SmithGGX(ndv,ndl,hdv,hdl, ndh, alpha,f0)) * lightColor * max(0,ndl); }引入Metallic使用Albedo控制漫反射项我们之前有提到过金属可以不用漫反射项非金属需要漫反射项。然而我们刚刚修改完后默认所有材质都具有漫反射。因此我们可以使用一种插值的方法使用Metallic对漫反射的ρ进行控制使其在albedo和0之间插值Metallic为0时和之前的式子一致为1时漫反射这一项去除变为纯金属。//渲染方程 float3 LightModel(float3 lightColor,float3 f0,float alpha,float3 lightDirection,float3 viewDirection,float3 normal,float3 albedo,float metallic) { float ndv dot(normal,viewDirection); float ndl dot(normal,lightDirection); //这里判断的意思就是当光源或视线在法线的背面时直接返回0避免出现负数的情况。 //如果出现负数的情况那么在BRDF_SmithGGX中就会出现除以0的情况导致结果为NaN。 if (ndv 0.0 || ndl 0.0) return float3(0.0, 0.0, 0.0); //半程向量 float3 h normalize(lightDirection viewDirection); float hdv dot(h,viewDirection); float hdl dot(h,lightDirection); float ndh saturate(dot(normal, h)); //*************以下发生修改*************** return PI * ((1-BRDF_Fresnel(hdv,f0))*(lerp(albedo, 0, metallic)/PI) BRDF_SmithGGX(ndv,ndl,hdv,hdl, ndh, alpha,f0)) * lightColor * max(0,ndl); }做到这里已经可以灵活使用金属度Metallic进行金属和非金属的转换也能使用Albedo来调节漫反射的颜色了。然而还不够因为两类材质的镜面反射颜色也不同。我们还要能调节镜面反射。使用Metallic控制Albedo调节镜面反射普通塑料的基础色主要体现在漫反射里镜面反射通常接近无色。常见简化我们之前默默做过了就是使得非金属的Fresnel项的f0的RGB三通道同时取一个值我们一般取0.04而金属的则是不同的值//非金属的f0值 float3 f0 float3(0.04, 0.04, 0.04);金属的颜色主要来自镜面反射所以金属情况下基础色应该用于 F0float3 f0 albedo;把金属和非金属的这两种情况合并来根据金属度情况决定f0的取值也就是高光的表现float3 f0 lerp(float3(0.04, 0.04, 0.04), albedo, metallic);所以其实Albedo不仅能控制漫反射颜色还能控制高光反射颜色。也正因为这一步albedo在金属度工作流中更适合被称为Base Color基础色非金属时它决定漫反射颜色。金属时它决定 F0也就是正入射镜面反射的颜色。所以同一张基础色贴图在两类材质中承担的作用不同我们把上述内容运用到当前的代码中同时优化一下布局方便阅读//渲染方程 float3 LightModel(float3 lightColor,float alpha,float3 lightDirection,float3 viewDirection,float3 normal,float3 albedo,float metallic) { float ndv dot(normal,viewDirection); float ndl dot(normal,lightDirection); //这里判断的意思就是当光源或视线在法线的背面时直接返回0避免出现负数的情况。 //如果出现负数的情况那么在BRDF_SmithGGX中就会出现除以0的情况导致结果为NaN。 if (ndv 0.0 || ndl 0.0) return float3(0.0, 0.0, 0.0); //半程向量 float3 h normalize(lightDirection viewDirection); float hdv dot(h,viewDirection); float hdl dot(h,lightDirection); float ndh saturate(dot(normal, h)); float3 f0 lerp(0.04,albedo,metallic); float3 rou_ss lerp(albedo, 0, metallic); float3 specular BRDF_SmithGGX(ndv,ndl,hdv,hdl, ndh, alpha,f0); float3 diffuse (1-BRDF_Fresnel(hdv,f0))* rou_ss/PI; return PI * (specular diffuse) * lightColor * max(0,ndl); }可见我们f0已经不再是直接指定了而是通过Albedo和Metallic进行插值得到。总结以上就是关于Unity PBR的基础实现方法之后为了方便我把使用的参数都作为Properties放到了面板上进行调节。同时我还引入了MainTex作为albedo的一部分使得材质可以被赋予贴图。以下是完整代码Shader MyHitStudy/SimplePBR2 { Properties { _MainTex (Texture, 2D) white {} _Color (Base Color, Color) (1, 1, 1, 1) _Metallic (Metallic, Range(0, 1)) 0.0 _Alpha (Roughness (Alpha), Range(0.01, 1)) 0.8 } SubShader { Pass { Tags { LightMode ForwardBase } CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc #include Lighting.cginc sampler2D _MainTex; float4 _MainTex_ST; float4 _Color; float _Metallic; float _Alpha; #define PI 3.1415926 float pow2(float x) { return x*x; } //这里为什么要用这种嵌套的写法主要是节省性能这样算只用进行3次乘法 float pow5(float x) { return pow2(pow2(x)) * x; } //F float3 BRDF_Fresnel(float hdv, float3 f0) { return f0 (1.0 - f0) * pow5(1.0 - saturate(hdv)); } //D float BRDF_D_GGX(float alpha, float ndh) { return (step(0,ndh) * alpha *alpha)/(PI * pow2(1pow2(ndh) * (pow2(alpha)-1))); } //G float BRDF_G_lambda(float nds, float alpha) { float a nds / (alpha * sqrt(1-pow2(nds))); return (-1 sqrt(1 1/pow2(a)))/2; } float BRDF_G_Smith(float ndv, float ndl, float hdv, float hdl, float alpha) { return step(0,hdv) * step(0,hdl)/(1BRDF_G_lambda(ndv,alpha) BRDF_G_lambda(ndl,alpha)); } //BRDF float3 BRDF_SmithGGX(float ndv, float ndl, float hdv, float hdl,float ndh,float alpha,float3 f0) { float3 dfg BRDF_D_GGX(alpha,ndh) * BRDF_Fresnel(hdv,f0) * BRDF_G_Smith(ndv,ndl,hdv,hdl,alpha); return dfg / (4*max(ndl,0)*max(ndv,0)); } //BRDF Vis float BRDF_Vis_Smith(float ndv, float ndl,float alpha) { return 0.5/lerp(2*abs(ndl) * abs(ndv), abs(ndl)abs(ndv),alpha); } float3 BRDF_SmithGGX_Vis(float ndv,float ndl,float hdv,float ndh,float alpha,float3 f0) { return BRDF_D_GGX(alpha,ndh) * BRDF_Fresnel(hdv,f0) * BRDF_Vis_Smith(ndv,ndl,alpha); } //渲染方程 float3 LightModel(float3 lightColor,float alpha,float3 lightDirection,float3 viewDirection,float3 normal,float3 albedo,float metallic) { float ndv dot(normal,viewDirection); float ndl dot(normal,lightDirection); //这里判断的意思就是当光源或视线在法线的背面时直接返回0避免出现负数的情况。 //如果出现负数的情况那么在BRDF_SmithGGX中就会出现除以0的情况导致结果为NaN。 if (ndv 0.0 || ndl 0.0) return float3(0.0, 0.0, 0.0); //半程向量 float3 h normalize(lightDirection viewDirection); float hdv dot(h,viewDirection); float hdl dot(h,lightDirection); float ndh saturate(dot(normal, h)); float3 f0 lerp(0.04,albedo,metallic); float3 rou_ss lerp(albedo, 0, metallic); float3 specular BRDF_SmithGGX(ndv,ndl,hdv,hdl, ndh, alpha,f0); float3 diffuse (1-BRDF_Fresnel(hdv,f0))* rou_ss/PI; return PI * (specular diffuse) * lightColor * max(0,ndl); } struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float3 normal : TEXCOORD1; float3 worldPos : TEXCOORD2; }; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.normal UnityObjectToWorldNormal(v.normal); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag (v2f i) : SV_Target { float3 albedo tex2D(_MainTex, i.uv).rgb * _Color.rgb; float3 finalLighting float3(0,0,0); float3 worldNormal normalize(i.normal); float3 viewDirection normalize(_WorldSpaceCameraPos.xyz - i.worldPos); float3 lightDirection normalize(UnityWorldSpaceLightDir(i.worldPos)); float alpha clamp(_Alpha, 0.01, 1.0); finalLighting LightModel(_LightColor0.rgb, alpha,lightDirection, viewDirection, worldNormal,albedo, _Metallic); return float4(finalLighting, 1.0); } ENDCG } } }

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询