==========================================================
1、PBS理论基础
立体角
是一个物体对于一个特定观察点在三维空间中的角度(观测到的大小),记作Ω,
单位为球面度(sr),即三维弧度,1球面度的立体角所对应的球面表面积为(
为球半径)
辐射通量 Radiant Flux
光源每秒钟发射的功率,记作Φ,单位为W
辐射亮度 Radiance
辐射源上某点在某方向上(单位投影面积在单位立体角内)的辐射通量,记作L,单位为
即某点在某方向上的亮度,是图形学光照方程最终要计算的量
辐射强度 Radiant Intensity
辐射源在某方向上(单位立体角内)的辐射通量的积分,记作I,单位为
在图形学中用来表示光源的辐射强度分布,这个分布是方向的函数
辐射照度 Irradiance
辐射源上某点(单位投影面积)在各方向的辐射通量的积分,记作E,单位为
在图形学中用来表示表面上一个点(单位面积)接收的所有光照
微面元 Microfacet
在微观上,表面上一点是由许多(方向不同的)微面元组成,
其中每个微面元都是绝对光滑的,即光线与这些微面元的交互符合反射定律和折射定律
只有那些法线等于半矢量h(入射方向l和观察方向v的中间向量)的微面元才可能被看到
微法线分布函数 NDF
表示表面上一点的微面元的法线在各方向上分布的概率,即有多少微面元的法线等于半矢量h,
函数形式,其中
是宏观法线n与半矢量h之间的夹角
GGX
Unity中使用的微法线分布函数,其中h为半矢量、n为宏观法线、roughness为粗糙度,
微面元遮挡函数
表示具有半矢量法线的微面元中,有多少是没有(在入射和反射方向上)被遮挡的
函数形式,其中
和
分别表示入射方向l和观察方向v与宏观法线n之间的夹角
Smith-Schlick
Unity中使用的微面元遮挡函数,其中l为入射方向、v为观察方向、n为宏观法线、roughness为粗糙度,
Unity5.3.7中,上述公式对应的函数为SmithGGXVisibilityTerm,而实际使用的函数为SmithJointGGXVisibilityTerm
菲涅耳公式
表示当光线由一种介质进入另一种介质(光滑表面),入射光分别被反射和折射的比率
我们主要关心其中(高光)反射的部分,而折射部分要么被吸收要么属于漫反射的范畴
Schlick菲涅耳近似
Unity中使用的菲涅耳近似等式,其中l为入射方向、h为半矢量,
入射光垂直于表面时的菲涅耳反射率(高光反射率,记作
)
双向反射分布函数 BRDF
表示反射方向(观察反方向、出射方向)上的辐射亮度增量与入射方向辐射照度增量的比率,
直观上讲,就是当光线沿入射方向到达表面某点时,有多少能量被反射到观察(反)方向上
函数形式,其中
为入射方向、
为出射方向
次表面散射 Subsurface Scattering
也称为漫反射,是指光线从(非金属且不完全透明的)物体表面上一点(折射)进入物体内部,
由于物体内部介质不均匀,经过若干次散射,
最终,一部分被吸收,一部分重新从物体表面散射出来(可能出射点不同于入射点)
金属会吸收掉所有折射进入的光,而透明物体会被折射进入的光穿过并且再次折射出去
双向次表面散射反射分布函数 BSSRDF
用作模拟次表面散射,在BRDF的基础上增加了两个参数:入射点位置和出射点位置
函数形式
当入射点和所有出射点的距离均小于1像素,则BSSRDF可用BRDF替代
漫反射部分的BRDF
传统的漫反射模型为Lambert模型,
首先,假设入射点和所有出射点相同(距离小于1像素);
并且,从BRDF的角度看,所有出射方向上的比率都一样:
其中为物体表面的“颜色”,即对入射光的RGB各分量的漫反射(未吸收)比例
Disney Diffuse
Unity中使用的漫反射计算公式,(相比)可以更好的模拟次表面散射
其中baseColor为物体表面的“颜色”、roughness为粗糙度、n为宏观法线、l为入射方向、v为出射方向
Unity5.3.7中,实际使用的公式,最终并没有除以π
高光反射部分的BRDF
高光反射是指光线直接从物体表面反射出去(未进入物体内部),基于微面元理论,可以得到下面的BRDF公式:
其中,为菲涅耳反射率、
为微面元遮挡函数、
为微法线分布函数、
为校正因子
Unity5.3.7中,实际使用的公式,并没有除以(n · l) (n · v),只除以4并且乘以π ?
最终着色结果
最终被看到的颜色(由被观察点射向摄像机的光线的亮度),为漫反射部分加上高光反射部分,即
金属与非金属
金属材质,没有漫反射,只有高光反射,而高光反射率RGB分量可能不同
非金属材质,漫反射率RGB分量可能不同,高光反射率RGB分量是一样的(大多数很接近<0.05, 0.05, 0.05>)
Unity的Standard.Shader中的Albedo属性就对应了这两种反射率(金属材质高光反射率、非金属材质漫反射率)
==========================================================
2、Unity中的实现
在Unity5中内置了两个基于PBR理论的Shader,Standard.Shader(金属流)和StandardSpecular.shader(反射流),
它们主要的区别是:
有一项输入属性(Property)不同,前者为金属度(Metallic),后者为高光反射度(Specular),
并且使用了不同的函数(MetallicSetup、SpecularSetup)根据各自的输入来初始化BRDF模型所需要的参数
金属流
高光反射率():按金属度在unity_ColorSpaceDielectricSpec.rgb和Albedo.rgb之间插值
漫反射率:按金属度在Albedo.rgb * unity_ColorSpaceDielectricSpec.a和Albedo.rgb * 0.0之间插值
反射流
高光反射率():就是高光反射度
漫反射率:按高光反射度(rgb中的最大值)在Albedo.rgb和<0, 0, 0>之间插值
==========================================================
3、Standard.Shader分析
下面以Standard.shader为例分析其中的具体实现(简单起见,只涉及一个Pass:ForwardBase)
完整代码请至官网下载,或者这里http://download.csdn.net/detail/liumazi/9863474
主要属性
_Color("Color", Color) = (1,1,1,1) // 反射率(整体),漫反射/高光反射(非金属/金属)
_MainTex("Albedo", 2D) = "white" {} // 反射率(纹理),漫反射/高光反射(非金属/金属)
_Cutoff("AlphaCutoff", Range(0.0, 1.0)) = 0.5 // 当RenderingMode为Cutoff时使用(alpha测试值)
_Glossiness("Smoothness", Range(0.0, 1.0)) = 0.5 // 光滑度(整体)
[Gamma]_Metallic("Metallic", Range(0.0, 1.0)) = 0.0 // 金属度(整体)
_MetallicGlossMap("Metallic", 2D) ="white" {} // 金属度+光滑度(纹理),当指定纹理时上面2项失效
_BumpScale("Scale", Float) = 1.0
_BumpMap("NormalMap", 2D) ="bump" {} // 法线纹理
顶点着色器参数
struct VertexInput
{
float4 vertex: POSITION; // 顶点位置
half3 normal : NORMAL; // 顶点法线
float2 uv0 : TEXCOORD0;// 纹理坐标
float2 uv1 : TEXCOORD1;// 纹理坐标2
#ifdef _TANGENT_TO_WORLD // 存在法线贴图时 = 1
half4 tangent: TANGENT; // 顶点切线
#endif
};
顶点着色器返回值/片元着色器参数
struct VertexOutputForwardBase
{
float4 pos: SV_POSITION; // 顶点位置(裁剪空间)
float4 tex: TEXCOORD0; // 纹理坐标(2套)
half3 eyeVec: TEXCOORD1; // 观察方向
half4 tangentToWorldAndParallax[3]: TEXCOORD2; // TBN矩阵(用于转换切空间法线)或法线(存于[2])
half4 ambientOrLightmapUV: TEXCOORD5; // SH or Lightmap UV
SHADOW_COORDS(6) // 阴影相关,忽略
UNITY_FOG_COORDS(7) // 雾相关,忽略
// next ones would not fit into SM2.0 limits, but theyare always for SM3.0+
#if UNITY_SPECCUBE_BOX_PROJECTION
float3 posWorld: TEXCOORD8; // 顶点位置(世界空间)
#endif
#if UNITY_OPTIMIZE_TEXCUBELOD
#if UNITY_SPECCUBE_BOX_PROJECTION
half3 reflUVW: TEXCOORD9;
#else
half3 reflUVW: TEXCOORD8;
#endif
#endif
};
顶点着色器
VertexOutputForwardBase vertForwardBase(VertexInput v)
{
VertexOutputForwardBase o;
UNITY_INITIALIZE_OUTPUT(VertexOutputForwardBase, o); // 初始化(清零)
float4 posWorld = mul(_Object2World, v.vertex); // 坐标->世界空间
#if UNITY_SPECCUBE_BOX_PROJECTION
o.posWorld = posWorld.xyz;
#endif
o.pos = mul(UNITY_MATRIX_MVP, v.vertex); // 坐标->裁剪空间
o.tex = TexCoords(v); // 纹理坐标变换,通过TRANSFORM_TEX
o.eyeVec = NormalizePerVertexNormal(posWorld.xyz - _WorldSpaceCameraPos); // 观察方向(相机到顶点)
float3 normalWorld = UnityObjectToWorldNormal(v.normal); // 法线->世界坐标
#ifdef _TANGENT_TO_WORLD // 存在法线贴图时 = 1
// 切线->世界坐标(注意v.tangent.w用来修正副法线binormal方向)
float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);
// 根据切线和法线计算副法线,从而得到TBN矩阵
float3x3 tangentToWorld = CreateTangentToWorldPerVertex(normalWorld, tangentWorld.xyz, tangentWorld.w);
o.tangentToWorldAndParallax[0].xyz = tangentToWorld[0];
o.tangentToWorldAndParallax[1].xyz = tangentToWorld[1];
o.tangentToWorldAndParallax[2].xyz = tangentToWorld[2];
#else
o.tangentToWorldAndParallax[0].xyz = 0;
o.tangentToWorldAndParallax[1].xyz = 0;
o.tangentToWorldAndParallax[2].xyz = normalWorld;
#endif
TRANSFER_SHADOW(o); // 阴影相关,忽略
o.ambientOrLightmapUV = VertexGIForward(v,posWorld, normalWorld); // GI相关,忽略
#ifdef _PARALLAXMAP // 不存在高度图,忽略
TANGENT_SPACE_ROTATION;
half3 viewDirForParallax = mul(rotation,ObjSpaceViewDir(v.vertex));
o.tangentToWorldAndParallax[0].w = viewDirForParallax.x;
o.tangentToWorldAndParallax[1].w = viewDirForParallax.y;
o.tangentToWorldAndParallax[2].w = viewDirForParallax.z;
#endif
#if UNITY_OPTIMIZE_TEXCUBELOD
o.reflUVW = reflect(o.eyeVec, normalWorld);
#endif
UNITY_TRANSFER_FOG(o, o.pos); // 雾相关,忽略
return o;
}
片元通用结构
struct FragmentCommonData
{
half3 diffColor, specColor; // 漫反射率、高光反射率(F0)
// Note: oneMinusRoughness & oneMinusReflectivity foroptimization purposes, mostly for DX9 SM2.0 level.
// Most of the math is being done on these (1-x) values,and that saves a few precious ALU slots.
half oneMinusReflectivity, oneMinusRoughness; // 漫反射总比率、光滑度
half3 normalWorld, eyeVec, posWorld; // 法线、观察方向、位置(均处于世界空间)
half alpha; // 透明度
#if UNITY_OPTIMIZE_TEXCUBELOD || UNITY_STANDARD_SIMPLE
half3 reflUVW;
#endif
};
金属度与两种反射率的关系
inline half3 DiffuseAndSpecularFromMetallic(half3 albedo, half metallic,
out half3 specColor, out half oneMinusReflectivity)
{
// 当材质为金属(metallic = 1.0),高光反射率(F0)= albedo
// 当材质为非金属(metallic = 0.0),高光反射率(F0)= unity_ColorSpaceDielectricSpec(电介质的F0)
// 当材质介于两者之间,按金属度 metallic插值
specColor = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, metallic); // 高光反射率(F0)
// 当材质为金属,漫反射总比率 = 0.0(没有漫反射)
// 当材质为非金属,漫反射总比率 = unity_ColorSpaceDielectricSpec.a = 1-reflectivity(接近1.0)
// 当材质介于两者之间,按金属度 metallic插值
oneMinusReflectivity = OneMinusReflectivityFromMetallic(metallic); // 漫反射总比率
return albedo * oneMinusReflectivity; // 漫反射率
}
金属流输入处理
inline FragmentCommonData MetallicSetup(float4 i_tex)
{
// 获取金属度、光滑度(从_MetallicGlossMap中采样或直接使用_Metallic和_Glossiness的值)
half2 metallicGloss = MetallicGloss(i_tex.xy);
half metallic = metallicGloss.x;
half oneMinusRoughness = metallicGloss.y; // this is 1 minusthe square root of real roughness m.
half oneMinusReflectivity;
half3 specColor;
half3 diffColor = DiffuseAndSpecularFromMetallic(
Albedo(i_tex), // _Color.rgb * _MainTex纹理采样值
metallic,
/*out*/specColor,
/*out*/oneMinusReflectivity);
FragmentCommonData o = (FragmentCommonData)0;
o.diffColor = diffColor; // 漫反射率
o.specColor = specColor; // 高光反射率(F0)
o.oneMinusReflectivity = oneMinusReflectivity;
o.oneMinusRoughness = oneMinusRoughness;
return o;
}
片元预处理
inline FragmentCommonData FragmentSetup(
float4 i_tex, // 纹理坐标
half3 i_eyeVec, // 观察方向
half3 i_viewDirForParallax, // 无高度纹理时为<0,0,0>
half4 tangentToWorld[3], // TBN矩阵(用于转换切空间法线)或法线(存于[2])
half3 i_posWorld) // 世界坐标位置
{
i_tex = Parallax(i_tex,i_viewDirForParallax); // 无高度纹理时直接返回i_tex
half alpha = Alpha(i_tex.xy); // tex2D(_MainTex,uv).a * _Color.a;
#if defined(_ALPHATEST_ON)
clip(alpha - _Cutoff); // Alpha测试
#endif
// 调用MetallicSetup,以初始化o的部分内容:diffColor、specColor、oneMinusReflectivity、oneMinusRoughness
FragmentCommonData o = UNITY_SETUP_BRDF_INPUT(i_tex);
o.normalWorld = PerPixelWorldNormal(i_tex,tangentToWorld); // 取世界空间法线(来自法线纹理或顶点法线)
o.eyeVec = NormalizePerPixelNormal(i_eyeVec); // 观察方向(归一化)
o.posWorld = i_posWorld; // 世界空间坐标
// NOTE: shader relies on pre-multiply alpha-blend(_SrcBlend = One, _DstBlend = OneMinusSrcAlpha)
o.diffColor = PreMultiplyAlpha(o.diffColor,alpha, o.oneMinusReflectivity,/*out*/o.alpha); // 预乘
return o;
}
片元着色器
half4 fragForwardBaseInternal(VertexOutputForwardBase i)
{
FRAGMENT_SETUP(s) // 调用 FragmentSetup初始化 FragmentCommonData s;
#if UNITY_OPTIMIZE_TEXCUBELOD
s.reflUVW = i.reflUVW;
#endif
UnityLight mainLight = MainLight(s.normalWorld); // 主光源信息:颜色、位置、反方向与法线的夹角余弦(点积)
half atten = SHADOW_ATTENUATION(i); // 阴影相关,忽略
half occlusion = Occlusion(i.tex.xy);
UnityGI gi = FragmentGI(s, occlusion, i.ambientOrLightmapUV, atten, mainLight); // GI相关,忽略
// 基于物理着色,分了3档,这里重点分析第1档(BRDF1_Unity_PBS)
half4 c = UNITY_BRDF_PBS(s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness,
s.normalWorld, -s.eyeVec, gi.light, gi.indirect); // 注意:观察方向有取反(从被观察点到摄像机)
c.rgb += UNITY_BRDF_GI(s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness,
s.normalWorld, -s.eyeVec, occlusion, gi); // GI相关,忽略
c.rgb += Emission(i.tex.xy); // 自发光,忽略
UNITY_APPLY_FOG(i.fogCoord, c.rgb); // 雾相关,忽略
return OutputForward(c, s.alpha); // 修改c.a(开启混合时=s.alpha,否则等于1.0)
}
基于物理着色
half4 BRDF1_Unity_PBS(half3 diffColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness,
half3 normal, half3 viewDir, UnityLight light, UnityIndirect gi)
{
half roughness = 1-oneMinusRoughness; // 粗糙度
half3 halfDir = Unity_SafeNormalize(light.dir + viewDir); // 半矢量(注意:两个向量都是从被观察点出发)
#define UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV 0
#if UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV
half shiftAmount = dot(normal, viewDir);
normal = shiftAmount < 0.0f ? normal + viewDir * (-shiftAmount + 1e-5f) : normal;
half nl = DotClamped(normal, light.dir);
#else
half nl = light.ndotl; // 宏观法线和光线(反)方向夹角余弦
#endif
half nh = BlinnTerm(normal, halfDir); // 宏观法线和半矢量夹角余弦
half nv = DotClamped(normal, viewDir); // 宏观法线和观察(反)方向夹角余弦
half lv = DotClamped(light.dir, viewDir); // 光线(反)方向和观察(反)方向夹角余弦
half lh = DotClamped(light.dir, halfDir); // 光线(反)方向和半矢量夹角余弦
#if UNITY_BRDF_GGX
half V = SmithJointGGXVisibilityTerm(nl, nv, roughness); // 微面元遮挡函数G
half D = GGXTerm(nh, roughness); // 微面元法线分布函数D
#else
half V = SmithBeckmannVisibilityTerm(nl, nv, roughness);
half D = NDFBlinnPhongNormalizedTerm(nh, RoughnessToSpecPower (roughness));
#endif
half nlPow5 = Pow5(1-nl);
half nvPow5 = Pow5(1-nv);
half Fd90 = 0.5 + 2 * lh * lh * roughness;
half disneyDiffuse = (1 + (Fd90-1) * nlPow5) * (1 + (Fd90-1) * nvPow5); // 漫反射项(部分,未除以Pi)
// HACK: theoretically we should divide by Pi diffuseTerm and not multiply specularTerm!
// BUT 1) that will make shader look significantly darker than Legacy ones
// and 2) on engine side "Non-important" lights have to be divided by Pi to in cases when they are injected into ambient SH
// NOTE: multiplication by Pi is part of single constant together with 1/4 now
// Torrance-Sparrow model, Fresnel is applied later (for optimization reasons)
half specularTerm = (V * D) * (UNITY_PI/4); // 高光反射项(部分,未加入菲涅耳反射率)
if (IsGammaSpace()) // 当前处于非线性空间,则做一些转换
specularTerm = sqrt(max(1e-4h, specularTerm));
specularTerm = max(0, specularTerm * nl); // 高光反射强度(部分,考虑光线反方向和法线的夹角余弦)
half diffuseTerm = disneyDiffuse * nl; // 漫反射强度(考虑光线反方向和法线的夹角余弦)
// surfaceReduction = Int D(NdotH) * NdotH * Id(NdotL>0) dH = 1/(realRoughness^2+1)
half realRoughness = roughness * roughness; // need to square perceptual roughness
half surfaceReduction;
if (IsGammaSpace()) // 1-0.28*x^3 as approximation for (1/(x^4+1))^(1/2.2) on the domain [0;1]
surfaceReduction = 1.0 - 0.28 * realRoughness * roughness;
else
surfaceReduction = 1.0 / (realRoughness * realRoughness + 1.0); // fade \in [0.5;1]
half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity));
half3 color =
diffColor * (gi.diffuse + light.color * diffuseTerm) + // 漫反射项(其中gi.diffuse与GI相关,忽略)
specularTerm * light.color * FresnelTerm(specColor/*F0*/, lh) + // 高光反射项(加入菲涅耳反射率)
surfaceReduction * gi.specular * FresnelLerp(specColor, grazingTerm, nv); // GI相关,忽略
return half4(color, 1);
}
==========================================================
4、相关参考