/ Ch03 光栅图像

第3章:光栅图像 — 零基础讲义

讲义说明
本讲义基于 Steve Marschner & Peter Shirley 所著《虎书5》第5版第3章(p.63-78)。
本章介绍图像的底层表示——像素、RGB颜色、Alpha合成。所有显示设备(LCD、打印机、相机)都是光栅设备。
本版插画采用 Guizang 材质插画风格重新绘制。

学习目标

  1. 理解光栅显示设备的原理(LCD/CRT/打印机/相机/扫描仪)
  2. 理解像素的几何含义和屏幕坐标系统
  3. 掌握 RGB 颜色模型的基本原理和加色混合
  4. 理解 Alpha 合成(Alpha Compositing)的工作原理
  5. 掌握 Gamma 校正的基本概念
  6. 了解图像存储格式(PNG/JPEG/EXR)的选择
  7. 掌握基本的图像 I/O 和像素操作

3.1 光栅设备(Raster Devices)

3.1.1 什么是光栅?先解决第一个问题

它解决什么问题?计算机生成的图像最终要显示在某个物理设备上——显示器、手机屏幕、打印机。这些设备都是怎么工作的?它们有一个共同点:都是像素阵列

光栅(Raster)= 像素的矩形阵列

"Pixel" 是 "Picture Element" 的缩写(图片元素)。所有现代显示设备本质上都是一个像素网格。

生活类比:想象一块巨大的乐高底板——你要用不同颜色的乐高颗粒拼出一幅画。每个乐高颗粒就是一个"像素"。乐高颗粒越多(分辨率越高),你的画就越精细。

那么,哪些设备是"光栅设备"?

像素网格——放大的像素阵列
图3-1:像素网格——每个像素是图像的最小单元,三个子像素(R/G/B)组成一个彩色像素
设备原理特点
LCD显示器液晶阵列,每个像素可独立发色最常见的输出设备
CRT显示器电子束扫描荧光涂层早期设备,有余光效应
打印机逐行扫描沉积墨水没有"物理像素网格",但本质是光栅
数码相机传感器感光元件网格采集光线光栅 输入 设备
扫描仪线性传感器扫描过页面光栅 输入 设备
💡 关键区分:打印机虽然没有"物理像素网格",但它通过逐行扫描在网格点上沉积墨水——本质上还是光栅格式。就像用点阵打印一张照片,虽然打印纸本身没有格子,但打印机的控制方式就是按格子来的。

3.1.2 显示分辨率与像素密度

分辨率:水平和垂直像素数。常见分辨率:

  • 1920 × 1080(Full HD)= 约200万像素
  • 3840 × 2160(4K)= 约800万像素
  • 7680 × 4320(8K)= 约3300万像素
  • 每个像素由红、绿、蓝三个子像素组成

像素密度(PPI,Pixels Per Inch):每英寸有多少像素。密度越高,肉眼越看不到单个像素。

  • 手机:~400-500 PPI("视网膜屏")
  • 电脑显示器:~100-200 PPI
  • 电视:~40-80 PPI(因为距离远)
🤔 想一想:为什么手机屏幕的PPI比电视高那么多,但看起来不如电视清晰?
因为观看距离不同!手机你拿在眼前20cm处看,电视你可能在3米外看。人眼的分辨率是按"视角"算的——一个距离远、PPI低的屏幕和一个距离近、PPI高的屏幕,在人眼视网膜上的成像可能完全一样精细。这就是为什么苹果说"视网膜屏"是综合考虑了PPI和典型观看距离的。

3.1.3 像素的纵横比

并非所有像素都是正方形——这是一个很多初学者忽略但重要的细节:

  • VGA:640×480,正方形像素(1:1)——标准
  • NTSC:非正方形像素(10:11)——老式电视标准
  • 现代显示器:几乎全部正方形像素(1:1)

为什么会有非正方形像素?老式电视的扫描线间距和行宽不同,导致像素在水平和垂直方向上物理尺寸不一样。如果显示一个正圆形,在非正方形像素的屏幕上就会显示成椭圆。现代显示器已经全部是正方形像素了,但这个历史遗留概念偶尔还会碰到。


3.2 图像、像素与几何(Images, Pixels, and Geometry)

3.2.1 像素坐标系统——一个让人容易混淆的地方

它解决什么问题?你要访问图像中的某个像素——比如第3行、第5列的像素。你需要一个坐标系统来定位它。

图像在内存中是二维数组:image[y][x]image[height][width]

注意!屏幕坐标系和数学课上的坐标系是相反的:

数学课:y 轴向上 ↗
屏幕:  y 轴向下 ↘

(0,0) ──── x ────→
  │
  y
  │
  ↓

为什么?历史原因——CRT显示器的电子束是从左上角开始,逐行向右、再逐行向下扫描的。这个"左上角为原点,y向下"的惯例被所有现代图形系统继承下来了。

3.2.2 像素是面积还是点?一个微妙的哲学问题

这取决于你从哪个角度看:

  • 采样视角:像素是"一个点的颜色样本"——就像你在一个网格上测量每个交叉点的颜色。这时像素是一个
  • 显示视角:像素是"一个小方块"——每个像素占据屏幕上的一个矩形区域,发出对应的颜色。这时像素是一块区域

两种视角都是正确的,取决于上下文。在渲染时(采样),你把像素当点;在显示时,你把像素当方块。

整数坐标 vs 浮点坐标

  • 整数坐标:像素(i, j) 表示第i列第j行的像素。简单直接,但"像素中心"在哪里?
  • 浮点坐标:像素(i, j) 的中心在 (i+0.5, j+0.5)。这个偏移很重要——我们会在第4章光线追踪中看到它的影响。

3.2.3 图像在内存中的存储

图像在内存中就是一段连续的字节序列:

R G B R G B R G B R G B ...
| pixel 0 | pixel 1 | pixel 2 | ...
格式每通道位数每像素字节数应用场景
RGB8888位3字节JPEG/PNG(最常见格式)
RGBA88888位 + Alpha4字节PNG带透明度
Float3232位浮点12字节EXR(HDR——高动态范围)
Float1616位浮点6字节EXR(半精度HDR)

计算例子:一张 1920×1080 的 RGBA8888 图像占用多少内存?

1920 × 1080 × 4 字节 = 8,294,400 字节 ≈ 7.9 MB

3.3 RGB颜色(RGB Color)

3.3.1 三基色原理

它解决什么问题?人眼有三种视锥细胞,分别对红、绿、蓝光最敏感。所以,任何颜色都可以用三种基本色光的不同强度混合来模拟。

RGB(红绿蓝)= 加色混合模型
RGB彩色立方体
图3-2:RGB彩色立方体——三个轴分别是R, G, B通道,八个角对应八种基本颜色

计算机显示器使用加色混合

  • R + G + B = 白色(三色全开 = 白)
  • 无 R/G/B = 黑色(三色全关 = 黑)
  • R + G = 黄色,R + B = 品红,G + B = 青色

生活类比——手电筒:想象你在一个黑暗的房间里,用三个手电筒分别照射红色、绿色和蓝色的光到同一面墙上:

  • 只开红色手电筒 → 墙上是红光
  • 开红色 + 绿色 → 墙上是黄色光(红+绿=黄)
  • 三个全开 → 墙上是白色

这与绘画的减色混合(颜料混合)恰好相反:

加色(显示器):黑 → 加 RGB → 白
减色(印刷):  白 → 减 CMY → 黑

3.3.2 颜色值的表示与转换

格式范围精度用途
8位整数0-255有限(256级)常见图片格式(JPEG/PNG)
浮点数0.0-1.0着色计算(渲染内部)
半精度浮点0.0-65504HDR图像(EXR格式)

关键转换——这是你每天都会用到的操作:

float r = pixel_r / 255.0f
// 8位整数 → 浮点数
float r = 128 / 255.0f = 0.502  // 约50%强度

// 浮点数 → 8位整数(存文件时)
uint8_t r_byte = (uint8_t)(clamp(r, 0.0f, 1.0f) * 255.0f)
🔑 黄金法则永远在线性空间做光照计算,最后再转sRGB输出——这是PBR(基于物理的渲染)的黄金法则。在"渲染内部"永远使用浮点0-1,只在输入/输出时做格式转换。

3.4 Alpha合成(Alpha Compositing)

3.4.1 什么是Alpha?

它解决什么问题?你想在一张背景图前面叠一张有透明区域的图片(比如一个半透明的窗口、一个带虚影的角色)。Alpha 通道就是告诉计算机"这个像素有多不透明"。

Alpha(α)= 不透明度
  • α = 1.0:完全不透明——前景完全盖住背景
  • α = 0.0:完全透明——背景完全可见
  • α = 0.5:半透明——前景和背景各贡献一半

3.4.2 Over操作——合成的基础

Alpha合成Over操作——前景叠背景
图3-3:Alpha合成Over操作——前景(半透明)叠加到背景上,产生混合效果
out = αf × cf + (1 - αf) × cb

每个符号的含义:

  • cf:前景颜色(如红色半透明层)
  • cb:背景颜色(如白色纸张)
  • αf:前景的透明度(如0.5 = 半透明)
  • out:最终显示的颜色

生活类比:在半透明红纸上放一张白纸——

  • 完全不透明(α=1.0)→ 全红(纸完全被挡住)
  • 半透明(α=0.5)→ 粉红(红+白各一半)
  • 完全透明(α=0.0)→ 白纸(红纸完全不挡)

数字例子

前景:红色 cf = (1.0, 0.0, 0.0),αf = 0.5
背景:白色 cb = (1.0, 1.0, 1.0)

out.r = 0.5 × 1.0 + (1-0.5) × 1.0 = 0.5 + 0.5 = 1.0
out.g = 0.5 × 0.0 + 0.5 × 1.0 = 0.5
out.b = 0.5 × 0.0 + 0.5 × 1.0 = 0.5

最终颜色 = (1.0, 0.5, 0.5) = 粉红色 ✓

3.4.3 预乘Alpha

很多渲染系统使用预乘Alpha格式——存储 (αR, αG, αB, α) 而不是 (R, G, B, α)。好处是合成公式更简单:

// 非预乘
out.r = αf * cf.r + (1-αf) * cb.r

// 预乘(cf已经乘以αf了)
out.r = cf.r + (1-αf) * cb.r   // 少一次乘法!

3.4.4 常见的合成模式

模式公式用途
Overαf·cf + (1-αf)·cb前景叠背景(最常见)
Under(1-αb)·cf + αb·cb背景叠前景
Addcf + cb粒子、火焰(叠加发光效果)
Multiplycf × cb暗化、遮罩
Screen1-(1-cf)×(1-cb)发光、亮化效果

3.4.5 合成的可结合性

Alpha合成是可结合的(Associative):

(A over B) over C = A over (B over C)

这意味着你可以分步合成再合并——这是分层渲染(Layer-based compositing)的理论基础。每个物体单独渲染成一层,最后在合成阶段统一组合。

🤔 想一想:如果你把三个半透明层 A, B, C 依次叠在白色背景上,但改变了合成顺序(先合A+B,结果再叠C),结果会一样吗?
由于合成的可结合性,结果是一样的。但是注意——这只是在标准的Over操作下成立。如果你用了Add模式或其他合成模式,可结合性不一定成立。

3.5 Gamma校正

3.5.1 问题的起源——CRT的物理特性

它解决什么问题?你给显示器输入 (0.5, 0.5, 0.5) 的中灰色,但显示器显示出来的物理亮度不是50%——只有大约22%。这是因为CRT显示器的物理特性:输出亮度 = 输入电压的γ次方,其中γ ≈ 2.2。

Gamma校正曲线
图3-4:Gamma校正曲线——sRGB Gamma ≈ 2.2,显示器的输出亮度与输入电压呈非线性关系
L_out = V_in ^ γ(其中 γ ≈ 2.2)

生活类比——给你的感觉:假设你在Photoshop里给一个图层设置50%的透明度,你期望看到的是"一半背景一半前景"的效果。但因为Gamma的存在,如果你在未做校正的显示器上看,实际效果只有22%的前景+78%的背景——看起来比你设想的要暗很多。

3.5.2 解决方案:存储Gamma (Encoding Gamma)

因为显示器有 Gamma = 2.2,图像在存储时需要预先做"反Gamma"(Gamma编码),使最终的显示亮度等于原始亮度:

存储值 = 物理亮度 ^ (1 / 2.2)

当显示器显示时:

实际亮度 = (存储值) ^ 2.2 = 物理亮度 ^ (1/2.2 × 2.2) = 物理亮度 ✓

逐步拆解——用一个数字例子:

你想在屏幕上显示 50% 的物理亮度(0.5)
→ 因为显示器有 Gamma = 2.2
→ 如果你直接存储 0.5,显示器实际显示 0.5^2.2 ≈ 0.218(只有22%!)
→ 所以你需要存储 0.5^(1/2.2) ≈ 0.73
→ 显示器显示 0.73^2.2 ≈ 0.5 ✓(正好是你想要的50%)

所以:文件里存的 0.73 才对应屏幕上的 50% 亮度。
这就是为什么 sRGB 图像看起来"比线性空间更亮"——因为像素值已经被"压缩"了。

3.5.3 sRGB Gamma 的精确公式

现代系统使用 sRGB 标准,不是简单的 2.2 幂函数——它在暗部使用线性段来避免数值问题:

// sRGB → 线性(解码:从文件到渲染)
c_linear = c_srgb / 12.92                     if c_srgb ≤ 0.04045
c_linear = ((c_srgb + 0.055) / 1.055)^2.4    if c_srgb > 0.04045

// 线性 → sRGB(编码:从渲染到文件)
c_srgb = c_linear * 12.92                       if c_linear ≤ 0.0031308
c_srgb = 1.055 × c_linear^(1/2.4) - 0.055      if c_linear > 0.0031308
💡 游戏开发实战:所有纹理加载时自动做 sRGB→线性转换,在 Shader 中做光照计算,最后输出时做线性→sRGB转换。大多数游戏引擎(Unity/UE)默认启用这个流程。如果你忘记做Gamma校正,你的光照计算会出问题——阴影太暗、亮部过曝、颜色偏差。
🤔 想一想:假如你在线性空间做光照计算,然后直接输出到显示器(不做Gamma校正),会看到什么?
画面整体会偏暗,特别是中间调部分。因为显示器自动应用了 Gamma=2.2,把线性值又"压缩"了一次。就好像你在照片上又加了一层暗化滤镜。正确的做法是在 Shader 最后做一次 sRGB 编码。

3.6 图像文件格式

3.6.1 常见格式对比

格式压缩AlphaHDR用途
PNG无损UI/图标/截图
JPEG有损照片/网络
GIFLZW✓(1bit)动画
BMP旧格式
EXR有损/无损HDR/特效
TGA无/RLE3D纹理

如何选择?

  • 需要透明背景 → PNG
  • 照片分享 → JPEG(体积小)
  • HDR数据 → EXR(浮点精度)
  • 简单动图 → GIF

3.6.2 JPEG压缩原理简介

RGB → YCbCr     // 把颜色分解为亮度(Y)和色度(Cb, Cr)
↓
4:2:0 降采样    // 色度分辨率降到1/4(人眼对色度变化不敏感)
↓
8×8 DCT        // 频域变换——把图像块转成频率系数
↓
量化            // 高频系数用更粗的精度存储(丢掉人眼不敏感的细节)
↓
Huffman/行程编码

为什么JPEG能做到10:1的压缩比而几乎看不出质量损失?因为它在第二步(色度降采样)和第四步(高频量化)中丢弃了人眼最不敏感的信息。这是"感知编码"(perceptual coding)的经典案例。


3.7 图像I/O编程要点

// 读取图片到内存
image = read("photo.png")
width = image.width, height = image.height
data = image.pixels    // [R,G,B,R,G,B,...] 或 [R,G,B,A,R,G,B,A,...]

// 逐个像素处理
for y in 0..height-1:
    for x in 0..width-1:
        r = image.get_pixel(x, y).r
        g = image.get_pixel(x, y).g
        b = image.get_pixel(x, y).b
        // 处理像素...
        image.set_pixel(x, y, Color(r', g', b'))

// 写入文件
write(image, "output.png")

全章总结

核心洞察:像素是图形学最基本的"原子"单位——掌握了光栅设备、RGB颜色模型、Alpha合成、Gamma校正和图像格式,就掌握了图像在计算机中存储和显示的完整链路。
概念一句话图形学应用
光栅像素阵列——所有显示/输入设备的共同基础LCD、CRT、打印机、相机
像素坐标原点在左上角,y轴向下所有图像/屏幕操作的基础
RGB加色混合—红+绿+蓝=白显示器发色原理
Alpha不透明度——α=1不透明,α=0透明图像合成、UI、特效
Over操作out = αf·cf + (1-αf)·cb前景叠背景的核心操作
Gamma校正sRGB↔线性的双向转换所有PBR渲染管线必须处理
图像格式PNG(无损透明)vs JPEG(有损照片)vs EXR(HDR)根据场景选择合适的格式
🔑 记住这一句
"渲染内部永远是线性空间(浮点0-1),输入输出时做Gamma校正——这是图形学的第一课,也是最容易被忽略的一课。"
目录 X
章节 X