5.2 内存与纹理优化


5.2 内存与纹理优化

本节摘要:纹理是显存的第一大户,一张贴图的占用等于尺寸乘通道再乘 mip 链,与画面复杂度无关、只与加载了什么有关。本节给出纹理显存速算、压缩格式选型、加载分档与释放复用四套手法,顺带处理脚本层的内存纪律。承接 5.1 的三本账(这里是带宽账),是移动端生存的必修课。

内存都花在哪

手机上跑三维应用,玩十分钟开始烫手、玩半小时闪退——这不是帧率问题,是内存问题。三维应用的内存大头从来不是代码,是资产:纹理、几何、音频、着色器缓存。其中纹理通常占一半以上,而且它有个阴险的特性:占用与"看不看得见"无关,只与"加载了没有"有关。八张 4K 贴图躺在后台,哪怕一个像素都没画,显存已经被吃掉几百兆。

先学会速算,一秒报出一张贴图的真实体量:

纹理显存占用速算口诀: 宽 乘 高 乘 每像素字节数 乘 1.33 每像素字节数按格式定: 未压缩 RGBA 4 字节 压缩格式如 KTX2 系 0.5 到 1 字节 那个 1.33 是 mip 链的附加:每级缩一半,总和多出约三分之一 例:2048 乘 2048 的 RGBA 贴图 2048 x 2048 x 4 x 1.33 约 22 兆 换压缩格式后约 3 到 6 兆:一张图省一部手机的零头

十张这样的贴图,未压缩就是 220 兆——中端手机的 WebGL 显存配额常在 256 到 512 兆之间,爆掉只是时间问题。爆掉的现象很有迷惑性:不是报错,是纹理被系统静默丢弃,画面上出现纯黑或纯白的网格。

四套手法:压、分、懒、放

:压缩纹理是唯一的"白吃午餐"。KTX2 这类 GPU 原生压缩格式,体积与显存占用同时降一个量级,画质损失在多数贴图上肉眼难辨。管线是把美术源文件离线转码成压缩格式,运行时按设备能力选变体。

// 加载端:优先要压缩变体,设备不支持时回退普通贴图 const supportsBasis = engine.textureFormatInUse?.includes("ktx") ?? false; const url = supportsBasis ? "wall.ktx2" : "wall.png"; // 源头分档:压不压按设备定

:按设备分档加载。手机要 512 的贴图,别硬塞 2048;贴图 URL 带上档位后缀,加载时按设备等级选。桌面、平板、手机三档各一套,美术导出时顺手的事,运行时收益巨大。

:延迟加载。开局用 256 的小档先把画面撑起来,玩家走近展品再换高清档——展厅类应用的标配手法,切档的瞬间做一次纹理上传,记得避开镜头正对它的时候。

:及时释放。展厅换展区、关卡切地图,旧资产的处置只有两个字:调 dispose。引擎不会替你猜"这张贴图还用不用",引用一断、内存才回:

// 关卡切换的资产处置:能放的都放,别指望引擎猜 function unloadRoom(room) { for (const mesh of room.meshes) mesh.dispose(); // 网格连同实例一起放 for (const tex of room.textures) tex.dispose(); // 贴图是显存大户,重点关照 for (const mat of room.materials) mat.dispose(); // 材质里还挂着贴图引用 // dispose 有个易漏点:材质不释放,它引用的贴图也不会真正回收 }

脚本层的内存纪律

资产之外,脚本层有一个慢性病:每帧新建对象。在帧回调里拼接字符串、创建临时向量,每秒产生几千个短命对象,垃圾回收器定期停顿全局清理——表现为"每隔几秒卡一下"的规律性尖刺。纪律是复用而非新建:

// 坏习惯:每帧新建两个向量,一秒 120 个垃圾对象 scene.onBeforeRenderObservable.add(() => { const dir = target.position.subtract(hero.position); // 新向量 const step = dir.normalize().scale(speed * dt); // 又一个 hero.position.addInPlace(step); }); // 好习惯:临时变量提到循环外,原地操作 const _dir = new BABYLON.Vector3(); scene.onBeforeRenderObservable.add(() => { hero.position.subtractToRef(target.position, _dir); // 结果写进既有向量 _dir.normalize().scaleInPlace(speed * dt); hero.position.addInPlace(_dir); // 全程零新建 });

凡带 InPlace 与 ToRef 后缀的方法都是引擎为此准备的"原地版本",性能敏感的帧回调里应当成为肌肉记忆。

⚠️ 常见坑:dispose 网格忘了 dispose 它的材质与纹理,显存只降了一点——网格是小头,纹理才是大款。处置顺序按"网格、材质、纹理"逐层放干净。

案例:展厅应用的半小时闪退

背景:博物馆展项应用在平板上运行半小时左右闪退,桌面端无恙。操作:第一步按现象定方向——"运行时长触发"指向内存累积而非帧率问题;第二步用 5.4 节的仪表盘看纹理计数,每进一个展区计数涨四十多张、离开不降——切展区只 dispose 了网格;第三步补处置,按"网格、材质、纹理"三层依次释放,并把静态贴图改为开局一次性加载、动态展品按区懒加载。结果:连跑两小时纹理计数稳定在两百张以内,闪退消失。解读:这类问题的隐蔽性在"桌面无恙"——桌面显存配额宽裕,同样的泄漏被遮住了,移动端才是内存纪律的考场。变式:若纹理计数稳定但内存仍在涨,嫌疑转向脚本层累积——事件监听器没解绑、缓存数组只进不出,拿两次堆快照对比差异就能定位。

收尾

  • 占用与可见无关:加载即付费,后台贴图照吃显存
  • 速算口诀:宽乘高乘字节乘 1.33,压缩格式降一个量级
  • 四套手法:压缩、分档、懒加载、及时释放,前两者在源头、后两者在运行时
  • 释放按层走:网格、材质、纹理逐层 dispose,材质不放下贴文不回收
  • 帧内零新建:InPlace 与 ToRef 是帧回调的标准姿势,规律性尖刺先查垃圾回收

显存管住了,还有一个"不花钱却在烧钱"的角落——静止不动的场景每帧仍在全量遍历与重算。下一节把静态世界的浪费彻底冻结。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U