颜色篇
在 WebGL 开发中,RGB 和 HSB 的混合使用极其频繁,甚至可以说是高级着色器(Shader)开发的常规操。
理解 RGB 和 HSB 的最好方式,是把它们看作两种不同的坐标系:一个是给机器(屏幕)看的,另一个是给人类(直觉)看的。
1. RGB:机器的“调色板”
RGB 是加色模型,它直接对应物理硬件。像素点由红、绿、蓝三个发光二极管组成。
- 逻辑:把颜色看作一个立方体(RGB Color Cube)。
- 原点 $(0, 0, 0)$ 是黑色。
- 对角顶点 $(1, 1, 1)$ 是白色。
- 三个轴分别是 R、G、B。
- 直觉难点:如果你想让一个深紫色变得“更鲜艳一点”,在 RGB 下你很难瞬间反应出该加多少 R、减多少 B。它描述的是成分,而不是感官。
2. HSB (也叫 HSV):人类的“直觉描述”
HSB 将颜色拆解为我们大脑感知颜色的三个维度,它通常被描述为一个圆锥体或圆柱体。
- H (Hue - 色相):“是什么颜色?”
- 用角度表示(0°–360°)。0° 是红,120° 是绿,240° 是蓝。
- S (Saturation - 饱和度):“颜色有多深?”
- 从灰色(0%)到最纯的颜色(100%)。
- B (Brightness/Value - 亮度):“开了多大的灯?”
- 从全黑(0%)到最亮(100%)。
3. 核心差异对比
| 特性 | RGB | HSB (HSV) |
|---|---|---|
| 视角 | 硬件/光线合成 | 视觉/艺术感知 |
| 几何形态 | 立方体 | 圆锥体 / 圆柱体 |
| 典型用途 | GPU 渲染、Buffer 存储 | UI 设计、动态颜色变换 |
| 操作直觉 | 极低(很难盲调出特定色) | 极高(想变亮就调 B,想变灰就调 S) |
4. 开发者视角:为什么要互相转换?
在 Shader 编写或 UI 开发中,我们经常需要在两者间反复横跳:
- RGB 用于渲染:显卡最终只认 RGB。在纹理采样或输出
gl_FragColor时,必须是 RGB。 - HSB 用于逻辑:
- 做彩虹动效:只需在每帧增加
Hue的值,再转回 RGB 即可。如果用 RGB 实现,你需要写复杂的if-else来控制三个分量的消长。 - 做阴影/高光:想让一个颜色变深做阴影?保持 H 和 S 不变,直接将 B 乘以 0.7。
- 做彩虹动效:只需在每帧增加
小贴士: 虽然 HSB 比 RGB 更符合直觉,但它在感知亮度上并不完美(比如 HSB 下纯黄色和纯蓝色亮度都是 100%,但肉眼觉得黄比蓝亮得多)。如果你在做高级 HMI 界面,需要极致的视觉均匀度,可以关注一下 OKLCH 这种新型颜色空间。
5. 进阶:在 GLSL 中快速理解
在 Shader 中,你会经常看到这样的 HSB 转 RGB 函数:
vec3 hsb2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
这段代码的核心其实就是利用了 fract 产生周期性(Hue 是圆环),并配合 mix 和 clamp 在不同的色相区间进行插值。
您是想在 WebGL 程序中动态生成一套配色方案,还是想在 CSS/Canvas 层面更方便地管理颜色变量?
虽然 GPU 最终只渲染 RGB,但逻辑上我们几乎离不开 HSB。以下是开发过程中常见的几种“混合”场景:
常见场景
1. 动态颜色变换(最常见)
如果你想实现一个随时间变化的“彩虹渐变”或“呼吸灯”效果,在 RGB 空间里写逻辑会非常痛苦。
- RGB 逻辑:你需要手动计算 R、G、B 三个分量复杂的上升和下降曲线。
- HSB 混合逻辑:你只需要在 Shader 内部将颜色转换为 HSB,让
Hue(色相)随时间u_time递增,然后再转回 RGB 输出。
// 在每一帧,色相都在 0.0 到 1.0 之间循环
float h = fract(u_time * 0.1);
vec3 color = hsb2rgb(vec3(h, 0.8, 1.0));
2. 这里的“混合”通常指:数据驱动视觉
在 Web HMI 或数据可视化中,我们经常需要把一个物理数值映射成颜色深度。
- 场景:比如车速表,速度越快,颜色从绿色过渡到红色。
- 做法:
- 拿到归一化后的速度值(0.0 到 1.0)。
- 将其作为 HSB 中的
Hue(色相)输入,比如从 $0.33$(绿色)变到 $0.0$(红色)。 - 通过 HSB 转换,保证颜色的亮度(Brightness)和饱和度(Saturation)在变换过程中保持恒定,从而视觉上更统一。
3. UI 交互中的“明暗处理”
在处理材质的 Hover(悬停)效果或点击反馈时,我们往往想在原色基础上“变暗一点”或“灰一点”。
- 逻辑混合:你会从 Uniforms 传入一个基准 RGB 颜色,在 Shader 内部转成 HSB,对
B(亮度)或S(饱和度)进行乘法运算,再转回 RGB。这比直接对 RGB 分量进行加减法更能保证颜色不“发脏”。
4. 这里的“坑”:线性空间与伽马校正
在 WebGL 中混合颜色时,最容易忽视的是 Color Space 的问题。
- 现象:如果你直接在 Shader 里
mix(colorA, colorB, 0.5),结果往往比你预想的要暗。 - 原因:因为 WebGL 默认在伽马空间(Gamma Space)操作,而物理光照混合应该在线性空间(Linear Space)进行。
- 混合流程:
输入 sRGB -> 转为 Linear -> 进行
mix或 HSB 变换 -> 转回 sRGB -> 输出。
5. 性能权衡:在哪里转换?
虽然 HSB 逻辑很爽,但 HSB 和 RGB 之间的转换函数涉及大量的 abs、fract 和 clamp 运算。
- 原则:
- 如果颜色是静态的,在 JS 端算好 RGB 传给 Uniform。
- 如果是像素级的动态变换(如流光、噪声颜色、粒子特效),则必须在 Fragment Shader 中实时转换。
总结
在 WebGL 开发中,RGB 是目的,HSB 是手段。你会发现自己经常在 Uniform 中接收 RGB 参数,在 Shader 内部转成 HSB 做数学魔改,最后再以 RGB 的姿态交给显卡。