跳到主要内容
浏览器
导航
图片浏览器

GPU 纹理上传与瓦片渲染

从 CPU 像素缓冲到 GPU 纹理的传输路径、像素格式与行对齐要求、纹理尺寸上限与二次幂约束的历史成因,以及分块加载、纹理图集、mipmap、纹理压缩与上传时机的取舍。

发布于 4 分钟阅读 1549 字

解码得到的是位于主内存中的像素数组,而显示要经过 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 / DXT4×44 或 8否
ETC4×44 或 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 与纹理压缩分别从采样质量与显存带宽两个方向产生影响;上传时机则决定会不会阻塞绘制。把这几项约束放在一起,才能为不同规模的图像设计出匹配的渲染方案。

图片浏览器 · 相关文章