1纹理重复的问题为什么大地形贴图总是看起来「假」在 Unity URP 开发大型地形时即使使用高精度贴图 重复铺设后依然会呈现出明显的周期性模式。 人眼对这种规律性非常敏感会本能地感知到「这片草地/岩石在循环」。核心洞察Texture Bombing 的本质不是使用更多贴图而是对同一张贴图进行「空间扭曲」让相邻像素看到不同的采样结果。2核心原理每个像素如何混合多个采样结果Texture Bombing 的核心思想是在渲染每个像素时 不再只采样一次而是采样 2-3 次 每次使用不同的UV 偏移和旋转角度 最后将结果混合。1生成随机种子基于像素世界坐标生成伪随机数作为该像素的「身份标识」。同一个像素每次运行时得到相同的随机值保证结果可复现。random hash( floor(worldPos.xz / cellSize) )2计算采样偏移根据随机种子计算多个不同的 UV 偏移量。每个偏移指向贴图上的不同区域避免相邻像素采样相同位置。offset random * tilingFactor % textureSize3应用旋转变换为每次采样应用不同的旋转角度打破贴图的轴向对称性让草地、岩石等纹理看起来更加自然分布。uv rotate(uv, random * 2π)4加权混合结果将 2-3 次采样结果按权重混合。可以使用简单的平均值或根据偏移距离给予中心采样更高权重。result weight1 * sample1 weight2 * sample2 ...3Shader 实现HLSL 代码逐行解析以下是完整的 Texture Bombing Shader 实现基于 Unity URP 的 ShaderLibrary 和 Input 结构。 代码经过优化在保持视觉效果的同时最小化计算开销。#ifndef TEXTURE_BOMBING_INCLUDED #define TEXTURE_BOMBING_INCLUDED // // 核心参数控制 Bombing 的行为 // struct TextureBombingParams { float cellSize; // 网格单元大小影响随机性密度 float rotationStrength; // 旋转强度0无旋转1最大旋转 float offsetStrength; // UV偏移强度 int sampleCount; // 采样次数2-3次为最佳平衡 TEXTURE2D(tex); SAMPLER(samplerTex); };首先定义参数结构体包含控制 Bombing 行为的关键参数。 cellSize 决定了随机性的「粒度」 过小会导致噪点过多过大则消除重复感不明显。// // 高质量伪随机数生成器 // 基于 Hash19输出 [0,1] 范围的均匀分布随机数 // inline float Hash19(float2 p) { // 黄金角 ~2.39996323 radians用于生成准均匀分布 const float GOLDEN_RATIO 2.39996323; // 第一步将 2D 坐标映射到 1D float h dot(p, float2(127.1, 311.7)); // 第二步sin 变换 偏移破坏线性关系 h sin(h) * 43758.5453123; // 第三步取小数部分实现「回绕」 return fract(h); } // 为每个采样生成独立的随机数序列 inline float GetSampleRandom(float2 cellId, int sampleIndex) { // 不同的 sampleIndex 产生不同的偏移避免序列相关 float2 offset float2(17.3, 23.7) * sampleIndex; return Hash19(cellId offset); }Hash19 函数使用黄金角比例实现准均匀的伪随机分布。 注意第 36 行的偏移技巧不同的 sampleIndex 产生完全独立的随机序列 确保多个采样不会「串谋」。// // 2D 旋转矩阵 // 旋转是打破纹理轴向对称性的关键 // inline float2 RotateUV(float2 uv, float angle) { float s sin(angle); // sin(θ) float c cos(angle); // cos(θ) // 旋转矩阵乘法: // | cosθ -sinθ | | x | | x*cosθ - y*sinθ | // | sinθ cosθ | × | y | | x*sinθ y*cosθ | return float2(uv.x * c - uv.y * s, uv.x * s uv.y * c); } // // 核心采样函数计算单次 Bombing 采样的 UV // inline float2 GetBombingUV(float2 baseUV, float2 cellId, float random, float tiling, float rotationStrength, float offsetStrength) { float2 uv baseUV * tiling; // Step 1: 计算 UV 偏移 // 随机偏移范围: [-0.5, 0.5] * offsetStrength float2 offset (float2(random, Hash19(cellId 1)) - 0.5) * offsetStrength; uv offset; // Step 2: 应用旋转 // 旋转范围: [0, 2π] * rotationStrength float angle random * 6.28318530718 * rotationStrength; uv RotateUV(uv - 0.5, angle) 0.5; return uv; }第 67 行是关键Hash19(cellId 1) 再次调用哈希函数 生成与 random 独立的第二个随机数 用于 X 和 Y 方向的偏移。第 72 行的 6.28318530718 即 2π 覆盖完整旋转周期。// // 主函数执行 Texture Bombing 采样 // inline float4 SampleTextureBombing(Texture2D tex, SamplerState samplerTex, float2 worldPos, float tiling, float cellSize, float rotationStrength, float offsetStrength, int sampleCount) { // Step 1: 计算网格单元 ID float2 cellId floor(worldPos / cellSize); // Step 2: 生成基础 UV float2 baseUV worldPos / cellSize - cellId; // Step 3: 多次采样并混合 float4 result float4(0, 0, 0, 0); float totalWeight 0; for (int i 0; i sampleCount; i) { // 生成该次采样的随机数 float rand GetSampleRandom(cellId, i); // 计算变换后的 UV float2 bombUV GetBombingUV(baseUV, cellId, rand, tiling, rotationStrength, offsetStrength); // 计算权重越中心的采样权重越高 float weight 1.0 - length(bombUV - 0.5) * 0.5; weight max(weight, 0.1); // 防止权重为 0 // 采样贴图 float4 sample SAMPLE_TEXTURE_LOD(tex, samplerTex, bombUV, 0); // 累加加权结果 result sample * weight; totalWeight weight; } // 归一化得到最终颜色 return result / totalWeight; } #endif // TEXTURE_BOMBING_INCLUDED完整的 SampleTextureBombing 函数。 第 90 行的 floor() 确保每个网格单元有唯一的 ID。 第 107 行的权重计算给予靠近中心的采样更高权重产生更自然的过渡效果。4技术优势为什么选择 Texture Bombing贴图内存不变只使用一张原始贴图通过计算换空间不增加任何贴图内存占用性能可控2-3 次采样在现代 GPU 上开销极低可通过 sampleCount 参数调节参数可调旋转强度、偏移幅度、采样次数均可暴露为材质参数设计师可自由调节方案内存占用Draw Call视觉质量适用场景传统 Tiling低1明显重复小面积、图案随机Texture Array高1优秀大作、高端画质Texture Bombing低1良好大地形、性能敏感Stitching/Mosaic极高多优秀手动拼接特殊需求5URP 中的集成使用如何在实际项目中应用在 URP 中集成 Texture Bombing 最常见的方式是作为 Custom Pass 或修改现有的 Terrain Lit Shader。// TerrainBombingLit.shader Shader URP/TerrainBombingLit { Properties { _MainTex(Albedo, 2D) white {} _BombingCellSize(Bombing Cell Size, Range(0.1, 10.0)) 1.0 _BombingRotation(Rotation Strength, Range(0, 1)) 0.5 _BombingOffset(UV Offset Strength, Range(0, 1)) 0.3 _SampleCount(Sample Count, Int) 3 } SubShader { Tags { RenderPipeline UniversalPipeline } Pass { HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include TextureBombing.hlsl CBUFFER_START(UnityPerMaterial) float4 _MainTex_ST; float _BombingCellSize; float _BombingRotation; float _BombingOffset; int _SampleCount; CBUFFER_END TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float4 Frag(Varyings input) : SV_Target { // 使用世界坐标而非 UV确保地形缩放时行为正确 float3 worldPos TransformObjectToWorld(float3(0, 0, 0)); worldPos input.positionWS; // 实际使用顶点着色器输出的世界坐标 // 调用 Texture Bombing 采样 float4 albedo SampleTextureBombing( _MainTex, sampler_MainTex, worldPos.xz, _MainTex_ST.x, // tiling _BombingCellSize, _BombingRotation, _BombingOffset, _SampleCount ); // 后续光照计算保持不变... return albedo; } ENDHLSL } } }使用世界坐标第 41 行使用 positionWS 而非 UV 坐标确保地形缩放或平移时贴图行为一致暴露材质参数第 7-10 行的 Properties 允许设计师在 Inspector 中实时调节效果无需修改代码保持光照兼容Bombing 仅修改采样阶段albedo 结果可无缝接入 URP 标准光照管线6总结与调优建议实践中的关键参数指南参数推荐值效果说明cellSize0.5 ~ 2.0地形单元大小。值越小随机性越密值越大模式越明显rotationStrength0.3 ~ 0.7超过 0.7 后视觉收益递减建议配合 offset 使用offsetStrength0.2 ~ 0.5UV 偏移幅度。过大(0.8)会导致贴图拉伸或断裂sampleCount2 ~ 3超过 3 次后性能收益比下降2 次适合性能敏感场景最佳实践Texture Bombing 是消除大地形纹理重复感的轻量级方案。 结合法线贴图混合使用时效果更佳——不同的旋转和偏移同时应用于 albedo 和 normal map 可以进一步增强自然感。建议在项目初期就确定参数范围建立可复用的 Shader 库。