本节摘要:人眼有三种视锥细胞,所以三个数字就足以表示任何颜色感受——这是 RGB 的生理学根基。本节讲 RGB 与 HSV 的互相转换、亮度计算的加权公式,以及最容易踩的伽马陷阱:在非线性空间做线性插值与光照混合,颜色会发灰发暗。承接 2.1 的帧缓冲格式,这一节决定格子里三个数的语义。
阅读完本节,你应当能够:
可见光是连续光谱,波长大约从 380 纳米到 780 纳米,理论上需要无穷多个数才能完整描述。但人眼的视锥细胞只有三种,分别对长、中、短波长敏感,任何光谱进入眼睛都被压缩成三个刺激值。这意味着一个惊人的事实:只要调配三种色光刺激三种视锥细胞到同样的比例,人眼无法区分它与原光谱——三原色混色不是工程妥协,是生理结构决定的捷径。
RGB 立方体沿三个轴张开,原点是黑,对角顶点是白;HSV 则把同一个颜色空间拧成圆柱:色相(Hue)绕圈,饱和度(Saturation)是离中心轴的距离,明度(Value)是高度。两套坐标描述同一世界,各有分工:
| 模型 | 擅长的场景 | 不擅长的场景 |
|---|---|---|
| RGB | 硬件输出、混合、帧缓冲存储 | 直观调色、按亮度过滤 |
| HSV | 拾色器、色调分离、按颜色选区 | 精确物理光照计算 |
def rgb_to_hsv(r, g, b): # 输入输出均为 0~1 浮点 mx, mn = max(r,g,b), min(r,g,b) d = mx - mn v = mx s = 0 if mx == 0 else d / mx if d == 0: h = 0 # 灰色,色相无定义 elif mx == r: h = ((g - b) / d) % 6 # 红色区间 elif mx == g: h = (b - r) / d + 2 # 绿色区间 else: h = (r - g) / d + 4 # 蓝色区间 return (h * 60, s, v) print(rgb_to_hsv(1.0, 0.5, 0.0)) # 输出: (30.0, 1.0, 1.0) —— 纯橙,色相30度,全饱和,最亮
转换的几何直觉:把 RGB 立方体沿白到黑的对角线竖起来往下看,投影是个六边形色轮,HSV 就是这个视角下的圆柱坐标。做"把画面里所有红色调暗一点"这类操作时,HSV 下的处理远比 RGB 直接——这也解释了为什么图像软件的拾色器几乎清一色用 HSV 或 HSL。
按物理能量算,等量 RGB 混出的是偏绿的灰白;人眼对绿色最敏感,对蓝色最迟钝。亮度(明度感知)的加权公式:
Y = 0.2126·R + 0.7152·G + 0.0722·B (线性空间,Rec.709 标准)
def luma(r, g, b): # 感知亮度:绿色占七成权重 return 0.2126*r + 0.7152*g + 0.0722*b print(luma(0, 1, 0), luma(0, 0, 1)) # 输出: 0.7152 0.0722 —— 同样"满格"的绿和蓝,亮度差近十倍
这个权重在把彩色图转灰度、文字对比度计算、第 6 章 Bloom 提取亮部时都要用。直接平均三个分量会得到一张对比度失真的灰度图,蓝天的亮度和草地差不多。

CRT 显示器的电压-亮度关系不是直线,大约是幂函数:输出亮度 ≈ 输入值的 2.2 次幂。为了在 CRT 上正确显示,图像标准干脆把存储值预先做了 0.45 次幂的编码——存的是亮度的"平方根"。现代 LCD/OLED 物理上是线性的,但为了兼容几十年积累的素材与习惯,整条链路仍保留这个非线性编码,称为 sRGB 空间。
陷阱在于:光照计算(乘法、点积、积分)只在物理上有意义的线性空间成立。在伽马空间做插值或混合,中间值会偏暗:
def srgb_to_linear(c): # 8位存储值 → 线性物理亮度 c = c / 255.0 return c ** 2.2 def linear_to_srgb(c): # 线性亮度 → 8位存储值 return round((max(c, 0) ** (1/2.2)) * 255) # 红蓝各半的"正确"中间色:应在线性空间取均值 mid_wrong = (200 + 50) // 2 # 直接平均:125 mid_right = linear_to_srgb((srgb_to_linear(200) + srgb_to_linear(50)) / 2) print(mid_wrong, mid_right) # 输出: 125 134 —— 伽马空间直接平均偏暗约 9 个色阶
单个像素差 9 阶看似不大,但在渐变背景、半透明混合、光照累积叠加时,误差层层放大,典型症状是暗部发闷、白色渐变中段出现脏灰色带。正确的流程是:纹理采样后先转线性空间,全部光照与混合计算在线性空间完成,最终输出前再编码回 sRGB。现代图形 API 提供 sRGB 纹理格式自动完成这个转换,省心且不占带宽。
⚠️ 常见坑:美术给的贴图素材几乎都是 sRGB 编码,数据贴图(法线图、金属度图)却是线性的——把法线图当 sRGB 采样,材质高光会整体错误。标记纹理用途再选格式,是渲染管线搭建时的必查项。
问:既然 HSV 这么直观,为什么帧缓冲不用 HSV 存储?
答:存储格式要迁就硬件与混合运算。透明混合、加法叠加、光照乘法在 RGB 空间都是分量级的线性运算,一行代码搞定;HSV 的色相是一个角度,两色混合不能简单平均角度(红 350 度与橙 10 度平均成 180 度的青色,荒谬)。正确姿势是:存算用 RGB(或线性 RGB),给人看、给人调时临时转 HSV。
问:美术说"这图发灰",多半是哪一步错了?
答:八成是伽马。要么纹理按 sRGB 采了却当线性用(画面整体偏暗发闷),要么线性计算的结果没编码直接上屏(整体泛白发灰)。排查顺序:查纹理格式标记、查渲染目标格式、查最后一步有没有做 2.2 次幂编码——三处任何一处漏掉,观感都会"说不出哪里不对但就是不对"。
格子里填什么数已定,下一节看这些数字如何被硬件变成真实的光。