04 数值映射与阈值

前置知识:第 2 章的 float、vec2、vec3 和第 3 章的 UV。
本章示例:阈值与遮罩,完整源码。

学完本章能做什么

你将能把连续的 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 负责把连续数值切成 0/1,min、max 负责组合遮罩,mix 把遮罩转成颜色,clamp 保证数值留在目标范围。下一章会让阈值附近的变化连续,而不是突然跳变。

本章练习