GameTech/URP 渲染实战

案例 16.6双面材质入门

目标:让只有一层面的网格(树叶、纸片、旗帜、布料、开口的碗)正反两面都正确受光,并能给背面不同的颜色。

原理:Cull Off 只是让背面也生成片元,但插值出来的法线依然是网格的正面法线。当我们从背面看时,法线背对着我们,光照结果就是“错的那一面”的光照。修正方法是在片元里判断当前片元来自正面还是背面,背面就把法线取反。HLSL 的系统值 SV_IsFrontFace 提供这个信息,URP(准确说是 Core RP 的平台抽象层)把它包装成了三个宏,屏蔽了平台差异:

// 摘自 Core RP 14 ShaderLibrary/API/Metal.hlsl(D3D11.hlsl 中相同)
#define FRONT_FACE_SEMANTIC SV_IsFrontFace
#define FRONT_FACE_TYPE bool
#define IS_FRONT_VFACE(VAL, FRONT, BACK) ((VAL) ? (FRONT) : (BACK))
Assets/Cookbook/Ch16/TwoSided.shader(节选:frag)
half4 frag(Varyings i, FRONT_FACE_TYPE face : FRONT_FACE_SEMANTIC) : SV_Target
{
    // IS_FRONT_VFACE 屏蔽了平台差异:D3D/Metal 是 bool,旧的 GL 是 float 符号
    bool isFront = IS_FRONT_VFACE(face, true, false);
    float3 N = normalize(i.normalWS);
    if (!isFront && _FlipBackNormal > 0.5) N = -N;
    half3 albedo = isFront ? _BaseColor.rgb : _BackColor.rgb;
    return half4(SimpleLighting(albedo, N, i.positionWS, 0.3h), 1);
}
void main()
{
    // GLSL 直接用内置的 gl_FrontFacing(bool)
    bool isFront = gl_FrontFacing;
    vec3 N = normalize(normalWS);
    if (!isFront && _FlipBackNormal > 0.5) N = -N;
    vec3 albedo = isFront ? _BaseColor.rgb : _BackColor.rgb;
    fragColor = vec4(SimpleLighting(albedo, N, positionWS, 0.3), 1.0);
}
图 16-10开口的半球碗与一张背对相机的薄片。左:Cull Back,碗内壁(背面)和薄片都被剔除,只剩碗外壁的一条边;中:Cull Off 但不翻转法线,背面用的是正面的法线,受光面反而发暗;右:翻转背面法线并换成浅色,碗内壁和薄片背面都正确受光
常见坑① 双面材质的阴影 Pass 也要 Cull Off,否则薄片只有一面投影,阴影会随光照角度时有时无。② 如果用了法线贴图,背面除了翻转几何法线,还要翻转 TBN 中的副切线方向(或者直接在世界空间翻转最终法线),否则凹凸方向是反的。③ 双面渲染不等于两倍开销:被剔除的三角形本来就不产生片元,Cull Off 只是让原本被剔除的那些也参与着色。真正的开销取决于背面实际被看到的面积。