GameTech/URP 渲染实战

案例 18.3方向溶解与球形溶解进阶

目标:让溶解有方向感——比如角色从头到脚被“传送”走;或者从一个点向外扩散——比如被魔法击中的位置先开始瓦解。

原理:换一种溶解值

clip 和边带的逻辑完全不变,只换溶解值 v 的来源:

纯几何的溶解值会得到一条笔直的切割线或者一个完美的圆,太“干净”了。所以再用 lerp(t, noise, _NoiseStrength) 掺一点噪声,切割线就变成了不规则的锯齿边。

Assets/Cookbook/Ch18/DissolveCore.hlsl(节选:dissolveValue)
// 返回 0~1 的“溶解值”:值越小越早消失。进度 p 时,值 < p 的像素被裁掉。
half DissolveValue(float2 uv, float3 positionOS, float3 positionWS)
{
    half noise = SAMPLE_TEXTURE2D(_NoiseMap, sampler_NoiseMap, uv * _NoiseMap_ST.xy + _NoiseMap_ST.zw).r;
#if defined(_MODE_DIRECTION)
    // 方向溶解:沿对象空间某方向的投影,归一化到 0~1,再掺一点噪声让边缘不规则
    float h = dot(positionOS, normalize(_DissolveDir.xyz));
    half t = saturate((h - _DirRange.x) / (_DirRange.y - _DirRange.x));
    return lerp(t, noise, _NoiseStrength);
#elif defined(_MODE_SPHERE)
    // 球形溶解:离球心越近越早消失,像被一个不断变大的球“吞掉”
    half t = saturate(distance(positionWS, _SphereCenter.xyz) / _SphereCenter.w);
    return lerp(t, noise, _NoiseStrength);
#else
    return noise;
#endif
}
// 返回 0~1 的“溶解值”:值越小越早消失。进度 p 时,值 < p 的像素被裁掉。
float DissolveValue(vec2 uv, vec3 positionOS, vec3 positionWS)
{
    float noise = texture(_NoiseMap, uv * _NoiseMap_ST.xy + _NoiseMap_ST.zw).r;
#if defined(_MODE_DIRECTION)
    // 方向溶解:沿对象空间某方向的投影,归一化到 0~1,再掺一点噪声让边缘不规则
    float h = dot(positionOS, normalize(_DissolveDir.xyz));
    float t = saturate((h - _DirRange.x) / (_DirRange.y - _DirRange.x));
    return mix(t, noise, _NoiseStrength);
#elif defined(_MODE_SPHERE)
    // 球形溶解:离球心越近越早消失,像被一个不断变大的球“吞掉”
    float t = saturate(distance(positionWS, _SphereCenter.xyz) / _SphereCenter.w);
    return mix(t, noise, _NoiseStrength);
#else
    return noise;
#endif
}

三种模式用 [KeywordEnum(Noise, Direction, Sphere)] _Mode 生成三个关键字 _MODE_NOISE / _MODE_DIRECTION / _MODE_SPHERE,Shader 里用 #pragma shader_feature_local 声明——它们是材质级别的选择,不会在运行时频繁切换,shader_feature 只编译实际用到的变体。注意三个 Pass 都要声明同样的关键字,否则阴影 Pass 会走默认分支,影子的溶解形状就和物体对不上了。

为什么方向溶解用对象空间、球形溶解用世界空间

方向溶解描述的是“物体自身从头到脚”,角色转身、倒地,头脚关系不变,所以用对象空间;球心通常来自游戏逻辑(被击中的点、爆炸中心),它天然是世界坐标,所以用世界空间。如果方向溶解想用世界方向(比如永远从上往下),在 C# 里用 transform.InverseTransformDirection(Vector3.down) 换算成对象空间再传进来即可。

图 18-5同一进度 0.45 下的三种溶解。左:噪声溶解;中:方向溶解(对象空间自上而下,NoiseStrength=0.22);右:球形溶解(球心在纽结左上方,最大半径 1.9 米)。均开启 Bloom。

从游戏逻辑驱动

球形溶解最常见的用法是“击中点开始瓦解”。C# 侧只需要在命中时记录世界坐标,然后用协程推进进度:

// 命中时调用:从 hitPoint 开始,duration 秒内溶解完
IEnumerator DissolveFrom(Renderer r, Vector3 hitPoint, float radius, float duration)
{
    var mat = r.material;                       // 材质实例,只影响这一个物体
    mat.EnableKeyword("_MODE_SPHERE");
    mat.SetVector("_SphereCenter", new Vector4(hitPoint.x, hitPoint.y, hitPoint.z, radius));
    for (float t = 0; t < duration; t += Time.deltaTime)
    {
        mat.SetFloat("_Progress", t / duration);
        yield return null;
    }
    r.gameObject.SetActive(false);
}
注意运行时 EnableKeyword 打开一个 shader_feature 关键字,前提是这个变体被打进了包。如果构建时没有任何材质用到 _MODE_SPHERE,变体会被剥离,运行时打开关键字也不会生效。解决办法是让工程里至少有一个材质使用它,或者改用 multi_compile_local(变体数更多),详见第 29 章。