Point(最近邻):直接取最近的像素,放大后有明显锯齿块状感。 Bilinear(双线性):取周围4个像素加权平均,放大后平滑但偏模糊,像素边缘还是有隐约的格子感。 Catmull-Rom(双三次):取周围16个像素(优化后4个),用负权重保留高频细节,放大后既平滑又锐利。

线性插值

正常两点之间的线性过渡公式如下

P = A + (B - A) * t

其实就是lerp函数的公式

float3 P = lerp(A, B, t);

atmull-Rom 双三次插值

根据四个点来计算插值 Catmull-Rom 样条的几何含义:曲线通过中间两点,且两端的切线由相邻点决定。

三次多项式

对于单个值(分量)

C(t) = a·t³ + b·t² + c·t + d

解三次多项式

这里假设有4个点,中心两个端点和两侧两个相邻点:P0->P1->P2->P3 根据定义:曲线通过中间两点,且两端的切线由相邻点决定。 所以需要计算中间的切向量: P0和P2中间的切向量,P1和P3中间的切向量,除以2是使曲线更平均,可以使用其他参数

C'(0) = (P2 - P0) / 2
C'(1) = (P3 - P1) / 2

所以现在的条件有:

C(0)  = P1
C(1)  = P2
C'(0) = (P2 - P0) / 2
C'(1) = (P3 - P1) / 2

然后解方程会得到abcd各个系数 d = P1 c = (P2 - P0) / 2 b = (2·P0 - 5·P1 + 4·P2 - P3) / 2 a = (-P0 + 3·P1 - 3·P2 + P3) / 2 代入回原公式:

C(t) = a*t³ + b*t² + c*t + d
= (1/2) * [
    (-P0 + 3P1 - 3P2 + P3) * t³ +
    (2P0 - 5P1 + 4P2 - P3) * t² +
    (-P0 + P2) * t +
    (2P1)
]
//按点整理后
C(t) = P0 * 0.5*(-t³ + 2t² - t)
     + P1 * 0.5*(3t³ - 5t² + 2)
     + P2 * 0.5*(-3t³ + 4t² + t)
     + P3 * 0.5*(t³ - t²)

分离权重

按照公式分离权重

// Catmull-Rom 权重(每个轴独立计算)
// w0 w1 w2 w3 对应 -1, 0, +1, +2 位置的像素
float2 w0 = 0.5 * t * (2.0 * t - t * t - 1.0);              // 0.5*(-t³ + 2t² - t)
float2 w1 = 0.5 * (3 * t * t * t - 5.0 * t * t + 2.0);      //0.5*(3t³ - 5t² + 2)
float2 w2 = 0.5 * t * (4.0 * t + 1.0 - 3.0 * t * t);        // 0.5*(-3t³ + 4t² + t)
float2 w3 = 0.5 * t * t * (t - 1.0);                        //0.5*(t³ - t²)

秦九韶算法,减少乘法次数的写法,但是非常不直观,可以节省性能

float2 w0 = t * (-0.5 + t * (1.0 - 0.5 * t));       
float2 w1 = 1.0 + t * t * (-2.5 + 1.5 * t);
float2 w2 = t * (0.5 + t * (2.0 - 1.5 * t));
float2 w3 = t * t * (-0.5 + 0.5 * t);

采样函数

像素坐标计算

合并两次偏移权重优化

p0 p1权重混合

目标绝对像素坐标=P0⋅w0+P1⋅w1w0+w1\text{目标绝对像素坐标} = \frac{P_0 \cdot w_0 + P_1 \cdot w_1}{w_0 + w_1}

因为 P0=P1−1P_0 = P_1 - 1,我们把这个关系直接代入分子:

目标绝对像素坐标=(P1−1)⋅w0+P1⋅w1w0+w1\text{目标绝对像素坐标} = \frac{(P_1 - 1) \cdot w_0 + P_1 \cdot w_1}{w_0 + w_1}

把分子拆开:

目标绝对像素坐标=P1⋅w0−1⋅w0+P1⋅w1w0+w1\text{目标绝对像素坐标} = \frac{P_1 \cdot w_0 - 1 \cdot w_0 + P_1 \cdot w_1}{w_0 + w_1}

把含有 P1P_1 的项合并到一起:

目标绝对像素坐标=P1⋅(w0+w1)−w0w0+w1\text{目标绝对像素坐标} = \frac{P_1 \cdot (w_0 + w_1) - w_0}{w_0 + w_1}

最后,把分母拆开分别相除:

目标绝对像素坐标=P1⋅(w0+w1)w0+w1−w0w0+w1\text{目标绝对像素坐标} = \frac{P_1 \cdot (w_0 + w_1)}{w_0 + w_1} - \frac{w_0}{w_0 + w_1}

左边的 (w0+w1)(w_0 + w_1) 约掉,就得到了:

目标绝对像素坐标=P1−w0w0+w1\text{目标绝对像素坐标} = P_1 - \frac{w_0}{w_0 + w_1}

p2 p3权重混合

目标是求 P2P_2 和 P3P_3 混合后的绝对像素位置:

目标绝对像素坐标=P2⋅w2+P3⋅w3w2+w3\text{目标绝对像素坐标} = \frac{P_2 \cdot w_2 + P_3 \cdot w_3}{w_2 + w_3}

因为 P3=P2+1P_3 = P_2 + 1,我们把这个关系直接代入分子:

目标绝对像素坐标=P2⋅w2+(P2+1)⋅w3w2+w3\text{目标绝对像素坐标} = \frac{P_2 \cdot w_2 + (P_2 + 1) \cdot w_3}{w_2 + w_3}

把分子拆开并合并含有 P2P_2 的项:

目标绝对像素坐标=P2⋅(w2+w3)+w3w2+w3\text{目标绝对像素坐标} = \frac{P_2 \cdot (w_2 + w_3) + w_3}{w_2 + w_3}

把分母拆开分别相除,约掉左边的 (w2+w3)(w_2 + w_3):

目标绝对像素坐标=P2+w3w2+w3\text{目标绝对像素坐标} = P_2 + \frac{w_3}{w_2 + w_3}

对应代码:

UV计算和权重解释补充

Catmull-Rom 本质上要采的是一个 4×44 \times 4 的正方形网格(共 16 个像素): 也就是找到对角线的4个点坐标然后对各分量组合就可以进行4x4的采样最后混合,但是这样是16tap的性能非常不好,所以有很多压缩办法,比如说4tap,9tap,这里是实现的4tap

16Tap对应采样颜色就是

因为混合了权重,相当于一次采样2x2了,p0p1形成一个区域,p2p3是一个区域,混合就有对应的混合轴,也是分量混合的权重,进行组合就可以得到4个焦点,也就是采样这4个交点就可以得到整个4x4的混合效果了

对应划分如下

参考代码:

效果参考

初始是双线性,右侧是 Catmull-Rom

完整代码

测试Shader代码

Shader "Test/CatmullRomTest"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Scale ("Scale", Float) = 1.0  // 控制采样密度,放大纹理让插值差异更明显
        _move ("move", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            float4 _MainTex_TexelSize;
            float4 _MainTex_ST;
            float _Scale;
            float _move;

            // 把你的 SampleTextureBicubic4Tap 函数粘贴到这里
            float4 SampleTextureBicubic4Tap(TEXTURE2D_PARAM(tex, texSampler), float2 uv, float4 tex_TexelSize)
            {
                float2 texSize = tex_TexelSize.zw;              // 纹理分辨率 (width, height)
                float2 invTexSize = tex_TexelSize.xy;
                float2 samplePos = uv * texSize;                // uv → 像素坐标
                float2 P1 = floor(samplePos - 0.5) + 0.5;       // 像素中心
                float2 t = samplePos - P1;                      // 插值权重

                // Catmull-Rom 权重(每个轴独立计算)
                // w0 w1 w2 w3 对应 -1, 0, +1, +2 位置的像素每个像素权重
                // float2 w0 = 0.5 * t * (2.0 * t - t * t - 1.0);              // 0.5*(-t³ + 2t² - t)
                // float2 w1 = 0.5 * (3 * t * t * t - 5.0 * t * t + 2.0);      // 0.5*(3t³ - 5t² + 2)
                // float2 w2 = 0.5 * t * (4.0 * t + 1.0 - 3.0 * t * t);        // 0.5*(-3t³ + 4t² + t)
                // float2 w3 = 0.5 * t * t * (t - 1.0);                        // 0.5*(t³ - t²)
                // 秦九韶算法优化
                float2 w0 = t * (-0.5 + t * (1.0 - 0.5 * t));       
                float2 w1 = 1.0 + t * t * (-2.5 + 1.5 * t);
                float2 w2 = t * (0.5 + t * (2.0 - 1.5 * t));
                float2 w3 = t * t * (-0.5 + 0.5 * t);

                // 两次合并成一次采样(利用双线性插值)
                float2 w01 = w0 + w1;  // 左组权重
                float2 w23 = w2 + w3;  // 右组权重

                // 左组:P0 坐标: P1 - 1.0; P1 坐标: P1
                // P1->P0偏移量
                float2 offset01 = P1 - (w0 / w01);
                // float2 P0 = P1 - 1.0;
                // float2 offset01 = P0 + (w1 / w01);
                
                // 右组:P2 坐标: P1 + 1.0; P3 坐标: P1 + 2.0
                // P2->P3偏移量
                float2 P2 = P1 + 1.0;
                float2 offset23 = P2 + (w3 / w23);
                

                // 转换到标准的 UV 空间(因为基于像素中心算,需-0.5)uv对应的像素索引而不是像素中心
                float uv01_x = (offset01.x - 0.5) * invTexSize.x;
                float uv23_x = (offset23.x - 0.5) * invTexSize.x;
                float uv01_y = (offset01.y - 0.5) * invTexSize.y;
                float uv23_y = (offset23.y - 0.5) * invTexSize.y;

                // 交叉重组 4 个十字路口焦点
                float2 uvTL = float2(uv01_x, uv01_y); // 左上
                float2 uvTR = float2(uv23_x, uv01_y); // 右上
                float2 uvBL = float2(uv01_x, uv23_y); // 左下
                float2 uvBR = float2(uv23_x, uv23_y); // 右下      

                //采样
                float4 color = 0;
                color += SAMPLE_TEXTURE2D_LOD(tex, texSampler, uvTL, 0) * (w01.x * w01.y);
                color += SAMPLE_TEXTURE2D_LOD(tex, texSampler, uvTR, 0) * (w23.x * w01.y);
                color += SAMPLE_TEXTURE2D_LOD(tex, texSampler, uvBL, 0) * (w01.x * w23.y);
                color += SAMPLE_TEXTURE2D_LOD(tex, texSampler, uvBR, 0) * (w23.x * w23.y);


                return color;
            }
            
            struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; };
            struct Varyings   { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; };

            Varyings vert(Attributes v)
            {
                Varyings o;
                o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
                o.uv = v.uv * _Scale +  _MainTex_ST.zw;  // 放大 uv,让纹理密度降低,插值差异更明显
                return o;
            }

            float4 frag(Varyings input) : SV_Target
            {
                float2 uv = input.uv;

                // 左半边双线性,右半边 Catmull-Rom
                if (uv.x % 1.0 < _move)
                    return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
                else
                    return SampleTextureBicubic4Tap(
                        TEXTURE2D_ARGS(_MainTex, sampler_MainTex), 
                        uv, 
                        _MainTex_TexelSize
                    );
            }
            ENDHLSL
        }
    }
}