2.6 纹理:CPU 解码 vs GPU 采样 本节摘要:纹理是 GPU 上按 UV 寻址的像素仓库。CPU 负责把压缩图片解码成位、选择内部格式、上传、生成 mipmap;GPU 在片元着色器里滤波采样。两端的决策互相惩罚:CPU 上传一张未预乘、无 mip 的 4K 图,GPU 会在远景闪烁并吃带宽;GPU 开了各向异性,CPU 若只给一层 lod,红利有限。纹理不是“贴张图那么简单”,而是解码合同加采样合同。
本节摘要:纹理是 GPU 上按 UV 寻址的像素仓库。CPU 负责把压缩图片解码成位、选择内部格式、上传、生成 mipmap;GPU 在片元着色器里滤波采样。两端的决策互相惩罚:CPU 上传一张未预乘、无 mip 的 4K 图,GPU 会在远景闪烁并吃带宽;GPU 开了各向异性,CPU 若只给一层 lod,红利有限。纹理不是“贴张图那么简单”,而是解码合同加采样合同。
阅读完本节,你应当能够:
sampler2D 的步骤,并标出拷贝发生在哪浏览器把 JPEG/PNG 解成位图,这步在 CPU(或解码器线程)。texImage2D 把位拷进 GPU 纹理对象。之后着色器用 texture2D / texture 按 UV 取值。CPU 改 Image 不会自动更新纹理,合同仍是拷贝。要更新,再上传。视频纹理每帧上传,是明确的 STREAM 路径,账单在解码加拷贝,不在 FS 里那一行采样。
UV 通常作为 Attribute 进 VS,插值进 FS。GPU 用 UV 乘纹理尺寸找到纹素,再按过滤模式混合邻居。CPU 从不“取某个像素给三角形”——除非你 readPixels,那是把数据拉回来,方向反了。
文件 → 解码成位图 → texImage2D → GPU 纹理 │ UV Attribute → 插值 → FS texture 采样 ┘
纹理单元是绑定槽:activeTexture(TEXTURE0) 再 bind,然后 Uniform 采样器填 0。槽位是 CPU 状态。绑错单元,FS 采到上一张图,表现为“材质串味”。引擎的贴图槽看起来像材质字段,底下仍是单元编号。单元数量有上限,PBR 一套贴图就能占多个槽;超了要合并图集或多次绘制。这是 CPU 侧的资源分配问题。
| 过滤 | 何时用 | 视觉 | 代价 |
|---|---|---|---|
| NEAREST 放大缩小 | 像素风、UI 需锐利 | 块状 | GPU 最便宜 |
| LINEAR 无 mip | 近景尚可 | 远景闪烁混叠 | 缩小仍只看一层 |
| LINEAR_MIPMAP_LINEAR | 3D 场景默认 | 远景稳 | 多占三分之一显存级 mip 链 |
| 各向异性 | 地面、轨道视角 | 斜看更清晰 | GPU 带宽与实现相关 |
没有 mip 的高分辨率贴图在远处会闪,因为相邻像素采样跳着吃纹素。生成 mipmap 是 CPU 调用 generateMipmap(或自己上传每一层),GPU 在运行时选层。只设 LINEAR 却不建 mip,等于告诉 GPU“缩小时仍在最高层里挣扎”。
| WRAP | UV 越界 | 适合 | 不适合 |
|---|---|---|---|
| REPEAT | 取小数部分 | 砖墙、地面平铺 | 图集里的单块,会把邻居渗进来 |
| CLAMP_TO_EDGE | 夹到边 | UI、不重复的照片 | 要平铺的材质 |
| MIRRORED_REPEAT | 镜像重复 | 某些无缝图案 | 图集 |
WebGL1 对非 2 的幂纹理限制更严:REPEAT 与 mipmap 常常要求 POT。WebGL2 放宽。资产管线若仍按 1.0 出图,继续保证 POT 或用图集,少在运行时分支。这是版本轴在纹理上的落点。
| 内容 | CPU 格式注意 | GPU 采样注意 |
|---|---|---|
| 固有色 | sRGB 与线性工作流要对齐 | 与光照相乘应在线性空间 |
| 法线贴图 | 不要当普通照片乱压 | FS 里解包并与切线空间结合 |
| UI | 预乘 alpha 与混合函数匹配 | 常关 mip,NEAREST 或 LINEAR |
| 数据纹理 | NEAREST,无 sRGB | VS/FS 当查找表,不是“看的图” |
gl.bindTexture(gl.TEXTURE_2D, tex); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); gl.generateMipmap(gl.TEXTURE_2D);
上传参数里内部格式与源格式在 WebGL1 常写 RGBA 对 RGBA;WebGL2 有更细的内部格式,包括整数、浮点、sRGB。混用 1.0 习惯到 2.0 枚举,会出现 invalid enum。对照第 1.1 节:合同不同。

图集把多张小图打进一张,减少绑定切换(CPU draw 税)和采样器占用。代价是 WRAP 不能 REPEAT 到邻居,UV 要偏置。UI 与 2D 精灵几乎总该图集;平铺砖墙不该和图集混同一张,除非自己在边界留垫。
压缩纹理(ETC、ASTC、S3TC 等)让 CPU 少传未压缩位、GPU 少占显存。浏览器与 GPU 支持组合是一张矩阵,常要多份资产。引擎加载器做回退;裸写就要自己检测扩展。这是封装轴在纹理上的具体价值:不是场景图,是资产回退表。
Y 翻转:WebGL 纹理坐标原点在左下,浏览器图片原点常在左上。UNPACK_FLIP_Y_WEBGL 让 CPU 在上传时翻。法线贴图和数据纹理乱翻会把方向搞反。约定比开关更重要:全项目要么翻图像,要么翻 UV,不要有的材质翻有的不翻。
⚠️ 常见坑:把带透明的 PNG 当不预乘,却用预乘混合函数;边缘一圈黑边。或反过来。混合合同在第 3.2 节,但源像素是否预乘是 CPU 上传时就要定的。
💡 关键直觉:纹理优化先问这张图是给眼睛看的还是给公式查的。看的图要 sRGB、mip、合适过滤;查的表要 NEAREST、线性空间、禁止乱滤波。
异步加载:Image 的 decode 完成前不要上传。占位用 1x1 颜色纹理,避免 FS 采到空对象。引擎这么做不是炫技,是防止第一帧黑块或报错。裸写也应有“纹理未就绪则用默认”的 CPU 状态,而不是假设 GPU 会等网络。
在近距离、屏幕占比大时可能。物体只占 100 像素宽时,GPU 主要在低 mip 里采,4K 的高层几乎只浪费 CPU 上传与显存。对照应看屏幕覆盖,不是看资产文件夹里的分辨率数字。移动端先砍尺寸,再谈压缩格式。
立方体贴图与三维纹理把“一张 2D”扩成多张或一层层切片,CPU 上传次数变多,GPU 采样用三维或立方体坐标。细节放到第 4.3 节。本节只需承认:通道仍是“CPU 填仓、GPU 按坐标取”,坐标维度变了,分界没变。
上传前 CPU 要回答四问。Y 是否翻转?项目级约定写死,法线与数据图单独标注。是否预乘?PNG 带透明时必须与 3.2 的 blendFunc 成套。尺寸是否满足当前版本的 POT 与对齐?WebGL1 的 REPEAT+mip 对非 POT 会变黑或报错,有人当成“贴图坏了”去重压 JPEG,越压越花。对象是否已绑定到正确目标与单元?texImage2D 打在 TEXTURE_CUBE_MAP 的错误面上,要等到采样才看见接缝。四问写成上传函数的断言,比出了黑图再猜便宜。
视频与 canvas 作为纹理源,每帧上传。这是明确的 STREAM。摄像头纹理还有格式与方向问题。不要用处理 JPEG 的那套 mip 每帧 generateMipmap,除非你真要远景视频墙。UI 视频通常 LINEAR 无 mip。生成 mip 的 CPU/GPU 税每帧付一次,会比解码本身更疼。
压缩纹理加载失败时必须有未压缩回退。失败静默会导致采样到空,FS 输出黑。启动时若关键贴图未就绪,用 1x1 颜色占位,并在控制台记一次。占位色选显眼的粉,比黑更容易发现漏加载。上线再换成灰。显眼占位是 CPU 侧的可观测性,不是品味。
图集 UV 计算在 CPU:偏移加缩放。艺术家改图集布局,UV 必须重算或重导出。运行时不要试图“自动识别岛屿”。引擎的图集工具把这步放在构建期。裸写也把 UV 变换当资产,不当每帧数学。每帧算 UV 变换是把构建期工作搬进热路径。
WebGL2 有采样器对象,把过滤与包裹从纹理对象上拆下来。同一张图可以NEAREST给数据、LINEAR给预览。WebGL1 过滤写在纹理对象上,换用途就要改参数或复制。这是版本轴在纹理上的另一处红利。没迁 2.0 时,数据图与显示图分开两份纹理更安全,避免改参数改漏。
lod bias 能让 mip 选得更糊或更锐。锐了远景闪,糊了虚。先保证 mip 链存在,再谈 bias。没有链时 bias 无意义。各向异性扩展失败就保持三线性。把各向异性当必选项,地面在某手机变黑,因为扩展指令或参数越界。画质档,不是功能档。2.6 与 4.5 在此会合。
单元没绑对、采样器 Uniform 没设、内部格式失败、还在用占位却没发现。用粉占位。仍黑则输出 UV 看是否在 0 到 1。UV 全零会采到边角一像素。Y 翻转导致采到透明边也会像黑。先输出纹理颜色不管光照,确认采样通道,再乘光。
更常见是无 mip 或 min filter 不含 mip。压缩格式若 mip 链没打进资产,也会闪。先 generateMipmap 或上传完整链。仍闪再查各向异性与 lod。压缩失败回退 PNG 时不要忘了给回退链建 mip。闪是缩小采样问题,不是“JPEG 质量 80 不够”。
CLAMP 没设,或 UV 算到了像素中心之外,mip 渗到隔壁。图集岛屿要留垫,UV 缩进半像素,min filter 对 UI 可不用 mip。UI 与 3D 平铺材质不要共一张图集。渗色是 2.6 的 WRAP 与 mip 合同,不是 FS 里的 alpha 能切干净的。
纹理字节预算写死。超了降 mip 最高层或拒绝加载 4K。解码并发限 2 到 4。iOS 上过多 Image 同时 decode 会卡死主线程,像 GPU 烫。其实是 CPU。视频纹理不要进 PBR 槽当反照率除非产品就是这样。视频每帧上传,PBR 再乘一层,两端都贵。UI 图与 3D 图分文件夹约定过滤默认值,减少漏设。漏设 REPEAT 在非 POT 上变黑,查了半天着色器。默认值表比记忆可靠。粉占位保留到预发,正式再换灰。预发看见粉就是漏加载,比用户看见黑块体面。
纹理验收分三步:粉占位消失、UV 可视化合理、接上光照。一步并做会把加载失败当成光照失败。图集与平铺砖分仓,WRAP 默认不同,写在上传函数的参数而不是靠调用者记得。压缩回退链在 CI 用一台不支持 ASTC 的环境跑,失败必须看到 PNG。看不到就是假回退。4K 源图在构建期生成 1K 与 512,运行时按档位选。运行时缩放不是 WebGL 的工作,是资产管线的工作。把 4K 丢给 GPU 再靠 mip 救,上传税已经付了。付过的税在 4.7 里表现为 CPU 尖刺,常被误诊为着色器。先看上传次数与字节,再看 FS。
上传函数做成唯一入口:翻转、预乘、过滤、WRAP、mip 全部当参数,禁止散落 texParameteri。散落必漏。入口打点字节数,加载条按字节走,不要按文件个数走。一个 4K 比二十个图标还重。点文件数会让进度条骗人。骗人的进度条会在最后 1% 卡死,用户以为死机。死机其实是 CPU 解码。唯一入口让账单可见。
下一章进入仍留在固定阶段的输出合并,以及矩阵乘法该放 CPU 还是 GPU。