GameTech/URP 渲染实战

案例 3.2纹理采样与 Tiling/Offset入门

目标:给物体贴一张图,并让材质面板上的 Tiling、Offset 生效。

Assets/Cookbook/Ch03/C03_02_Texture.shader
// 第 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、贴图的声明与采样。

Assets/Cookbook/Ch03/C03_02_Texture.shader(节选:decl)
// 纹理对象和采样器分开声明(DX11 风格),宏会替各平台展开成正确的写法
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);    // 命名为 sampler + 纹理名,Unity 会自动使用该纹理的过滤/寻址设置

float4 _BaseMap_ST;          // Unity 自动填充:xy = Tiling,zw = Offset
half4  _BaseColor;
// GLSL 里纹理和采样器是一个对象 sampler2D,过滤/寻址设置跟着纹理走
uniform sampler2D _BaseMap;

uniform vec4 _BaseMap_ST;     // Unity 自动填充:xy = Tiling,zw = Offset
uniform vec4 _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)
FTiling (1,1)Offset (0,0)01 FFFFTiling (2,2)uv × 2 → 重复两次 FFOffset (0.5,0)uv + 0.5 → 图案左移半格 寻址模式为 Repeat 时,超出 [0,1] 的 UV 会回绕;Clamp 时会拉伸边缘像素
图 3-4Tiling 缩放 UV、Offset 平移 UV。注意平移的是“采样坐标”,所以 Offset 为正时图案看起来是往反方向移动

为什么在顶点着色器里做而不是片元里?因为它是 UV 的线性变换,在三角形内插值前做和插值后做结果完全一样,放在顶点里每个顶点算一次,比每个像素算一次便宜。这是一条通用原则:对插值结果线性的计算,尽量前移到顶点阶段。

图 3-5uv_grid 贴图。左:Tiling (1,1);中:Tiling (2,2),每个面重复 2×2 次;右:Offset (0.5, 0.25)。球体的 UV 是经纬展开,所以 U 方向环绕一圈、V 方向从南极到北极
[MainTexture] 的用处加上 [MainTexture] 后,C# 的 material.mainTexture、material.mainTextureScale 就会操作 _BaseMap;SpriteRenderer、UI 等组件读取“主贴图”时也认这个标记。