Skip to content

Lesson 046|Texture、Sampler、纹理格式与显存 ​

Tags: #Creator2.x #Stage04 #Texture #Sampler #MemoryDifficulty: ⭐⭐⭐⭐☆


真实问题:安装包只多了 8 MB,运行内存为什么多了 60 MB ​

美术新增几张高分辨率 PNG,压缩后总共只有 8 MB。低端机进入页面时纹理内存却大幅上涨,首次显示还卡顿。

磁盘文件只是 Texture 生命周期的一种形态:

text
压缩文件
→ 读取缓冲
→ CPU 解码像素
→ 可能进行格式转换
→ 上传 GPU Texture
→ mipmap / 对齐 / 驱动资源

磁盘压缩率不能直接推导 GPU 显存。

🎯 本课目标 ​

图片资源不只是磁盘上的 PNG。本课建立:

text
源图片
→ 导入和压缩
→ 运行时 Texture
→ GPU 纹理内存
→ Sampler 采样

二、Texture 的多个形态 ​

text
源文件:PNG / JPG 等
编辑器资源:导入配置和元数据
运行时对象:Texture
GPU 资源:显存中的纹理对象

这些对象的生命周期和内存位置不同。销毁一个 SpriteFrame 不一定立即释放 GPU Texture,因为其他对象可能仍引用它。

显示一张 PNG 时,加载峰值中可能同时存在:

text
磁盘或下载得到的压缩文件
+ 读取缓冲
+ CPU 解码后的像素
+ GPU 上传暂存数据
+ GPU Texture
+ 运行时 Texture / SpriteFrame 对象

它们不一定长期共存,但在加载和转场时可能重叠。因此“稳定后只占 16 MiB”不能证明加载瞬间也只需要 16 MiB。

释放过程也不是简单的一对一关系:

text
Sprite 不再使用 Frame
≠ Frame 一定无引用
≠ Texture 一定无引用
≠ GPU 资源立刻释放

Prefab、缓存、常驻节点、其他 SpriteFrame 和 Asset Bundle 都可能继续持有依赖。资源释放要查看引用关系和真机内存,而不是只检查是否调用了 destroy。


三、纹理内存为什么容易膨胀 ​

未压缩 RGBA 纹理可以粗略估算:

text
width × height × 4 bytes

例如 2048×2048 的 RGBA 纹理,基础数据约 16 MB,还可能有:

  • Mipmap。
  • 对齐和平台开销。
  • 解码临时内存。
  • CPU 副本。
  • GPU 上传缓冲。

实际占用应以平台工具测量为准。


四、Sampler 做什么 ​

Sampler 决定如何读取纹理:

text
Filter:放大缩小过滤
Wrap:超出范围如何处理
Mip:是否使用多级纹理

同一张 Texture 用不同采样状态,可能影响渲染状态兼容性。


五、图集的收益和代价 ​

图集把多个小图放在一张大 Texture 中:

text
多个 SpriteFrame
    ↓ 共享一张 Texture
减少纹理切换机会
提高合批可能性

代价包括:

  • 图集尺寸和显存峰值。
  • 不同生命周期资源被绑在一起。
  • 更新或加载图集时一次带入更多像素。

图集规划要考虑使用频率和页面生命周期。


六、纹理格式与平台 ​

不同平台支持的压缩格式不同,需在导入和构建阶段选择:

text
压缩率
画质
解码成本
GPU 采样支持
包体大小
显存占用

不能只按磁盘文件大小判断运行时内存。

选择格式时至少有四个不同目标:

text
下载或包体更小
CPU 解码更快
GPU 内存更小
GPU 可以直接高效采样

它们不保证同时成立。PNG 文件可能很小,但运行时仍要解码;GPU 块压缩格式可能直接采样压缩数据,却依赖设备支持并产生不同画质。必须按 Android、iOS、Web 等目标分别构建验证。


七、纹理上传的首次卡顿 ​

首次显示资源可能包含:

text
读取文件
解码
创建 GPU Texture
上传像素
创建或更新 SpriteFrame

因此“资源已经 load 完”不一定代表首次绘制完全没有成本。启动和场景切换应关注首次使用尖峰。

load 回调保证到哪一步,取决于 Creator 小版本、资源类型和平台。可靠做法是拆成两段测量:

text
开始 load → load 回调
load 回调 → 第一次实际显示完成

如果前一段稳定、后一段尖峰,说明首次使用路径仍有工作。预热要走到与正式显示足够接近的路径,但也不能把所有资源集中预热成新的启动峰值。


八、案例:大图背景和 UI 图集 ​

把所有资源放进一个超级图集可能导致:

  • 首页只显示一张图,却加载整个图集。
  • UI 和战斗资源生命周期互相绑定。
  • 显存峰值增大。
  • 场景切换释放不彻底。

应根据页面、功能和生命周期拆分图集,而不是只追求图集数量少。


九、常见误区 ​

误区一:PNG 文件多大,显存就多大 ​

GPU 纹理通常已解码,可能远大于压缩文件。

误区二:释放 SpriteFrame 就释放 Texture ​

还要看依赖和其他引用。

误区三:图集越大合批越好 ​

过大的图集会增加加载、显存和生命周期成本。

误区四:纹理压缩只影响包体 ​

它还影响解码、显存、带宽和 GPU 采样。



九、深入推导:纹理内存先做数量级计算 ​

未压缩 RGBA8888 基础级近似:

text
width × height × 4 bytes

例如 2048×2048:

text
2048 × 2048 × 4
= 16,777,216 bytes
≈ 16 MiB

开启完整 mipmap 链时,理论上再增加约三分之一;实际还受格式、对齐、平台、CPU 副本和驱动管理影响。

这个估算用于发现数量级错误,不替代真机工具。

“乘 4”来自 RGBA8888 的四个通道,每个通道 8 bit:

像素格式每像素基础数据2048×2048 基础级近似
RGBA88884 bytes16 MiB
RGB8883 bytes12 MiB
RGB565 / RGBA44442 bytes8 MiB
单通道 8 bit1 byte4 MiB

GPU 压缩纹理通常按固定像素块计算,不能继续套用“每像素四字节”,应根据目标格式的 block 大小估算并以真机验证。

完整 mipmap 为什么约多三分之一,也可以推出来:

text
基础级:1
下一层宽高各一半:1/4
再下一层:1/16
继续相加:1 + 1/4 + 1/16 + ...
极限约等于 4/3

所以 16 MiB 的基础级加完整 mipmap,理论纹理数据约为 21.3 MiB,而不是 32 MiB。临时解码、上传缓冲和 CPU 副本要另外计算。

十、Sampler 决定“怎样读纹理” ​

Sampler 相关设置主要包括:

Filter ​

Nearest 与 Linear 影响纹理放大/缩小时怎样插值。像素风常需要 Nearest,普通 UI 常使用 Linear。

Wrap ​

Clamp、Repeat 等决定 UV 超出 0~1 时怎样取样。图集通常需要谨慎处理边界,避免采到相邻区域。

Mipmap ​

缩小时从较低分辨率级采样,改善闪烁与缓存效率,但增加内存和导入/上传数据。

采样设置不同可能让两个对象无法共享完全相同的纹理状态或导致视觉差异。

Texture 和 Sampler 必须分开理解:

text
Texture 回答:像素数据存在哪里?
Sampler 回答:给定 UV 时怎样读取这些像素?

同一张 Texture 可以用 Nearest 得到硬边像素,也可以用 Linear 得到插值结果。图片没有变化,读取规则却变了;对 Renderer 来说,这仍可能是需要切换的采样状态。

十一、Creator 2.4.x 实验:文件大小不等于纹理大小 ​

图片导入设置全部通过 Creator 编辑器 Inspector 修改。替换已有图片时保留原 .meta,不要直接修改生成目录。

准备 ​

导入三张内容不同但尺寸可控的图片:

text
solid-1024.png:大面积纯色,PNG 很小
noise-1024.png:随机噪声,PNG 较大
photo-2048.png:照片类内容

实验步骤 ​

  1. 记录源文件字节大小。
  2. 在 Inspector 记录宽高、格式、Filter、Wrap 和 mipmap。
  3. 分别显示它们,记录首次显示帧和稳定纹理内存。
  4. 比较两张同为 1024×1024 的图片:PNG 大小不同,未压缩像素数量却相同。
  5. 调整导入压缩格式后通过编辑器重新导入并构建目标平台。
  6. 在真机比较画质、包体、加载时间和内存。

TextureProbe.ts ​

ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class TextureProbe extends cc.Component {
    @property(cc.Sprite)
    target: cc.Sprite = null;

    show(path: string) {
        const begin = Date.now();
        cc.resources.load(
            path,
            cc.SpriteFrame,
            (err, frame: cc.SpriteFrame) => {
                if (err) {
                    cc.error('[texture] failed', path, err);
                    return;
                }

                const texture = frame.getTexture();
                cc.log(
                    '[texture]',
                    path,
                    'loadMs=', Date.now() - begin,
                    'size=', texture.width, texture.height
                );
                this.target.spriteFrame = frame;
            }
        );
    }
}

十二、为什么 load 完成后首次显示仍可能卡 ​

加载回调完成不一定代表所有 GPU 工作都已经摊完。首次使用可能涉及:

  • 图片解码;
  • GPU Texture 创建;
  • 像素上传;
  • shader/材质首次准备;
  • Atlas 或字形纹理更新;
  • 同一帧大量资源集中提交。

预加载若只读文件、不触发实际显示路径,仍可能把上传卡顿留到首帧。需要用目标版本与平台实验确定预热边界。

十三、真实项目故障:角色立绘导致切页峰值 ​

现象 ​

旧页面立绘尚未释放,新页面同时加载三张 4K 立绘。切换中出现峰值,稳定后内存能下降,但低内存设备已经被系统终止。

根因 ​

text
旧 Texture 常驻
+ 新文件读取
+ 新解码缓冲
+ 新 GPU Texture
→ 转场峰值远高于任一页面稳定值

修复 ​

  • 根据产品体验决定先释放旧页还是先准备新页;
  • 使用过渡图和分阶段加载;
  • 降低立绘实际分辨率;
  • 采用目标平台支持的压缩格式;
  • 控制并发解码/上传;
  • 监控峰值而不只看稳定值。

十四、纹理故障诊断 ​

变模糊 ​

检查源分辨率、显示缩放、Filter、压缩格式、mipmap、设计分辨率和设备像素比。

图集边缘渗色 ​

检查 Linear Filter、Atlas padding/extrude、Wrap、UV 和缩放。

内存与估算差异很大 ​

确认纹理格式、mipmap、重复加载键、CPU 副本、RenderTexture、Atlas 实际尺寸和 Profiler 指标口径。

十五、练习、推导答案与版本边界 ​

纹理压缩格式强依赖 Web、Android、iOS 和 GPU 支持。Creator 2.4.x 导入面板配置必须在目标构建验证;不能只看编辑器预览。

  1. 4096×4096 RGBA8888 基础级约多大? 4096 × 4096 × 4 = 64 MiB,mipmap 与额外副本另算。
  2. 两张 1024 图 PNG 文件大小差十倍,GPU 内存也差十倍吗? 未必。若都解码到同一未压缩格式,基础像素内存接近。
  3. Mipmap 的收益和代价是什么? 缩小时改善采样质量/缓存行为,但增加约三分之一纹理数据并提高构建与上传成本。
  4. 为什么纹理格式要按平台配置? GPU 支持、质量、包体、解码与运行内存差异都与平台相关。

能力验收 ​

你应该能估算任意 RGBA8888 纹理的基础内存,说明 mipmap 为什么大约再增加三分之一;能区分磁盘文件、解码缓冲、运行时 Texture 与 GPU 资源;并能用真机实验验证首次显示尖峰来自读取、解码还是上传。

本课总结 ​

text
Texture 是运行时像素资源。
Sampler 决定如何采样。
图集能减少纹理切换,但会改变加载和生命周期。
磁盘大小不能直接代表显存占用。
首次纹理上传可能造成卡顿。

十一、下一课预告 ​

text
Lesson047|Blend、Depth、Stencil 与 Render State