四、BRDF1_Unity_PBS
// 主物理基BRDF实现
// 基于Disney工作并以Torrance-Sparrow微面模型为基础
// 公式:
// BRDF = kD / π + kS * (D * V * F) / 4
// I = BRDF * (N · L)
//
// * NDF(法线分布函数)可根据 UNITY_BRDF_GGX 选择:
// a) 归一化 BlinnPhong
// b) GGX
// * 可见性项采用Smith模型
// * Fresnel采用Schlick近似
half4 BRDF1_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness,
float3 normal, float3 viewDir,
UnityLight light, UnityIndirect gi)
{
// 将smoothness转换为感知粗糙度
float perceptualRoughness = SmoothnessToPerceptualRoughness(smoothness);
// 计算半向量 H = normalize(light.dir + viewDir)
float3 halfDir = Unity_SafeNormalize(float3(light.dir) + viewDir);
// 处理 N·V(法线与视线的点积)负值问题
// 理论上对于可见像素 NdotV 不应该为负,但透视投影和法线贴图可能会导致负值
// 如果为负,应调整法线使其面向摄像机,以免产生异常,但这会增加少量ALU开销
// 可通过宏 UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV 控制是否进行校正,默认禁用(0)
#define UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV 0
#if UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV
// 根据 dot(normal, viewDir) 计算偏移量,当小于0时将法线向视线方向平移
half shiftAmount = dot(normal, viewDir);
normal = shiftAmount < 0.0f ? normal + viewDir * (-shiftAmount + 1e-5f) : normal;
// 注意:理论上应对normal重新归一化,但为了节省ALU开销,此处略去
float nv = saturate(dot(normal, viewDir)); // TODO: 可能不需要saturate
#else
// 简单采用绝对值处理,虽然不完全正确但能抑制伪影
half nv = abs(dot(normal, viewDir));
#endif
// 计算法线与光源方向的点积 N·L,饱和到 [0,1]
float nl = saturate(dot(normal, light.dir));
// 计算法线与半向量的点积 N·H,饱和到 [0,1]
float nh = saturate(dot(normal, halfDir));
// 计算光源方向与视线之间的点积 L·V,饱和到 [0,1]
half lv = saturate(dot(light.dir, viewDir));
// 计算光源方向与半向量之间的点积 L·H,饱和到 [0,1]
half lh = saturate(dot(light.dir, halfDir));
// ----------------- 漫反射项 -----------------
// 使用DisneyDiffuse函数计算漫反射项,传入 nv, nl, lh 和感知粗糙度
// 最后乘以 nl 模拟光照强度随光线入射角衰减
half diffuseTerm = DisneyDiffuse(nv, nl, lh, perceptualRoughness) * nl;
// ----------------- 镜面反射项 -----------------
// 注意:理论上应将 diffuseTerm 除以π,但为了与Legacy Shader一致并考虑非重要光源的处理,采用乘法
// 将感知粗糙度转换为实际粗糙度
float roughness = PerceptualRoughnessToRoughness(perceptualRoughness);
#if UNITY_BRDF_GGX
// 对于GGX分支:为了防止粗糙度为0导致无镜面反射,取 roughness 的最小值0.002
roughness = max(roughness, 0.002);
// 计算可见性项 V 使用 SmithJointGGXVisibilityTerm,输入 N·L、N·V和粗糙度
float V = SmithJointGGXVisibilityTerm(nl, nv, roughness);
// 计算法线分布函数 D 使用 GGXTerm,输入 N·H 和粗糙度
float D = GGXTerm(nh, roughness);
#else
// Legacy 分支:采用SmithBeckmannVisibilityTerm和归一化 BlinnPhong NDF计算
half V = SmithBeckmannVisibilityTerm(nl, nv, roughness);
half D = NDFBlinnPhongNormalizedTerm(nh, PerceptualRoughnessToSpecPower(perceptualRoughness));
#endif
// 根据 Torrance-Sparrow 模型,镜面反射项 = V * D * π
float specularTerm = V * D * UNITY_PI;
# ifdef UNITY_COLORSPACE_GAMMA
// 如果处于Gamma空间下,对 specularTerm 取平方根以调整亮度
specularTerm = sqrt(max(1e-4h, specularTerm));
# endif
// 为防止在Metal平台上 specularTerm * nl 出现 NaN,取最大值确保合理性
specularTerm = max(0, specularTerm * nl);
#if defined(_SPECULARHIGHLIGHTS_OFF)
// 如果禁用了 specular highlights,则将 specularTerm 置为0
specularTerm = 0.0;
#endif
// ----------------- 表面缩减因子 -----------------
// surfaceReduction = 1 / (roughness^2 + 1)
// 在 Gamma 空间下,用 1 - 0.28 * roughness * perceptualRoughness 作为近似
# ifdef UNITY_COLORSPACE_GAMMA
half surfaceReduction = 1.0 - 0.28 * roughness * perceptualRoughness; // 近似 1-0.28*x^3
# else
half surfaceReduction = 1.0 / (roughness * roughness + 1.0); // 使数值在 [0.5,1] 范围内渐变
# endif
// 为了获得真正的Lambert光照,需要能够完全消除 specular 部分
// 如果 specColor 全为0,则 specularTerm 乘以0
specularTerm *= any(specColor) ? 1.0 : 0.0;
// ----------------- 边缘高光项 -----------------
// grazingTerm 用于模拟在低视角(接近边缘)下镜面反射的增强效果
half grazingTerm = saturate(smoothness + (1 - oneMinusReflectivity));
// ----------------- 综合颜色计算 -----------------
// 最终颜色由三部分构成:
// 1. 漫反射部分:diffColor 乘以 (间接漫反射 gi.diffuse + 主光 diffuseTerm * light.color)
// 2. 镜面反射部分:specularTerm 乘以 light.color 和经过 FresnelTerm 调整后的 specColor
// 3. 间接镜面反射部分:surfaceReduction 乘以 gi.specular 和经过 FresnelLerp 插值的 specColor(基于 grazingTerm 和 nv)
half3 color = diffColor * (gi.diffuse + light.color * diffuseTerm)
+ specularTerm * light.color * FresnelTerm(specColor, lh)
+ surfaceReduction * gi.specular * FresnelLerp(specColor, grazingTerm, nv);
// 返回最终颜色,alpha 固定为1(完全不透明)
return half4(color, 1);
}
1.SmoothnessToPerceptualRoughness
float SmoothnessToPerceptualRoughness(float smoothness)
{
return (1 - smoothness);
}
2.DisneyDiffuse
// 注意:Disney Diffuse 的结果必须在外部乘以 (diffuseAlbedo / π)
// 这个函数仅计算用于漫反射的散射系数
half DisneyDiffuse(half NdotV, half NdotL, half LdotH, half perceptualRoughness)
{
// 计算 fd90,代表当视角或光线与法线呈90度时的散射因子
// fd90 = 0.5 + 2 * (LdotH)^2 * perceptualRoughness
// 这里 LdotH 越大(光与半向量接近),fd90 越大;同时粗糙度越大,fd90 越高
half fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness;
// 下面使用 Schlick 近似计算 Fresnel 散射项
// 对于光线方向(light scatter):
// 计算公式:1 + (fd90 - 1) * Pow5(1 - NdotL)
// 当 NdotL 接近 1 时,Pow5(1 - NdotL) 约等于 0,lightScatter 接近 1
// 当 NdotL 接近 0 时,Pow5(1 - NdotL) 接近 1,lightScatter 接近 fd90
half lightScatter = (1 + (fd90 - 1) * Pow5(1 - NdotL));
// 对于视线方向(view scatter):
// 计算公式:1 + (fd90 - 1) * Pow5(1 - NdotV)
// 同理,当 NdotV 较大时,viewScatter 约等于 1;当 NdotV 较小时,viewScatter 约等于 fd90
half viewScatter = (1 + (fd90 - 1) * Pow5(1 - NdotV));
// 最终返回的散射系数为两个方向散射项的乘积
// 这个结果反映了漫反射部分对光照散射的综合响应
return lightScatter * viewScatter;
}
公式如下:
Disney Diffuse模型通过粗糙度和视角/光源方向的散射增强项计算漫反射反射率:
f
diffuse
=
diffColor
π
⋅
ScatteringFactor
f_{\text{diffuse}} = \frac{\text{diffColor}}{\pi} \cdot \text{ScatteringFactor}
fdiffuse=πdiffColor⋅ScatteringFactor
其中:
- ScatteringFactor(散射系数):
ScatteringFactor = lightScatter ∗ viewScatter = [ 1 + ( F D 90 − 1 ) ⋅ ( 1 − N ⋅ L ) 5 ] ⋅ [ 1 + ( F D 90 − 1 ) ⋅ ( 1 − N ⋅ V ) 5 ] \text{ScatteringFactor} = \text{lightScatter} * \text{viewScatter } = \left[1 + (F_{D90} - 1) \cdot (1 - N \cdot L)^5\right] \cdot \left[1 + (F_{D90} - 1) \cdot (1 - N \cdot V)^5\right] ScatteringFactor=lightScatter∗viewScatter =[1+(FD90−1)⋅(1−N⋅L)5]⋅[1+(FD90−1)⋅(1−N⋅V)5] -
F
D
90
F_{D90}
FD90(散射因子在90度视角时的值):
F D 90 = 0.5 + 2 ⋅ ( L ⋅ H ) 2 ⋅ perceptualRoughness F_{D90} = 0.5 + 2 \cdot (L \cdot H)^2 \cdot \text{perceptualRoughness} FD90=0.5+2⋅(L⋅H)2⋅perceptualRoughness- L ⋅ H L \cdot H L⋅H:光源方向 L L L 与半角向量 H H H 的夹角余弦( H = L + V ∣ L + V ∣ H = \frac{L + V}{|L + V|} H=∣L+V∣L+V)。
- perceptualRoughness = 1 − smoothness \text{perceptualRoughness} = 1 - \text{smoothness} perceptualRoughness=1−smoothness(光滑度转换为感知粗糙度)。
下图来自Unity shader 入门精要
3.PerceptualRoughnessToRoughness
float PerceptualRoughnessToRoughness(float perceptualRoughness)
{
return perceptualRoughness * perceptualRoughness;
}
4.SmithJointGGXVisibilityTerm
// 参考文献: http://jcgt.org/published/0003/02/03/paper.pdf
inline float SmithJointGGXVisibilityTerm(float NdotL, float NdotV, float roughness)
{
#if 0
// 原始公式:
// lambda_v = (-1 + sqrt(a2 * (1 - NdotL2) / NdotL2 + 1)) * 0.5f;
// lambda_l = (-1 + sqrt(a2 * (1 - NdotV2) / NdotV2 + 1)) * 0.5f;
// G = 1 / (1 + lambda_v + lambda_l);
// 重新排序代码以提高效率
half a = roughness; // 粗糙度参数
half a2 = a * a; // 粗糙度平方
// 计算视角方向和光源方向的几何阴影因子
half lambdaV = NdotL * sqrt((-NdotV * a2 + NdotV) * NdotV + a2);
half lambdaL = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2);
// 可见性项简化为:(2.0f * NdotL * NdotV) / ((4.0f * NdotL * NdotV) * (lambda_v + lambda_l + 1e-5f));
// 返回值,注意这里的epsilon是为了避免除零错误,由于不在移动设备上运行,所以可以使用较小的epsilon值
return 0.5f / (lambdaV + lambdaL + 1e-5f);
#else
// 上述公式的近似形式(简化了sqrt计算,虽然不完全数学准确但足够接近)
float a = roughness; // 粗糙度参数
// 近似计算视角方向的几何阴影因子
float lambdaV = NdotL * (NdotV * (1 - a) + a);
// 近似计算光源方向的几何阴影因子
float lambdaL = NdotV * (NdotL * (1 - a) + a);
// 根据不同的Shader API选择合适的epsilon值
#if defined(SHADER_API_SWITCH)
// 如果是Switch平台,则使用UNITY_HALF_MIN作为epsilon值
return 0.5f / (lambdaV + lambdaL + UNITY_HALF_MIN);
#else
// 其他平台使用1e-5f作为epsilon值,防止除零错误
return 0.5f / (lambdaV + lambdaL + 1e-5f);
#endif
#endif
}
1.Smith-Joint GGX 原始公式:
λ V = ( − 1 + a 2 ⋅ ( 1 − ( N ⋅ L ) 2 ) ( N ⋅ L ) 2 + 1 ) ⋅ 1 2 \lambda_V = \left( -1 + \sqrt{ \frac{a2 \cdot \left(1 - (N \cdot L)^2\right)}{(N \cdot L)^2} + 1 } \right) \cdot \frac{1}{2} λV=(−1+(N⋅L)2a2⋅(1−(N⋅L)2)+1)⋅21
λ L = ( − 1 + a 2 ⋅ ( 1 − ( N ⋅ V ) 2 ) ( N ⋅ V ) 2 + 1 ) ⋅ 1 2 \lambda_L = \left( -1 + \sqrt{ \frac{a2 \cdot \left(1 - (N \cdot V)^2\right)}{(N \cdot V)^2} + 1 } \right) \cdot \frac{1}{2} λL=(−1+(N⋅V)2a2⋅(1−(N⋅V)2)+1)⋅21
G = 1 1 + λ V + λ L G = \frac{1}{1 + \lambda_V + \lambda_L} G=1+λV+λL1
其中:
- a 2 a2 a2 表示粗糙度平方( α 2 \alpha^2 α2)。
- N d o t L NdotL NdotL 和 N d o t V NdotV NdotV 分别表示法线与光源、视角方向的点积(即 cos θ L \cos\theta_L cosθL 和 cos θ V \cos\theta_V cosθV)。
2.简化后的表达式(代码中给出的中间步骤):
λ V = N ⋅ L ⋅ [ ( 1 − α 2 ) ( N ⋅ V ) 2 + α 2 ] , λ L = N ⋅ V ⋅ [ ( 1 − α 2 ) ( N ⋅ L ) 2 + α 2 ] . \begin{aligned} \lambda_V &= N \cdot L \cdot \sqrt{ \left[ (1 - \alpha^2) (N \cdot V)^2 + \alpha^2 \right] }, \\ \lambda_L &= N \cdot V \cdot \sqrt{ \left[ (1 - \alpha^2) (N \cdot L)^2 + \alpha^2 \right] }. \end{aligned} λVλL=N⋅L⋅[(1−α2)(N⋅V)2+α2],=N⋅V⋅[(1−α2)(N⋅L)2+α2].
G
=
0.5
λ
V
+
λ
L
+
ϵ
,
G = \frac{0.5}{\lambda_V + \lambda_L + \epsilon},
G=λV+λL+ϵ0.5,
其中
ϵ
\epsilon
ϵ 是防止除零的小常数(如
1
×
1
0
−
5
1 \times 10^{-5}
1×10−5)。
3.近似公式(实际使用的高效实现)
代码中实际使用的近似公式通过简化平方根运算,以提高计算效率,同时保持视觉效果的接近:
λ V ≈ N ⋅ L ⋅ ( ( N ⋅ V ) ⋅ ( 1 − α ) + α ) , λ L ≈ N ⋅ V ⋅ ( ( N ⋅ L ) ⋅ ( 1 − α ) + α ) . \begin{aligned} \lambda_V &\approx N \cdot L \cdot \left( (N \cdot V) \cdot (1 - \alpha) + \alpha \right), \\ \lambda_L &\approx N \cdot V \cdot \left( (N \cdot L) \cdot (1 - \alpha) + \alpha \right). \end{aligned} λVλL≈N⋅L⋅((N⋅V)⋅(1−α)+α),≈N⋅V⋅((N⋅L)⋅(1−α)+α).
5.GGXTerm
inline float GGXTerm(float NdotH, float roughness)
{
// 粗糙度的平方
float a2 = roughness * roughness;
// 计算GGX分布函数中的分母部分
// 这里使用了两个乘加运算(MAD: Multiply-Add)
// d = (NdotH * a2 - NdotH) * NdotH + 1.0f;
// 其中NdotH是法线与半角向量的点积,a2是粗糙度的平方
float d = (NdotH * a2 - NdotH) * NdotH + 1.0f;
// 返回GGX分布函数的结果
// UNITY_INV_PI是一个预定义的常数,等于1/π,用于归一化
// 分母加上一个很小的值(epsilon),防止除零错误
// 注意:此函数不适合在移动设备上运行,因此这里的epsilon值比half类型能表示的最小值还要小
return UNITY_INV_PI * a2 / (d * d + 1e-7f);
}
数学表达式
D G G X ( N ⋅ H , α ) = α 2 π ( ( α 2 − 1 ) ( N ⋅ H ) 2 + 1 ) 2 D_{GGX}( \mathbf{N} \cdot \mathbf{H}, \alpha) = \frac{\alpha^2}{\pi \left( (\alpha^2 - 1)(\mathbf{N} \cdot \mathbf{H})^2 + 1 \right)^2} DGGX(N⋅H,α)=π((α2−1)(N⋅H)2+1)2α2
其中:
- N \mathbf{N} N 是表面法线,
- H \mathbf{H} H 是半角向量(即视线方向和光源方向的中间方向),
-
α
\alpha
α 是粗糙度参数,等于代码中的
roughness
的平方 (a2
), -
N
⋅
H
\mathbf{N} \cdot \mathbf{H}
N⋅H 表示法线与半角向量的点积,对应于代码中的
NdotH
, -
ϵ
\epsilon
ϵ (在这里是
1e-7f
)是一个小值,被加到分母上以避免除零错误。
下图来自Unity shader 入门精要
图中少了一个括号
6.SmithBeckmannVisibilityTerm
// 基于 Smith-Schlick 模型推导出的用于 Beckmann 分布的可见性项
inline half SmithBeckmannVisibilityTerm(half NdotL, half NdotV, half roughness)
{
// 常数 c = sqrt(2 / Pi),用于后续计算
// 这个常数是从 Beckmann 分布推导出来的,用于调整粗糙度参数
half c = 0.797884560802865h; // 精确值为 sqrt(2 / Pi)
// 计算 k,k 是一个调整后的粗糙度参数
// 使用 c 对原始粗糙度进行缩放,使得其适应 Beckmann 分布
half k = roughness * c;
// 调用 SmithVisibilityTerm 函数计算可见性项,并乘以 0.25f
// 这里的 0.25f 是因为 Smith 可见性项通常需要除以 4 来归一化
return SmithVisibilityTerm(NdotL, NdotV, k) * 0.25f;
}
// 通用的 Smith-Schlick 可见性项
inline half SmithVisibilityTerm(half NdotL, half NdotV, half k)
{
// 计算视角方向的几何阴影因子 gL
// 公式: gL = NdotL * (1 - k) + k
// 其中 NdotL 是法线与光源方向的点积,k 是一个调整参数,通常与粗糙度相关
half gL = NdotL * (1 - k) + k;
// 计算光源方向的几何阴影因子 gV
// 公式: gV = NdotV * (1 - k) + k
// 其中 NdotV 是法线与视角方向的点积,k 同样是一个调整参数
half gV = NdotV * (1 - k) + k;
// 返回可见性项的倒数,加上一个小的 epsilon 值以防止除零错误
// 这里的 epsilon 值较小,因为此函数不适合在移动设备上运行
return 1.0 / (gL * gV + 1e-5f);
}
数学表达式
G S c h l i c k ( N ⋅ L , N ⋅ V , k ) = 1 G L ( N ⋅ L , k ) × G V ( N ⋅ V , k ) G_{Schlick}( \mathbf{N} \cdot \mathbf{L}, \mathbf{N} \cdot \mathbf{V}, k ) = \frac{1}{G_L(\mathbf{N} \cdot \mathbf{L}, k) \times G_V(\mathbf{N} \cdot \mathbf{V}, k)} GSchlick(N⋅L,N⋅V,k)=GL(N⋅L,k)×GV(N⋅V,k)1
其中:
- N \mathbf{N} N 是表面法线,
- L \mathbf{L} L 是光源方向,
- V \mathbf{V} V 是视角方向,
-
N
⋅
L
\mathbf{N} \cdot \mathbf{L}
N⋅L 表示法线与光源方向的点积,对应于代码中的
NdotL
, -
N
⋅
V
\mathbf{N} \cdot \mathbf{V}
N⋅V 表示法线与视角方向的点积,对应于代码中的
NdotV
, - k k k 是一个调整参数,通常与粗糙度相关联,
- G L G_L GL 和 G V G_V GV 分别是从视角和光源方向考虑的几何阴影因子。
具体到每个几何阴影因子 G L G_L GL 和 G V G_V GV,它们按照Schlick的近似公式定义如下:
G
L
=
N
⋅
L
(
N
⋅
L
)
(
1
−
k
)
+
k
G_L = \frac{\mathbf{N} \cdot \mathbf{L}}{(\mathbf{N} \cdot \mathbf{L})(1-k) + k}
GL=(N⋅L)(1−k)+kN⋅L
G
V
=
N
⋅
V
(
N
⋅
V
)
(
1
−
k
)
+
k
G_V = \frac{\mathbf{N} \cdot \mathbf{V}}{(\mathbf{N} \cdot \mathbf{V})(1-k) + k}
GV=(N⋅V)(1−k)+kN⋅V
近似公式(实际使用的高效实现)
g
L
=
(
N
⋅
L
)
×
(
1
−
k
)
+
k
gL = (\mathbf{N} \cdot \mathbf{L}) \times (1 - k) + k
gL=(N⋅L)×(1−k)+k
g
V
=
(
N
⋅
V
)
×
(
1
−
k
)
+
k
gV = (\mathbf{N} \cdot \mathbf{V}) \times (1 - k) + k
gV=(N⋅V)×(1−k)+k
最终的可见性项 G S c h l i c k G_{Schlick} GSchlick 通过以下方式计算:
G S c h l i c k = 1 g L × g V + ϵ G_{Schlick} = \frac{1}{gL \times gV + \epsilon} GSchlick=gL×gV+ϵ1
这里加上了一个很小的值
ϵ
\epsilon
ϵ (在代码中是 1e-5f
)以防止除零错误。
综合表达式
S
m
i
t
h
B
e
c
k
m
a
n
n
V
i
s
i
b
i
l
i
t
y
T
e
r
m
(
N
⋅
L
,
N
⋅
V
,
r
o
u
g
h
n
e
s
s
)
=
0.25
(
(
N
⋅
L
)
×
(
1
−
k
)
+
k
)
×
(
(
N
⋅
V
)
×
(
1
−
k
)
+
k
)
+
1
e
−
5
SmithBeckmannVisibilityTerm(\mathbf{N} \cdot \mathbf{L}, \mathbf{N} \cdot \mathbf{V}, roughness) = \frac{0.25}{\left( (\mathbf{N} \cdot \mathbf{L}) \times (1 - k) + k \right) \times \left( (\mathbf{N} \cdot \mathbf{V}) \times (1 - k) + k \right) + 1e-5}
SmithBeckmannVisibilityTerm(N⋅L,N⋅V,roughness)=((N⋅L)×(1−k)+k)×((N⋅V)×(1−k)+k)+1e−50.25
其中
k
=
r
o
u
g
h
n
e
s
s
×
2
π
k = roughness \times \sqrt{\frac{2}{\pi}}
k=roughness×π2
ϵ 以防止除零错误 \epsilon以防止除零错误 ϵ以防止除零错误
下图来自Unity shader 入门精要
可以发现K值不一样,我用的是Unity2021.3.23f1
8.NDFBlinnPhongNormalizedTerm
// 使用归一化的 Blinn-Phong 作为法线分布函数 (NDF)
// 在微表面模型中的使用:spec = D * G * F
// 参考文献: https://dl.dropboxusercontent.com/u/55891920/papers/mm_brdf.pdf 中的公式 19
inline half NDFBlinnPhongNormalizedTerm(half NdotH, half n)
{
// 归一化因子 norm = (n + 2) / (2 * Pi)
// 这个因子确保 Blinn-Phong 分布函数在整个半球上的积分等于 1
half normTerm = (n + 2.0) * (0.5 / UNITY_PI);
// 计算 Blinn-Phong 高光项,其中 NdotH 是法线与半角向量的点积,n 是光泽度参数
half specTerm = pow(NdotH, n);
// 返回归一化的高光项
return specTerm * normTerm;
}
数学表达式
归一化的Blinn-Phong NDF可以表示为:
D B l i n n − P h o n g ( N ⋅ H , n ) = n + 2 2 π ( N ⋅ H ) n D_{Blinn-Phong}( \mathbf{N} \cdot \mathbf{H}, n ) = \frac{n + 2}{2\pi} (\mathbf{N} \cdot \mathbf{H})^n DBlinn−Phong(N⋅H,n)=2πn+2(N⋅H)n
其中:
- N \mathbf{N} N 是表面法线,
- H \mathbf{H} H 是半角向量(即视线方向和光源方向的中间方向),
-
N
⋅
H
\mathbf{N} \cdot \mathbf{H}
N⋅H 表示法线与半角向量的点积,对应于代码中的
NdotH
, - n n n 是光泽度参数,控制高光的锐利度,
- n + 2 2 π \frac{n + 2}{2\pi} 2πn+2 是归一化因子,确保分布函数在整个半球上的积分等于1。
下图来自Unity shader 入门精要
9.PerceptualRoughnessToSpecPower
// 将感知粗糙度转换为光泽度参数
inline half PerceptualRoughnessToSpecPower(half perceptualRoughness)
{
// 调用 PerceptualRoughnessToRoughness 函数,将感知粗糙度转换为实际学术上的粗糙度 m
// 感知粗糙度是为了在用户界面中提供更直观的控制而设计的,而实际粗糙度 m 是用于计算的
half m = PerceptualRoughnessToRoughness(perceptualRoughness); // m 是实际的学术粗糙度
// 计算粗糙度的平方,并确保其最小值为 1e-4f 以避免数值问题
half sq = max(1e-4f, m * m);
// 根据公式 (2.0 / sq) - 2.0 计算光泽度参数 n
// 参考文献: https://dl.dropboxusercontent.com/u/55891920/papers/mm_brdf.pdf
half n = (2.0 / sq) - 2.0;
// 确保光泽度参数 n 的最小值为 1e-4f,以防止 pow(0,0) 的情况发生
// 当粗糙度为 1.0 且 NdotH 为零时,可能会出现这种情况
n = max(n, 1e-4f);
return n;
}
数学表达式
n = max ( 2 max ( 1 e − 4 , m 2 ) − 2 , 1 e − 4 ) n = \max\left(\frac{2}{\max(1e-4, m^2)} - 2, 1e-4\right) n=max(max(1e−4,m2)2−2,1e−4)
其中:
m
=
p
e
r
c
e
p
t
u
a
l
R
o
u
g
h
n
e
s
s
2
m = perceptualRoughness^2
m=perceptualRoughness2
这里
m
m
m 是实际粗糙度,而 perceptualRoughness
是用户界面中提供的感知粗糙度值。
10.FresnelTerm
inline half3 FresnelTerm (half3 F0, half cosA)
{
half t = Pow5 (1 - cosA); // 按照 Schlick 插值方法
return F0 + (1-F0) * t;
}
数学表达式
根据Schlick的近似方法,菲涅耳项 F ( θ ) F(\theta) F(θ) 可以表示为:
F ( θ ) = F 0 + ( 1 − F 0 ) ( 1 − cos θ ) 5 F(\theta) = F_0 + (1 - F_0)(1 - \cos\theta)^5 F(θ)=F0+(1−F0)(1−cosθ)5
其中:
- F 0 F_0 F0 是法线入射时(即当入射角 (\theta) 为0度时)的反射率,
-
cos
θ
\cos\theta
cosθ 是视角方向和表面法线之间的点积(即
cosA
在代码中), - ( 1 − cos θ ) 5 (1 - \cos\theta)^5 (1−cosθ)5 是Schlick近似的核心部分,用来模拟随着视角变化的反射率变化。
下图来自Unity shader 入门精要
11.FresnelLerp
inline half3 FresnelLerp (half3 F0, half3 F90, half cosA)
{
half t = Pow5 (1 - cosA); // 按照 Schlick 插值方法
return lerp (F0, F90, t);
}
数学表达式
根据Schlick的近似方法,菲涅耳项 F ( θ ) F(\theta) F(θ) 可以表示为:
F ( θ ) = F 0 + ( F 90 − F 0 ) ( 1 − cos θ ) 5 F(\theta) = F_0 + (F_{90} - F_0)(1 - \cos\theta)^5 F(θ)=F0+(F90−F0)(1−cosθ)5
其中:
- F 0 F_0 F0 是法线入射时(即当入射角 (\theta) 为0度时)的反射率,
- F 90 F_{90} F90 是当入射角接近90度时的反射率,
-
cos
θ
\cos\theta
cosθ 是视角方向和表面法线之间的点积(即
cosA
在代码中), - ( 1 − cos θ ) 5 (1 - \cos\theta)^5 (1−cosθ)5 是Schlick近似的核心部分,用来模拟随着视角变化的反射率变化。
在代码实现中,使用了线性插值(lerp)来简化这一过程。线性插值的公式为:
lerp ( A , B , t ) = A + t ( B − A ) \text{lerp}(A, B, t) = A + t(B - A) lerp(A,B,t)=A+t(B−A)
因此,结合Schlick近似与线性插值,可以得到:
F ( θ ) = lerp ( F 0 , F 90 , t ) = F 0 + t ( F 90 − F 0 ) F(\theta) = \text{lerp}(F_0, F_{90}, t) = F_0 + t(F_{90} - F_0) F(θ)=lerp(F0,F90,t)=F0+t(F90−F0)
其中 :
t
=
(
1
−
cos
θ
)
5
t = (1 - \cos\theta)^5
t=(1−cosθ)5
12.总结
BRDF1_Unity_PBS 的实现基于 Disney 工作,并以 Torrance-Sparrow 微面模型为基础,使用了多种不同的组件来计算最终的 BRDF(双向反射分布函数)。
1.核心公式
BRDF 主要由漫反射项和镜面反射项组成:
B R D F = k D π + k S ⋅ D ⋅ V ⋅ F 4 BRDF = \frac{kD}{\pi} + kS \cdot \frac{D \cdot V \cdot F}{4} BRDF=πkD+kS⋅4D⋅V⋅F
其中:
- k D kD kD 是漫反射颜色,
- k S kS kS 是镜面反射颜色,
- D D D 是法线分布函数(NDF),
- V V V 是几何可见性函数,
- F F F 是菲涅尔项。
2. 漫反射项
漫反射项通过 DisneyDiffuse
函数计算,考虑了视角方向、光源方向、半角向量与感知粗糙度之间的关系:
d i f f u s e T e r m = D i s n e y D i f f u s e ( N d o t V , N d o t L , L d o t H , p e r c e p t u a l R o u g h n e s s ) × N d o t L diffuseTerm = DisneyDiffuse(NdotV, NdotL, LdotH, perceptualRoughness) \times NdotL diffuseTerm=DisneyDiffuse(NdotV,NdotL,LdotH,perceptualRoughness)×NdotL
这里,DisneyDiffuse
的具体形式如下:
f
d
90
=
0.5
+
2
⋅
(
L
d
o
t
H
)
2
⋅
p
e
r
c
e
p
t
u
a
l
R
o
u
g
h
n
e
s
s
fd90 = 0.5 + 2 \cdot (LdotH)^2 \cdot perceptualRoughness
fd90=0.5+2⋅(LdotH)2⋅perceptualRoughness
l
i
g
h
t
S
c
a
t
t
e
r
=
(
1
+
(
f
d
90
−
1
)
⋅
(
1
−
N
d
o
t
L
)
5
)
lightScatter = (1 + (fd90 - 1) \cdot (1 - NdotL)^5)
lightScatter=(1+(fd90−1)⋅(1−NdotL)5)
v
i
e
w
S
c
a
t
t
e
r
=
(
1
+
(
f
d
90
−
1
)
⋅
(
1
−
N
d
o
t
V
)
5
)
viewScatter = (1 + (fd90 - 1) \cdot (1 - NdotV)^5)
viewScatter=(1+(fd90−1)⋅(1−NdotV)5)
d
i
f
f
u
s
e
T
e
r
m
=
l
i
g
h
t
S
c
a
t
t
e
r
⋅
v
i
e
w
S
c
a
t
t
e
r
×
N
d
o
t
L
diffuseTerm = lightScatter \cdot viewScatter \times NdotL
diffuseTerm=lightScatter⋅viewScatter×NdotL
3. 镜面反射项
镜面反射项依赖于三个主要组件:法线分布函数NDF( D D D),几何可见性函数( V V V ),和菲涅尔项( F F F)。
GGX 分支
当选择 GGX 作为 NDF 时:
r
o
u
g
h
n
e
s
s
=
m
a
x
(
r
o
u
g
h
n
e
s
s
,
0.002
)
roughness = max(roughness, 0.002)
roughness=max(roughness,0.002)
V
=
S
m
i
t
h
J
o
i
n
t
G
G
X
V
i
s
i
b
i
l
i
t
y
T
e
r
m
(
N
d
o
t
L
,
N
d
o
t
V
,
r
o
u
g
h
n
e
s
s
)
V = SmithJointGGXVisibilityTerm(NdotL, NdotV, roughness)
V=SmithJointGGXVisibilityTerm(NdotL,NdotV,roughness)
D
=
G
G
X
T
e
r
m
(
N
d
o
t
H
,
r
o
u
g
h
n
e
s
s
)
D = GGXTerm(NdotH, roughness)
D=GGXTerm(NdotH,roughness)
s p e c u l a r T e r m = V ⋅ D ⋅ π specularTerm = V \cdot D \cdot \pi specularTerm=V⋅D⋅π
其中,SmithJointGGXVisibilityTerm
和 GGXTerm
分别为:
S
m
i
t
h
J
o
i
n
t
G
G
X
V
i
s
i
b
i
l
i
t
y
T
e
r
m
=
0.5
l
a
m
b
d
a
V
+
l
a
m
b
d
a
L
+
1
e
−
5
f
SmithJointGGXVisibilityTerm = \frac{0.5}{lambdaV + lambdaL + 1e-5f}
SmithJointGGXVisibilityTerm=lambdaV+lambdaL+1e−5f0.5
G
G
X
T
e
r
m
=
r
o
u
g
h
n
e
s
s
2
π
⋅
(
(
N
d
o
t
H
⋅
r
o
u
g
h
n
e
s
s
2
−
N
d
o
t
H
)
⋅
N
d
o
t
H
+
1
)
2
+
1
e
−
7
f
GGXTerm = \frac{roughness^2}{\pi \cdot ((NdotH \cdot roughness^2 - NdotH) \cdot NdotH + 1)^2 + 1e-7f}
GGXTerm=π⋅((NdotH⋅roughness2−NdotH)⋅NdotH+1)2+1e−7froughness2
Beckmann 分支
如果选择了 Beckmann 分布,则使用 SmithBeckmannVisibilityTerm
和 NDFBlinnPhongNormalizedTerm
:
k
=
r
o
u
g
h
n
e
s
s
⋅
2
π
k = roughness \cdot \sqrt{\frac{2}{\pi}}
k=roughness⋅π2
V
=
S
m
i
t
h
V
i
s
i
b
i
l
i
t
y
T
e
r
m
(
N
d
o
t
L
,
N
d
o
t
V
,
k
)
⋅
0.25
V = SmithVisibilityTerm(NdotL, NdotV, k) \cdot 0.25
V=SmithVisibilityTerm(NdotL,NdotV,k)⋅0.25
D
=
n
+
2
2
π
⋅
(
N
d
o
t
H
)
n
D = \frac{n + 2}{2\pi} \cdot (NdotH)^n
D=2πn+2⋅(NdotH)n
其中,SmithVisibilityTerm
如下:
g
L
=
N
d
o
t
L
⋅
(
1
−
k
)
+
k
gL = NdotL \cdot (1 - k) + k
gL=NdotL⋅(1−k)+k
g
V
=
N
d
o
t
V
⋅
(
1
−
k
)
+
k
gV = NdotV \cdot (1 - k) + k
gV=NdotV⋅(1−k)+k
S
m
i
t
h
V
i
s
i
b
i
l
i
t
y
T
e
r
m
=
1
g
L
⋅
g
V
+
1
e
−
5
f
SmithVisibilityTerm = \frac{1}{gL \cdot gV + 1e-5f}
SmithVisibilityTerm=gL⋅gV+1e−5f1
4. 菲涅尔项
对于菲涅尔项,使用 Schlick 近似:
F = F 0 + ( 1 − F 0 ) ⋅ ( 1 − c o s A ) 5 F = F0 + (1 - F0) \cdot (1 - cosA)^5 F=F0+(1−F0)⋅(1−cosA)5
对于间接镜面反射部分,使用 FresnelLerp
:
F = l e r p ( F 0 , F 90 , ( 1 − c o s A ) 5 ) F = lerp(F0, F90, (1 - cosA)^5) F=lerp(F0,F90,(1−cosA)5)
5.综合颜色计算
最终的颜色由以下三部分构成:
- 漫反射部分: d i f f C o l o r × ( g i . d i f f u s e + l i g h t . c o l o r × d i f f u s e T e r m ) diffColor \times (gi.diffuse + light.color \times diffuseTerm) diffColor×(gi.diffuse+light.color×diffuseTerm)
- 镜面反射部分: s p e c u l a r T e r m × l i g h t . c o l o r × F r e s n e l T e r m ( s p e c C o l o r , l h ) specularTerm \times light.color \times FresnelTerm(specColor, lh) specularTerm×light.color×FresnelTerm(specColor,lh)
- 间接镜面反射部分: s u r f a c e R e d u c t i o n × g i . s p e c u l a r × F r e s n e l L e r p ( s p e c C o l o r , g r a z i n g T e r m , n v ) surfaceReduction \times gi.specular \times FresnelLerp(specColor, grazingTerm, nv) surfaceReduction×gi.specular×FresnelLerp(specColor,grazingTerm,nv)
通过这些步骤,BRDF1_Unity_PBS 实现了一个物理基础的渲染模型,支持不同的微表面分布和可见性模型,提供了灵活且真实的材质表现。