GameTech/URP 渲染实战

案例 12.3拉丝金属与同心圆拉丝进阶

目标:用各向异性 GGX 做出直线拉丝(纽结、圆环)和同心圆拉丝(锅底、旋钮),并处理各向异性下的环境反射。

着色函数只是把 D、V 换成各向异性版本:

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

    float D = AdvD_GGXAniso(dot(m.T, H), dot(m.B, H), NoH, m.at, m.ab);
    float Vis = AdvV_SmithGGXAniso(dot(m.T, m.V), dot(m.B, m.V), NoV,
                                   dot(m.T, L), dot(m.B, L), NoL, m.at, m.ab);
    float3 F = AdvF_Schlick(m.f0, 1.0, VoH);

    float3 brdf = m.diffuse * ADV_INV_PI + D * Vis * F;
    return brdf * PI * light.color * (NoL * light.distanceAttenuation * light.shadowAttenuation);
}
vec3 ShadeOneLight(MaterialCtx m, Light light)
{
    vec3 L = light.direction;
    vec3 H = normalize(m.V + L);
    float NoL = saturate(dot(m.N, L));
    float NoV = max(dot(m.N, m.V), 1e-4);
    float NoH = saturate(dot(m.N, H));
    float VoH = saturate(dot(m.V, H));

    float D = AdvD_GGXAniso(dot(m.T, H), dot(m.B, H), NoH, m.at, m.ab);
    float Vis = AdvV_SmithGGXAniso(dot(m.T, m.V), dot(m.B, m.V), NoV,
                                   dot(m.T, L), dot(m.B, L), NoL, m.at, m.ab);
    vec3 F = AdvF_Schlick(m.f0, 1.0, VoH);

    vec3 brdf = m.diffuse * ADV_INV_PI + D * Vis * F;
    return brdf * PI * light.color * (NoL * light.distanceAttenuation * light.shadowAttenuation);
}

拉丝方向有三种来源,用一个 KeywordEnum 切换:网格切线(纹路沿 U)、网格副切线(纹路沿 V),以及“以 UV 中心为圆心的同心圆”。最后一种在片元里现算:从 UV 中心指向当前点的方向是半径方向,把它旋转 90° 就是圆周方向,再用 T、B 把这个二维方向转到世界空间。拉丝条纹贴图(公共贴图 noise_streak,纹路沿 U)的采样坐标也要跟着改:同心圆模式下只按半径取样,条纹就变成一圈圈的同心圆:

Assets/Cookbook/Ch12/Anisotropic.shader(节选:dir)
    // 选择拉丝方向;条纹贴图的纹路沿 U 方向,采样坐标也跟着转
    float2 streakUV = i.uv;
#if defined(_DIR_BITANGENT)
    float3 tmp = T; T = B; B = -tmp;
    streakUV = i.uv.yx;
#elif defined(_DIR_RADIAL)
    // 以 UV 中心为圆心的同心圆拉丝(锅底、CD 纹):切线 = 圆周方向
    float2 d = i.uv - 0.5;
    float2 c = normalize(float2(-d.y, d.x) + 1e-5);
    T = normalize(c.x * T + c.y * B);
    B = cross(N, T);
    streakUV = float2(0.5, length(d) * 2.0);   // 条纹只随半径变化 → 同心圆
#endif
    // 选择拉丝方向;条纹贴图的纹路沿 U 方向,采样坐标也跟着转
    vec2 streakUV = uv;
#if defined(_DIR_BITANGENT)
    vec3 tmp = T; T = B; B = -tmp;
    streakUV = uv.yx;
#elif defined(_DIR_RADIAL)
    // 以 UV 中心为圆心的同心圆拉丝(锅底、CD 纹):切线 = 圆周方向
    vec2 d = uv - 0.5;
    vec2 c = normalize(vec2(-d.y, d.x) + 1e-5);
    T = normalize(c.x * T + c.y * B);
    B = cross(N, T);
    streakUV = vec2(0.5, length(d) * 2.0);   // 条纹只随半径变化 → 同心圆
#endif

环境反射:弯曲法线近似

直接光好办,环境光就麻烦了:反射探针的 mip 链是按各向同性的 GGX 预滤波的,各向异性的波瓣没法从中直接取到。完全正确的做法要做多次采样或预滤波更多维度的数据,实时渲染里普遍采用一个巧妙的近似(Filament、HDRP 都在用):把法线朝“粗糙方向”弯一弯,用弯过的法线算反射向量,再按各向同性的方式采样。

Assets/Cookbook/Ch12/Anisotropic.shader(节选:bent)
// 各向异性的环境反射:没法预滤波,常用“弯曲法线”近似(Filament 的做法)
// grooveDir 是光滑的方向(划痕走向);anisoN = V 投影到垂直于它的平面上,
// 把法线朝 anisoN 扭一扭,反射就会沿“粗糙方向”被拉长
float3 grooveDir = _Anisotropy >= 0.0 ? T : B;
float3 anisoT = cross(grooveDir, V);
float3 anisoN = cross(anisoT, grooveDir);
float bend = abs(_Anisotropy) * saturate(5.0 * pr) * _AnisoIBL;
float3 bentN = normalize(lerp(N, anisoN, bend));
float3 R = reflect(-V, bentN);
// 各向异性的环境反射:没法预滤波,常用“弯曲法线”近似(Filament 的做法)
// grooveDir 是光滑的方向(划痕走向);anisoN = V 投影到垂直于它的平面上,
// 把法线朝 anisoN 扭一扭,反射就会沿“粗糙方向”被拉长
vec3 grooveDir = _Anisotropy >= 0.0 ? T : B;
vec3 anisoT = cross(grooveDir, V);
vec3 anisoN = cross(anisoT, grooveDir);
float bend = abs(_Anisotropy) * saturate(5.0 * pr) * _AnisoIBL;
vec3 bentN = normalize(mix(N, anisoN, bend));
vec3 R = reflect(-V, bentN);

思路是:anisoN 是把视线 V 投影到“垂直于纹路方向的平面”上得到的向量。法线越往它靠,反射向量在粗糙方向上的变化就越大——相当于把各向同性的反射图案沿粗糙方向“抹开”。弯曲量乘了 saturate(5 * 感知粗糙度):非常光滑的表面几乎不存在各向异性,不该弯。

图 12-9环面纽结上的拉丝金属,纹路沿管子的长度方向(网格切线)。各向异性度从左到右 0、0.5、0.85

k = 0 时三个光源(主光与两盏点光)各留下一个圆斑;随着 k 增大,圆斑沿管子的圆周方向越拉越长,最后变成一圈圈环绕管子的亮带——正是“垂直于纹路”的方向。

图 12-10平板上的同心圆拉丝(锅底)。左:各向同性(k = 0),两盏点光各一个圆斑;右:k = 0.85,高光沿半径方向拉成放射状的亮线
图 12-11只有环境光时的各向异性球(纹路沿水平的纬线)。左:环境反射不做处理,地平线清晰锐利,看起来像普通的光滑金属;右:使用弯曲法线,地平线和太阳倒影被竖直拉开
常见坑① 切线必须连续。网格的 UV 接缝两侧切线方向不同,各向异性高光会在接缝处断开;拉丝件要专门展一套“沿纹路方向”的 UV。② 用了法线贴图时,切线也要用法线贴图扰动后的 N 重新正交化,否则 T 不在切平面内。③ 一个物体上 T 方向混乱(例如自动展 UV 的模型)时,各向异性只会产生满屏的噪点,不如直接用各向同性。④ 条纹贴图要开 mip,并尽量用各向异性过滤,远处条纹闪烁会非常明显。