GameTech/URP 渲染实战

案例 21.3Flowmap:沿任意方向流动进阶

目标:让贴图沿一张“流向图”规定的方向流动,绕过石头、在漩涡处打转,并且没有周期性的跳变。

UV 滚动只能整体沿一个方向流。Flowmap 用一张贴图存下每个位置的流速向量:R、G 两个通道分别是 x、y 方向的速度,映射到 0..1 存储,0.5 表示静止。Shader 里先解码回 −1..1,再用它代替常数速度:uv − flow × t。

问题马上出现:不同位置的速度不同,随着 t 增大,相邻像素的 UV 偏移差越来越大,图案被越拉越长,最终完全变形。解决办法是让 t 只在一个周期内增长——frac(t)——周期结束时 UV 跳回原位。拉伸被限制住了,但每个周期末尾图案会“啪”地复位。

双相位(Dual Phase)消除这个跳变:用两套相位相差半个周期的 UV 各采样一次,权重随相位呈三角波:某一套 UV 即将复位的时刻,它的权重恰好为 0,此时画面完全由另一套提供;复位之后它的权重再慢慢升上来。

时间 t10 相位 0 的权重 w₀ = 1 − |1 − 2·frac(t)|(圆点:该相位复位,权重为 0) 相位 0.5 的权重 = 1 − w₀
图 21-4双相位的权重:两条三角波此起彼伏,任一相位复位(UV 跳回原位)的瞬间,它的权重都是 0
Assets/Cookbook/Ch21/UVAnim.hlsl(节选:flowmap)
// Flowmap 双相位采样:两套相位差半个周期的 UV,各自沿流向“漂”,权重呈三角波交替。
// 某一相位重置(跳回原位)的瞬间,它的权重恰好为 0,所以看不到跳变。
struct FlowSample
{
    float2 uv0, uv1;
    float  w0;           // 第一套的权重,第二套为 1 - w0
};

FlowSample FlowUV(float2 uv, float2 flow, float time)
{
    FlowSample s;
    float p0 = frac(time);
    float p1 = frac(time + 0.5);
    s.uv0 = uv - flow * p0;
    s.uv1 = uv - flow * p1 + float2(0.37, 0.21);  // 第二套错开一点,避免两层图案重合时出现“重影节拍”
    s.w0 = 1.0 - abs(1.0 - 2.0 * p0);             // p0 = 0 或 1(重置时)权重为 0,p0 = 0.5 时为 1
    return s;
}
// Flowmap 双相位采样:两套相位差半个周期的 UV,各自沿流向“漂”,权重呈三角波交替。
// 某一相位重置(跳回原位)的瞬间,它的权重恰好为 0,所以看不到跳变。
struct FlowSample
{
    vec2 uv0, uv1;
    float w0;            // 第一套的权重,第二套为 1 - w0
};

FlowSample FlowUV(vec2 uv, vec2 flow, float time)
{
    FlowSample s;
    float p0 = fract(time);
    float p1 = fract(time + 0.5);
    s.uv0 = uv - flow * p0;
    s.uv1 = uv - flow * p1 + vec2(0.37, 0.21);    // 第二套错开一点,避免两层图案重合时出现“重影节拍”
    s.w0 = 1.0 - abs(1.0 - 2.0 * p0);             // p0 = 0 或 1(重置时)权重为 0,p0 = 0.5 时为 1
    return s;
}

第二套 UV 额外加了一个小偏移 (0.37, 0.21):如果两套 UV 采到的图案完全相同,只是错开半周期,人眼仍能察觉到画面每半个周期“呼吸”一次;错开采样位置后,这种规律性被打散。

Assets/Cookbook/Ch21/Flowmap.shader(节选:frag)
half4 frag(Varyings i) : SV_Target
{
    half4 flowTex = SAMPLE_TEXTURE2D(_FlowMap, sampler_FlowMap, i.uv);
    if (_Mode < 0.5) return half4(flowTex.rg, 0, 1);          // 直接显示流向图

    // RG 存的是 0..1,映射回 -1..1 的方向向量(0.5 灰 = 静止)
    float2 flow = (flowTex.rg * 2.0 - 1.0) * _FlowStrength;
    float t = (_Time.y + _Phase) * _Speed;
    float2 uv = TRANSFORM_TEX(i.uv, _BaseMap);

    half4 c;
    if (_Mode < 1.5)
    {
        // 单相位:UV 沿流向越漂越远,到 frac 归零时整张图“啪”地跳回原位
        c = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv - flow * frac(t));
    }
    else
    {
        FlowSample fs = FlowUV(uv, flow, t);
        half4 c0 = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, fs.uv0);
        half4 c1 = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, fs.uv1);
        c = c0 * fs.w0 + c1 * (1.0 - fs.w0);
    }

    if (_UseRamp > 0.5)
    {
        half k = smoothstep(0.2, 0.9, c.r);
        c.rgb = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(k, 0.5)).rgb * (k * k * _Intensity);
    }
    return half4(c.rgb, 1);
}
vec4 frag()
{
    vec4 flowTex = texture(_FlowMap, uv);
    if (_Mode < 0.5) return vec4(flowTex.rg, 0.0, 1.0);          // 直接显示流向图

    // RG 存的是 0..1,映射回 -1..1 的方向向量(0.5 灰 = 静止)
    vec2 flow = (flowTex.rg * 2.0 - 1.0) * _FlowStrength;
    float t = (_Time.y + _Phase) * _Speed;
    vec2 uvB = TransformTex(uv, _BaseMap_ST);

    vec4 c;
    if (_Mode < 1.5)
    {
        // 单相位:UV 沿流向越漂越远,到 fract 归零时整张图“啪”地跳回原位
        c = texture(_BaseMap, uvB - flow * fract(t));
    }
    else
    {
        FlowSample fs = FlowUV(uvB, flow, t);
        vec4 c0 = texture(_BaseMap, fs.uv0);
        vec4 c1 = texture(_BaseMap, fs.uv1);
        c = c0 * fs.w0 + c1 * (1.0 - fs.w0);
    }

    if (_UseRamp > 0.5)
    {
        float k = smoothstep(0.2, 0.9, c.r);
        c.rgb = texture(_RampTex, vec2(k, 0.5)).rgb * (k * k * _Intensity);
    }
    return vec4(c.rgb, 1.0);
}
图 21-5左:公共贴图 flow_swirl(RG 两通道的漩涡流场);中:单相位,在一个周期接近结束时(phase = 0.92)网格被严重拉扯,下一刻将整张跳回;右:双相位,同一时刻两套 UV 加权,拉伸被抵消,代价是能看到轻微的双层叠影

双相位的叠影在网格这种规则图案上很明显,在水面、熔岩这种随机纹理上则几乎看不见。所以 Flowmap 主要用于噪声类、法线类贴图:

图 21-6同一张漩涡流向图驱动噪声,再经过火焰渐变映射,得到一池缓慢旋转的熔岩
深入:为什么是三角波权重两套采样的权重之和必须恒为 1,否则整体亮度会随相位起伏;同时每套的权重必须在它复位的瞬间为 0。三角波 1 − |1 − 2p| 与错开半周期的另一条三角波相加恰好处处等于 1,满足这两个条件且最便宜。也可以用 sin²(πp) 与 cos²(πp),过渡更圆滑,但要多算三角函数。无论哪种,混合两张不相关的噪声都会让对比度在 w₀ = 0.5 附近下降(两张图平均后方差减半),画面会有轻微的“周期性变灰”,相位错开采样位置、并在混合后略微提高对比度可以缓解。
坑:Flowmap 不能压缩成普通格式流向图是数据,必须关闭 sRGB;更重要的是不要用 DXT1/ETC 这类按 4×4 块压缩的格式——平滑的方向场会被压成块状台阶,流动时能看到一格一格的“抖动”。用 BC5(双通道)、BC7,或者干脆不压缩。本章配图为此在拍摄时直接读取 PNG 生成了一张不压缩的贴图。
参数与制作_FlowStrength 决定一个周期内最多漂多远,_Speed 决定周期多长;两者的乘积才是视觉上的流速。强度太大会让每个周期内的拉伸重新变得明显,一般控制在贴图平铺的 0.1–0.4 之间。流向图本身可以用专门的流向绘制工具画,也可以从流体模拟或河道样条线烘焙出来;在 Houdini、Substance Designer 一类工具里都能生成。水面法线的 Flowmap 用法见第 19 章。