【Shader基础】UV 与纹理采样 Part1

一、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) 的定义决定,无法在运行时动态切换。

相关推荐
zdr尽职尽责2 小时前
Unity录像功能
学习·ui·unity·游戏引擎
真鬼1232 小时前
【Unity Cursor】AI规矩
unity·游戏引擎
mxwin2 小时前
Unity Shader 深入理解 LinearEyeDepth 与 DepthTexture
unity·游戏引擎
小贺儿开发2 小时前
Unity3D VR 全景图游览
unity·渲染·vr·虚拟现实·全景图·漫游·互动
avi911113 小时前
Unity 商业插件之(四)粒子系统,古法射击子弹轨迹 ,附加:HDRP Built-in Particle Shaders 最新的高级管线粒子Shader
unity·游戏引擎·粒子系统·particle·拖尾效果
魔士于安21 小时前
Shader forge技术美术专用
游戏·unity·游戏引擎·贴图·技术美术·模型
Y学院1 天前
C#游戏脚本开发全流程(Unity通用完整版)
游戏·unity·c#
codeaideaai1 天前
使用UV创建python项目
python·fastapi·uv
milo.qu1 天前
Python工程工具链:uv + 虚拟环境
uv