Shader - Fabric
Demo
测试的材质表现了四种组合类型:
- 普通布料
- 普通布料 + 细节纹理
- 普通布料 + 细节纹理 + 绒毛纹理
- 各向异性丝绸 + 细节纹理
Workflow
设计布料Shader的工作流时,考虑到项目角色模型的美术风格。多数模型会混搭有布料材质和金属材质。
布料材质需要高光工作流,但是金属材质需要金属工作流。在不拆分Mesh的情况下,为了能同时表现这两种材质,就需要结合两种工作流和着色模型,再依赖贴图通道Mask来在像素级别区分材质类型。
这种设计能满足材质类型混搭的需要,但是也造成了Shader性能指标ALU的明显上升。
Detail Map
布料类材质表面通常具有高频细节。除了PBR需要的基础纹理外,还需要连续的细节纹理。通过对UV进行Tilling来调整其在材质表面的密集程度。



在细节贴图和基础贴图的混合方式中,其中比较特殊的是切线空间的法线,细节贴图只提供RG两个通道的法线信息,混合方式可以理解是二维方向矢量的偏移。
half3 detailNormal = UnpackNormalScale( half4(detailNormalTS.x, detailNormalTS.y, 1.0, 1.0), 1.0);detailNormal = half3(detailNormal.rg * detailNormalStrength, lerp(1, detailNormal.b, saturate(detailNormalStrength)) );normalTS = SafeNormalize( half3(normalTS.rg + detailNormal.rg, normalTS.b * detailNormal.b) );
区别于常规的细节贴图,布料类的细节贴图AO会遮蔽基础贴图Albedo,材质表面的光照反射率会跟随细节变化,进而改变材质的整体亮暗表现。
half detailAlbedoOcclusion = lerp(1, occlusion, saturate(threadAlbedoStrength) );albedo = albedo * detailAlbedoOcclusion;
布料类材质表面通常具有高频细节。除了PBR需要的基础纹理外,还需要连续的细节纹理。通过对UV进行Tilling来调整其在材质表面的密集程度。
在细节贴图和基础贴图的混合方式中,其中比较特殊的是切线空间的法线,细节贴图只提供RG两个通道的法线信息,混合方式可以理解是二维方向矢量的偏移。
half3 detailNormal = UnpackNormalScale( half4(detailNormalTS.x, detailNormalTS.y, 1.0, 1.0), 1.0);
detailNormal = half3(detailNormal.rg * detailNormalStrength,
lerp(1, detailNormal.b, saturate(detailNormalStrength)) );
normalTS = SafeNormalize( half3(normalTS.rg + detailNormal.rg, normalTS.b * detailNormal.b) );
区别于常规的细节贴图,布料类的细节贴图AO会遮蔽基础贴图Albedo,材质表面的光照反射率会跟随细节变化,进而改变材质的整体亮暗表现。
half detailAlbedoOcclusion = lerp(1, occlusion, saturate(threadAlbedoStrength) );
albedo = albedo * detailAlbedoOcclusion;
Silk & Satin & Nylon
Specular BRDF
丝绸类材质的表面具有各向异性镜面高光的物理特性。
void ConvertValueAnisotropyToValueTB(real value, real anisotropy, out real valueT, out real valueB)
{
// Use the parametrization of Sony Imageworks.
// Ref: Revisiting Physically Based Shading at Imageworks, p. 15.
valueT = value * (1 + anisotropy);
valueB = value * (1 - anisotropy);
}
void ConvertAnisotropyToRoughness(real perceptualRoughness, real anisotropy, out real roughnessT, out real roughnessB)
{
real roughness = PerceptualRoughnessToRoughness(perceptualRoughness);
ConvertValueAnisotropyToValueTB(roughness, anisotropy, roughnessT, roughnessB);
}
基于微平面理论的Microfacet Cook-Torrance BRDF。准备BRDF数据时,各向异性还需要将材质的粗糙度分解成两个值,分别对应的是切线方向和次切线方向。各向异性的偏移值,决定了在两个方向上的粗糙度占比。
D项:Anisotropic GGX Distribution
// roughnessT -> roughness in tangent direction
// roughnessB -> roughness in bitangent direction
real D_GGXAnisoNoPI(real TdotH, real BdotH, real NdotH, real roughnessT, real roughnessB)
{
real a2 = roughnessT * roughnessB;
real3 v = real3(roughnessB * TdotH, roughnessT * BdotH, a2 * NdotH);
real s = dot(v, v);
// If roughness is 0, returns (NdotH == 1 ? 1 : 0).
// That is, it returns 1 for perfect mirror reflection, and 0 otherwise.
return SafeDiv(a2 * a2 * a2, s * s);
}
real D_GGXAniso(real TdotH, real BdotH, real NdotH, real roughnessT, real roughnessB)
{
return INV_PI * D_GGXAnisoNoPI(TdotH, BdotH, NdotH, roughnessT, roughnessB);
}
V项:Anisotropic GGX-Smith Correlated Joint Approximate
real GetSmithJointGGXAnisoPartLambdaV(real TdotV, real BdotV, real NdotV, real roughnessT, real roughnessB)
{
return length(real3(roughnessT * TdotV, roughnessB * BdotV, NdotV));
}
// Note: V = G / (4 * NdotL * NdotV)
// Ref: https://cedec.cesa.or.jp/2015/session/ENG/14698.html The Rendering Materials of Far Cry 4
real V_SmithJointGGXAniso(real TdotV, real BdotV, real NdotV, real TdotL, real BdotL, real NdotL, real roughnessT, real roughnessB, real partLambdaV)
{
real lambdaV = NdotL * partLambdaV;
real lambdaL = NdotV * length(real3(roughnessT * TdotL, roughnessB * BdotL, NdotL));
return 0.5 / (lambdaV + lambdaL);
}
real V_SmithJointGGXAniso(real TdotV, real BdotV, real NdotV, real TdotL, real BdotL, real NdotL, real roughnessT, real roughnessB)
{
real partLambdaV = GetSmithJointGGXAnisoPartLambdaV(TdotV, BdotV, NdotV, roughnessT, roughnessB);
return V_SmithJointGGXAniso(TdotV, BdotV, NdotV, TdotL, BdotL, NdotL, roughnessT, roughnessB, partLambdaV);
}
F项:Schlick
real F_Schlick(real f0, real f90, real u)
{
real x = 1.0 - u;
real x2 = x * x;
real x5 = x * x2 * x2;
return (f90 - f0) * x5 + f0; // sub mul mul mul sub mad
}
Diffuse BRDF
注意这里没有使用和镜面反射匹配的基于物理的漫反射模型:PBR diffuse for GGX+Smith。
函数返回的结果是漫反射量,还需要和反射颜色相乘,才能得到最终的漫反射光照颜色。
基于物理型 Disney Diffuse
real DisneyDiffuseNoPI(real NdotV, real NdotL, real LdotV, real perceptualRoughness)
{
// (2 * LdotH * LdotH) = 1 + LdotV
// real fd90 = 0.5 + (2 * LdotH * LdotH) * perceptualRoughness;
real fd90 = 0.5 + (perceptualRoughness + perceptualRoughness * LdotV);
// Two schlick fresnel term
real lightScatter = F_Schlick(1.0, fd90, NdotL);
real viewScatter = F_Schlick(1.0, fd90, NdotV);
// Normalize the BRDF for polar view angles of up to (Pi/4).
// We use the worst case of (roughness = albedo = 1), and, for each view angle,
// integrate (brdf * cos(theta_light)) over all light directions.
// The resulting value is for (theta_view = 0), which is actually a little bit larger
// than the value of the integral for (theta_view = Pi/4).
// Hopefully, the compiler folds the constant together with (1/Pi).
return rcp(1.03571) * (lightScatter * viewScatter);
}
real DisneyDiffuse(real NdotV, real NdotL, real LdotV, real perceptualRoughness)
{
return INV_PI * DisneyDiffuseNoPI(NdotV, NdotL, LdotV, perceptualRoughness);
}
Cotton & Wool
在实际落地到项目前,期间迭代了一个重要版本。初版是参考Unity HDRP中的设计思路,BRDF中镜面反射光照模型中D项用的是Charlie和V项用的是Ashikhmin,漫反射模型是基于传统型Lambert。
// A diffuse term use with fabric done by tech artist - empirical
real FabricLambertNoPI(real roughness)
{
return lerp(1.0, 0.5, roughness);
}
real FabricLambert(real roughness)
{
return INV_PI * FabricLambertNoPI(roughness);
}
初版存在的几个问题:
- Shader的ALU数量变多,性能的负担明显提高了,但是最终的渲染效果没有大的改善
- 光源正常的情况下,由于HDRP中环境光的差异,材质渲染出的颜色和Albedo贴图的颜色偏差大,对美术的工作流程不友好
- 渲染方程式中也没有包含Microfacet Sheen BRDF(“软纸张”),不能很好的渲染出天鹅绒等类型材质的特性
在项目中实际测试后,考虑到初版BRDF渲染出的效果,易用性和性能,得到的结果反而是弊大于利。于是废弃了初版的BRDF,改成金属工作流和Minimalist CookTorrance BRDF,有效的降低了BRDF部分的ALU数量。最后再加上Detail Map和Fuzz Map的搭配使用,有了一个平衡性能和效果后可用的版本。
// Computes the scalar specular term for Minimalist CookTorrance BRDF
// NOTE: needs to be multiplied with reflectance f0, i.e. specular color to complete
half DirectBRDFSpecular(BRDFData brdfData, half3 normalWS, half3 lightDirectionWS, half3 viewDirectionWS)
{
float3 lightDirectionWSFloat3 = float3(lightDirectionWS);
float3 halfDir = SafeNormalize(lightDirectionWSFloat3 + float3(viewDirectionWS));
float NoH = saturate(dot(float3(normalWS), halfDir));
half LoH = half(saturate(dot(lightDirectionWSFloat3, halfDir)));
// GGX Distribution multiplied by combined approximation of Visibility and Fresnel
// BRDFspec = (D * V * F) / 4.0
// D = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2
// V * F = 1.0 / ( LoH^2 * (roughness + 0.5) )
// See "Optimizing PBR for Mobile" from Siggraph 2015 moving mobile graphics course
// https://community.arm.com/events/1155
// Final BRDFspec = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2 * (LoH^2 * (roughness + 0.5) * 4.0)
// We further optimize a few light invariant terms
// brdfData.normalizationTerm = (roughness + 0.5) * 4.0 rewritten as roughness * 4.0 + 2.0 to a fit a MAD.
float d = NoH * NoH * brdfData.roughness2MinusOne + 1.00001f;
half d2 = half(d * d);
half LoH2 = LoH * LoH;
half specularTerm = brdfData.roughness2 / (d2 * max(half(0.1), LoH2) * brdfData.normalizationTerm);
// On platforms where half actually means something, the denominator has a risk of overflow
// clamp below was added specifically to "fix" that, but dx compiler (we convert bytecode to metal/gles)
// sees that specularTerm have only non-negative terms, so it skips max(0,..) in clamp (leaving only min(100,...))
#if defined (SHADER_API_MOBILE) || defined (SHADER_API_SWITCH)
specularTerm = specularTerm - HALF_MIN;
specularTerm = clamp(specularTerm, 0.0, 100.0); // Prevent FP16 overflow on mobiles
#endif
return specularTerm;
}
评论
发表评论