Unity Shader UV 坐标与纹理平铺Tiling & Offset 深度解析

从 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 参数。代码逐行出现,帮助你逐步理解每个环节。

cs 复制代码
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,可以叠加时间驱动的动态偏移:

cs 复制代码
// 在 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 动画、程序化效果的常用手段。

cs 复制代码
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);

    }

}
cs 复制代码
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(性能最优方案)

cs 复制代码
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 属性节点可调试
相关推荐
SiYuanFeng2 小时前
uv初步介绍及简单的使用方法例子
开发语言·python·uv
致宏Rex2 小时前
uv 教程:安装、常用命令、项目结构与关键文件
python·pip·uv
chao1898443 小时前
基于STM32F1的声源定位系统设计与实现
stm32·嵌入式硬件·unity
七夜zippoe13 小时前
OpenClaw 内置工具详解
unity·ai·游戏引擎·openclaw·内置工具
mxwin18 小时前
Unity Shader 细节贴图技术在不增加显存开销的前提下,有效提升近距离纹理细节的渲染质量
unity·游戏引擎·贴图
魔士于安19 小时前
unity 低多边形 动物 带场景 有氛围感
游戏·unity·游戏引擎·贴图
小贺儿开发20 小时前
Unity3D 摩斯与中文电码转换工具
科技·unity·人机交互·工具·实践·实用·科普应用
魔士于安21 小时前
unity 动物包 大象 鹿 狐狸
游戏·unity·游戏引擎·贴图·模型
mxwin1 天前
Unity URP 中 Mipmap 纹理多级渐远技术 解决远处纹理闪烁(摩尔纹)与性能优化的完整指南
unity·游戏引擎