GameTech/URP 渲染实战

案例 12.5丝绒:Charlie sheen 布料进阶

目标:实现布料着色,对比“把布当塑料(GGX)”、Charlie sheen、Charlie + 次表面色三种做法,并加一层程序化的平纹织物纹理。

Assets/Cookbook/Ch12/Cloth.shader(节选:shade)
half3 ShadeOneLight(MaterialCtx m, Light light)
{
    float3 L = light.direction;
    float3 H = normalize(m.V + L);
    float NoLu = dot(m.N, L);
    float NoL = saturate(NoLu);
    float NoV = max(dot(m.N, m.V), 1e-4);
    float NoH = saturate(dot(m.N, H));
    float atten = light.distanceAttenuation * light.shadowAttenuation;

#if defined(_MODEL_GGX)
    // 对照组:把布当成普通电介质
    float3 brdf = m.albedo * ADV_INV_PI + AdvSpecularGGX(m.N, m.V, L, m.a, 0.04);
    return brdf * PI * light.color * (NoL * atten);
#else
    // 镜面:Charlie 分布 × Neubelt 可见性 × sheen 颜色(布料没有明显的菲涅尔变化,直接用颜色当 F)
    float3 Fr = AdvD_Charlie(NoH, m.sheenA) * AdvV_Neubelt(NoV, NoL) * m.sheen;
  #if defined(_MODEL_SUBSURFACE)
    // 漫反射:包裹 + 次表面色,光“渗”进暗部并带上颜色
    float3 Fd = m.albedo * ADV_INV_PI * AdvWrapDiffuse(NoLu, _Wrap);
    Fd *= saturate(_SubsurfaceColor.rgb + NoL);
    return (Fd + Fr * NoL) * PI * light.color * atten;
  #else
    float3 Fd = m.albedo * ADV_INV_PI;
    return (Fd + Fr) * PI * light.color * (NoL * atten);
  #endif
#endif
}
vec3 ShadeOneLight(MaterialCtx m, Light light)
{
    vec3 L = light.direction;
    vec3 H = normalize(m.V + L);
    float NoLu = dot(m.N, L);
    float NoL = saturate(NoLu);
    float NoV = max(dot(m.N, m.V), 1e-4);
    float NoH = saturate(dot(m.N, H));
    float atten = light.distanceAttenuation * light.shadowAttenuation;

#if defined(_MODEL_GGX)
    // 对照组:把布当成普通电介质
    vec3 brdf = m.albedo * ADV_INV_PI + AdvSpecularGGX(m.N, m.V, L, m.a, 0.04);
    return brdf * PI * light.color * (NoL * atten);
#else
    // 镜面:Charlie 分布 × Neubelt 可见性 × sheen 颜色(布料没有明显的菲涅尔变化,直接用颜色当 F)
    vec3 Fr = AdvD_Charlie(NoH, m.sheenA) * AdvV_Neubelt(NoV, NoL) * m.sheen;
  #if defined(_MODEL_SUBSURFACE)
    // 漫反射:包裹 + 次表面色,光“渗”进暗部并带上颜色
    vec3 Fd = m.albedo * ADV_INV_PI * AdvWrapDiffuse(NoLu, _Wrap);
    Fd *= saturate(_SubsurfaceColor.rgb + NoL);
    return (Fd + Fr * NoL) * PI * light.color * atten;
  #else
    vec3 Fd = m.albedo * ADV_INV_PI;
    return (Fd + Fr) * PI * light.color * (NoL * atten);
  #endif
#endif
}

注意次表面版本里 Fd 没有乘 N·L:包裹漫反射本身已经包含了余弦衰减(只是被“包”宽了),再乘一次 N·L 就把包裹的效果抵消了;镜面项 Fr 照常乘 N·L。

织物纹理用两组正弦和棋盘式的交替模拟经纬线的上下穿插,只轻微调制反照率和 sheen 颜色。它在远处会因为频率太高而闪烁,正式项目应该用法线贴图 + mip 来做细节:

Assets/Cookbook/Ch12/Cloth.shader(节选:weave)
// 程序化平纹:经纬线交替压在上面,用两组正弦的乘积近似线的明暗
float2 w = i.uv * _WeaveScale;
float warp = abs(sin(w.x * PI)) * step(0.5, frac(w.y * 0.5 + floor(w.x) * 0.5));
float weft = abs(sin(w.y * PI)) * (1.0 - step(0.5, frac(w.y * 0.5 + floor(w.x) * 0.5)));
float thread = saturate(warp + weft);
half weave = lerp(1.0, lerp(0.7, 1.1, thread), _WeaveStrength);
// 程序化平纹:经纬线交替压在上面,用两组正弦的乘积近似线的明暗
vec2 w = uv * _WeaveScale;
float warp = abs(sin(w.x * PI)) * step(0.5, fract(w.y * 0.5 + floor(w.x) * 0.5));
float weft = abs(sin(w.y * PI)) * (1.0 - step(0.5, fract(w.y * 0.5 + floor(w.x) * 0.5)));
float thread = saturate(warp + weft);
float weave = mix(1.0, mix(0.7, 1.1, thread), _WeaveStrength);
图 12-16逆光下的丝绒抱枕与纽结。左:GGX(光滑度 0.35),顶部出现塑料似的高光;中:Charlie sheen,轮廓和掠射区域泛起粉色柔光,正面保持深色;右:再加包裹漫反射与次表面色,明暗交界更软、暗部更红
图 12-17sheen 粗糙度从左到右 0.15 → 1.0。粗糙度小时 sheen 只是轮廓上的一条细边,大时铺满整个受光面,像蒙了一层绒
关于环境光的 sheen直接光部分是严格的公式,环境光部分就不是了。正确的做法和 split-sum 一样:把 DCharlieVNeubelt 对半球积分,预先算成一张以 (N·V, α) 为坐标的 LUT(Filament 把它放在 DFG 表的第三个通道里)。本案例为了不再引入一张贴图,用了一个随掠射角增强的经验系数,代码里已经注明。需要精确时,可以参照第 11 章生成环境 BRDF LUT 的方法,把积分核换成布料的 D·V 再算一张。

调参上,布料的 sheen 颜色通常比底色更亮、饱和度更低:深红丝绒的 sheen 偏粉白,深蓝的偏灰蓝。棉布、亚麻这类没有明显绒毛的布料,sheen 很弱,用普通的高粗糙度 GGX 也差不多;sheen 真正出彩的是丝绒、绒面皮革和毛毡。