一、UV 坐标系的数学定义
1.1 形式化定义
UV 坐标是定义在 R2\mathbb{R}^2R2 (Rn\mathbb{R}^nRn表示 n 维实数空间)中的一个二维参数化映射 ϕ:0,1→R2\phi:0,1\to\mathbb{R}^2ϕ:0,1→R2,用于建立三维网格表面顶点与二维纹理空间之间的双射关系 。
对于网格上的任意三角面片,其三个顶点各携带一组UV坐标 (ui,vi)∈0,12(u_i,v_i)\in0,1^2(ui,vi)∈0,12。经过光栅化后,三角形内部每个片元的UV由重心坐标插值获得:
uv\]fragment=λ1\[u1v1\]+λ2\[u2v2\]+λ2\[u3v3\]\\begin{bmatrix}u \\\\ v\\end{bmatrix}_{fragment}=\\lambda_1\\begin{bmatrix}u_1 \\\\ v_1\\end{bmatrix}+\\lambda_2\\begin{bmatrix}u_2 \\\\ v_2\\end{bmatrix}+\\lambda_2\\begin{bmatrix}u_3 \\\\ v_3\\end{bmatrix}\[uv\]fragment=λ1\[u1v1\]+λ2\[u2v2\]+λ2\[u3v3
其中λ1+λ2+λ3=1,λi≥0\lambda_1+\lambda_2+\lambda_3=1,\lambda_i\ge0λ1+λ2+λ3=1,λi≥0为片元相对于三角形顶点的重心坐标权重。
1.2 坐标系规定
| 属性 | U轴 | V轴 |
|---|---|---|
| 对应方向 | 水平(纹理宽度方向) | 垂直(纹理高度方向) |
| 取值范围 | 0,1(标准归一化) | 0,1(标准归一化) |
| 原点位置 | 纹理左下角 (0,0) | 纹理左下角 (0,0) |
| 增长方向 | 向右 | 向上 |
| 对应顶点属性 | TEXCOORD0.x | TEXCOORD0.y |
重要约定 :UV 空间的 V 轴向上递增,这与屏幕空间像素坐标(Y 轴向下)方向相反。DirectX 平台的原点在左上角,OpenGL 在左下角。Unity 内部做了平台适配,但在手动处理屏幕纹理时需注意此差异。
1.3 UV超出0,1范围时的行为
UV 值允许超出 0,1 的标准范围。超出部分的行为由纹理的寻址模式 (Wrap Mode) 决定。
二、纹理映射的管线级流程
从 Mesh 顶点到最终片元颜色,UV 数据经历以下处理链:
2.1 顶点阶段 (Vertex Shader)
Mesh 的每个顶点携带 UV 坐标(存储在 TEXCOORD0 语义中),顶点着色器将其透传给光栅化阶段,通常需要应用 Tiling/Offset 变换:
cpp
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0; // 模型空间的 UV 坐标
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0; // 传递给片元着色器
};
v2f vert (appdata v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex); // Tiling/Offset 变换
return o;
}
2.2 光栅化阶段的透视校正插值
这是 UV 映射中最关键的数学环节。
简单的线性插值在透视投影下会产生纹理游移 (texture swimming) 伪影,因为透视投影导致远处物体的间距被压缩。GPU 采用透视校正插值 (Perspective-Correct Interpolation) 解决此问题。
纹理游移 (Texture Swimming)是光栅化渲染中一种典型的视觉瑕疵,表现为纹理在物体表面随视角 / 位置变化时出现非自然的抖动、滑动或扭曲,尤其在透视投影下的大平面、远距离表面上最为明显。这一现象与 UV 插值方式、投影变换和采样机制直接相关,是早期 3D 硬件(如 PS1)的标志性图形缺陷之一。
下面这个shader可以复现 纹理游移的效果
cpp
Shader "Custom/UV_Interp_AllCompare_BuiltIn"
{
// Properties 会显示在 Unity 材质面板中,用来暴露可调节参数。
Properties
{
// 主纹理:片元着色器最终会用 UV 去采样这张纹理。
_MainTex ("Main Texture", 2D) = "white" {}
// KeywordEnum 会在材质 Inspector 中生成一个下拉菜单。
// 这里的三个选项会自动对应三个 Shader Keyword:
// Default -> _INTERPMODE_DEFAULT
// NoPersp -> _INTERPMODE_NOPERSP
// NoInterp -> _INTERPMODE_NOINTERP
[KeywordEnum(Default, NoPersp, NoInterp)] _InterpMode ("插值模式", Float) = 0
}
SubShader
{
// RenderType=Opaque 表示按不透明物体渲染。
// Queue=Geometry 表示进入默认几何体渲染队列。
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
Pass
{
CGPROGRAM
// 指定顶点着色器入口函数。
#pragma vertex vert
// 指定片元着色器入口函数。
#pragma fragment frag
// 声明 Shader 关键字变体。
// Unity 会根据材质当前启用的 Keyword 编译对应版本。
// shader_feature 的特点是:最终打包时通常只保留实际被材质使用到的变体。
#pragma shader_feature _INTERPMODE_DEFAULT _INTERPMODE_NOPERSP _INTERPMODE_NOINTERP
// 引入 Unity 常用 CG/HLSL 工具函数,例如 UnityObjectToClipPos 和 TRANSFORM_TEX。
#include "UnityCG.cginc"
// 主纹理采样器。
sampler2D _MainTex;
// Unity 自动生成的纹理 Tiling/Offset 参数,TRANSFORM_TEX 会使用它。
float4 _MainTex_ST;
// 顶点输入结构:描述从模型网格传入顶点着色器的数据。
struct appdata
{
// 模型空间顶点坐标。
float4 vertex : POSITION;
// 模型第一套 UV 坐标。
float2 uv : TEXCOORD0;
};
// 顶点到片元的传递结构:描述顶点着色器输出给片元着色器的数据。
struct v2f
{
// 裁剪空间坐标,GPU 用它把三角形光栅化到屏幕上。
float4 pos : SV_POSITION;
// 下面的 #if 是编译期条件,不会在每个片元运行时判断。
// 它根据材质选择的插值模式,决定 uv 这个 varying 使用哪种插值限定符。
#if defined(_INTERPMODE_NOPERSP)
// noperspective:非透视校正插值。
// 它按屏幕空间线性插值,适合观察"没有透视修正时 UV 会怎样变化"。
noperspective float2 uv : TEXCOORD0;
#elif defined(_INTERPMODE_NOINTERP)
// nointerpolation:不进行片元级插值。
// 片元会直接使用某个顶点的 UV,通常会看到明显的三角面块状效果。
nointerpolation float2 uv : TEXCOORD0;
#else
// 默认插值:透视正确插值。
// 普通贴图采样一般都使用这种方式,能在有透视变化的表面上保持纹理正确。
float2 uv : TEXCOORD0;
#endif
};
// 顶点着色器:计算裁剪空间位置,并把经过 Tiling/Offset 处理后的 UV 传给片元阶段。
v2f vert (appdata v)
{
v2f o;
// 把模型空间顶点坐标转换为裁剪空间坐标。
o.pos = UnityObjectToClipPos(v.vertex);
// 对输入 UV 应用材质面板中 Main Texture 的 Tiling 和 Offset。
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
// 片元着色器:用插值后的 UV 采样主纹理,并输出最终颜色。
fixed4 frag (v2f i) : SV_Target
{
// i.uv 的具体插值方式由上方 v2f 中选中的插值限定符决定。
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
// 当前 Shader 不支持时,回退到 Unity 内置 Diffuse Shader。
FallBack "Diffuse"
}
实际效果:

工程意义:在标准顶点/片元着色器管线中,GPU 硬件自动执行透视校正插值,开发者无需手动实现。但若在片元着色器中手动进行屏幕空间导数计算(如 ddx/ddy),需理解此机制以避免数值异常。
2.3 片元阶段 (Fragment Shader)
片元着色器接收插值后的 UV,使用纹理采样函数从纹理贴图中提取颜色:
cpp
fixed4 frag (v2f i) : SV_Target {
fixed4 col = tex2D(_MainTex, i.uv); // CG 版本
return col;
}
三、纹理采样函数体系
3.1 CG / Built-in 管线
cpp
// 声明
sampler2D _MainTex; // 纹理对象(合并了纹理 + 采样器状态)
float4 _MainTex_ST; // 自动生成:xy = Tiling, zw = Offset
// 基本采样(自动计算 LOD)
half4 tex2D(sampler2D s, float2 uv);
// 带梯度采样(手动指定屏幕空间导数)
half4 tex2Dgrad(sampler2D s, float2 uv, float2 ddx, float2 ddy);
// 手动 LOD 采样
half4 tex2Dlod(sampler2D s, float4 uv); // uv.w = mip level
3.2 HLSL / URP 管线
URP 将纹理对象和采样器状态分离声明,遵循 DirectX 11+ 的规范:
cpp
// 声明
TEXTURE2D(_MainTex); // 纹理资源对象 (t register)
SAMPLER(sampler_MainTex); // 采样器状态对象 (s register)
float4 _MainTex_ST; // Tiling/Offset
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
float4 _MainTex_TexelSize; // 1/width, 1/height, width, height
CBUFFER_END
// 基本采样
half4 SAMPLE_TEXTURE2D(TEXTURE2D_PARAM(_MainTex, sampler_MainTex), sampler_MainTex, uv);
// 手动 LOD
half4 SAMPLE_TEXTURE2D_LOD(_MainTex, sampler_MainTex, uv, lod);
// LOD 偏移
half4 SAMPLE_TEXTURE2D_BIAS(_MainTex, sampler_MainTex, uv, bias);
// 手动导数(用于动态分支或屏幕空间操作后)
half4 SAMPLE_TEXTURE2D_GRAD(_MainTex, sampler_MainTex, uv, ddx, ddy);
3.3 纹理声明分离的学术动机
DirectX 10 之前的架构将纹理资源 (Texture) 和采样器状态 (Sampler) 绑定为单一对象。DirectX 10+ 引入分离,原因包括:
- 资源复用:同一张纹理可以用不同的过滤/寻址模式采样(如 Albedo 贴图用 Linear+Repeat,但同一张纹理做 MRT 输出时可能需要 Point+Clamp)
- Bindless 架构:现代 GPU 允许动态索引纹理数组,分离后更灵活
- SRP Batcher 兼容:URP 要求属性在 CBUFFER 中,采样器独立管理
四、Tiling 与 Offset 变换
cpp
// TRANSFORM_TEX 的宏展开
o.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;
// _MainTex_ST 由 Unity 自动注入:
// _MainTex_ST.x = Tiling X (默认 1)
// _MainTex_ST.y = Tiling Y (默认 1)
// _MainTex_ST.z = Offset X (默认 0)
// _MainTex_ST.w = Offset Y (默认 0)
UV 动画
cpp
// 水平滚动 UV(河流、传送带)
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uv.x += _Time.y * _ScrollSpeed;
// 旋转 UV(漩涡效果)
float2 center = float2(0.5, 0.5);
float2 uv = i.uv - center;
float s, c;
sincos(_Time.y * _RotSpeed, s, c);
uv = float2(uv.x * c - uv.y * s, uv.x * s + uv.y * c);
uv += center;
half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
五、纹理寻址模式 (Wrap Mode)
当 UV 超出 0,1 标准范围时,GPU 根据纹理导入设置中的 Wrap Mode 进行处理。此行为发生在纹理采样硬件单元中,对开发者透明。
| 模式 | 数学操作 | 效果描述 | 典型应用 |
|---|---|---|---|
| Repeat | u′=fract(u)=u−∣u∣u^′=fract(u)=u-\lvert u \rvertu′=fract(u)=u−∣u∣ | 纹理在 UV 空间中无限平铺重复 | 地面纹理、墙壁砖块、水面 |
| Clamp | u′=clamp(u,0,1)u^′=clamp(u,0,1)u′=clamp(u,0,1) | UV 超出部分被截断到边缘值,产生边缘拉伸 | Lightmap、天空盒面、Decal |
| Mirror | u′=1−∣∣fract(u)−0.5∣∣×2u^′=1-\lvert\lvert fract(u)-0.5\rvert\rvert\times2u′=1−∣∣fract(u)−0.5∣∣×2 | 纹理以镜像方式对称重复,接缝处连续 | 减少平铺可见接缝的水面/地面 |
| MirrorOnce | 镜像一次后截断到边缘 | 介于 Mirror 和 Clamp 之间 | 特殊 Decal 效果 |
在 Shader 中覆盖寻址模式
cpp
// Unity 内置宏(仅 CG)
half4 col = tex2D(_MainTex, i.uv); // 使用纹理导入设置的 Wrap Mode
half4 col = tex2Dclamp(_MainTex, float4(i.uv, 0, 0)); // 强制 Clamp
URP 中寻址模式完全由纹理导入设置和 SAMPLER(sampler_xxx) 的定义决定,无法在运行时动态切换。