计算机图形学 00:从一个像素到一张图像
一张图左右颠倒,错误可能只是一行数组索引;颜色发暗,原因也可能发生在写文件之前。只检查程序退出码,不能判断这两类错误。图形学实验的第一个可用结果,是一张能追查每个色块来源的图,以及一组独立于画面观感的检查。
这个实验生成一张 16×16 的非对称小图。左上、右上、左下、右下分别是红、绿、蓝、黄,内部有黑红交替的竖条纹,绿色分量向下分四级增加,坐标 (3,5) 放一个品红标记。它没有相机、光源或三维模型,全部颜色由整数坐标直接决定。
图为程序输出的 PNG 转换版,以最近邻方式放大显示。放大的方块用于检查样本排列,不能据此推断真实显示器的发光结构。原始数据也可下载:diagnostic.ppm。
像素数组先确定什么
用 (x,y) 给一个样本编号,需要先规定原点与方向。这里原点是图像左上角,横坐标向右增加,纵坐标向下增加,合法范围为 0≤x<16、0≤y<16。这些是图像存储约定,不代表今后的三维世界也必须让 Y 向下。
一个 RGB 样本保存红、绿、蓝三个通道。本篇每个通道是 0 到 255 的整数码值,0 表示相应通道的低端,255 表示高端。红色标记是 (255,0,0),不能把它写成 BGR 再期待查看器替程序纠正。
把二维图像放进一维数组时,按行排列。宽为 ,位置 (x,y) 对应的像素索引为
这里 的单位是“像素条目”。若把 RGB 三通道铺成同一个标量数组,红通道的位置才是 ,绿色与蓝色再分别加 1 和 2。像素索引、通道索引和字节偏移是三个不同量;只有通道恰好用单字节保存时,后两者的数字才会相同。
对 (3,5),像素索引为 5×16+3=83,RGB 展平后的通道索引为 249、250、251。完整图像有 256 个 RGB 条目、768 个通道值。用这些计数检查输出,比从图片是否“大致完整”推断数组是否正确更可靠。
配套程序的 Rgb8 使用 std::array<int,3>。名称表示通道的合法数值范围是 8 位,不表示对象在内存中只占三个字节;当前使用 int,便于发现越界数值。std::vector<Rgb8> 拥有像素存储,退出作用域时自动释放,无需手工 new 或 delete。访问函数返回条目的引用,因此 image.at(3,5)={255,0,255} 修改的就是数组里的那一项。
为什么用不对称的图案
如果四角颜色相同,整幅图上下翻转以后仍可能看起来正确。居中的圆和棋盘格也常有这种问题。诊断图需要让错误改变可识别的东西:四角区分翻转,条纹区分轴与通道,偏心标记区分转置。
实验里的普通样本按下式产生,其中 / 是非负整数除法:
1 | |
横坐标的奇偶性决定红通道,纵坐标所在的四行分组决定绿通道。因而 (8,8) 应为 (0,170,0);(9,8) 应为 (255,170,0)。四角和品红点随后覆盖基础图案,检查这些点时必须采用覆盖后的值。
颜色表面上能表示方向,数值也能表示方向。把写出顺序从 y=0..15 改成 y=15..0,左下蓝点就会出现在文件的第一行。这个反例不涉及投影矩阵或 GPU,可以直接从文件定位。面对更复杂的渲染错误,也应先寻找能区分候选原因的输入,而不急于加载更复杂的模型。
从整数到 PPM 文件
Netpbm 的 PPM 格式说明区分普通二进制形式 P6 和文本形式 P3。实验采用 P3,开头是:
1 | |
前三行说明类型、宽高和最大码值。后面的数字按从上到下、每行从左到右的顺序组成 RGB 三元组。文本换行主要方便阅读,不能把“文本的一行”直接当成“图像的一行”:本程序每个像素写一行,因此会有 256 行像素数据。
写出函数保留了最容易检查的形态:
1 | |
十进制字符 255 占多个文本字节,所以不能根据“256 个像素,每个三个通道”就断定 P3 文件只有 768 字节。768 是通道值的数量,实际文件还有数字字符、空格、换行与头部。P6 能减少这种文本开销,但首篇优先让数据可以直接阅读。
PPM 规范对颜色还有语义要求:严格 PPM 使用指定的非线性 BT.709 样本;实际工具中也常见 sRGB 或线性值变体。本图只验证坐标与通道,故明确称为诊断码值图,不用截图证明物理亮度正确。第 04 篇会保存独立的线性浮点数据,并区分色调映射与 sRGB 输出编码。
一张黑白各占一半的图,其码值平均可以算出 127.5;这个数字是否代表一半光强,还取决于编码。存储格式把数写对,只解决传输的一层问题。
编译并运行最小程序
完整源码位于仓库的 examples/computer-graphics/。在仓库根目录运行:
1 | |
Makefile 使用 clang++ -std=c++17 -O2 -Wall -Wextra -Wpedantic,本次实测是 Apple Clang 21、ARM64、macOS 27.0。其他符合 C++17 的编译器可以通过 make CXX=g++ 选择,但这里没有把未运行的系统列为兼容性已验证。-O2 启用优化;当前图太小,没有进行耗时或加速比比较。
make check 先构建生成程序与独立检查器,运行第 00 个实验,再检查帮助命令和非法参数。单独生成另一份文件时,使用:
1 | |
输出路径是显式参数。图像写进文件,终端只收到状态或错误,不会把“开始渲染”混到 PPM 数字中。std::ofstream 开启了写入异常,关闭时也检查失败;路径不能打开或写入失败时,程序返回非零。
程序的 try/catch 将错误信息写到标准错误流。正常返回 0 只说明这次程序没有报告失败,还需要检查文件。对大型渲染任务,这种区别尤其重要:写出成功与图像正确是不同验收条件。
检查器为什么独立读取输出
如果生成程序写完后只打印“完成 256 像素”,它可能只是打印了循环上限,实际文件却少了一行。检查器重新打开已经写出的 PPM,解析头部、读取通道、检查额外数据,并核对约定样本。预期值来自诊断图定义,不调用生成图案的函数重新生成一遍。
本次运行得到:
1 | |
其中四角检查覆盖了首像素、首行末像素、末行首像素和尾像素。品红点定位偏心标记,内部绿值检查条纹生成。检查器还验证构造宽度为零会失败,访问 x=16 和 x=-1 都会抛出越界异常。
只检查线性索引是否小于数组长度并不够。例如 (16,0) 算出的索引是 16,仍位于数组内,却错误地指向下一行开头。因此 at(x,y) 先检查两个轴的范围,再计算索引。合法存储位置不一定是合法二维坐标,这一判断也适用于纹理访问和网格索引。
检查器只实现本程序使用的无注释 P3 子集。它没有宣称能读完 Netpbm 的所有合法格式;把它当通用图像库会扩大未经验证的边界。
文件正确以后,还要检查显示
macOS 自带的 sips 在本机成功读取了生成的 PPM,并转成 PNG:
1 | |
转换保留了 16×16 尺寸。浏览器中显示 PNG,是为了让博客读者无需安装 PPM 插件即可查看。原始 PPM 与转换版一起保留,可以区分“源文件错了”和“转换或显示过程改变了画面”。
图像放大到 320×320 时,一个源样本占据 20×20 的 CSS 区域。这里指定最近邻式显示,便于数格子;若使用平滑缩放,查看器会在相邻样本之间插值,竖条纹边缘可能出现中间颜色。那些额外颜色未必存在于源数组。
PBRT 第四版的采样理论把连续图像函数、离散样本与显示重建分开讨论。数组里的一个 RGB 条目不必对应显示器的一个物理发光单元,图像缩放已经给出了反例。后续抗锯齿实验必须同时保留原始样本和显示条件。
三维图像会增加哪些步骤
同一套写文件代码可以接收不同算法给出的像素。软件光栅器从三角形出发,经过模型与相机变换、裁剪、屏幕覆盖测试、深度比较和着色,把可见样本写进数组。光线追踪器从相机样本发出射线,求最近交点,再计算表面收到和反射的光。两条路径可以使用同一个输出模块,颜色是如何得到的却不同。
几何模块负责表示与修改形状。动画模块让场景状态随时间变化;物理模拟还需要规定力、质量与积分步长。把某一帧冻结后,渲染器仍然要完成可见性和颜色计算。一个物体能移动,并不说明它的材质、遮挡或光照已经算对。
CPU 实验采用 C++17,从当前小程序累计演进。GPU 实验采用 JavaScript、WGSL 与 WebGPU,需要另外验证设备、资源布局与浏览器交互。环境预检已经在 Chrome 153 上取得非回退的 Apple/Metal 适配器,并执行一次整数计算、回读 [0,1,4,9];它证明当前浏览器可以提交这次 GPU 工作,三角形绘制仍由第 25 篇单独验收。
练习与自检
练习一:索引与单位。 一张宽 7、高 3 的 RGB 图,样本 (5,2) 的像素索引是多少?若每通道用一个字节,红通道的字节偏移是多少?将 Rgb8 改成三个 int 后还能直接复用这个字节偏移吗?
答案:像素索引为 2×7+5=19。紧密排列的单字节 RGB 数组里,红通道偏移为 3×19=57。三个 int 的对象布局不是三个单字节,不能继续把 57 当内存字节偏移;序列化代码必须按存储协议写每个通道,不应直接倾倒对象内存。
练习二:损坏文件与翻转。 把输出头部改成 16 17,像素数据仍为原来的 256 组,会发生什么?若头部不变,只把 16 行图像的次序倒过来,计数检查能否发现?应该补什么断言?
答案:17 行需要 272 组 RGB、816 个通道,实际少 48 个通道,完整解析应在结束前发现截断。行序倒置仍有 768 个通道,计数不会失败;首像素应为红色、末行首像素应为蓝色等固定位置断言才会发现。不能以某个宽容查看器仍显示部分图像判定文件有效。
| 观察到的现象 | 优先核对的量 | 当前实验的判据 |
|---|---|---|
| 画面转置或翻转 | 原点、轴方向、行序 | 四角与 (3,5) 标记 |
| 颜色通道交换 | RGB 写出次序 | 红绿蓝角点的整数值 |
| 图片末尾缺失 | 尺寸与通道数量 | 16×16×3=768 |
| 放大后出现新颜色 | 查看器重建方式 | 对照原始码值与最近邻显示 |
系列导航
当前篇为系列入口,前篇:无。下一篇:01:点、向量与矩阵怎样表达几何。
系列按 00–04 数学与图像、05–11 软件光栅、12–16 几何、17–24 光追、25–28 GPU、29–32 动画、33–35 综合实验递进,另包含 E01–E08 全部研究选修。各篇将在实际验收后加入这里;未完成篇目不冒充可访问文章。
已验收篇目:00 图像输出、01 几何变换、02 相机投影、03 采样与混叠、04 线性颜色。
参考资料与复跑记录
- Netpbm:PPM Format Specification,THE FORMAT 与 Plain PPM,核对头部、行序、通道、P3/P6 和颜色语义;查阅日期 2026-09-19。
- Ray Tracing in One Weekend:Output an Image,参考最小 C++ 图像输出与诊断路径。本文诊断图和检查器为独立设计,没有采用其渐变场景。
- PBRT 4ed:Sampling Theory,开头与 8.1.4 Understanding Pixels,区分样本、连续函数与显示单元。
配套目录:examples/computer-graphics/;原始运行输出:writing-plans/computer-graphics/evidence/00-cpu.txt;环境与浏览器能力:同目录 environment.md。本实验没有随机采样,未产生性能结论。所有文章与实验仅完成本地工作,不表示已发布。




