从 UV 空间的数学本质出发,理解 URP 中纹理坐标的缩放(Tiling)与偏移(Offset)控制原理, 并掌握 Shader Graph、HLSL、C# 三种维度的实践技巧。

UV 坐标系基础

在实时渲染中,UV 坐标是将二维纹理贴图映射到三维网格表面的桥梁。 每个顶点都携带一组 (u, v) 值,顶点着色器将其传递给片段着色器,用于在纹理中查找颜色。 U 表示水平方向,V 表示垂直方向,两者共同构成一个 [0, 1] × [0, 1] 的归一化坐标空间。

Unity 的 UV 坐标系以左下角为原点 (0, 0),右上角为 (1, 1)。当 UV 值超出 [0, 1] 范围时, 纹理的采样行为取决于 Texture Wrap Mode 设置:

Wrap Mode 行为描述 典型用途
Repeat 超出部分重复平铺,等价于 frac(uv) 地板、墙壁、草地、岩石
Clamp 边界像素被拉伸,不重复 UI 元素、精灵图
Mirror 镜像翻转重复,接缝处无缝 对称纹理、无缝拼接
Mirror Once 仅镜像一次,之后 Clamp 特殊边界过渡效果

💡

Unity 支持最多 8 套 UV 通道(TEXCOORD0~TEXCOORD7)。 Lightmap 通常占用 TEXCOORD1,自定义效果层可使用 TEXCOORD2 及更高。

纹理平铺(Tiling)原理

Tiling(平铺/缩放)通过对 UV 坐标进行乘法缩放来实现纹理的重复。 其本质是将原本覆盖 [0,1]×[0,1] 的单一纹理"挤压",使更多重复单元出现在同一表面上。

数学定义

⚠️

Tiling 值为 0 时,所有片段都采样同一点(UV = 0),纹理退化为纯色块,通常是意外情况。 Tiling 值为负数时,纹理会被镜像翻转,这有时是有意为之的效果。

偏移(Offset)原理

Offset(偏移)通过对 UV 坐标进行加法平移来滑动纹理的起始位置。 Unity 规定 Offset 在 Tiling 变换之后叠加,完整公式如下:

Offset 最常见的运行时用途是 UV 动画——每帧将偏移值随时间累加,实现水流、火焰、云朵等流动效果,无需修改网格。

配合 frac() 函数,Offset 可永远保持在 [0,1] 范围内循环, 避免长时间运行后浮点精度问题导致的 UV 抖动(UV Jitter)。

URP 管线中的 UV 流动

理解 UV 数据如何在 URP 渲染管线中流动,是正确控制 Tiling/Offset 的前提。

_MainTex_ST 向量布局

Unity 材质中每个纹理属性 _MainTex 都会自动关联一个 float4 _MainTex_ST(ST = Scale-Translation):

宏 TRANSFORM_TEX(uv, tex) 展开后等价于:uv.xy * tex_ST.xy + tex_ST.zw, 其中 .xy 是 Tiling,.zw 是 Offset。

⚠️

在 URP 中,_MainTex_ST 必须声明在 CBUFFER_START(UnityPerMaterial) 块中, 否则在 SRP Batcher 下会导致材质合批失效,严重影响性能。

HLSL 手写 Shader 实现

以下是完整的 URP Unlit Shader,展示如何正确声明、传递并应用 Tiling/Offset 参数。代码逐行出现,帮助你逐步理解每个环节。

Shader "Custom/URP_UV_TilingOffset"

{

    Properties

    {

        // 声明纹理,Unity 自动为其关联 _MainTex_ST

        _MainTex ("Main Texture", 2D) = "white" {}

        _Color   ("Tint Color",    Color) = (1,1,1,1)

    }

    SubShader

    {

        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }

        Pass

        {

            HLSLPROGRAM

            #pragma vertex   vert

            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"


            // ── CBUFFER:SRP Batcher 合批必需 ──

            CBUFFER_START(UnityPerMaterial)

                float4 _MainTex_ST;  // .xy=Tiling .zw=Offset

                float4 _Color;

            CBUFFER_END


            // 纹理与采样器(URP 分离声明规范)

            TEXTURE2D(_MainTex);

            SAMPLER(sampler_MainTex);


            // ── 顶点输入 ──

            struct Attributes

            {

                float4 positionOS : POSITION;

                float2 uv         : TEXCOORD0;

            };


            // ── 顶点→片段插值 ──

            struct Varyings

            {

                float4 positionHCS : SV_POSITION;

                float2 uv          : TEXCOORD0;

            };


            // ── 顶点着色器 ──

            Varyings vert(Attributes IN)

            {

                Varyings OUT;

                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);

                // 核心:应用 Tiling 和 Offset

                OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);

                return OUT;

            }


            // ── 片段着色器 ──

            half4 frag(Varyings IN) : SV_TARGET

            {

                half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv);

                return col * _Color;

            }

            ENDHLSL

        }

    }

}

UV 动画:流动水面示例

在片段着色器中手动展开 TRANSFORM_TEX,可以叠加时间驱动的动态偏移:

// 在 CBUFFER 中添加流速参数

CBUFFER_START(UnityPerMaterial)

    float4 _MainTex_ST;

    float2 _FlowDir;      // 流动方向,e.g. (1,0)=向右

    float  _FlowSpeed;    // 流速

CBUFFER_END


// 顶点着色器中:只传递原始 UV,不做 TRANSFORM_TEX

Varyings vert(Attributes IN)

{

    Varyings OUT;

    OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);

    OUT.uv = IN.uv;  // 保留原始 UV,片段中再处理

    return OUT;

}


// 片段着色器中手动拆解

half4 frag(Varyings IN) : SV_TARGET

{

    // 1. 手动缩放(Tiling)

    float2 uv = IN.uv * _MainTex_ST.xy;


    // 2. 材质面板 Offset + 时间驱动动画

    float2 animOffset = _FlowDir * _FlowSpeed * _Time.y;

    uv += _MainTex_ST.zw + animOffset;


    // 3. frac() 防止长时间浮点漂移

    uv = frac(uv);


    return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);

}

Shader Graph 可视化实现

Unity Shader Graph 提供了 Tiling And Offset 节点,封装了完整的缩放与平移计算。

节点参数说明

Shader Graph 节点 输入/输出 说明
UV 输出 UV (Vector2) 读取网格的 UV 通道,默认 TEXCOORD0
Tiling And Offset → Tiling 输入 Vector2 横纵平铺倍数,默认 (1,1)
Tiling And Offset → Offset 输入 Vector2 横纵偏移量,默认 (0,0)
Tiling And Offset → Out 输出 Vector2 变换后的最终 UV,送入采样节点
Sample Texture 2D → UV 输入 Vector2 接收变换后的 UV

在 Shader Graph 中,将 Tiling 和 Offset 的输入连接到 Vector2 Property(属性节点), 即可在材质 Inspector 面板中实时调整,或通过 C# 脚本动态控制。

C# 脚本动态控制

通过 C# 脚本在运行时修改材质的 Tiling 和 Offset,是实现 UV 动画、程序化效果的常用手段。

using UnityEngine;


public class TextureTilingControl : MonoBehaviour

{

    [Header("Tiling")]

    public Vector2 tiling = new Vector2(2f, 2f);

    [Header("Offset")]

    public Vector2 offset = Vector2.zero;


    private Material _mat;


    void Start()

    {

        // GetComponent 获取渲染器,取材质实例(避免修改共享材质)

        _mat = GetComponent<Renderer>().material;


        // 方法一:SetTextureScale / SetTextureOffset(推荐,语义清晰)

        _mat.SetTextureScale("_MainTex", tiling);

        _mat.SetTextureOffset("_MainTex", offset);

    }

}
using UnityEngine;


public class UVAnimator : MonoBehaviour

{

    public Vector2 flowDirection = new Vector2(1f, 0f);

    public float  flowSpeed    = 0.5f;


    private Material _mat;

    private Vector2  _offset;


    void Start()

    {

        _mat = GetComponent<Renderer>().material;

    }


    void Update()

    {

        // 每帧累加偏移

        _offset += flowDirection * flowSpeed * Time.deltaTime;


        // 使用 Repeat 将偏移限制在 [0, 1] 范围,防止浮点精度劣化

        _offset.x = Mathf.Repeat(_offset.x, 1f);

        _offset.y = Mathf.Repeat(_offset.y, 1f);


        _mat.SetTextureOffset("_MainTex", _offset);

    }

}

⚠️

使用 renderer.material 会自动创建材质实例,避免修改 sharedMaterial(会影响场景中所有使用该材质的对象)。 在频繁更新时,优先使用 MaterialPropertyBlock 以完全避免材质实例化,保持合批。

MaterialPropertyBlock(性能最优方案)

using UnityEngine;


public class UVAnimatorMPB : MonoBehaviour

{

    static readonly int MainTexST = Shader.PropertyToID("_MainTex_ST");


    public Vector2 tiling = Vector2.one;

    public float  speed  = 0.3f;


    Renderer             _renderer;

    MaterialPropertyBlock _mpb;


    void Awake()

    {

        _renderer = GetComponent<Renderer>();

        _mpb      = new MaterialPropertyBlock();

    }


    void Update()

    {

        float t = Time.time * speed;

        // _MainTex_ST: .xy = Tiling, .zw = Offset

        var st = new Vector4(tiling.x, tiling.y, t, 0f);

        _mpb.SetVector(MainTexST, st);

        _renderer.SetPropertyBlock(_mpb);

    }

}

常见场景与最佳实践

性能与最佳实践总结

场景 推荐方案 注意事项
静态纹理缩放 材质 Inspector 面板直接设置 无运行时开销,推荐首选
一次性运行时设置 mat.SetTextureScale/Offset 会创建材质实例,注意内存
每帧更新(动画) MaterialPropertyBlock 不破坏 GPU 合批,性能最优
Shader 内动画 片段着色器 _Time.y 驱动 无 CPU 开销,避免 UV Jitter
Shader Graph 项目 Tiling And Offset 节点 + Property 连接 Vector2 属性节点可调试
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐