GameTech/URP 渲染实战

案例 18.6能量护盾(上):深度交接高亮进阶

目标:一个半球形能量护盾罩在地面上,护盾与地面、与穿过它的物体相交的地方出现一圈亮线,就像能量场被“挤压”一样。

原理:比较两个深度

护盾是透明物体,它在 Transparent 队列里绘制。到那个时候,所有不透明物体都已经画完了,URP 会把它们的深度复制到 _CameraDepthTexture(需要 URP Asset 里开启 Depth Texture)。于是在护盾的每个像素上,我们知道两个距离:

两者之差 gap = 场景深度 − 护盾深度。gap 接近 0,说明护盾表面紧贴着某个不透明表面——这就是“相交处”。用 1 − saturate(gap / width) 就得到一条宽度为 width 米的交接遮罩。

地面(不透明,写入深度图) 护盾(透明,不写深度) 相机 护盾深度 场景深度 gap 较大:不亮 gap ≈ 0:交接线
图 18-11同一条视线上,护盾片元的深度与深度图里的场景深度之差。越靠近护盾与地面的交线,差值越小。

URP 14 里读取深度图

URP 提供了现成的头文件 DeclareDepthTexture.hlsl,摘录如下(URP 14 ShaderLibrary/DeclareDepthTexture.hlsl):

TEXTURE2D_X_FLOAT(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);

float SampleSceneDepth(float2 uv)
{
    return SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, UnityStereoTransformScreenSpaceTex(uv)).r;
}

它返回的是原始设备深度:非线性,并且在 Metal/D3D/Vulkan 等平台上是反转的(近处为 1,远处为 0,即 Reversed-Z,第 1 章)。要和“米”比较,必须用 LinearEyeDepth(raw, _ZBufferParams) 转成观察空间深度。屏幕 UV 用 GetNormalizedScreenSpaceUV(positionCS)——在片元着色器里,SV_POSITION 的 xy 是像素坐标,这个函数除以屏幕尺寸并处理了平台的 UV 翻转。

护盾自身的深度在顶点着色器里算:-TransformWorldToView(positionWS).z。观察空间是右手系,相机看向 −Z,所以取负。也可以用 Common.hlsl 里的重载 LinearEyeDepth(positionWS, UNITY_MATRIX_V),结果相同(第 19 章就用了这个写法)。

Assets/Cookbook/Ch18/Shield.shader(节选:frag)
half4 frag(Varyings i, FRONT_FACE_TYPE face : FRONT_FACE_SEMANTIC) : SV_Target
{
    float t = ShieldTime();
    half3 normalWS = normalize(i.normalWS) * IS_FRONT_VFACE(face, 1.0h, -1.0h);
    half3 viewWS = GetWorldSpaceNormalizeViewDir(i.positionWS);

    // 1) 深度交接:场景深度 - 自身深度 越小,说明越贴近别的物体表面
    float2 screenUV = GetNormalizedScreenSpaceUV(i.positionCS);
    float sceneEye = LinearEyeDepth(SampleSceneDepth(screenUV), _ZBufferParams);
    float gap = sceneEye - i.eyeDepth;
    half intersect = 1.0h - saturate(gap / max(_IntersectWidth, 1e-4h));
    intersect = intersect * intersect * _IntersectStrength;

    // 2) 菲涅尔轮廓(背面也算,球的内壁边缘同样会亮)
    half rim = FresnelRim(normalWS, viewWS, _RimPower);

    // 3) 六边形:球面 U 绕一圈(2πR),V 从南极到北极(πR),
    //    所以 V 方向只铺 U 的一半,格子才不被拉伸;_HexScale 取整数,U 的接缝处就能无缝衔接
    float2 hp = i.uv * float2(_HexScale, _HexScale * 0.5);
    float4 hex = HexGrid(hp);
    half edge = smoothstep(0.5 - _HexLine, 0.5, HexDist(hex.xy));
    // 每个格子一个随机相位,缓慢呼吸明暗
    half cellPulse = 0.5 + 0.5 * sin(t * 2.0 + Hash21(hex.zw) * 6.2831);
    half hexMask = (edge + cellPulse * 0.15h) * _HexStrength * i.poleFade;

    // 4) 受击:环经过的地方六边形整体点亮
    half hit = HitRing(i.positionWS);
    half hitHex = hit * (0.35h + edge) * _HitStrength;

    half back = IS_FRONT_VFACE(face, 1.0h, 0.45h);    // 内壁暗一些,前后有层次
    half intensity = (_BaseAlpha + rim + hexMask * (0.3h + rim)) * back + intersect + hitHex;
    return half4(_ShieldColor.rgb * intensity, 1);
}
void main()
{
    float t = ShieldTime();
    vec3 N = normalize(normalWS) * (gl_FrontFacing ? 1.0 : -1.0);
    vec3 viewWS = GetWorldSpaceNormalizeViewDir(positionWS);

    // 1) 深度交接:场景深度 - 自身深度 越小,说明越贴近别的物体表面
    vec2 screenUV = GetNormalizedScreenSpaceUV(gl_FragCoord);
    float sceneEye = LinearEyeDepth(SampleSceneDepth(screenUV), _ZBufferParams);
    float gap = sceneEye - eyeDepth;
    float intersect = 1.0 - saturate(gap / max(_IntersectWidth, 1e-4));
    intersect = intersect * intersect * _IntersectStrength;

    // 2) 菲涅尔轮廓(背面也算,球的内壁边缘同样会亮)
    float rim = FresnelRim(N, viewWS, _RimPower);

    // 3) 六边形:球面 U 绕一圈(2πR),V 从南极到北极(πR),
    //    所以 V 方向只铺 U 的一半,格子才不被拉伸;_HexScale 取整数,U 的接缝处就能无缝衔接
    vec2 hp = uv * vec2(_HexScale, _HexScale * 0.5);
    vec4 hex = HexGrid(hp);
    float edge = smoothstep(0.5 - _HexLine, 0.5, HexDist(hex.xy));
    // 每个格子一个随机相位,缓慢呼吸明暗
    float cellPulse = 0.5 + 0.5 * sin(t * 2.0 + Hash21(hex.zw) * 6.2831);
    float hexMask = (edge + cellPulse * 0.15) * _HexStrength * poleFade;

    // 4) 受击:环经过的地方六边形整体点亮
    float hit = HitRing(positionWS);
    float hitHex = hit * (0.35 + edge) * _HitStrength;

    float back = gl_FrontFacing ? 1.0 : 0.45;    // 内壁暗一些,前后有层次
    float intensity = (_BaseAlpha + rim + hexMask * (0.3 + rim)) * back + intersect + hitHex;
    fragColor = vec4(_ShieldColor.rgb * intensity, 1.0);
}
图 18-12交接高亮。左:关闭;中:宽度 0.12 米,一条细亮线;右:宽度 0.6 米,交线处是一片较宽的光晕。注意左侧立方体穿过护盾的地方同样出现了交线。

常见坑