相关参考:
Pekelis et al. 2015, "A Data-Driven Light Scattering Model for Hair"
Marschner et al. 2003, "Light Scattering from Human Hair Fibers"
Marschner 论文相关的分析 part I part II part III 翻译
《GPU Gems 2》中头发制作的章节
http://www.fseraph.com/?p=683
Marschner 的头发散射
Marschner 将反射分成了三个部分。
- R:在头发表面反射的光线
- TT:光线折射进头发然后折射出的光线
- TRT:光线折射进头丝,再在内部反射后最终折射出的光线
R 表示反射 (reflection),T 表示穿过 (transmission)。R 和 TRT 都是正面反射的光,但是形成了两块完全不同的区域。并且,R 因为没有穿过头发表面而呈现白色,TRT 则带有头发的颜色。
Marschner 论文中需要的参数如下图:
- u:头发切线,从发根到发梢
- w:头发法线(v垂直于w,wv平面为法平面)
- wi:光线方向
- wr:摄像机方向
- θi:光线与法平面夹角
- θr:摄像机与法平面夹角
- φi:光线方位角(wi在法平面的投影与v的夹角)
- φr:摄像机方位角(wr在法平面的投影与v的夹角)
对于每个像素P都有R,TT,TRT 三项。然后把每项分解成 M 和 N,其中 M 项是纵切面的散射分布(Longitudinal scattering),N 项是横切面的散射分布(Azimuthal scattering)。这样将4D散射,分解成2个2D的部分。
R,TT,TRT 中的 M 项
在 Unreal 的实现中,M 项是一个正态分布
其中 μ 为中位数,代码中为 Shift。
float Shift = 0.035;
float Alpha[] =
{
-Shift * 2,
Shift,
Shift * 4,
};
R 使用的是 Shift, TT 和 TRT 使用的分别是 Alpha[1], Alpha[2]。
σ 是方差,代码中与 Roughness 相关。
float B[] =
{
Area + Pow2( ClampedRoughness ),
Area + Pow2( ClampedRoughness ) / 2,
Area + Pow2( ClampedRoughness ) * 2,
};
B[0],B[1],B[2] 分别对应R,TT,TRT 中的 σ 。Area 固定为0。Roughness 越小反射却强烈,而集中。
x 是 SinThetaL + SinThetaV。与在头发纵切面上,光线和摄像机方向夹角有关。
// R
float Mp = Hair_g( B[0] * sqrt(2.0) * CosHalfPhi, SinThetaL + SinThetaV - Shift );
...
// TT
float Mp = Hair_g( B[1], SinThetaL + SinThetaV - Alpha[1] );
...
// TRT
float Mp = Hair_g( B[2], SinThetaL + SinThetaV - Alpha[2] );
...
R,TT,TRT 中的其他部分
R
R 部分是菲涅尔项和普通镜面反射相乘。
float Np = 0.25 * CosHalfPhi;
float Fp = Hair_F( sqrt( saturate( 0.5 + 0.5 * VoL ) ) );
S += Mp * Np * Fp * ( GBuffer.Specular * 2 ) * lerp( 1, Backlit, saturate(-VoL) );
Mp 是上面讲的 M 项(下同)。
Fp 是菲涅尔项。
Np 中的 CosHalfPhi,是方位角差的半角,Np 是公式中 N 项。
Specular 越大,反射越强。
因为光线没有进入发丝,所以没有头发的 BaseColor。
TT
float a = 1 / n_prime;
float h = CosHalfPhi * rsqrt( 1 + a*a - 2*a * sqrt( 0.5 - 0.5 * CosPhi ) );
float yt = asin(h / n_prime);
float3 Tp = pow( GBuffer.BaseColor, 0.5 * cos(yt) / CosThetaD );
float f = Hair_F( CosThetaD * sqrt( saturate( 1 - h*h ) ) );
float Fp = Pow2(1 - f);
float s = 0.3;
float Np = exp( (Phi - PI) / s ) / ( s * Pow2( 1 + exp( (Phi - PI) / s ) ) );
S += Mp * Np * Fp * Tp * Backlit;
Tp 相当于上面公式中 pow(cosθd, 2)
Backlit 控制这一项的强度,也就是背光时头发的透光度。
TRT
float f = Hair_F( CosThetaD * 0.5 );
float Fp = Pow2(1 - f) * f;
float3 Tp = pow( GBuffer.BaseColor, 0.8 / CosThetaD );
float Np = (1/PI) * 2.6 * exp( 2 * 2.6 * (CosPhi - 1) );
S += Mp * Np * Fp * Tp;
思路与TT相同,只不过各项的公式不同。
漫反射
float3 FakeNormal = normalize( V - N * dot(V,N) );
N = FakeNormal;
// Hack approximation for multiple scattering.
float Wrap = 1;
float NoL = saturate( ( dot(N, L) + Wrap ) / Square( 1 + Wrap ) );
float DiffuseScatter = (1 / PI) * NoL * GBuffer.Metallic;
float Luma = Luminance( GBuffer.BaseColor );
float3 ScatterTint = pow( GBuffer.BaseColor / Luma, 1 - Shadow );
S += sqrt( GBuffer.BaseColor ) * DiffuseScatter * ScatterTint;
漫反射部分用一个 Hack 的方法模拟次表面散射材质。
公式中 w 是 0~1 的一个参数,用来加亮远离光线方向的面,Unreal 直接用了1。 Metallic 控制了散射的强度。
材质编辑器
在材质编辑器中 Shading Model 属性选择 Hair,就可以使用 Hair 的光照模型。选择 Masked 是使用 Dither Mask 技术代替半透明。
Hair 的 Material Inputs 与普通的有所不同。
Metallic 变成了 Scatter。从上文的分析中也可以看出,Metallic 控制着散射强度。
Normal 变成了 Tangent。这个切线应该是指向发根的。
Custom Data 0 变成了 Backlit。这个输入本应该是控制透光度,但是实际上并没有用到。
Opacity Mask,因为选了 Masked,所以有这个选项。可以使用 DitherTemporalAA 节点将半透明的部分变成网点,配合 TemporalAA 产生半透明的效果。虽然效果比真正的半透明差一点,但是不会产生排序问题。
另外比较重要的输入,Base Color,Specular,Roughness 的作用已经分析过了。