GameTech/URP 渲染实战

案例 29.6Mip 级别可视化入门

目标:把 GPU 实际选择的 Mip 级别画成颜色,检查贴图分辨率和平铺是否合理。

Assets/Cookbook/Ch29/MipLevelViz.shader(节选:mip)
float ComputeMipLevel(float2 uv, float2 texSize)
{
    // 相邻像素之间 uv 变化了多少个纹素
    float2 dx = ddx(uv * texSize);
    float2 dy = ddy(uv * texSize);
    float maxSq = max(dot(dx, dx), dot(dy, dy));
    return 0.5 * log2(maxSq);   // = log2(最大纹素跨度);<0 表示放大(一个纹素盖住多个像素)
}

half3 MipColor(float mip)
{
    static const half3 c[7] = { half3(0.1, 0.35, 1), half3(0.15, 0.8, 0.3), half3(0.95, 0.9, 0.15),
                                half3(1, 0.55, 0.1), half3(0.95, 0.15, 0.1), half3(0.8, 0.1, 0.7), half3(0.35, 0.1, 0.5) };
    // c[0] 给“放大”区(mip<0),c[1] 起依次是 mip 0,1,2...
    float m = clamp(mip + 1, 0, 6);
    int i = (int)floor(m);
    return lerp(c[i], c[min(i + 1, 6)], frac(m));
}
float ComputeMipLevel(vec2 uv, vec2 texSize)
{
    // 相邻像素之间 uv 变化了多少个纹素
    vec2 dx = dFdx(uv * texSize);
    vec2 dy = dFdy(uv * texSize);
    float maxSq = max(dot(dx, dx), dot(dy, dy));
    return 0.5 * log2(maxSq);   // = log2(最大纹素跨度);<0 表示放大(一个纹素盖住多个像素)
}

vec3 MipColor(float mip)
{
    const vec3 c[7] = vec3[7](vec3(0.1, 0.35, 1), vec3(0.15, 0.8, 0.3), vec3(0.95, 0.9, 0.15),
                              vec3(1, 0.55, 0.1), vec3(0.95, 0.15, 0.1), vec3(0.8, 0.1, 0.7), vec3(0.35, 0.1, 0.5));
    // c[0] 给“放大”区(mip<0),c[1] 起依次是 mip 0,1,2...
    float m = clamp(mip + 1.0, 0.0, 6.0);
    int i = int(floor(m));
    return mix(c[i], c[min(i + 1, 6)], fract(m));
}

GPU 选 Mip 的依据就是屏幕空间导数:相邻像素之间 UV 跨过了多少个纹素。ddx(uv * texSize) 给出水平方向相邻像素的纹素差,取两个方向中较大的长度,log2 之后就是 Mip 级别(硬件的实际公式还涉及各向异性过滤,这里是各向同性的近似)。_BaseMap_TexelSize 是 Unity 自动为每张贴图属性设置的变量,(1/宽, 1/高, 宽, 高)。

图 29-5左:平铺的棋盘格地面与左侧墙面;右:Mip 级别可视化(绿 = Mip 0、黄 = 1、橙 = 2、红 = 3、品红 = 4、紫 = 5 及以上;蓝 = 放大)。地面随距离平滑升级,而左侧墙面面积不大却整面是 Mip 4 以上——它的贴图平铺次数太多,512 像素的贴图被压缩到很小的屏幕面积里,大部分分辨率被浪费

Rendering Debugger 的 Material 页也提供了 Mipmap 相关的调试信息(URP 14 源码中有 DebugMipInfoMode 的 Level/Count/Ratio 模式),具体 UI 中可用的选项以实际版本为准。