
一、UV 坐标系的数学定义1.1 形式化定义UV 坐标是定义在R2\mathbb{R}^2R2Rn\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λ31,λi≥0\lambda_1\lambda_2\lambda_31,\lambda_i\ge0λ1λ2λ31,λi≥0为片元相对于三角形顶点的重心坐标权重。1.2 坐标系规定属性U轴V轴对应方向水平纹理宽度方向垂直纹理高度方向取值范围[0,1]标准归一化[0,1]标准归一化原点位置纹理左下角 (0,0)纹理左下角 (0,0)增长方向向右向上对应顶点属性TEXCOORD0.xTEXCOORD0.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.posUnityObjectToClipPos(v.vertex);o.uvTRANSFORM_TEX(v.uv,_MainTex);// Tiling/Offset 变换returno;}2.2 光栅化阶段的透视校正插值这是 UV 映射中最关键的数学环节。简单的线性插值在透视投影下会产生纹理游移(texture swimming) 伪影因为透视投影导致远处物体的间距被压缩。GPU 采用透视校正插值(Perspective-Correct Interpolation) 解决此问题。纹理游移Texture Swimming是光栅化渲染中一种典型的视觉瑕疵表现为纹理在物体表面随视角 / 位置变化时出现非自然的抖动、滑动或扭曲尤其在透视投影下的大平面、远距离表面上最为明显。这一现象与 UV 插值方式、投影变换和采样机制直接相关是早期 3D 硬件如 PS1的标志性图形缺陷之一。下面这个shader可以复现纹理游移的效果ShaderCustom/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{// RenderTypeOpaque 表示按不透明物体渲染。// QueueGeometry 表示进入默认几何体渲染队列。Tags{RenderTypeOpaqueQueueGeometry}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。#includeUnityCG.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.posUnityObjectToClipPos(v.vertex);// 对输入 UV 应用材质面板中 Main Texture 的 Tiling 和 Offset。o.uvTRANSFORM_TEX(v.uv,_MainTex);returno;}// 片元着色器用插值后的 UV 采样主纹理并输出最终颜色。fixed4frag(v2f i):SV_Target{// i.uv 的具体插值方式由上方 v2f 中选中的插值限定符决定。returntex2D(_MainTex,i.uv);}ENDCG}}// 当前 Shader 不支持时回退到 Unity 内置 Diffuse Shader。FallBackDiffuse}实际效果工程意义在标准顶点/片元着色器管线中GPU 硬件自动执行透视校正插值开发者无需手动实现。但若在片元着色器中手动进行屏幕空间导数计算如 ddx/ddy需理解此机制以避免数值异常。2.3 片元阶段 (Fragment Shader)片元着色器接收插值后的 UV使用纹理采样函数从纹理贴图中提取颜色fixed4frag(v2f i):SV_Target{fixed4 coltex2D(_MainTex,i.uv);// CG 版本returncol;}三、纹理采样函数体系3.1 CG / Built-in 管线// 声明sampler2D _MainTex;// 纹理对象合并了纹理 采样器状态float4 _MainTex_ST;// 自动生成xy Tiling, zw Offset// 基本采样自动计算 LODhalf4tex2D(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 贴图用 LinearRepeat但同一张纹理做 MRT 输出时可能需要 PointClampBindless 架构现代 GPU 允许动态索引纹理数组分离后更灵活SRP Batcher 兼容URP 要求属性在 CBUFFER 中采样器独立管理四、Tiling 与 Offset 变换// TRANSFORM_TEX 的宏展开o.uvv.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.uvTRANSFORM_TEX(v.uv,_MainTex);o.uv.x_Time.y*_ScrollSpeed;// 旋转 UV漩涡效果float2 centerfloat2(0.5,0.5);float2 uvi.uv-center;floats,c;sincos(_Time.y*_RotSpeed,s,c);uvfloat2(uv.x*c-uv.y*s,uv.x*suv.y*c);uvcenter;half4 colSAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,uv);五、纹理寻址模式 (Wrap Mode)当 UV 超出 [0,1] 标准范围时GPU 根据纹理导入设置中的 Wrap Mode 进行处理。此行为发生在纹理采样硬件单元中对开发者透明。模式数学操作效果描述典型应用Repeatu′fract(u)u−∣u∣u^′fract(u)u-\lvert u \rvertu′fract(u)u−∣u∣纹理在 UV 空间中无限平铺重复地面纹理、墙壁砖块、水面Clampu′clamp(u,0,1)u^′clamp(u,0,1)u′clamp(u,0,1)UV 超出部分被截断到边缘值产生边缘拉伸Lightmap、天空盒面、DecalMirroru′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 内置宏仅 CGhalf4 coltex2D(_MainTex,i.uv);// 使用纹理导入设置的 Wrap Modehalf4 coltex2Dclamp(_MainTex,float4(i.uv,0,0));// 强制 ClampURP 中寻址模式完全由纹理导入设置和 SAMPLER(sampler_xxx) 的定义决定无法在运行时动态切换。