一、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,1]2(u_i,v_i)\in[0,1]^2(ui,vi)∈[0,1]2。经过光栅化后,三角形内部每个片元的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 变换:
structappdata{float4 vertex:POSITION;float2 uv:TEXCOORD0;// 模型空间的 UV 坐标};structv2f{float4 pos:SV_POSITION;float2 uv:TEXCOORD0;// 传递给片元着色器};v2fvert(appdata v){v2f o;o.pos=UnityObjectToClipPos(v.vertex);o.uv=TRANSFORM_TEX(v.uv,_MainTex);// Tiling/Offset 变换returno;}2.2 光栅化阶段的透视校正插值
这是 UV 映射中最关键的数学环节。
简单的线性插值在透视投影下会产生纹理游移(texture swimming) 伪影,因为透视投影导致远处物体的间距被压缩。GPU 采用透视校正插值(Perspective-Correct Interpolation) 解决此问题。
纹理游移(Texture Swimming)是光栅化渲染中一种典型的视觉瑕疵,表现为纹理在物体表面随视角 / 位置变化时出现非自然的抖动、滑动或扭曲,尤其在透视投影下的大平面、远距离表面上最为明显。这一现象与 UV 插值方式、投影变换和采样机制直接相关,是早期 3D 硬件(如 PS1)的标志性图形缺陷之一。
下面这个shader可以复现纹理游移的效果
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// 指定顶点着色器入口函数。#pragmavertex vert// 指定片元着色器入口函数。#pragmafragment frag// 声明 Shader 关键字变体。// Unity 会根据材质当前启用的 Keyword 编译对应版本。// shader_feature 的特点是:最终打包时通常只保留实际被材质使用到的变体。#pragmashader_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;// 顶点输入结构:描述从模型网格传入顶点着色器的数据。structappdata{// 模型空间顶点坐标。float4 vertex:POSITION;// 模型第一套 UV 坐标。float2 uv:TEXCOORD0;};// 顶点到片元的传递结构:描述顶点着色器输出给片元着色器的数据。structv2f{// 裁剪空间坐标,GPU 用它把三角形光栅化到屏幕上。float4 pos:SV_POSITION;// 下面的 #if 是编译期条件,不会在每个片元运行时判断。// 它根据材质选择的插值模式,决定 uv 这个 varying 使用哪种插值限定符。#ifdefined(_INTERPMODE_NOPERSP)// noperspective:非透视校正插值。// 它按屏幕空间线性插值,适合观察“没有透视修正时 UV 会怎样变化”。noperspective float2 uv:TEXCOORD0;#elifdefined(_INTERPMODE_NOINTERP)// nointerpolation:不进行片元级插值。// 片元会直接使用某个顶点的 UV,通常会看到明显的三角面块状效果。nointerpolation float2 uv:TEXCOORD0;#else// 默认插值:透视正确插值。// 普通贴图采样一般都使用这种方式,能在有透视变化的表面上保持纹理正确。float2 uv:TEXCOORD0;#endif};// 顶点着色器:计算裁剪空间位置,并把经过 Tiling/Offset 处理后的 UV 传给片元阶段。v2fvert(appdata v){v2f o;// 把模型空间顶点坐标转换为裁剪空间坐标。o.pos=UnityObjectToClipPos(v.vertex);// 对输入 UV 应用材质面板中 Main Texture 的 Tiling 和 Offset。o.uv=TRANSFORM_TEX(v.uv,_MainTex);returno;}// 片元着色器:用插值后的 UV 采样主纹理,并输出最终颜色。fixed4frag(v2f i):SV_Target{// i.uv 的具体插值方式由上方 v2f 中选中的插值限定符决定。returntex2D(_MainTex,i.uv);}ENDCG}}// 当前 Shader 不支持时,回退到 Unity 内置 Diffuse Shader。FallBack"Diffuse"}实际效果:
工程意义:在标准顶点/片元着色器管线中,GPU 硬件自动执行透视校正插值,开发者无需手动实现。但若在片元着色器中手动进行屏幕空间导数计算(如 ddx/ddy),需理解此机制以避免数值异常。
2.3 片元阶段 (Fragment Shader)
片元着色器接收插值后的 UV,使用纹理采样函数从纹理贴图中提取颜色:
fixed4frag(v2f i):SV_Target{fixed4 col=tex2D(_MainTex,i.uv);// CG 版本returncol;}三、纹理采样函数体系
3.1 CG / Built-in 管线
// 声明sampler2D _MainTex;// 纹理对象(合并了纹理 + 采样器状态)float4 _MainTex_ST;// 自动生成:xy = Tiling, zw = Offset// 基本采样(自动计算 LOD)half4tex2D(sampler2D s,float2 uv);// 带梯度采样(手动指定屏幕空间导数)half4tex2Dgrad(sampler2D s,float2 uv,float2 ddx,float2 ddy);// 手动 LOD 采样half4tex2Dlod(sampler2D s,float4 uv);// uv.w = mip level3.2 HLSL / URP 管线
URP 将纹理对象和采样器状态分离声明,遵循 DirectX 11+ 的规范:
// 声明TEXTURE2D(_MainTex);// 纹理资源对象 (t register)SAMPLER(sampler_MainTex);// 采样器状态对象 (s register)float4 _MainTex_ST;// Tiling/OffsetCBUFFER_START(UnityPerMaterial)float4 _MainTex_ST;float4 _MainTex_TexelSize;// 1/width, 1/height, width, heightCBUFFER_END// 基本采样half4SAMPLE_TEXTURE2D(TEXTURE2D_PARAM(_MainTex,sampler_MainTex),sampler_MainTex,uv);// 手动 LODhalf4SAMPLE_TEXTURE2D_LOD(_MainTex,sampler_MainTex,uv,lod);// LOD 偏移half4SAMPLE_TEXTURE2D_BIAS(_MainTex,sampler_MainTex,uv,bias);// 手动导数(用于动态分支或屏幕空间操作后)half4SAMPLE_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 变换
// 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 动画
// 水平滚动 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;floats,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 中覆盖寻址模式
// Unity 内置宏(仅 CG)half4 col=tex2D(_MainTex,i.uv);// 使用纹理导入设置的 Wrap Modehalf4 col=tex2Dclamp(_MainTex,float4(i.uv,0,0));// 强制 ClampURP 中寻址模式完全由纹理导入设置和 SAMPLER(sampler_xxx) 的定义决定,无法在运行时动态切换。