news 2026/5/24 20:59:53

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 参数。代码逐行出现,帮助你逐步理解每个环节。

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 属性节点可调试
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/1 11:51:22

Qwen3-ForcedAligner效果展示:高精度语音文本对齐案例分享

Qwen3-ForcedAligner效果展示&#xff1a;高精度语音文本对齐案例分享 1. 引言 语音文本对齐技术正在改变我们处理音频内容的方式。想象一下&#xff0c;你有一段会议录音和对应的文字记录&#xff0c;想要快速找到某个关键词出现的具体时间点&#xff1b;或者你有一段外语学…

作者头像 李华
网站建设 2026/4/1 11:51:03

Qwen3.5-4B模型IDEA集成实战:本地化智能编程体验配置

Qwen3.5-4B模型IDEA集成实战&#xff1a;本地化智能编程体验配置 1. 引言 作为一名长期使用IntelliJ IDEA进行开发的工程师&#xff0c;我一直在寻找能够提升编码效率的智能辅助工具。最近尝试将Qwen3.5-4B模型本地部署并与IDEA集成后&#xff0c;发现这套方案不仅响应速度快…

作者头像 李华
网站建设 2026/4/1 11:50:32

Vue项目实战:el-menu多级路由高亮避坑指南(附完整代码)

Vue项目实战&#xff1a;el-menu多级路由高亮避坑指南&#xff08;附完整代码&#xff09; 在Vue项目开发中&#xff0c;尤其是后台管理系统这类复杂应用&#xff0c;el-menu作为Element UI提供的导航菜单组件&#xff0c;经常需要处理多级路由的高亮问题。很多开发者在使用过…

作者头像 李华
网站建设 2026/4/1 11:50:02

Phi-3 Forest Laboratory在量化金融中的潜力展示:财报摘要与风险提示生成

Phi-3 Forest Laboratory在量化金融中的潜力展示&#xff1a;财报摘要与风险提示生成 最近在琢磨AI模型在专业领域的应用&#xff0c;特别是那些需要处理大量文本、提取关键信息的场景。正好手头有个挺有意思的模型叫Phi-3 Forest Laboratory&#xff0c;就想试试它在金融分析…

作者头像 李华
网站建设 2026/4/1 11:49:12

不用RANSAC也能搞定低Inlier Ratio?最新点云配准方法揭秘

突破传统限制&#xff1a;低Inlier Ratio点云配准的深度学习解决方案 点云配准技术正经历一场静默革命——当传统RANSAC算法在低Inlier Ratio场景中频频失效时&#xff0c;新一代基于深度学习的配准方法正在改写游戏规则。想象一下&#xff0c;在自动驾驶汽车遇到暴雨天气导致…

作者头像 李华
网站建设 2026/4/1 11:46:44

郭老师-最高级的活法:不渡无缘之人

最高级的活法 ——不干涉他人的因果“说教只会引来仇恨&#xff0c; 疼痛才是最好的老师。”&#x1f33f; 真正的慈悲&#xff0c; 不是拉人上岸&#xff0c; 而是—— 允许他沉下去&#xff0c;再自己浮起来。⚖️ 一、四大悲哀&#xff1a;强行渡人&#xff0c;反被拖下水行…

作者头像 李华