28 · 地形与特殊纹理混合
山脚长草、山顶露石、陡坡不长草,是“用几何信息控制材质”的典型需求。本章不替换整个光照模型,而是在采样阶段混合两种基础颜色,再用同一权重控制粗糙度,让 Standard 后续完成光照。
完整可运行场景:texture-blending.js。代码在内存生成两张棋盘 CanvasTexture,不依赖网络或第三方图片;这些测试图用于清楚观察混合和 UV 重复,不追求写实。
1. 输入数据的含义
示例用 CPU 把 XZ 网格变成小山,然后 computeVertexNormals()。height 来自局部 position.y;normal.y 是单位法线和局部上方向的点积,越接近 1 越平坦,越小越陡。
// vertex 全局
varying vec2 vDemoUv;
varying vec2 vDemoTerrain;
// begin_vertex 后
vDemoUv = uv;
vDemoTerrain = vec2(position.y, normal.y);
这里自己传 UV,避免依赖内置 vMapUv 只在某些 map 宏启用时才存在。测试几何明确有 uv 属性;迁移到没有 UV 的模型必须补充 UV 或使用平面/三平面投射。
把 normal.y 存成标量并插值是一个坡度近似。它适合此处平滑、静态地形;要求片元精度的世界坡度时,应传递并归一化完整法线,在相同空间与世界上向量点积。不能用观察空间 normal.y 充当世界坡度,否则相机转动会改变草石分布。
2. 从高度与坡度合成权重
float demoRockWeight = max(
smoothstep(0.9, 1.5, vDemoTerrain.x),
1.0 - smoothstep(0.7, 0.95, vDemoTerrain.y)
);
第一项让较高处成为岩石;第二项让陡处成为岩石;max 表示任何一个条件满足就提高岩石权重。它不是概率公式,而是有明确美术含义的遮罩组合。
如果改成两项相乘,就变为“同时高而且陡才是岩石”。如果使用纹理遮罩,通常要明确使用哪个通道,遮罩属于数据纹理,应保持 NoColorSpace。
3. 采样与混合位置
在 map_fragment 后注入:
vec3 demoGrass = texture2D(uDemoGrass, vDemoUv * uDemoRepeat).rgb;
vec3 demoRock = texture2D(uDemoRock, vDemoUv * uDemoRepeat).rgb;
diffuseColor.rgb *= mix(demoGrass, demoRock, demoRockWeight);
保留内置 map_fragment,让材质未来存在 map 时仍能先应用基础贴图;本例不设置 map,所以默认 diffuse 白色被混合颜色调制。若需求是完全替换 base map,必须明确移除或绕开原采样,而不是不小心多乘一张图。
两个 CanvasTexture 标注 SRGBColorSpace。r185 WebGLRenderer 对适用的颜色纹理使用相应纹理格式完成采样解码,混合的是工作线性空间颜色,不应再盲目手写一次 sRGB 解码。自定义 sampler 不会替你猜纹理语义,必须先设置正确 colorSpace。normal、roughness、metalness 和 mask 等数据纹理不设 sRGB。
4. 与粗糙度共享遮罩
// roughnessmap_fragment 后,roughnessFactor 已定义。
roughnessFactor *= mix(0.95, 0.7, demoRockWeight);
草石颜色比例和粗糙度比例使用同一个权重,避免颜色已经是岩石,高光仍属于另一层。这里的常数是教学参数,不代表所有岩石都更光滑。后续 lights_physical_fragment 会把 roughnessFactor 组装进实际光照材质参数。
5. 为什么法线贴图不能总是照搬 mix
颜色直接 mix 有明确插值意义;法线编码在 [0,1] 纹理值里,先要解码到 [-1,1],保证两张图在相同切线基底下,再选择适当合成方法。两种互斥材质层的法线插值可以归一化混合;“底层大起伏加表面细节”通常需要重定向法线混合等方法,而不是把两个法线平均后削弱细节。
不同 UV 方向、三平面投射或不同 tangent 基底又需要额外转换。不要在 map_fragment 中随意写 normal:此处片元 normal 还未按内置链路建立。
验证与资源管理
把权重强制设为 0 和 1,先验证两张图各自能正确显示;随后仅启用高度、仅启用坡度,最后合成。把 repeat 从 1 调到 6 应增加重复,不应改变混合边界。示例用 onDispose 显式释放自定义 sampler 的纹理,避免仅 dispose 材质却留下纹理。
练习
- 将 max 改为乘法,并给出适合这种分布的真实场景。
- 添加第三层雪,设计归一化权重,避免三层相加使颜色变亮。
- 用一张线性遮罩纹理代替高度条件,规定所用通道及 colorSpace。
- 旋转地形后保持世界上向量决定坡度,列出需要改变的数据传递步骤。