URP 渲染实战 / 第 4 章 Shader 里的数学工具箱与程序化图案 / 案例 4.8
案例 4.8域扭曲:大理石与流体纹理高阶
目标:用噪声扭曲噪声的输入坐标,从普通的 fBm 得到大理石、玛瑙、烟雾般的流动纹理。
域扭曲(Domain Warping)的思想很简单:与其对 f(p) 采样,不如对 f(p + h(p)) 采样,其中 h 是另一个噪声生成的二维位移场。原本规整的噪声被“揉皮”一样地拉扯,就出现了流体般的卷曲。这个技巧可以嵌套:
F 是 fBm,o₁、o₂、o₃ 是任意的固定偏移(让两个分量互不相关),k 是扭曲强度。这一系列做法因 Inigo Quilez 的介绍而广为人知。
Assets/Cookbook/Ch04/C04_08_DomainWarp.shader(节选:warp)
// 第 1 层:q 是一个二维“位移场”,两个分量用两个不同偏移的 fBm float2 q = 0; if (_Levels >= 1) q = float2(F(p + float2(0.0, 0.0) + t), F(p + float2(5.2, 1.3) - t)); // 第 2 层:拿被 q 扭过的坐标再求一次位移场 r float2 r = 0; if (_Levels >= 2) r = float2(F(p + _Strength * q + float2(1.7, 9.2)), F(p + _Strength * q + float2(8.3, 2.8))); // 最终:在被扭曲的坐标上采样 float2 warped = p + _Strength * ((_Levels >= 2) ? r : q); float f = F(warped) * 0.5 + 0.5;
// 第 1 层:q 是一个二维“位移场”,两个分量用两个不同偏移的 fBm vec2 q = vec2(0.0); if (_Levels >= 1.0) q = vec2(F(p + vec2(0.0, 0.0) + t), F(p + vec2(5.2, 1.3) - t)); // 第 2 层:拿被 q 扭过的坐标再求一次位移场 r vec2 r = vec2(0.0); if (_Levels >= 2.0) r = vec2(F(p + _Strength * q + vec2(1.7, 9.2)), F(p + _Strength * q + vec2(8.3, 2.8))); // 最终:在被扭曲的坐标上采样 vec2 warped = p + _Strength * ((_Levels >= 2.0) ? r : q); float f = F(warped) * 0.5 + 0.5;
Assets/Cookbook/Ch04/C04_08_DomainWarp.shader(节选:color)
// 用 f、|q|、r.x 三个量混色:位移场本身也是很好的“着色依据” half3 col = lerp(_Color1.rgb, _Color2.rgb, saturate(f * f * 2.2)); col = lerp(col, _Color4.rgb, saturate(length(q) * 0.9)); col = lerp(col, _Color3.rgb, saturate(abs(r.x) * 1.2) * step(1.5, _Levels)); col *= 0.6 + 0.6 * f;
着色部分体现了域扭曲的另一个妙处:中间产物 q、r 本身就是很好的“着色依据”。用最终值 f 决定基础色,用 |q| 混入第二种颜色,用 r.x 混入高光色,同一个位置的几个量彼此相关又不完全相同,颜色层次就自然地“缠绕”在一起,比只用一个噪声值映射颜色丰富得多。

贵两层扭曲要调用 5 次 fBm(q 两次、r 两次、最终一次),每次 5 个八度,一共 25 次 Gradient Noise、100 次哈希,每个像素都要算。这样的 Shader 适合离线生成贴图、做加载界面背景或低分辨率的特效层;用作大面积的实时材质前,请先在目标机型上测试。