04 数值映射与阈值
学完本章能做什么
你将能把连续的 UV 值变成 0/1 遮罩,用遮罩选择颜色,并通过组合遮罩画出竖条、矩形和十字。你还会知道 min、max、clamp、mix 各自解决什么问题。
先看现象
打开本章示例。默认画面被一条竖直边界分成两色;拖动“横向阈值”,边界左右移动。切换到竖条、矩形、十字时,颜色并不是按顶点手工指定的:片元着色器用当前片元的 UV 算出一个遮罩,再用它选颜色。最后的“过渡”模式用连续的 0.0~1.0 值混合两色。
示例平面只有一个分段。它依然能显示清晰的图案,因为每个片元都会计算自己的 vUv。增加网格分段主要影响顶点位移,不会凭空提高这类片元图案的分辨率。
1. 把数值限制在范围内
先认识三个常用函数:
| 函数 | 含义 | 例子 |
|---|---|---|
min(a, b) |
取较小值 | min(0.3, 0.8) 得 0.3 |
max(a, b) |
取较大值 | max(0.3, 0.8) 得 0.8 |
clamp(x, low, high) |
把 x 限制在 [low, high] |
clamp(1.4, 0.0, 1.0) 得 1.0 |
clamp(x, 0.0, 1.0) 经常用于把可显示的颜色或遮罩保持在 0.0~1.0。它也可以理解成 min(max(x, low), high)。三个函数既能处理 float,也有对应的向量形式;向量形式逐分量执行。
例如,想把一段以 uThreshold 为中心、宽度约 0.25 的 UV 范围改写成 0.0~1.0,可以先线性放大,再限制范围:
float t = clamp( ( vUv.x - uThreshold ) * 4.0 + 0.5, 0.0, 1.0 );
当 vUv.x == uThreshold,结果为 0.5。每向右移动 0.25,未限制的值会增加 1.0;clamp 让范围外停留在端点。这只是一个线性过渡,边缘更柔和的 smoothstep 留到下一章。
2. 用 mix 选择或混合颜色
vec3 color = mix( darkColor, lightColor, t );
当 t = 0.0,得到 darkColor;当 t = 1.0,得到 lightColor;当 t = 0.5,得到两者的中间值。数学上是 (1.0 - t) * darkColor + t * lightColor。mix 的第三个参数并不会自动限制在 0.0~1.0;若超出范围,会产生外推结果。因此需要稳定的两色过渡时,可先用 clamp 限制 t。
到这里,我们只需要找到合适的 t,就能控制哪里出现哪种颜色。
3. step:把连续值变成开关
step(edge, x) 的第一个参数是阈值:
x < edge → 0.0
x >= edge → 1.0
注意等于阈值时返回 1.0。下面代码让左侧使用暗色,右侧使用亮色:
float mask = step( uThreshold, vUv.x );
vec3 color = mix( darkColor, lightColor, mask );
UV 的 x 从左到右增加;超过阈值的片元遮罩为 1。若想反过来,让左侧为 1,可以用 1.0 - step(uThreshold, vUv.x)。不要把参数顺序写成 step(vUv.x, uThreshold) 后仍按原来的左右关系理解。
4. 从两个阈值到一条竖带
设带子的左边界为 uThreshold - uHalfWidth,右边界为 uThreshold + uHalfWidth:
float left = step( uThreshold - uHalfWidth, vUv.x );
float right = step( uThreshold + uHalfWidth, vUv.x );
float verticalBand = left - right;
从左向右走:两条边界之前为 0 - 0 = 0;两条边界之间为 1 - 0 = 1;越过右边界后为 1 - 1 = 0。右边界点本身归于外部,这是 step 在相等时返回 1 的结果。
同理,用 vUv.y 可以得到一条横带:
float bottom = step( 0.5 - uHalfWidth, vUv.y );
float top = step( 0.5 + uHalfWidth, vUv.y );
float horizontalBand = bottom - top;
每条带的值都只有 0 或 1,因而很适合作为图形遮罩。
5. 遮罩的交集和并集
float rectangle = min( verticalBand, horizontalBand );
float cross = max( verticalBand, horizontalBand );
当两条带都为 1 时,min 才为 1,得到交集:矩形。只要任意一条带为 1,max 就为 1,得到并集:十字。对严格的 0/1 遮罩,交集也可以写成乘法 verticalBand * horizontalBand;对连续的软遮罩,乘法与 min 的过渡形状不同,下一章会看到这一点。
还可以用 1.0 - mask 取反,或用 max(maskA - maskB, 0.0) 得到“保留 A、挖去 B”的遮罩。以后画多个图形时仍会反复用这些基本操作。
6. 从遮罩到最终颜色
示例的片元着色器根据模式算出 mask,最后统一输出:
vec3 color = mix( darkColor, lightColor, mask );
gl_FragColor = vec4( color, 1.0 );
#include <colorspace_fragment>
此处 mask 控制两种颜色的混合,不等于透明度。gl_FragColor 的第四个分量仍是 1.0,整个平面仍然不透明。真正的透明混合还涉及材质设置和混合方式,后续再讲。
常见问题
- **边界左右颠倒:**核对
step(edge, x)的参数顺序,以及 UV 的 x 轴方向。 - **带子没有显示:**确认半宽大于 0,两个边界顺序正确,且阈值位于可见 UV 范围内。
- **以为
mix会自动裁剪系数:**它允许外推;需要限制时显式clamp。 - **矩形看起来只有四个角被染色:**不要用顶点颜色代替本章的片元遮罩;一分段平面也能绘制完整矩形。
- 想画软边却始终是硬边:
step只返回 0 或 1。下一章使用smoothstep。
本章小结
step 负责把连续数值切成 0/1,min、max 负责组合遮罩,mix 把遮罩转成颜色,clamp 保证数值留在目标范围。下一章会让阈值附近的变化连续,而不是突然跳变。