Skip to main content

颜色篇

在 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. 核心差异对比

特性RGBHSB (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 是圆环),并配合 mixclamp 在不同的色相区间进行插值。

您是想在 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 或数据可视化中,我们经常需要把一个物理数值映射成颜色深度

  • 场景:比如车速表,速度越快,颜色从绿色过渡到红色。
  • 做法
    1. 拿到归一化后的速度值(0.0 到 1.0)。
    2. 将其作为 HSB 中的 Hue(色相)输入,比如从 $0.33$(绿色)变到 $0.0$(红色)。
    3. 通过 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 之间的转换函数涉及大量的 absfractclamp 运算。

  • 原则
    • 如果颜色是静态的,在 JS 端算好 RGB 传给 Uniform。
    • 如果是像素级的动态变换(如流光、噪声颜色、粒子特效),则必须在 Fragment Shader 中实时转换。

总结

在 WebGL 开发中,RGB 是目的,HSB 是手段。你会发现自己经常在 Uniform 中接收 RGB 参数,在 Shader 内部转成 HSB 做数学魔改,最后再以 RGB 的姿态交给显卡。