案例 3.2纹理采样与 Tiling/Offset入门
目标:给物体贴一张图,并让材质面板上的 Tiling、Offset 生效。
// 第 2 步:采样一张贴图,支持材质面板上的 Tiling / Offset。 Shader "Cookbook/Ch03/Textured" { Properties { [MainTexture] _BaseMap ("Base Map", 2D) = "white" {} [MainColor] _BaseColor ("Base Color", Color) = (1, 1, 1, 1) } SubShader { Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" } Pass { Name "Unlit" Tags { "LightMode"="UniversalForward" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; // 第 0 套 UV }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; // 插值器:TEXCOORDn 只是“通用槽位”的名字 }; // 纹理对象和采样器分开声明(DX11 风格),宏会替各平台展开成正确的写法 TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); // 命名为 sampler + 纹理名,Unity 会自动使用该纹理的过滤/寻址设置 float4 _BaseMap_ST; // Unity 自动填充:xy = Tiling,zw = Offset half4 _BaseColor; Varyings vert (Attributes input) { Varyings output; output.positionCS = TransformObjectToHClip(input.positionOS.xyz); // 等价于 input.uv * _BaseMap_ST.xy + _BaseMap_ST.zw output.uv = TRANSFORM_TEX(input.uv, _BaseMap); return output; } half4 frag (Varyings input) : SV_Target { half4 tex = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); return tex * _BaseColor; } ENDHLSL } } }
// 第 2 步:采样一张贴图,支持材质面板上的 Tiling / Offset。 Shader "Cookbook/Ch03/Textured" { SubShader { Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" } Pass { Name "Unlit" Tags { "LightMode"="UniversalForward" } GLSLPROGRAM // 顶点输入 texcoord0 是第 0 套 UV VARYING vec2 uv; // 插值器:HLSL 里 TEXCOORDn 只是“通用槽位”的名字,GLSL 直接按变量名对应 // GLSL 里纹理和采样器是一个对象 sampler2D,过滤/寻址设置跟着纹理走 uniform sampler2D _BaseMap; uniform vec4 _BaseMap_ST; // Unity 自动填充:xy = Tiling,zw = Offset uniform vec4 _BaseColor; #ifdef VERTEX void main() { gl_Position = TransformObjectToHClip(positionOS.xyz); // 等价于 uv * _BaseMap_ST.xy + _BaseMap_ST.zw(HLSL 的 TRANSFORM_TEX) uv = TransformTex(texcoord0.xy, _BaseMap_ST); } #endif #ifdef FRAGMENT void main() { vec4 tex = texture(_BaseMap, uv); fragColor = tex * _BaseColor; } #endif ENDGLSL } } }
和上一步相比多了三样东西:顶点输入里的 UV、Varyings 里传给片元的 UV、贴图的声明与采样。
// 纹理对象和采样器分开声明(DX11 风格),宏会替各平台展开成正确的写法 TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); // 命名为 sampler + 纹理名,Unity 会自动使用该纹理的过滤/寻址设置 float4 _BaseMap_ST; // Unity 自动填充:xy = Tiling,zw = Offset half4 _BaseColor;
Built-in 时代写 sampler2D _MainTex; 和 tex2D(_MainTex, uv),纹理和采样器捆在一起。URP 采用 DX11 以来的“纹理对象与采样器分离”模型:TEXTURE2D(_BaseMap) 声明纹理,SAMPLER(sampler_BaseMap) 声明采样器,SAMPLE_TEXTURE2D(纹理, 采样器, uv) 采样。它们都是 core 包 API/ 目录下按平台定义的宏,例如 Metal 上:
// 摘自 core 14.0.12 ShaderLibrary/API/Metal.hlsl #define TEXTURE2D(textureName) Texture2D textureName #define SAMPLER(samplerName) SamplerState samplerName #define SAMPLE_TEXTURE2D(textureName, samplerName, coord2) PLATFORM_SAMPLE_TEXTURE2D(textureName, samplerName, coord2)
用宏而不是直接写 Texture2D,是为了让同一份代码在 OpenGL ES 这类不支持分离采样器的平台上也能展开成正确的写法。另外 URP 的 Core.hlsl 会在支持的平台上重定义 SAMPLE_TEXTURE2D,自动加上全局 Mip 偏移 _GlobalMipBias,这是为了配合渲染缩放(Render Scale)和超分辨率时选到合适的 Mip 层级——这也是“别绕过宏直接调用 .Sample()”的一个理由。
采样器的名字有讲究:写成 sampler + 纹理名,Unity 会自动把这张贴图导入设置里的过滤模式(Point/Bilinear/Trilinear)和寻址模式(Repeat/Clamp)绑定上去。你也可以声明名字里带关键词的“内联采样器”,如 SAMPLER(sampler_linear_clamp),它会按名字固定使用线性过滤 + Clamp,不受贴图设置影响,适合多张贴图共用一个采样器(某些平台对采样器数量有限制)。
_ST 与 TRANSFORM_TEX
材质面板上每张贴图下面的 Tiling 和 Offset,会被 Unity 自动打包进一个名为 贴图名 + _ST 的 float4:xy 是 Tiling(Scale),zw 是 Offset(Translate),“ST”就是这两个词的首字母。只要你声明了这个变量,它就会被赋值。TRANSFORM_TEX 是 core 包 Macros.hlsl 里的一行宏:
// 摘自 core 14.0.12 ShaderLibrary/Macros.hlsl #define TRANSFORM_TEX(tex, name) ((tex.xy) * name##_ST.xy + name##_ST.zw)
为什么在顶点着色器里做而不是片元里?因为它是 UV 的线性变换,在三角形内插值前做和插值后做结果完全一样,放在顶点里每个顶点算一次,比每个像素算一次便宜。这是一条通用原则:对插值结果线性的计算,尽量前移到顶点阶段。

[MainTexture] 后,C# 的 material.mainTexture、material.mainTextureScale 就会操作 _BaseMap;SpriteRenderer、UI 等组件读取“主贴图”时也认这个标记。