GameTech/URP 渲染实战

案例 29.4远离原点的精度事故入门

目标:复现“用 half 保存世界坐标再算 UV”在远离原点时的问题。由于桌面平台可能把 half 当 float,这里按 half 的格式手动舍入来模拟:

Assets/Cookbook/Ch29/HalfPrecisionDemo.shader(节选:frag)
// half:1 位符号 + 5 位指数 + 10 位尾数。数值落在 [2^e, 2^(e+1)) 时,相邻两个可表示值相差 2^(e-10)
float2 ToHalfAndBack(float2 v)
{
    float2 e = floor(log2(max(abs(v), 6.1e-5)));   // 6.1e-5 ≈ half 最小正规数
    float2 ulp = exp2(e - 10);
    return round(v / ulp) * ulp;
}

half4 frag(Varyings i) : SV_Target
{
    float2 p = i.positionWS.xz;
    if (_EmulateHalf > 0.5) p = ToHalfAndBack(p);      // 相当于 half2 p = positionWS.xz;
    float2 uv = p * _Tiling;
    half3 c = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv).rgb;
    return half4(c, 1);
}
// half:1 位符号 + 5 位指数 + 10 位尾数。数值落在 [2^e, 2^(e+1)) 时,相邻两个可表示值相差 2^(e-10)
vec2 ToHalfAndBack(vec2 v)
{
    vec2 e = floor(log2(max(abs(v), vec2(6.1e-5))));   // 6.1e-5 ≈ half 最小正规数
    vec2 ulp = exp2(e - 10.0);
    return round(v / ulp) * ulp;
}

void main()
{
    vec2 p = positionWS.xz;
    if (_EmulateHalf > 0.5) p = ToHalfAndBack(p);      // 相当于 half2 p = positionWS.xz;
    vec2 uv = p * _Tiling;
    vec3 c = texture(_BaseMap, uv).rgb;
    fragColor = vec4(c, 1.0);
}
图 29-2场景位于世界坐标 (300, 0, 300) 附近。左:float 计算世界空间 UV,网格贴图正常;右:模拟 half 存储,坐标只能取 0.25 米的整数倍,贴图被采样成一块块的色块,恰好落在网格线上的位置则整条变成白线

精度选择的经验规则:

✓ 可以用 half
颜色、HDR 亮度(不超过几万)、单位向量(法线、光照方向、视线方向)、0~1 的系数(金属度、粗糙度、AO)、小范围内的 UV 偏移。
✗ 必须用 float
位置(世界、观察、裁剪空间)、深度及其重建、大范围的 UV(世界空间 UV、大地形 UV)、时间(_Time.y 随运行时长增大)、需要做差的大数、矩阵运算的中间结果。

URP/core 库里还有一个 real 类型:在支持原生 half 的平台上(定义了 PREFER_HALF 一类的条件)是 half,否则是 float,用于“精度要求不高、但希望在移动端省一点”的库函数。自己写 Shader 时,明确写 half 或 float 更清楚。

“时间越久越抖”:_Time.y 是从启动开始的秒数,运行几个小时后达到数千,任何用 half 存储它或 sin(_Time.y * 很大的系数) 的动画都会开始卡顿、跳帧。长时间运行的应用(展厅、挂机游戏)要用 frac 或在 C# 里自己维护一个循环的时间参数。