ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

一个勾选框,白吃你一半的模型内存——聊聊 Read/Write Enabled

一个勾选框,白吃你一半的模型内存——聊聊 Read/Write Enabled 一个几乎每个 Unity 项目都在犯的错先做个小测试。打开你的 Unity 项目随便点开一个模型fbx看看它的导入设置里那个Read/Write Enabled是勾着的还是没勾。如果你从来没注意过这个选项那大概率——你项目里一批模型正在白白吃掉双倍的内存而你毫不知情。这个设置极其隐蔽。它不报错、不警告游戏跑起来一切正常你甚至永远不会发现它有问题。它只是安安静静地让你的每个网格在内存里存了两份然后在某天你排查内存占用时让你百思不得其解“我模型不多啊内存怎么这么高”这篇文章就来把这个坑讲透。而讲透它的过程会顺带纠正一个几乎人人都有的直觉误解——“模型都显示出来了那数据肯定得留着读吧”这个听起来天经地义的想法恰恰是错的。先说结论它到底在干什么Read/Write Enabled 开启后会让网格数据在内存里额外保留一份 CPU 可读写的副本。也就是说同一个网格的数据存了两份内存直接翻倍。Read/Write 关闭 网格数据 → 上传到 GPU 显存后CPU 内存里那份就释放掉 → 最终只留一份 → 省内存 Read/Write 开启 网格数据 → 上传到 GPU 显存后CPU 内存里那份【硬留着】 → GPU 一份 CPU 一份 → 内存翻倍看到这你可能立刻会有那个经典的疑问“等一下。模型要显示在屏幕上那肯定得有人去读它的顶点数据才能画出来啊。既然要读那份数据不就必须留着吗关掉它模型还怎么显示”这个疑问非常好因为它精准地命中了整个问题的核心误解。要解开它我们得先搞清楚一件事网格数据到底有几个读者。关键网格数据有两拨完全不同的读者一个模型的网格数据顶点坐标、三角面等理论上会被两拨完全不同的角色读取。搞混这两拨人就是所有困惑的根源。网格数据谁会来读它 读者一GPU显卡 目的把模型画到屏幕上渲染 → 这是【显示】必须的 读者二CPU你的 C# 代码 目的在代码里读取或修改顶点数据 → 这是【只有特殊需求】才用到的现在回到你的疑问。你说显示要读数据——没错但读数据的是读者一 GPU不是读者二 CPU。而Read/Write Enabled 管的是读者二 CPU能不能读。它跟显示读者一 GPU 的工作没有任何关系。这就是那层窗户纸你把两个读者当成了一个人。你以为显示 要读 CPU 读 需要 Read/Write但实际上显示是 GPU 的活走的是完全独立的另一条路。显示模型时CPU 其实早就撒手了我们把模型加载并显示的完整流程走一遍你就明白 CPU 是怎么全程置身事外的① 模型加载 → 网格数据先进内存CPU 这边 │ ② 上传给 GPU → 数据被送进显存GPU 那边 │ ③ 之后每一帧的显示 → GPU 从【自己的显存】里读数据来画 │ ④ 此时 CPU 内存里那份原始数据呢 → 对于显示来说已经完全用不到了 → Read/Write 关着的话系统就把它释放掉省下这份内存盯住第 ③④ 步。一旦数据上传到 GPU 显存之后每一帧画模型都是GPU 从它自己的显存里读——GPU 手里有自己的一份根本不需要回头找 CPU 那份。所以 CPU 内存里最初那份数据在上传给 GPU之后对显示而言就功成身退、再无价值了。既然没用了Read/Write 关着的话系统就顺手把它释放内存就省下来了。Read/Write 关数据上传 GPU 后 → CPU 那份释放 → 只剩显存一份 → 显示照常 Read/Write 开数据上传 GPU 后 → CPU 那份硬留 → 显存内存两份 → 白白翻倍所以关掉它模型就显示不出来这个担心是完全多余的。显示靠的是 GPU 显存里那份CPU 那份留不留跟显示一点关系都没有。关掉它模型该怎么显示还怎么显示你在游戏里看不出任何区别只是内存少占了一半网格数据而已。用一个类比彻底说透如果还有点绕来个类比。把网格数据想象成一份施工图纸上传给 GPU 把图纸交给施工队GPU他们照着盖楼显示 施工队自己会复印一份带在工地上 Read/Write 关 图纸交出去后你手里那份原件就不留了 → 楼照样盖显示照常你桌上少堆一份纸省内存 Read/Write 开 图纸交出去后你手里【也硬留一份原件】 → 目的是你自己随时想改图、想查图代码读写 → 但如果你根本不打算改留着就是白占桌面你的疑问相当于问“楼都盖起来了那我手里肯定得留着图纸吧”不需要啊。楼是施工队照着他们那份复印件盖的跟你手里留不留原件毫无关系。你手里那份只在你自己还想改图时才有意义。那这份 CPU 副本到底给谁用既然显示不需要它那开着 Read/Write、留着这份 CPU 数据是给谁用的给你的 C# 代码用的。而且是相当特殊的需求——只有当你想在运行时用代码直接读取或修改网格本身时CPU 手里才需要留着这份可读写的数据真正需要开 Read/Write 的场景都比较特殊 → 运行时用代码读取顶点坐标做计算 比如想精确知道网格上某个点在哪 → 运行时用代码修改网格形状 比如程序化捏脸、地形实时变形、把网格切开 → 某些需要 CPU 侧访问网格的特殊功能 比如运行时重新烘焙碰撞网格、某些网格合并操作它们的共同点是你要在代码里碰这个网格的数据。而碰这个动作是 CPU 干的所以 CPU 手里必须留一份可读写的数据——这才是 Read/Write 开着的唯一意义。现在你回头想想你项目里绝大多数模型——石头、房子、桌子、角色、道具、树、宝箱——你会在运行时用代码去读它们的顶点、改它们的形状吗不会。它们导入进来就那样摆着、动着、显示着你的代码从来不去触碰它们的网格数据。对这些模型也就是绝大多数模型那份 CPU 副本就是纯粹白留的——没有任何代码读它它就在那儿干占内存。这就是它隐蔽又常见的原因默默留着一份谁都不用的数据而你浑然不觉。顺手解决一个连带的困惑物体移动算不算读写讲到这很多人会冒出第二个疑问“我的模型在游戏里到处跑、会旋转、会缩放这难道不算在’修改’它、不需要 Read/Write 吗”不算。这又是一个容易混的点一并说清物体移动、旋转、缩放 → 改的是这个物体的【位置 / 朝向 / 大小】Transform → 网格数据本身顶点的相对位置一个字节都没变 → 不需要 Read/Write Read/Write 管的是 → 改【网格数据本身】把顶点从这挪到那让模型变形 → 这才需要 Read/Write打个比方一辆车开来开去移动车的形状没变而 Read/Write 管的是把车顶敲扁改形状本身。前者是绝大多数物体的日常不需要 Read/Write后者才是特殊需求。实践建议怎么处理它道理讲透了落到操作上① 默认就该是关的绝大多数模型都不用开。你的判断标准只有一句话“我会在运行时用代码去读取或改变这个网格本身吗”答案是不会对绝大多数模型都是→ 保持关闭白省一半网格内存。② 它在模型导入设置里随时可改很安全。这个设置不改动源文件勾错了随时能改回来。放心地关去游戏里看一眼你会发现显示毫无变化。③ 用自动化守住底线别靠人工逐个检查。项目大了靠人一个个点开检查不现实。用AssetPostprocessor在导入时按规则自动处理比如某目录下的静态道具一律关掉 Read/WritepublicclassMeshImportRule:AssetPostprocessor{voidOnPreprocessModel(){ModelImporterimporter(ModelImporter)assetImporter;// 默认一律关闭 Read/Write需要的模型再单独例外处理importer.isReadablefalse;}}让机器帮你守住这条底线比指望每个人都记得靠谱得多。总结回到开头那个勾选框。现在你应该彻底看清它了你的直觉显示模型肯定要读数据所以得开 Read/Write 真相 ① 显示模型的读是 GPU 从显存读不是 CPU 读 ② Read/Write 管的是CPU 能不能读跟显示无关 ③ 数据上传 GPU 后显示就不再需要 CPU 那份了 ④ 所以关掉它显示完全正常只是省了 CPU 那份内存 ⑤ 只有当你要用【代码】读写网格本身时才真需要开 ⑥ 绝大多数模型你根本不会用代码碰它 → 该关这个坑之所以隐蔽根源在于我们太容易把显示需要数据直觉地等同于CPU 得留着数据却忽略了显示其实是 GPU 的活用的是 GPU 自己那份。而更深一层它其实是一个通用原则的缩影别为你用不到的能力买单。Read/Write 提供的是用代码读写网格这个能力——你用不到它就没有任何理由为它付出双倍内存的代价。Unity 里这样的能力开关还有不少比如模型上用不到的骨骼、顶点色、多余的 UV 通道同理都在为用不到的能力占着内存。当你养成一个习惯——看到任何一个开关先问它到底在为谁服务我用得到吗——你就不会再被这些设置的表象绕进去也不会再在某天对着莫名其妙的内存占用抓耳挠腮了。去检查一下你项目里那些模型的 Read/Write 吧。可能有一批双份内存正等着你一键省下来。
返回列表