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权重混合
因为 ,我们把这个关系直接代入分子:
把分子拆开:
把含有 的项合并到一起:
最后,把分母拆开分别相除:
左边的 约掉,就得到了:
p2 p3权重混合
目标是求 和 混合后的绝对像素位置:
因为 ,我们把这个关系直接代入分子:
把分子拆开并合并含有 的项:
把分母拆开分别相除,约掉左边的 :
对应代码:

UV计算和权重解释补充
Catmull-Rom 本质上要采的是一个 的正方形网格(共 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
}
}
}