解码得到的是位于主内存中的像素数组,而显示要经过 GPU。这两者之间隔着一次数据传输,以及一组由图形 API 规定的格式与尺寸约束。在图片浏览器里,把整幅大图作为一个纹理上传往往不可行,于是需要把它切分为若干瓦片分别上传与绘制。
这条路径上的每个环节都有明确的限制条件:行对齐、尺寸上限、显存容量、上传时机。理解它们有助于解释为什么大图的加载策略通常是分块的。
纹理上传路径与像素格式
像素数据从主内存到达纹理对象,通常经过这几个步骤:解码或缩放后的像素先写入一块 CPU 侧缓冲,再通过 API 提交上传,驱动把数据复制到 GPU 可访问的显存,随后纹理对象在着色器中被采样。
上传时的关键约束是行对齐。许多驱动按 4 字节或 8 字节对齐每行的起始地址,若像素格式的每像素字节数与宽度组合不能自然对齐,就需要显式声明行跨度或调整对齐参数:
const gl = canvas.getContext("webgl2");
// 每行按 1 字节对齐,适合单通道 8 位或宽度为任意值的格式
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, rowLengthInPixels); // WebGL2
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8,
width, height, 0,
gl.RGBA, gl.UNSIGNED_BYTE, pixels);
若忽略对齐,驱动会按默认行跨度读取数据,导致图像出现倾斜或错位的条纹。部分实现还要求上传缓冲的地址满足特定对齐,否则会触发一次内部复制,带来额外的带宽开销。
尺寸上限与二次幂约束
纹理尺寸受硬件能力限制,这个上限可以通过 API 查询(如 MAX_TEXTURE_SIZE),常见范围从几千像素到上万像素不等,具体取决于设备与驱动版本。超过上限的纹理无法直接创建。
早期图形硬件对纹理的长宽有二次幂(power of two)要求,成因与 mipmap 的逐级减半、以及纹理寻址用位运算实现取模有关。现代 API 已放宽这一限制,允许任意尺寸的「非二次幂」(NPOT)纹理,但某些操作(如环绕模式 REPEAT、自动生成 mipmap)在部分实现上仍要求二次幂。因此在上传前把图像补齐到二次幂尺寸,在某些实现下仍是必要的步骤,代价是显存占用上升。
分块与瓦片策略
当图像尺寸超过纹理上限,或远超屏幕可显示范围时,常见做法是把图像切成固定尺寸的瓦片(如 256×256 或 512×512),每块作为一个纹理,只上传与当前视口相交的瓦片:
| 策略 | 上传单位 | 特点 | 代价 |
|---|---|---|---|
| 整图单纹理 | 整幅图像 | 绘制调用少 | 受尺寸上限约束,显存占用高 |
| 固定瓦片 | 单个瓦片 | 可按视口增量加载 | 绘制调用增多,需要维护瓦片索引 |
| 渐进瓦片 | 瓦片加层级 | 先粗后细,支持多分辨率 | 需要多级金字塔与缓存淘汰 |
多分辨率瓦片金字塔与地图瓦片的组织方式同源,在WebGL 瓦片加载中有更完整的讨论。
纹理图集与 mipmap
把多个小图打包进一张大纹理(纹理图集)可以减少绘制调用与状态切换,代价是布局管理复杂,且图集边缘可能出现采样溢出:相邻图案的像素被插值引入。常见的缓解方式是留出内边距并在采样时限制坐标范围。
mipmap 为纹理生成一组逐级减半的副本,采样时依据屏幕导数选择层级,用来减少缩小场景下的混叠。完整金字塔的显存占用约为原图的 4/3。对瓦片而言,若某块始终以接近 1:1 的比例显示,为其生成完整 mipmap 链会带来不必要的开销,因此多数实现只对确实需要缩放的层级生成。
纹理压缩格式
纹理压缩把像素在显存中按固定大小的块编码,解码由硬件在采样时完成,因此能同时降低显存占用与带宽。多数方案以 4×4 像素为一个块独立编解码,便于随机访问:
| 格式族 | 常见块大小 | 常见每像素位数 | 是否要求二次幂尺寸 |
|---|---|---|---|
| S3TC / DXT | 4×4 | 4 或 8 | 否 |
| ETC | 4×4 | 4 或 8 | 否 |
| ASTC | 可变(4×4 起) | 可变 | 否 |
| PVRTC | 与整张纹理相关 | 2 或 4 | 通常要求 |
各方案的可用性取决于目标设备支持的扩展,编码工具与运行时的支持范围不同。具体能使用哪几种,需要以实际部署环境的扩展查询结果为依据。
双缓冲与上传时机
上传是相对昂贵的操作:一次提交可能触发同步等待或内部复制,打断绘制流水。实践中常用的做法包括:
- 在同一帧内批量提交多个瓦片,减少 API 调用次数
- 使用像素缓冲对象(PBO)等中转方式,让传输与绘制重叠
- 对即将进入视口的瓦片提前上传,避免在绘制时阻塞
- 用双缓冲思路维护两份瓦片集合,新瓦片上传完成后再切换可见集合,避免画面出现半成品
// 使用 PBO 中转上传:先写入缓冲,再解绑定后提交为纹理
const pbo = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, pbo);
gl.bufferData(gl.PIXEL_UNPACK_BUFFER, byteLength, gl.STREAM_DRAW);
gl.bufferSubData(gl.PIXEL_UNPACK_BUFFER, 0, pixels);
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, tileTexture);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, w, h,
gl.RGBA, gl.UNSIGNED_BYTE, 0);
小结
纹理上传把主内存中的像素搬到显存,路径上受行对齐、尺寸上限与格式支持三类约束。超过尺寸上限的图像需要分块,分块策略决定了加载粒度与绘制调用数量;mipmap 与纹理压缩分别从采样质量与显存带宽两个方向产生影响;上传时机则决定会不会阻塞绘制。把这几项约束放在一起,才能为不同规模的图像设计出匹配的渲染方案。