简介:这套脚本专门面向Unity开发者,用于高效转换.unity3d格式文件并优化资源处理流程;资源共3个文件,包括2个JavaScript脚本和1个C#脚本,压缩包仅5KB,轻量且易于集成到现有工程。脚本主要涉及通过AssetDatabase等接口操作资源、处理序列化与反序列化逻辑,以及自动化导出与编辑器扩展辅助等功能,可帮助减少手工重复劳动;其中JavaScript部分负责自动化流程,C#部分专注编辑器集成,既能降低模型面数、压缩纹理,又能将模型、纹理、动画等分散资源合并为单一Unity3D包,便于统一加载与管理。已有336人浏览该资源,说明其在社区中具备一定参考价值;适合需要批量转换或统一资源格式的中高级开发者,使用者可将其放入Editor目录定制转换规则,结合项目需求调整逻辑,从而提升团队协作效率并降低大型游戏资源维护成本。这套脚本覆盖了常见导出流程中的关键节点,适合作为资源管线优化的参考实现,便于开发者根据项目需要快速二次扩展。
1. 先厘清“转换 .unity”到底在转什么:不是格式,是场景工程
很多初接触的人会以为 .unity 是某一种 3D 模型格式,类似 .obj、.fbx,拿来就能一键转成别的格式。实际上 .unity 是 Unity 场景的序列化文件,用文本编辑器打开能看到大量带缩进的 YAML 记录,里面存的是这个场景里的每一个 GameObject、组件、层级关系,以及指向模型、材质、贴图资源的引用。你在网络上找“转换 .unity 3D 格式脚本”,真正要解决的不是“格式转换”,而是把一个场景工程里的模型资产导出为通用格式。
这篇文章会把这件事拆开讲:先让你看到 .unity 里到底有什么,再给出两条实际能落地的转换路径——一条是在 Unity 内跑 C# 脚本批量导出 FBX,另一条是不装 Unity、直接用 Python 解析场景文件提取资源引用。两条路都用过的人会告诉你,前者可靠,后者适合快速盘点;最后集中整理转换过程中出现过的大多数问题,包括模型全丢、材质变灰、单位放大一百倍、脚本编译报错等常见失败,希望帮你少走弯路。
2. .unity 文件里有什么:为什么不能像 OBJ 那样一行命令转出来
2.1 场景文件的 YAML 骨架:编辑器里看到的东西和磁盘上“失踪”的东西
在 Unity 默认的文本序列化模式下,.unity 场景文件是可以用文本编辑器直接打开的。整个文件由多个以---开头的 YAML 文档组成,每个文档代表一个对象,头部形如--- !u!1 &10000001:
--- !u!1 &19800001 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - component: {fileID: 19800002} m_Layer: 0 m_Name: Desk_Low m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1头部里的!u!1是对象类型 ID,1 代表 GameObject;后面的&19800001是这个对象在本文件里的内部编号。你会发现这个 GameObject 本身只有名字、Layer、组件列表这些描述,没有任何“形状”数据。形状在哪?在它引用的 MeshFilter 组件里。
通常场景文件里还有这样一段:
--- !u!33 &19800002 MeshFilter: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 19800001} m_Mesh: {fileID: 4300000, guid: 8f4d6c2a0a1c3f4418cabe93d4e2b9aa, type: 3}m_Mesh后面是一个花括号引用,里面的guid才是真正网格资源的全局身份证。网格通常不在场景文件里,而是作为一个独立资源存在Assets目录下的某个.fbx、.obj或.asset文件中,与之配套的.meta文件里记录着这个 guid。也就是说,.unity 是一个连接“场景结构与具体资源”的装配图,而不是模型本身。
这解释了为什么网上的通用转换工具多半不好用:只给你一个.unity文件,不给你整个 Assets 目录和 .meta 体系,里面的 guid 引用就是一堆断链。你缺的不是“格式转换器”,是一套能把引用对回资源文件的解析逻辑,或者一个能按引用把资源重新导出的 Unity 环境。
2.2 两种易翻车的序列化情况:二进制场景与内联网格
文本序列化只是默认情况,不是唯一情况。Unity 在 Project Settings > Editor > Asset Serialization Mode 里提供 Mixed、Force Text、Force Binary 三个选项。Mixed 模式下场景、预制体默认仍是文本;一旦项目切到 Force Binary,场景文件开头是UnityFS,后面跟着一大段二进制流,文本编辑器打开全是乱码,常规解析脚本直接失效。
另一种更隐蔽的情况是“网格被内联到场景文件里”。在文本序列化模式下,部分程序化生成的 Mesh,或者从未被单独保存成资源文件的网格,会直接把顶点数组、索引数组、法线和 UV 数据写进场景 YAML 的 Mesh 对象块中。这类数据展开后可能是几千行m_Vertices、m_Indices列表,体积比普通场景大一个量级。
遇到内联网格,理论上可以纯脚本提取并重建 OBJ,但很多人的实际做法是不碰它:因为顶点数据顺序、索引类型、压缩标记、多个子网格分组这些细节,对一个大型工程来说解析成本非常高,稍有偏差导出来的模型就是破面或错位。比起自己写解析器,不如让 Unity 打开一次再导出,这个工程量差距指数量级。引用式网格才是场景文件里最常见的形态,后面给的 Python 方案也按这个形态来讲。
2.3 三条主流转换路径:编辑器脚本、资源提取、第三方工具
我接触到的实际项目里,想“转换 .unity”的人基本来自三种诉求:一是要把场景里的模型交给下游美术或外部工具;二是从别人给的工程里把可重用资源挑出来;三是在不开 Unity 的情况下统计工程里到底有哪些模型。三条诉求对应的路径完全不同。
第一条路径是 Unity 编辑器内导出,用官方 FBX Exporter 包或自写 C# 脚本,把场景里的 MeshFilter、SkinnedMeshRenderer 批量导出成 FBX。优点是转换结果带层级、材质引用和单位信息,最接近你编辑器中看到的样子;缺点是必须装对应版本的 Unity,且工程能正常打开。
第二条路径是直接解析 .unity 文件里的 YAML,把 GameObject、MeshFilter、资源 guid 提取出来,再通过 .meta 文件反向定位资源路径。这条路径不需要 Unity,适合做资产盘点、引用检查、批量改名,但因为网格数据本身不在场景文件里,直接把它当成“转换”用会非常吃力。
第三条路径是运行时导出,即在游戏运行中用代码把 MeshFilter.sharedMesh 的顶点和索引实时保存成 OBJ/PLY。适合程序化生成的模型,不适合保存原始场景的完整结构,而且运行时拿到的往往是经过蒙皮、LOD 或合批处理之后的网格,和编辑器里看到的原始资源并不完全一致。
| 转换路径 | 需要环境 | 输出质量 | 适合场景 |
|---|---|---|---|
| 编辑器脚本导出 FBX | 对应版本 Unity | 高,保留层级与材质引用 | 需要交付给外部工具链 |
| YAML 解析提取引用 | Python 等 | 只出资源清单,网格需二次处理 | 工程盘点、引用定位 |
| 运行时 Mesh 导出 | 运行中的游戏环境 | 中,只导出运行时网格 | 程序化生成内容 |
选哪条先看你能不能打开 Unity。能打开,优先走编辑器脚本,这是最不吃亏的一条路;打不开或者只想快速看一眼工程里有什么,再走 Python 解析。下面两章分别把这两条路讲透。
3. 用 Unity 编辑器脚本批量导出 FBX:核心路径与可直接抄的菜单脚本
3.1 先装 FBX Exporter 包:包管理器安装与选项确认
要做批量导出,先把 Unity 官方维护的 FBX Exporter 包装上。打开 Package Manager,点左上角 + 号选择“Add package by name”,填入com.unity.formats.fbx,让 Unity 自己拉取对应版本。装完后菜单栏里会出现Tools > Fbx Exporter相关入口,同时项目的 Package 目录下也能看到这个包。
这个包解决的问题是:Unity 本身不提供从场景直接导出 FBX 的菜单,但 MeshFilter、SkinnedMeshRenderer、Transform 层级这些数据它都有,只是需要一条受控路径把它们写出去。FBX Exporter 提供的就是这条路径,它能把选中的 GameObject 连同子层级一起导出,并生成模型文件、连接资源引用。如果你在某个工程里找不到这个包,也可以打开 Package Manager 看 Installed 列表里是否已被项目级依赖移除,重新安装不需要改动业务代码。
剩下两个准备工作:确认当前打开的是目标场景,确认导出目录存在。因为批量导出脚本是照着当前激活场景来枚举物体的,场景开错,导出结果就会张冠李戴。我的习惯是先把场景文件在 Project 面板双击打开一次,再用脚本导出。
3.2 一个能导出整场景模型的 C# 菜单脚本
把下面这段代码放到Assets/Editor/ExportSceneFBX.cs,等待编译通过后,Unity 菜单栏最顶层会多出一个Tools菜单,里面有一项“Export Scene Models to FBX”。
// ExportSceneFBX.cs // 放在 Assets/Editor 目录下,Unity 才会在编辑器模式下编译它 using System.IO; using System.Collections.Generic; using UnityEngine; using UnityEditor; using UnityEditor.Formats.Fbx.Exporter; public static class ExportSceneFBX { [MenuItem("Tools/Export Scene Models to FBX")] public static void ExportAllActiveSceneMesh() { string exportDir = "Assets/ExportedFBX"; if (!Directory.Exists(exportDir)) Directory.CreateDirectory(exportDir); // 拿到当前激活场景的所有根物体 GameObject[] roots = UnityEngine.SceneManagement.SceneManager .GetActiveScene().GetRootGameObjects(); List<GameObject> exportList = new List<GameObject>(); HashSet<string> usedNames = new HashSet<string>(); foreach (GameObject root in roots) { // 第二个参数传 true,会把隐藏物体也带进来; // 改成 false 会漏掉场景里被隐藏的参考模型 foreach (Transform t in root.GetComponentsInChildren<Transform>(true)) { GameObject go = t.gameObject; MeshFilter mf = go.GetComponent<MeshFilter>(); SkinnedMeshRenderer smr = go.GetComponent<SkinnedMeshRenderer>(); // 两类网格组件都判断,缺一个就会漏模型 bool hasMesh = (mf != null && mf.sharedMesh != null) || (smr != null && smr.sharedMesh != null); if (!hasMesh) continue; // 需要按 Layer 过滤时,打开下面这行并改成你的 Layer 名 // if (go.layer != LayerMask.NameToLayer("Model")) continue; exportList.Add(go); } } foreach (GameObject go in exportList) { string baseName = SanitizeName(go.name); string path = Path.Combine(exportDir, baseName + ".fbx"); // 同名物体加实例ID后缀,避免互相覆盖 if (usedNames.Contains(baseName)) path = Path.Combine(exportDir, baseName + "_" + go.GetInstanceID() + ".fbx"); usedNames.Add(baseName); // ModelExporter.ExportObject 会把整个 GameObject 连同子层级导出为 FBX try { ModelExporter.ExportObject(path, go); Debug.Log("Exported: " + path, go); } catch (System.Exception e) { Debug.LogError("Export failed: " + go.name + " : " + e.Message); } } AssetDatabase.Refresh(); Debug.Log("Export finished. Count=" + exportList.Count); } private static string SanitizeName(string name) { string clean = string.IsNullOrEmpty(name) ? "Untitled" : name; foreach (char c in Path.GetInvalidFileNameChars()) clean = clean.Replace(c, '_'); return clean; } }这段脚本做了四件事:拿到当前场景所有根物体;递归查找带网格的物体;把所有符合条件的目标收集起来;逐个调用ModelExporter.ExportObject导出 FBX。这里最关键的是“递归查找”和“共享网格判空”两处,很多遗漏模型的问题都出在这两个判断上。
参数说明:
GetRootGameObjects()只返回场景根节点。根节点本身通常没有网格,所以用GetComponentsInChildren向下找。GetComponentsInChildren<Transform>(true)的第二个参数true表示包含 inactive 状态物体。场景里用于碰撞体、隐藏参考的模型经常是灰的,漏掉它们会让导出数量和你肉眼看到的不一致。- 判断
hasMesh时同时检查MeshFilter和SkinnedMeshRenderer,是因为角色模型通常走 SkinnedMeshRenderer,而这正是脚本最容易漏的一类。 ModelExporter.ExportObject(path, go)是包提供的主要导出入口,第二个参数传go时,导出内容包含它的全部子物体、材质槽位和 Transform 层级。- 同名物体通过
GetInstanceID()加后缀,避免多个同名物体互相覆盖,导出后人工重命名即可。
如果只想导出选中物体而不是整场景,可以把循环部分换成Selection.gameObjects,配合[MenuItem("Tools/Export Selected to FBX")]和[MenuItem("Tools/Export Selected to FBX", true)]做菜单灰显校验,逻辑不变。
3.3 把脚本挂到菜单项:参数与命名冲突处理
菜单入口由MenuItem特性决定,写在Tools下是为了避免和 Unity 内置菜单混在一起。脚本编译后无需额外配置,点击菜单即可执行。如果菜单里看不到,先确认脚本确实在Assets/Editor下,再检查 Console 窗口有没有编译错误。
自动化场景里,另一个常见做法是用命令行批量执行。在构建机上,脚本的执行路径可以写成这样:
/path/to/Unity -batchmode -nographics -quit \ -projectPath /path/to/project \ -executeMethod ExportSceneFBX.ExportAllActiveSceneMesh命令行执行时有一个陷阱:-batchmode不会自动加载任何场景,直接调用脚本时GetActiveScene()往往是空场景,导出的结果自然是 0 个模型。我一般会在脚本入口最前面加一句显式加载,或者先确认场景已经被打开。这个问题会在第五章详细展开。
4. 不装 Unity 能转吗:解析 .unity 场景文件,提取网格引用的边角方案
4.1 先把场景文件切成一段一段:流式 YAML 的对象边界
不开 Unity 确实也能处理 .unity 文件,但前提是你得接受一个现实:你能拿到的是资源引用清单,不是现成的网格二进制。整个思路是先按对象的文档边界把 YAML 拆开,再分别提取每个对象里的引用字段。
Unity 场景文件是流式 YAML,对象之间用---分隔,每个对象头部带!u!<类型ID>和&<文件ID>两个标记。这种文件用 PyYAML 直接加载会报错,因为!u!1是 Unity 自定义标签,PyYAML 不认;用re.split按行切段是更稳定的办法。
另外要说明一个前提:这里切的是文本序列化的场景。如果工程把序列化模式切成了 Force Binary,你的 .unity 文件是二进制流,脚本读不出任何引用信息,这个时候只能找 Unity 环境来做转换,没有捷径。
4.2 提取 GameObject / MeshFilter / 资源 GUID 的 Python 脚本
下面这段脚本可以帮你快速盘点一个场景里的模型引用。用法是python parse_unity_scene.py xxx.unity,它会输出场景里的 GameObject 数量、带网格的 MeshFilter 数量,以及每个网格引用的资源路径。
# parse_unity_scene.py # 用法: python parse_unity_scene.py scene.unity import re import sys from pathlib import Path def split_documents(unity_text: str): """ Unity 场景文件是流式 YAML,每个对象以 '--- !u!<类型ID> &<文件ID>' 开头。 用正则切块,再识别类型,比一次性装进 yaml 库更稳。 """ parts = re.split(r'(?m)^---\s*!u!(\d+)\s+&(\d+)\s*$', unity_text) # parts 结构: [前导文本, 类型1, id1, 内容1, 类型2, id2, 内容2, ...] docs = [] for i in range(1, len(parts) - 2, 3): docs.append({ "type": int(parts[i]), "file_id": int(parts[i + 1]), "body": parts[i + 2], }) return docs def find_assets(docs): """从对象列表里挑出 GameObject 和带网格引用的 MeshFilter。""" gameobjects = [] meshfilters = [] for doc in docs: if doc["type"] == 1: # GameObject m = re.search(r'm_Name:\s*(.+)', doc["body"]) name = m.group(1).strip() if m else "Unnamed" gameobjects.append((doc["file_id"], name)) elif doc["type"] == 33: # MeshFilter m = re.search(r'm_Mesh:\s*\{fileID:\s*\d+,\s*guid:\s*([a-f0-9]+)', doc["body"]) if m: meshfilters.append({ "obj_file_id": doc["file_id"], "mesh_guid": m.group(1), }) return gameobjects, meshfilters def guid_to_asset_path(root_dir: Path, guid: str): """ 在工程目录里查 .meta 文件,找到 guid 对应的资源路径。 对大工程比较慢,做一次性盘点够用。 """ for meta in root_dir.rglob("*.meta"): text = meta.read_text(encoding="utf-8", errors="ignore") if f"guid: {guid}" in text: return meta.with_suffix("").relative_to(root_dir) return None if __name__ == "__main__": scene_path = Path(sys.argv[1]) project_root = scene_path.parent # 通常是 Assets 的上级目录 text = scene_path.read_text(encoding="utf-8", errors="ignore") docs = split_documents(text) gos, mfs = find_assets(docs) print(f"GameObject: {len(gos)}, MeshFilter with mesh: {len(mfs)}") for mf in mfs[:20]: rel = guid_to_asset_path(project_root, mf["mesh_guid"]) print("mesh_guid", mf["mesh_guid"], "->", rel)脚本的逻辑是三步:用正则把场景切成独立对象段;按类型 ID 过滤出 GameObject(1)和 MeshFilter(33);从 MeshFilter 的m_Mesh行里提取 guid,再到工程目录的.meta文件里反向查找资源路径。
几个要注意的点:
re.split的正则同时捕获类型 ID 和文件 ID,所以返回的列表里每三个元素构成一个对象段。处理超大场景时,脚本速度和内存都在可接受范围。m_Mesh行中的guid是资源全局标识,不是文件 ID。同一个网格被多个场景引用时 guid 完全相同,这正是官方资源的通用索引方式。.meta搜索用rglob,只有几百个文件的小工程没问题,上万个资源的工程可能要跑几十秒。想提速的话,可以按Assets下的目录缩小搜索范围。
4.3 没有 .meta 文件时,GUID 就是找不到的黑匣子
这套解析方案有一个硬前提:工程里的资源文件旁边必须有对应的.meta文件。.meta是 Unity 给每个资源分配身份的地方,里面存放guid: xxxx一行;如果别人发工程前把.meta文件删了,或者压缩包只带了场景文件没带资源目录,那么 MeshFilter 里的 guid 就成了一个无法对回实际文件的孤值。
我曾经遇到过 A 同学传来一个“遗失了 meta”的工程,场景文件倒是齐的,所有模型路径全部解析不出来。最终解决办法是让他把整个Assets目录重新打包,确认.meta文件都在,再重新跑脚本才恢复清单。这个教训不是脚本本身的缺陷,而是 Unity 资源体系的特性:场景与资源之间的连接依赖 guid,guid 依赖 .meta,任何一环缺失都会断链。
如果你是拿到的工程来自版本管理工具,也要特别小心.gitignore里把*.meta排除掉的情况。很多团队第一次配置版本管理时会忽略 meta,结果换台机器打开场景模型全红,这几乎成了 Unity 工程项目里最经典的翻车现场之一。正确的做法是保留 meta 到版本库,除非你明确知道自己在做什么。
5. 转换 .unity 的五个常见踩坑:模型丢失、材质变灰与坐标轴翻转
5.1 导出后模型全是空物体:MeshFilter 和 SkinnedMeshRenderer 分别处理
现象:点完菜单导出成功,FBX 也确实生成了,但拖进 Blender 或直接重新导入 Unity,看到的全是空节点,没有任何网格数据。
原因:导出脚本只有MeshFilter判断,没有处理角色模型用的SkinnedMeshRenderer。动画角色模型在场景里通常只有一个 SkinnedMeshRenderer,网格放在sharedMesh里,MeshFilter组件是不存在的。另一方面,如果sharedMesh为空,也会被误判成“无网格”,于是整条分支被跳过。
解决:在收集逻辑里同时判断两类组件,并且用sharedMesh != null过滤空网格。也就是第三章脚本里的:
MeshFilter mf = go.GetComponent<MeshFilter>(); SkinnedMeshRenderer smr = go.GetComponent<SkinnedMeshRenderer>(); bool hasMesh = (mf != null && mf.sharedMesh != null) || (smr != null && smr.sharedMesh != null);这段判断能覆盖场景中绝大多数静态模型和角色模型。仍然有例外,比如部分粒子系统、被 LOD Group 替换的网格、程序化创建的运行时网格,它们要么没有持久化到场景,要么不在 MeshRenderer 路径上,需要按业务单独追加导出规则。
5.2 材质全是默认色,纹理不跟着走:FBX 的纹理嵌入选项
现象:FBX 导入外部软件后,所有材质都变成统一的灰白色,贴图一张都看不到。
原因:FBX Exporter 默认不把纹理拷到导出目录,也不把纹理文件写进 FBX。FBX 格式支持相对路径引用贴图,但 Unity 导出时经常只是把材质对应的纹理路径记录下来,外部软件没有这个相对路径,自然加载不到。
解决:在 FBX Exporter 的导出设置里打开导出选项,把纹理相关选项改成“复制到导出目录”或“嵌入 FBX”,再重新导出。如果项目不允许改包设置,也可以手动把Assets下的纹理按 FBX 的引用路径复制到导出目录,并保持相对路径一致。另一种更稳妥的方式是不要只导 FBX,把贴图文件一起放进交付目录,并在说明文档里写清楚对应关系。
5.3 模型放到其他软件里巨大或翻转:单位与轴向
现象:导出的模型在原场景里只有 1.8 米高,拖到 Blender 里变成 180 米;或者模型在 Unity 里朝向前方,到 Blender 里躺平了。
原因:Unity 使用米作为单位,而 FBX 内部单位通常是厘米;Unity 使用左手坐标系、Y 轴向上,Blender 是 Z 轴向上,两个坐标系之间需要一次 90 度旋转。如果导出时没有显式指定单位换算,外部软件只能用默认值解释,结果就是巨大的模型和奇怪的朝向。
解决:在 FBX Exporter 导出选项中设置单位,常见做法是把单位设为“米”,让外部软件按米的单位导入。轴向问题在 Blender 导入时选择“Forward: -Z, Up: Y”这类转换预设,或是在 Unity 里把根节点旋转 90 度后再导出。我的习惯是导出一个已知尺寸的参考物体,比如一个 1 米见方的立方体,用它来判断单位换算是否正常,一次校准后后续结果稳定。
5.4 批处理导出结果是空的:场景没有先加载
现象:命令行-batchmode -executeMethod跑完,Console 里显示 Count=0,一个文件都没有。
原因:Unity 在批处理模式下不会自动加载任何 .unity 场景,脚本里SceneManager.GetActiveScene()拿到的是空场景或默认的 Untitled 场景。
解决:在调用导出方法之前,先用EditorSceneManager.OpenScene打开目标场景,或者在脚本入口里加入一个显式加载分支:
using UnityEditor.SceneManagement; string scenePath = "Assets/Scenes/Town.unity"; EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);把这个逻辑放在ExportAllActiveSceneMesh()最前面,命令行跑批处理就不会导出空结果。注意场景路径必须是带Assets/前缀的工程内路径,直接写绝对路径也能用,但不建议。
5.5 脚本编译报错“找不到类型或命名空间”:Editor 目录和程序集定义
现象:把脚本放进项目后,Console 立刻报CS0246: The type or namespace name 'ModelExporter' could not be found,或者报UnityEditor相关类型找不到。
原因:脚本没有放进Assets/Editor目录,或者项目存在自定义 Assembly Definition(asmdef),而脚本所在的程序集没有引用 FBX Exporter 的编辑器程序集。UnityEditor 命名空间下的 API 只允许在 Editor 程序集中使用,普通脚本目录默认不会进入 Editor 程序集。
解决:把脚本固定放在Assets/Editor下,这是最省事的做法。如果项目里有 asmdef,在 asmdef 的 Assembly Definition References 中加上 FBX Exporter 对应的编辑器程序集引用,再重新编译。判断是否已经生效,可以看菜单栏是否出现Tools/Export Scene Models to FBX,这个菜单项只有在脚本成功编译后才会出现。
6. 每次导出后必做的一个验证:用导出清单核对网格数量与体积
导出成功不等于转换完成。我会在每次批量导出后,顺手生成一份包含文件名、网格数量、三角面数和包围盒体积的导出清单,再拿这份清单和 Unity 场景里的 MeshRenderer 数量对表。这个动作能拦住九成“白干”的情况。
导出清单目前用最简单的 Python + trimesh 就能生成,FBX 文件被轻量读取后,顶点、三角面和包围盒数据都能拿到:
# 用法: python check_fbx_list.py ./ExportedFBX import sys from pathlib import Path import trimesh for fbx in Path(sys.argv[1]).glob("*.fbx"): mesh = trimesh.load(str(fbx), force="mesh") aabb = mesh.bounds[1] - mesh.bounds[0] print(fbx.stem, len(mesh.vertices), len(mesh.faces), [round(v, 2) for v in aabb.tolist()])输出里每行是一列模型:名字、顶点数、三角面数、包围盒尺寸。用这份文本和场景里的模型数量对比,数量对得上,再抽查三五个模型的体积是否符合米制预期,转换结果就基本可信。如果三角面数与 Unity 显示的面数有偏差,大概率是导出前 LOD 或网格压缩选项被勾选,需要回去检查导出设置。
我养成这个习惯是因为前几年在某项目里导出一批迭代资源,文件名全对、数量也对,结果交付时对方反馈所有模型都是默认材质的灰壳,最后定位到是导出时没拷贝纹理。从那以后我把“材质纹理清单”也并进验证步骤,输出文件夹里必须同时能看到 FBX、贴图文件清单,以及一个可读的export_report.txt。整个过程多花五分钟,但换来的是下游不用反复找你要文件和描述关系。
希望帮到你。
本文还有配套的精品资源,点击获取