1. 项目概述从“消融”效果切入理解_ST的实战价值很多刚接触Unity Shader的朋友一看到_MainTex_ST这个变量名就有点发懵。_ST后缀到底代表什么为什么我的贴图在材质球上调整Tiling平铺和Offset偏移没反应为什么别人的Shader代码里总有一句TRANSFORM_TEX今天我们不谈枯燥的理论直接用一个酷炫的“消融”Dissolve效果作为实战案例把_ST、Tiling和Offset这几个概念彻底讲透让你以后用到它们时再也不迷糊。消融效果大家应该不陌生物体从某个点开始逐渐化为灰烬或粒子消失。实现它的核心思路之一就是使用一张噪声贴图Noise Texture作为“溶解地图”。我们通过一个阈值比如一个从0到1变化的_Cutoff值来采样这张噪声图当像素点的噪声值小于阈值时就丢弃clip这个像素让它“消失”反之则保留。为了让消融过程更可控、更自然我们常常需要调整这张噪声贴图的平铺和偏移。比如平铺值调小噪声纹理变大消融的“块状感”就更明显平铺值调大噪声纹理变小且重复消融边缘会更细腻。而偏移则可以控制消融起始的中心点位置。你看一个具体的、有视觉反馈的效果立刻就把_ST这个抽象概念的应用场景给具象化了。接下来我们就一步步拆解看看如何通过代码和材质面板精准地操控这个消融过程。2. _ST后缀的本质Scale与Translation的封装2.1 命名溯源与内部结构首先我们必须直面这个有点奇怪的命名。_ST是“Scale”和“Translation”的缩写。这里的Scale对应材质面板上的“Tiling”平铺Translation对应“Offset”偏移。为什么不用更直观的_TilingOffset呢这涉及到Unity的历史遗留问题。早期的Unity材质面板确实使用“Scale”和“Translate”作为标签但后来为了更贴近美术人员的理解改成了“Tiling”和“Offset”。然而Shader中已经大量使用了_ST这个命名约定为了向下兼容这个命名就一直保留了下来。所以请记住这个对应关系_STTilingOffset。在代码层面_MainTex_ST是一个float4类型的变量。它的四个分量xyzw有明确的职责分工_MainTex_ST.xy: 存储的是**Tiling平铺**值。这是一个二维向量分别对应U轴和V轴方向上的缩放倍数。默认值是(1, 1)表示不进行平铺。_MainTex_ST.zw: 存储的是**Offset偏移**值。同样是一个二维向量分别对应U轴和V轴方向上的偏移量。默认值是(0, 0)表示不进行偏移。所以当你把一个名为_MainTex的纹理属性暴露给材质面板时Unity会自动为你生成一个配套的float4 _MainTex_ST变量并且将材质面板上“Tiling”和“Offset”两个输入框的值分别绑定到这个变量的.xy和.zw分量上。你不需要在Properties块里显式声明_MainTex_ST但需要在CGPROGRAM内部的变量声明区声明它这样才能在Shader代码中使用。2.2 TRANSFORM_TEX宏便捷的UV变换工具手动用_MainTex_ST.xy去乘UV再用_MainTex_ST.zw去加虽然直观但写起来麻烦。Unity在UnityCG.cginc这个常用的头文件中为我们提供了一个非常方便的宏TRANSFORM_TEX。它的定义非常简单#define TRANSFORM_TEX(tex, name) (tex.xy * name##_ST.xy name##_ST.zw)这个宏做了两件事拼接变量名通过##操作符将你传入的name例如_MainTex和_ST拼接起来得到_MainTex_ST这个变量。执行变换计算将输入的UV坐标tex.xy乘以平铺值name##_ST.xy再加上偏移值name##_ST.zw返回变换后的新UV坐标。因此在顶点着色器或片段着色器中我们通常这样使用它// 首先声明配套的_ST变量 float4 _MainTex_ST; // ... 其他代码 // 在需要变换UV的地方 o.uv TRANSFORM_TEX(v.uv, _MainTex); // v.uv是模型原始的UV坐标这行代码执行后o.uv就是应用了材质面板上Tiling和Offset设置后的新UV坐标用于后续的纹理采样。注意TRANSFORM_TEX宏默认操作的是输入坐标的.xy分量。如果你的UV数据存储在float2变量中直接传入即可如果存储在float4或其他结构中可能需要明确指定.xy如TRANSFORM_TEX(v.texcoord.xy, _MainTex)。3. 实战构建可调节的消融Shader理论铺垫完毕现在进入实战环节。我们将编写一个支持通过材质面板调节噪声图Tiling和Offset的消融Shader。3.1 Shader属性与变量声明首先在Shader的Properties块中我们定义用户可调节的参数。除了消融阈值和边缘颜色最关键的是声明我们的噪声纹理并为其预留Tiling和Offset的调节接口。Shader Custom/DissolveWithST { Properties { _MainTex (Albedo (RGB), 2D) white {} // 主纹理 _NoiseTex (Dissolve Noise, 2D) white {} // 消融噪声纹理 _Cutoff (Dissolve Threshold, Range(0, 1)) 0.5 // 消融阈值 _EdgeWidth (Edge Width, Range(0, 0.2)) 0.1 // 消融边缘宽度 _EdgeColor (Edge Color, Color) (1, 0.5, 0, 1) // 边缘颜色 _DissolveProgress (Progress, Range(0, 1)) 0.0 // 消融进度用于动画 }这里_NoiseTex就是我们将要应用_ST变换的核心纹理。注意我们没有在Properties里写_NoiseTex_ST。这个float4变量是Unity根据_NoiseTex自动为我们准备的。接下来在CGPROGRAM内部我们需要声明与Properties对应的变量特别是要声明_NoiseTex_ST。CGPROGRAM #pragma surface surf Standard fullforwardshadows addshadow #pragma target 3.0 sampler2D _MainTex; sampler2D _NoiseTex; float4 _NoiseTex_ST; // 关键声明噪声纹理的_ST变量 float _Cutoff; float _EdgeWidth; float4 _EdgeColor; float _DissolveProgress; struct Input { float2 uv_MainTex; float2 uv_NoiseTex; // 如果噪声图使用独立的UV通道 };这里有一个细节在Surface Shader中Input结构体里声明的uv_NoiseTex会自动获取模型第二个UV通道即UV1的数据。如果你希望噪声图使用和主纹理一样的UV可以只声明uv_MainTex然后在surf函数里都用它。使用独立UV通道的好处是可以对噪声图的映射进行单独控制例如通过脚本修改模型的UV1实现更复杂的效果。3.2 在表面着色器中应用UV变换核心逻辑在surf函数中。我们需要采样噪声纹理并根据阈值进行消融判断。void surf (Input IN, inout SurfaceOutputStandard o) { // 1. 正常采样主纹理 fixed4 c tex2D (_MainTex, IN.uv_MainTex); o.Albedo c.rgb; o.Alpha c.a; // 2. 应用_ST变换采样噪声纹理 // 方法A使用TRANSFORM_TEX宏推荐 float2 noiseUV TRANSFORM_TEX(IN.uv_NoiseTex, _NoiseTex); // 方法B手动计算效果等同 // float2 noiseUV IN.uv_NoiseTex * _NoiseTex_ST.xy _NoiseTex_ST.zw; fixed4 noiseSample tex2D(_NoiseTex, noiseUV); float noiseValue noiseSample.r; // 通常使用单通道噪声图取r通道 // 3. 根据消融进度调整实际阈值 float currentThreshold _DissolveProgress; // 4. 消融核心逻辑 if (noiseValue currentThreshold) { discard; // 或者 clip(noiseValue - currentThreshold); } // 5. 边缘光效果可选增强 if (noiseValue currentThreshold _EdgeWidth) { float edgeFactor (noiseValue - currentThreshold) / _EdgeWidth; o.Emission _EdgeColor.rgb * (1 - edgeFactor); o.Albedo lerp(_EdgeColor.rgb, o.Albedo, edgeFactor); } } ENDCG关键点解析第2步float2 noiseUV TRANSFORM_TEX(IN.uv_NoiseTex, _NoiseTex);这是整个Shader的“灵魂”所在。这一行代码将材质面板上为_NoiseTex设置的Tiling和Offset值应用到了模型原始的uv_NoiseTex坐标上生成了最终用于采样噪声图的UV。手动计算注释掉的方法B展示了其等价计算过程。IN.uv_NoiseTex * _NoiseTex_ST.xy实现了平铺缩放 _NoiseTex_ST.zw实现了偏移。理解这个公式你就彻底理解了_ST的作用。视觉反馈现在你可以在材质面板上修改_NoiseTex的Tiling和Offset立刻就能看到消融图案的密度和位置发生变化。Tiling值大于1噪声图变小、重复消融边缘更碎、更细腻Tiling值小于1噪声图变大消融块状更明显。Offset则直接滑动消融图案的位置。3.3 在顶点-片段着色器中的处理如果你编写的是顶点-片段着色器Vert-Frag Shader逻辑也是类似的。通常在顶点着色器中进行UV变换然后将变换后的UV传递给片段着色器。v2f vert (appdata v) { v2f o; // ... 其他顶点变换如MVP变换 o.uv TRANSFORM_TEX(v.uv, _MainTex); // 主纹理UV变换 o.uvNoise TRANSFORM_TEX(v.uv, _NoiseTex); // 噪声纹理UV变换 // 如果使用第二套UV可能是 v.uv1 // o.uvNoise TRANSFORM_TEX(v.uv1, _NoiseTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv); fixed4 noise tex2D(_NoiseTex, i.uvNoise); // 使用变换后的UV采样 clip(noise.r - _Cutoff); // ... 其余片段着色逻辑 return col; }将UV变换放在顶点着色器是一种常见优化每个顶点只计算一次然后在片段间进行插值。虽然对于Tiling/Offset这种线性变换在片段着色器里计算也没太大性能损耗但放在顶点着色器是更规范的做法。4. 材质面板交互与脚本控制4.1 材质面板的直观调节Shader编写完成后将其赋给一个材质球。在Inspector面板中你会发现_NoiseTex纹理属性下方自动出现了“Tiling”和“Offset”两组X、Y滑块。这正是_NoiseTex_ST变量在材质面板上的可视化体现。调节Tiling尝试将X和Y从1改为2。你会发现场景中物体的消融纹理密度增加了一倍图案变得更小、更密集消融边缘的“颗粒感”更强。改为0.5则纹理被放大消融的块状区域更大。调节Offset滑动X或Y的值你会看到整个消融图案在物体表面发生平移。这可以用来控制消融的起始点或者让消融过程产生“扫描”动画效果。通过实时调节这些参数并观察效果你能最直观地建立起“_ST变量值”与“视觉表现”之间的强关联这是彻底理解这个概念的最佳途径。4.2 通过脚本动态修改_ST值有时我们需要通过代码动态控制平铺和偏移比如实现一个随时间变化的滚动纹理或者根据游戏状态改变消融图案的密度。这时就需要在C#脚本中访问和修改_ST值。在Unity中我们可以使用Material.SetTextureScale和Material.SetTextureOffset这两个方法它们就是专门用来设置_ST.xy和_ST.zw的。using UnityEngine; public class DynamicDissolveController : MonoBehaviour { public Material dissolveMaterial; // 持有消融材质的引用 public float scrollSpeedX 0.1f; public float tilingSpeed 0.5f; private float currentTiling 1.0f; private Vector2 currentOffset Vector2.zero; void Start() { // 获取材质实例避免修改共享材质 dissolveMaterial GetComponentRenderer().material; } void Update() { // 1. 动态修改Offset实现纹理滚动 currentOffset.x Time.deltaTime * scrollSpeedX; // 设置偏移这会修改 _NoiseTex_ST.zw dissolveMaterial.SetTextureOffset(_NoiseTex, currentOffset); // 2. 动态修改Tiling实现纹理缩放动画 currentTiling 1.0f Mathf.Sin(Time.time * tilingSpeed) * 0.5f; Vector2 dynamicTiling new Vector2(currentTiling, currentTiling); // 设置平铺这会修改 _NoiseTex_ST.xy dissolveMaterial.SetTextureScale(_NoiseTex, dynamicTiling); // 3. 也可以直接设置整个_ST向量较少用但可行 // Vector4 stVector new Vector4(tilingX, tilingY, offsetX, offsetY); // dissolveMaterial.SetVector(_NoiseTex_ST, stVector); } }脚本操作要点SetTextureOffset和SetTextureScale的第一个参数是纹理在Shader中的属性名字符串必须完全匹配这里是_NoiseTex。修改的是材质的属性。如果多个物体共享同一个材质会同时影响所有物体。通常建议通过Renderer.material获取一个该材质的实例副本进行操作。你也可以通过Material.GetTextureOffset和Material.GetTextureScale来获取当前值。实操心得在脚本中修改材质属性是性能敏感操作。如果每帧都需要修改确保是对材质实例material而非共享材质sharedMaterial进行操作并且考虑在属性没有变化时避免重复设置。对于大量物体使用MaterialPropertyBlock是更高效的选择。5. 常见问题与深度排查指南即使理解了原理在实际使用_ST时还是会遇到一些典型问题。下面我结合自己的踩坑经验整理了一份排查清单。5.1 问题一调整了材质球的Tiling/Offset为什么没效果这是新手最常遇到的问题。可能的原因和解决方案如下问题现象可能原因解决方案完全无变化1. Shader代码中未声明对应的_XXXX_ST变量。2. 没有使用TRANSFORM_TEX或手动计算应用变换。3. 在Shader中使用了错误的UV坐标如始终用v.uv未用变换后的UV采样。1. 在CGPROGRAM中纹理变量声明下方添加float4 _YourTex_ST;。2. 在采样纹理前确保使用TRANSFORM_TEX(uv, _YourTex)计算新UV。3. 检查传递给TRANSFORM_TEX的第一个参数是否正确是模型原始UV还是其他UV通道。只有Tiling有效Offset无效或反之手动计算UV变换时公式写错。例如uv * _ST.xy _ST.xy错误偏移用了平铺值。检查公式uv * _ST.xy _ST.zw。确保.xy用于乘.zw用于加。效果和预期相反如Tiling调大纹理却放大对Tiling值的理解有误。Tiling是“平铺数”值越大同一空间内贴图重复次数越多视觉上纹理图案变小。建立正确认知Tiling X2 意味着U方向将纹理压缩为原来的一半大小并重复两次。可以想象成把一张小图贴满一面墙Tiling越大原始图越小。深度排查步骤检查变量声明在CGPROGRAM中确保为每个需要在面板调节Tiling/Offset的纹理都声明了float4 _TexName_ST;。检查变换应用在纹理采样行设置断点或使用return float4(transformedUV.xy, 0, 1);临时输出变换后的UV值到颜色查看其是否随面板参数变化。检查宏使用确保TRANSFORM_TEX宏的第一个参数是float2类型。如果模型UV是float4需使用.xy分量如TRANSFORM_TEX(v.texcoord.xy, _MainTex)。5.2 问题二TRANSFORM_TEX宏报错或未定义这通常是因为没有包含必要的头文件或者宏作用域不对。缺少头文件TRANSFORM_TEX宏定义在UnityCG.cginc中。确保你的Shader在CGPROGRAM开头包含了这个文件#include UnityCG.cginc。对于URPUniversal Render PipelineShader可能需要包含Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl并使用其中定义的类似宏或函数如TRANSFORM_TEX在URP中通常也可用但需确认包含链。宏作用域TRANSFORM_TEX必须在CGPROGRAM/ENDCG块内使用。5.3 问题三多纹理共用UV与独立UV的策略选择在一个Shader中使用多张纹理如主纹理、法线贴图、金属度贴图、噪声图时需要决定它们是否共用一套UV变换。共用UV所有纹理都使用_MainTex_ST。优点是材质面板上只需调节一次Tiling/Offset所有纹理同步变化。在Properties中只需声明_MainTex在代码中只声明float4 _MainTex_ST所有纹理采样都使用TRANSFORM_TEX(IN.uv_MainTex, _MainTex)。这适用于纹理是同一套UV布局的情况如颜色、法线、高光来自同一套UV。独立UV每张纹理都有自己的_ST变量。例如噪声图_NoiseTex使用独立的_NoiseTex_ST。这需要模型提供多套UV通常在UV1通道或者在Input结构体中声明uv_NoiseTex。优点是每张纹理可以独立控制映射方式灵活性极高。我们的消融案例就采用了这种方式以便单独控制噪声图案的密度和位置。选择建议如果纹理是服务于同一视觉属性如PBR流程中的各贴图通常共用UV。如果纹理是用于实现独立、可调节的特效如噪声、遮罩、细节叠加则使用独立UV和独立的_ST控制。5.4 问题四Tiling/Offset在Shader Graph中如何使用如果你使用Shader Graph视觉化着色器编辑器操作会更加直观。在Graph中创建一个Texture 2D属性节点命名为NoiseTex。在Graph的Blackboard属性面板中选中NoiseTex属性在Node Settings里你会看到Reference如_NoiseTex。更重要的是下方有一个**Use Tiling and Offset的复选框**。务必勾选它。勾选后NoiseTex节点会自动变成一个Sample Texture 2D节点并且其UV输入端口前会连接一个Tiling And Offset节点。这个节点内部就是UV * Tiling Offset的计算。材质面板上会自动为NoiseTex属性显示Tiling和Offset输入框。你可以直接将一个Vector2节点连接到Tiling And Offset节点的Tiling或Offset输入口实现动态控制。Shader Graph避坑点如果不勾选Use Tiling and Offset那么在材质面板上将不会出现Tiling和Offset的调节选项你也无法通过默认节点流程应用这些变换。这是从代码Shader转向Shader Graph时的一个常见困惑点。6. 性能考量与最佳实践理解了怎么用我们还要知道怎么用得好。_ST变换虽然计算简单但在大规模、高性能要求的项目中仍需注意一些细节。6.1 计算开销与优化选择UV * Scale Offset是一次乘加运算MAD在现代GPU上开销极小几乎可以忽略不计。因此在绝大多数情况下你不需要担心使用_ST变换带来的性能问题。真正的性能考量点在于纹理采样次数每多使用一张纹理就多一次纹理采样Texture Fetch这才是更耗性能的操作。因此应尽可能复用纹理和UV坐标。例如如果法线贴图和高度贴图使用与主纹理相同的UV变换就应该让它们共用一套变换后的UV而不是各自计算一次。顶点着色器 vs 片段着色器如前所述将TRANSFORM_TEX计算放在顶点着色器结果通过插值传递给片段着色器是标准且高效的做法。这避免了在每个像素上都进行相同的计算。6.2 材质实例化与MaterialPropertyBlock当需要通过脚本为大量物体动态设置不同的Tiling/Offset时比如让一片草地中的每棵草有不同的噪声偏移直接修改每个物体的Material实例会产生大量材质实例增加内存和Draw Call。此时应优先使用MaterialPropertyBlock。// 性能更优的动态设置方式 MaterialPropertyBlock props new MaterialPropertyBlock(); Renderer renderer GetComponentRenderer(); // 设置_ST的xy分量Tiling props.SetTextureScale(_NoiseTex, new Vector2(randomTilingX, randomTilingY)); // 设置_ST的zw分量Offset props.SetTextureOffset(_NoiseTex, new Vector2(randomOffsetX, randomOffsetY)); renderer.SetPropertyBlock(props);使用MaterialPropertyBlock可以在不创建新材质实例的情况下覆盖材质的某些属性。所有使用同一材质的物体可以通过不同的PropertyBlock实现个性化的_ST值同时保持合批Batching的可能性对性能非常友好。6.3 非均匀缩放与纹理拉伸最后提一个进阶话题模型本身的缩放Scale是否会影响_ST变换答案是不会直接影响。_ST变换作用于纹理空间UV空间而模型缩放作用于模型空间。但是如果模型在建模时UV就被非均匀地展开那么应用相同的Tiling值在模型的不同部位可能会产生视觉上的拉伸。例如一个长方体正面和侧面的UV面积可能不同同样的Tiling值会导致正面和侧面的纹理密度不一致。解决方案对于需要精确纹理密度控制的情况如砖墙、地板可以考虑在建模阶段确保UV均匀分布。在Shader中使用世界空间或物体空间坐标来驱动纹理采样Triplanar Mapping等技术但这会显著增加计算量。针对不同的模型部位使用多套材质或材质属性通过顶点颜色或第二套UV来索引不同的_ST值。通过这个从理论到实战、从使用到排查、从基础到进阶的完整梳理相信你已经对Unity Shader中的_ST后缀、Tiling和Offset有了透彻的理解。记住它们本质就是一组控制纹理如何映射到模型表面的缩放和偏移参数TRANSFORM_TEX宏则是应用这组参数的快捷工具。下次在Shader中看到它们你大可以自信地说不过如此。
Unity Shader中_ST后缀与TRANSFORM_TEX宏的实战解析:以消融效果为例
发布时间:2026/7/12 6:35:32
1. 项目概述从“消融”效果切入理解_ST的实战价值很多刚接触Unity Shader的朋友一看到_MainTex_ST这个变量名就有点发懵。_ST后缀到底代表什么为什么我的贴图在材质球上调整Tiling平铺和Offset偏移没反应为什么别人的Shader代码里总有一句TRANSFORM_TEX今天我们不谈枯燥的理论直接用一个酷炫的“消融”Dissolve效果作为实战案例把_ST、Tiling和Offset这几个概念彻底讲透让你以后用到它们时再也不迷糊。消融效果大家应该不陌生物体从某个点开始逐渐化为灰烬或粒子消失。实现它的核心思路之一就是使用一张噪声贴图Noise Texture作为“溶解地图”。我们通过一个阈值比如一个从0到1变化的_Cutoff值来采样这张噪声图当像素点的噪声值小于阈值时就丢弃clip这个像素让它“消失”反之则保留。为了让消融过程更可控、更自然我们常常需要调整这张噪声贴图的平铺和偏移。比如平铺值调小噪声纹理变大消融的“块状感”就更明显平铺值调大噪声纹理变小且重复消融边缘会更细腻。而偏移则可以控制消融起始的中心点位置。你看一个具体的、有视觉反馈的效果立刻就把_ST这个抽象概念的应用场景给具象化了。接下来我们就一步步拆解看看如何通过代码和材质面板精准地操控这个消融过程。2. _ST后缀的本质Scale与Translation的封装2.1 命名溯源与内部结构首先我们必须直面这个有点奇怪的命名。_ST是“Scale”和“Translation”的缩写。这里的Scale对应材质面板上的“Tiling”平铺Translation对应“Offset”偏移。为什么不用更直观的_TilingOffset呢这涉及到Unity的历史遗留问题。早期的Unity材质面板确实使用“Scale”和“Translate”作为标签但后来为了更贴近美术人员的理解改成了“Tiling”和“Offset”。然而Shader中已经大量使用了_ST这个命名约定为了向下兼容这个命名就一直保留了下来。所以请记住这个对应关系_STTilingOffset。在代码层面_MainTex_ST是一个float4类型的变量。它的四个分量xyzw有明确的职责分工_MainTex_ST.xy: 存储的是**Tiling平铺**值。这是一个二维向量分别对应U轴和V轴方向上的缩放倍数。默认值是(1, 1)表示不进行平铺。_MainTex_ST.zw: 存储的是**Offset偏移**值。同样是一个二维向量分别对应U轴和V轴方向上的偏移量。默认值是(0, 0)表示不进行偏移。所以当你把一个名为_MainTex的纹理属性暴露给材质面板时Unity会自动为你生成一个配套的float4 _MainTex_ST变量并且将材质面板上“Tiling”和“Offset”两个输入框的值分别绑定到这个变量的.xy和.zw分量上。你不需要在Properties块里显式声明_MainTex_ST但需要在CGPROGRAM内部的变量声明区声明它这样才能在Shader代码中使用。2.2 TRANSFORM_TEX宏便捷的UV变换工具手动用_MainTex_ST.xy去乘UV再用_MainTex_ST.zw去加虽然直观但写起来麻烦。Unity在UnityCG.cginc这个常用的头文件中为我们提供了一个非常方便的宏TRANSFORM_TEX。它的定义非常简单#define TRANSFORM_TEX(tex, name) (tex.xy * name##_ST.xy name##_ST.zw)这个宏做了两件事拼接变量名通过##操作符将你传入的name例如_MainTex和_ST拼接起来得到_MainTex_ST这个变量。执行变换计算将输入的UV坐标tex.xy乘以平铺值name##_ST.xy再加上偏移值name##_ST.zw返回变换后的新UV坐标。因此在顶点着色器或片段着色器中我们通常这样使用它// 首先声明配套的_ST变量 float4 _MainTex_ST; // ... 其他代码 // 在需要变换UV的地方 o.uv TRANSFORM_TEX(v.uv, _MainTex); // v.uv是模型原始的UV坐标这行代码执行后o.uv就是应用了材质面板上Tiling和Offset设置后的新UV坐标用于后续的纹理采样。注意TRANSFORM_TEX宏默认操作的是输入坐标的.xy分量。如果你的UV数据存储在float2变量中直接传入即可如果存储在float4或其他结构中可能需要明确指定.xy如TRANSFORM_TEX(v.texcoord.xy, _MainTex)。3. 实战构建可调节的消融Shader理论铺垫完毕现在进入实战环节。我们将编写一个支持通过材质面板调节噪声图Tiling和Offset的消融Shader。3.1 Shader属性与变量声明首先在Shader的Properties块中我们定义用户可调节的参数。除了消融阈值和边缘颜色最关键的是声明我们的噪声纹理并为其预留Tiling和Offset的调节接口。Shader Custom/DissolveWithST { Properties { _MainTex (Albedo (RGB), 2D) white {} // 主纹理 _NoiseTex (Dissolve Noise, 2D) white {} // 消融噪声纹理 _Cutoff (Dissolve Threshold, Range(0, 1)) 0.5 // 消融阈值 _EdgeWidth (Edge Width, Range(0, 0.2)) 0.1 // 消融边缘宽度 _EdgeColor (Edge Color, Color) (1, 0.5, 0, 1) // 边缘颜色 _DissolveProgress (Progress, Range(0, 1)) 0.0 // 消融进度用于动画 }这里_NoiseTex就是我们将要应用_ST变换的核心纹理。注意我们没有在Properties里写_NoiseTex_ST。这个float4变量是Unity根据_NoiseTex自动为我们准备的。接下来在CGPROGRAM内部我们需要声明与Properties对应的变量特别是要声明_NoiseTex_ST。CGPROGRAM #pragma surface surf Standard fullforwardshadows addshadow #pragma target 3.0 sampler2D _MainTex; sampler2D _NoiseTex; float4 _NoiseTex_ST; // 关键声明噪声纹理的_ST变量 float _Cutoff; float _EdgeWidth; float4 _EdgeColor; float _DissolveProgress; struct Input { float2 uv_MainTex; float2 uv_NoiseTex; // 如果噪声图使用独立的UV通道 };这里有一个细节在Surface Shader中Input结构体里声明的uv_NoiseTex会自动获取模型第二个UV通道即UV1的数据。如果你希望噪声图使用和主纹理一样的UV可以只声明uv_MainTex然后在surf函数里都用它。使用独立UV通道的好处是可以对噪声图的映射进行单独控制例如通过脚本修改模型的UV1实现更复杂的效果。3.2 在表面着色器中应用UV变换核心逻辑在surf函数中。我们需要采样噪声纹理并根据阈值进行消融判断。void surf (Input IN, inout SurfaceOutputStandard o) { // 1. 正常采样主纹理 fixed4 c tex2D (_MainTex, IN.uv_MainTex); o.Albedo c.rgb; o.Alpha c.a; // 2. 应用_ST变换采样噪声纹理 // 方法A使用TRANSFORM_TEX宏推荐 float2 noiseUV TRANSFORM_TEX(IN.uv_NoiseTex, _NoiseTex); // 方法B手动计算效果等同 // float2 noiseUV IN.uv_NoiseTex * _NoiseTex_ST.xy _NoiseTex_ST.zw; fixed4 noiseSample tex2D(_NoiseTex, noiseUV); float noiseValue noiseSample.r; // 通常使用单通道噪声图取r通道 // 3. 根据消融进度调整实际阈值 float currentThreshold _DissolveProgress; // 4. 消融核心逻辑 if (noiseValue currentThreshold) { discard; // 或者 clip(noiseValue - currentThreshold); } // 5. 边缘光效果可选增强 if (noiseValue currentThreshold _EdgeWidth) { float edgeFactor (noiseValue - currentThreshold) / _EdgeWidth; o.Emission _EdgeColor.rgb * (1 - edgeFactor); o.Albedo lerp(_EdgeColor.rgb, o.Albedo, edgeFactor); } } ENDCG关键点解析第2步float2 noiseUV TRANSFORM_TEX(IN.uv_NoiseTex, _NoiseTex);这是整个Shader的“灵魂”所在。这一行代码将材质面板上为_NoiseTex设置的Tiling和Offset值应用到了模型原始的uv_NoiseTex坐标上生成了最终用于采样噪声图的UV。手动计算注释掉的方法B展示了其等价计算过程。IN.uv_NoiseTex * _NoiseTex_ST.xy实现了平铺缩放 _NoiseTex_ST.zw实现了偏移。理解这个公式你就彻底理解了_ST的作用。视觉反馈现在你可以在材质面板上修改_NoiseTex的Tiling和Offset立刻就能看到消融图案的密度和位置发生变化。Tiling值大于1噪声图变小、重复消融边缘更碎、更细腻Tiling值小于1噪声图变大消融块状更明显。Offset则直接滑动消融图案的位置。3.3 在顶点-片段着色器中的处理如果你编写的是顶点-片段着色器Vert-Frag Shader逻辑也是类似的。通常在顶点着色器中进行UV变换然后将变换后的UV传递给片段着色器。v2f vert (appdata v) { v2f o; // ... 其他顶点变换如MVP变换 o.uv TRANSFORM_TEX(v.uv, _MainTex); // 主纹理UV变换 o.uvNoise TRANSFORM_TEX(v.uv, _NoiseTex); // 噪声纹理UV变换 // 如果使用第二套UV可能是 v.uv1 // o.uvNoise TRANSFORM_TEX(v.uv1, _NoiseTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv); fixed4 noise tex2D(_NoiseTex, i.uvNoise); // 使用变换后的UV采样 clip(noise.r - _Cutoff); // ... 其余片段着色逻辑 return col; }将UV变换放在顶点着色器是一种常见优化每个顶点只计算一次然后在片段间进行插值。虽然对于Tiling/Offset这种线性变换在片段着色器里计算也没太大性能损耗但放在顶点着色器是更规范的做法。4. 材质面板交互与脚本控制4.1 材质面板的直观调节Shader编写完成后将其赋给一个材质球。在Inspector面板中你会发现_NoiseTex纹理属性下方自动出现了“Tiling”和“Offset”两组X、Y滑块。这正是_NoiseTex_ST变量在材质面板上的可视化体现。调节Tiling尝试将X和Y从1改为2。你会发现场景中物体的消融纹理密度增加了一倍图案变得更小、更密集消融边缘的“颗粒感”更强。改为0.5则纹理被放大消融的块状区域更大。调节Offset滑动X或Y的值你会看到整个消融图案在物体表面发生平移。这可以用来控制消融的起始点或者让消融过程产生“扫描”动画效果。通过实时调节这些参数并观察效果你能最直观地建立起“_ST变量值”与“视觉表现”之间的强关联这是彻底理解这个概念的最佳途径。4.2 通过脚本动态修改_ST值有时我们需要通过代码动态控制平铺和偏移比如实现一个随时间变化的滚动纹理或者根据游戏状态改变消融图案的密度。这时就需要在C#脚本中访问和修改_ST值。在Unity中我们可以使用Material.SetTextureScale和Material.SetTextureOffset这两个方法它们就是专门用来设置_ST.xy和_ST.zw的。using UnityEngine; public class DynamicDissolveController : MonoBehaviour { public Material dissolveMaterial; // 持有消融材质的引用 public float scrollSpeedX 0.1f; public float tilingSpeed 0.5f; private float currentTiling 1.0f; private Vector2 currentOffset Vector2.zero; void Start() { // 获取材质实例避免修改共享材质 dissolveMaterial GetComponentRenderer().material; } void Update() { // 1. 动态修改Offset实现纹理滚动 currentOffset.x Time.deltaTime * scrollSpeedX; // 设置偏移这会修改 _NoiseTex_ST.zw dissolveMaterial.SetTextureOffset(_NoiseTex, currentOffset); // 2. 动态修改Tiling实现纹理缩放动画 currentTiling 1.0f Mathf.Sin(Time.time * tilingSpeed) * 0.5f; Vector2 dynamicTiling new Vector2(currentTiling, currentTiling); // 设置平铺这会修改 _NoiseTex_ST.xy dissolveMaterial.SetTextureScale(_NoiseTex, dynamicTiling); // 3. 也可以直接设置整个_ST向量较少用但可行 // Vector4 stVector new Vector4(tilingX, tilingY, offsetX, offsetY); // dissolveMaterial.SetVector(_NoiseTex_ST, stVector); } }脚本操作要点SetTextureOffset和SetTextureScale的第一个参数是纹理在Shader中的属性名字符串必须完全匹配这里是_NoiseTex。修改的是材质的属性。如果多个物体共享同一个材质会同时影响所有物体。通常建议通过Renderer.material获取一个该材质的实例副本进行操作。你也可以通过Material.GetTextureOffset和Material.GetTextureScale来获取当前值。实操心得在脚本中修改材质属性是性能敏感操作。如果每帧都需要修改确保是对材质实例material而非共享材质sharedMaterial进行操作并且考虑在属性没有变化时避免重复设置。对于大量物体使用MaterialPropertyBlock是更高效的选择。5. 常见问题与深度排查指南即使理解了原理在实际使用_ST时还是会遇到一些典型问题。下面我结合自己的踩坑经验整理了一份排查清单。5.1 问题一调整了材质球的Tiling/Offset为什么没效果这是新手最常遇到的问题。可能的原因和解决方案如下问题现象可能原因解决方案完全无变化1. Shader代码中未声明对应的_XXXX_ST变量。2. 没有使用TRANSFORM_TEX或手动计算应用变换。3. 在Shader中使用了错误的UV坐标如始终用v.uv未用变换后的UV采样。1. 在CGPROGRAM中纹理变量声明下方添加float4 _YourTex_ST;。2. 在采样纹理前确保使用TRANSFORM_TEX(uv, _YourTex)计算新UV。3. 检查传递给TRANSFORM_TEX的第一个参数是否正确是模型原始UV还是其他UV通道。只有Tiling有效Offset无效或反之手动计算UV变换时公式写错。例如uv * _ST.xy _ST.xy错误偏移用了平铺值。检查公式uv * _ST.xy _ST.zw。确保.xy用于乘.zw用于加。效果和预期相反如Tiling调大纹理却放大对Tiling值的理解有误。Tiling是“平铺数”值越大同一空间内贴图重复次数越多视觉上纹理图案变小。建立正确认知Tiling X2 意味着U方向将纹理压缩为原来的一半大小并重复两次。可以想象成把一张小图贴满一面墙Tiling越大原始图越小。深度排查步骤检查变量声明在CGPROGRAM中确保为每个需要在面板调节Tiling/Offset的纹理都声明了float4 _TexName_ST;。检查变换应用在纹理采样行设置断点或使用return float4(transformedUV.xy, 0, 1);临时输出变换后的UV值到颜色查看其是否随面板参数变化。检查宏使用确保TRANSFORM_TEX宏的第一个参数是float2类型。如果模型UV是float4需使用.xy分量如TRANSFORM_TEX(v.texcoord.xy, _MainTex)。5.2 问题二TRANSFORM_TEX宏报错或未定义这通常是因为没有包含必要的头文件或者宏作用域不对。缺少头文件TRANSFORM_TEX宏定义在UnityCG.cginc中。确保你的Shader在CGPROGRAM开头包含了这个文件#include UnityCG.cginc。对于URPUniversal Render PipelineShader可能需要包含Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl并使用其中定义的类似宏或函数如TRANSFORM_TEX在URP中通常也可用但需确认包含链。宏作用域TRANSFORM_TEX必须在CGPROGRAM/ENDCG块内使用。5.3 问题三多纹理共用UV与独立UV的策略选择在一个Shader中使用多张纹理如主纹理、法线贴图、金属度贴图、噪声图时需要决定它们是否共用一套UV变换。共用UV所有纹理都使用_MainTex_ST。优点是材质面板上只需调节一次Tiling/Offset所有纹理同步变化。在Properties中只需声明_MainTex在代码中只声明float4 _MainTex_ST所有纹理采样都使用TRANSFORM_TEX(IN.uv_MainTex, _MainTex)。这适用于纹理是同一套UV布局的情况如颜色、法线、高光来自同一套UV。独立UV每张纹理都有自己的_ST变量。例如噪声图_NoiseTex使用独立的_NoiseTex_ST。这需要模型提供多套UV通常在UV1通道或者在Input结构体中声明uv_NoiseTex。优点是每张纹理可以独立控制映射方式灵活性极高。我们的消融案例就采用了这种方式以便单独控制噪声图案的密度和位置。选择建议如果纹理是服务于同一视觉属性如PBR流程中的各贴图通常共用UV。如果纹理是用于实现独立、可调节的特效如噪声、遮罩、细节叠加则使用独立UV和独立的_ST控制。5.4 问题四Tiling/Offset在Shader Graph中如何使用如果你使用Shader Graph视觉化着色器编辑器操作会更加直观。在Graph中创建一个Texture 2D属性节点命名为NoiseTex。在Graph的Blackboard属性面板中选中NoiseTex属性在Node Settings里你会看到Reference如_NoiseTex。更重要的是下方有一个**Use Tiling and Offset的复选框**。务必勾选它。勾选后NoiseTex节点会自动变成一个Sample Texture 2D节点并且其UV输入端口前会连接一个Tiling And Offset节点。这个节点内部就是UV * Tiling Offset的计算。材质面板上会自动为NoiseTex属性显示Tiling和Offset输入框。你可以直接将一个Vector2节点连接到Tiling And Offset节点的Tiling或Offset输入口实现动态控制。Shader Graph避坑点如果不勾选Use Tiling and Offset那么在材质面板上将不会出现Tiling和Offset的调节选项你也无法通过默认节点流程应用这些变换。这是从代码Shader转向Shader Graph时的一个常见困惑点。6. 性能考量与最佳实践理解了怎么用我们还要知道怎么用得好。_ST变换虽然计算简单但在大规模、高性能要求的项目中仍需注意一些细节。6.1 计算开销与优化选择UV * Scale Offset是一次乘加运算MAD在现代GPU上开销极小几乎可以忽略不计。因此在绝大多数情况下你不需要担心使用_ST变换带来的性能问题。真正的性能考量点在于纹理采样次数每多使用一张纹理就多一次纹理采样Texture Fetch这才是更耗性能的操作。因此应尽可能复用纹理和UV坐标。例如如果法线贴图和高度贴图使用与主纹理相同的UV变换就应该让它们共用一套变换后的UV而不是各自计算一次。顶点着色器 vs 片段着色器如前所述将TRANSFORM_TEX计算放在顶点着色器结果通过插值传递给片段着色器是标准且高效的做法。这避免了在每个像素上都进行相同的计算。6.2 材质实例化与MaterialPropertyBlock当需要通过脚本为大量物体动态设置不同的Tiling/Offset时比如让一片草地中的每棵草有不同的噪声偏移直接修改每个物体的Material实例会产生大量材质实例增加内存和Draw Call。此时应优先使用MaterialPropertyBlock。// 性能更优的动态设置方式 MaterialPropertyBlock props new MaterialPropertyBlock(); Renderer renderer GetComponentRenderer(); // 设置_ST的xy分量Tiling props.SetTextureScale(_NoiseTex, new Vector2(randomTilingX, randomTilingY)); // 设置_ST的zw分量Offset props.SetTextureOffset(_NoiseTex, new Vector2(randomOffsetX, randomOffsetY)); renderer.SetPropertyBlock(props);使用MaterialPropertyBlock可以在不创建新材质实例的情况下覆盖材质的某些属性。所有使用同一材质的物体可以通过不同的PropertyBlock实现个性化的_ST值同时保持合批Batching的可能性对性能非常友好。6.3 非均匀缩放与纹理拉伸最后提一个进阶话题模型本身的缩放Scale是否会影响_ST变换答案是不会直接影响。_ST变换作用于纹理空间UV空间而模型缩放作用于模型空间。但是如果模型在建模时UV就被非均匀地展开那么应用相同的Tiling值在模型的不同部位可能会产生视觉上的拉伸。例如一个长方体正面和侧面的UV面积可能不同同样的Tiling值会导致正面和侧面的纹理密度不一致。解决方案对于需要精确纹理密度控制的情况如砖墙、地板可以考虑在建模阶段确保UV均匀分布。在Shader中使用世界空间或物体空间坐标来驱动纹理采样Triplanar Mapping等技术但这会显著增加计算量。针对不同的模型部位使用多套材质或材质属性通过顶点颜色或第二套UV来索引不同的_ST值。通过这个从理论到实战、从使用到排查、从基础到进阶的完整梳理相信你已经对Unity Shader中的_ST后缀、Tiling和Offset有了透彻的理解。记住它们本质就是一组控制纹理如何映射到模型表面的缩放和偏移参数TRANSFORM_TEX宏则是应用这组参数的快捷工具。下次在Shader中看到它们你大可以自信地说不过如此。