GameTech/URP 渲染实战

案例 4.4极坐标图案:花瓣、螺旋与放射光进阶

目标:在极坐标下画三个经典图案,并处理 atan2 接缝带来的抗锯齿问题。

Assets/Cookbook/Ch04/C04_04_Polar.shader(节选:flower)
half3 Flower(float2 p)
{
    float2 pol = ToPolar(p);
    float theta = pol.y * TWO_PI;
    // 半径随角度起伏:r(θ) = 基础半径 + 振幅·cos(Nθ),N 片花瓣
    float edge = 0.55 + 0.25 * cos(_Count * theta);
    float petal = FillAA(pol.x - edge);
    // 花瓣内部再按半径做一层渐变,中心是一颗小圆
    half3 col = lerp(_BgColor.rgb, lerp(_ColorA.rgb, _ColorA.rgb * 0.55, pol.x / edge), petal);
    col = lerp(col, _ColorB.rgb, FillAA(SdCircle(p, 0.2)));
    return col;
}
vec3 Flower(vec2 p)
{
    vec2 pol = ToPolar(p);
    float theta = pol.y * TWO_PI;
    // 半径随角度起伏:r(θ) = 基础半径 + 振幅·cos(Nθ),N 片花瓣
    float edge = 0.55 + 0.25 * cos(_Count * theta);
    float petal = FillAA(pol.x - edge);
    // 花瓣内部再按半径做一层渐变,中心是一颗小圆
    vec3 col = mix(_BgColor.rgb, mix(_ColorA.rgb, _ColorA.rgb * 0.55, pol.x / edge), petal);
    col = mix(col, _ColorB.rgb, FillAA(SdCircle(p, 0.2)));
    return col;
}

花瓣:让“边界半径”随角度起伏:edge(θ) = 0.55 + 0.25·cos(Nθ),N 是花瓣数。像素的 r 小于 edge(θ) 就在花里。r − edge(θ) 不是严格的距离场(它沿径向测量,花瓣侧面会偏大),但用作 FillAA 的输入已经足够平滑。把 cos 换成 abs(cos)、pow、或几个不同频率的叠加,就能得到星形、齿轮、雪花等变化。

Assets/Cookbook/Ch04/C04_04_Polar.shader(节选:spiral)
half3 Spiral(float2 p)
{
    float2 pol = ToPolar(p);
    // 角度 + log(半径):沿径向每走 e 倍,条纹转过 _Twist 圈 → 对数螺线
    float s = frac(pol.y * _Count + log(max(pol.x, 1e-4)) * _Twist);
    float band = abs(s - 0.5) * 2.0;
    float aa = fwidth(band);
    float m = smoothstep(0.5 - aa, 0.5 + aa, band);
    half3 col = lerp(_ColorA.rgb, _ColorB.rgb, m);
    // 中心处条纹无限变细,必然出摩尔纹;用一个圆盖住
    return lerp(col, _BgColor.rgb, FillAA(SdCircle(p, 0.06)));
}
vec3 Spiral(vec2 p)
{
    vec2 pol = ToPolar(p);
    // 角度 + log(半径):沿径向每走 e 倍,条纹转过 _Twist 圈 → 对数螺线
    float s = fract(pol.y * _Count + log(max(pol.x, 1e-4)) * _Twist);
    float band = abs(s - 0.5) * 2.0;
    float aa = fwidth(band);
    float m = smoothstep(0.5 - aa, 0.5 + aa, band);
    vec3 col = mix(_ColorA.rgb, _ColorB.rgb, m);
    // 中心处条纹无限变细,必然出摩尔纹;用一个圆盖住
    return mix(col, _BgColor.rgb, FillAA(SdCircle(p, 0.06)));
}

螺旋:条纹的“相位”同时由角度和 log(r) 决定。沿半径方向每放大同样的倍数,条纹转过同样的角度,这就是对数螺线——鹦鹉螺、台风云图的形状。中心处条纹无限变细,必然出现摩尔纹,所以用一个小圆盖住。

Assets/Cookbook/Ch04/C04_04_Polar.shader(节选:sunburst)
half3 Sunburst(float2 p)
{
    float2 pol = ToPolar(p);
    float t = pol.y * _Count;                  // 每条光芒占 1 个单位
    float tri = abs(frac(t) - 0.5) * 2.0;
    // 坑:pol.y 在 +X 轴处从 1 跳回 0,fwidth 会在那条线上暴涨,画出一道“裂缝”。
    // 解决:用另一个在别处断开的角度(偏移半圈)求导,两者取小。
    float a2 = frac(pol.y + 0.5) * _Count;
    float aa = min(fwidth(t), fwidth(a2)) * 2.0;
    float ray = smoothstep(0.5 - aa, 0.5 + aa, tri);
    half3 col = lerp(_ColorA.rgb, _ColorB.rgb, ray);
    // 向外渐暗 + 中心太阳
    col *= lerp(1.15, 0.55, saturate(pol.x));
    return lerp(col, half3(1.0, 0.85, 0.45), FillAA(SdCircle(p, 0.22)));
}
vec3 Sunburst(vec2 p)
{
    vec2 pol = ToPolar(p);
    float t = pol.y * _Count;                  // 每条光芒占 1 个单位
    float tri = abs(fract(t) - 0.5) * 2.0;
    // 坑:pol.y 在 +X 轴处从 1 跳回 0,fwidth 会在那条线上暴涨,画出一道“裂缝”。
    // 解决:用另一个在别处断开的角度(偏移半圈)求导,两者取小。
    float a2 = fract(pol.y + 0.5) * _Count;
    float aa = min(fwidth(t), fwidth(a2)) * 2.0;
    float ray = smoothstep(0.5 - aa, 0.5 + aa, tri);
    vec3 col = mix(_ColorA.rgb, _ColorB.rgb, ray);
    // 向外渐暗 + 中心太阳
    col *= mix(1.15, 0.55, saturate(pol.x));
    return mix(col, vec3(1.0, 0.85, 0.45), FillAA(SdCircle(p, 0.22)));
}

放射光:frac(a01 * N) 在角度方向重复 N 次。因为 N 是整数,图案本身在接缝处是连续的;但 fwidth(t) 在接缝处会因 a01 从 1 跳到 0 而暴涨,抗锯齿宽度变得极大,画出一条模糊的“裂缝”。修法是再造一个在另一处断开的角度(加半圈再取 frac),两个 fwidth 取较小者——任何一个像素最多只靠近其中一条接缝。这个技巧对所有“对周期坐标求导”的情形都适用。

三个图案在最后都乘了一个旋转:Rotate2D(p, radians(_Angle + _Speed * _Time.y))。运行时它们会自己转起来,静帧时由 _Angle 控制朝向。

图 4-8左:6 瓣花(旋转 90° 让一片花瓣朝上);中:4 臂对数螺旋;右:14 道放射光,+X 方向没有接缝裂缝