简介:这是一款面向Unity开发者(尤其是中高级用户)的场景导出工具插件,解决在Unity中将复杂场景(含GameObject与Terrain)或FBX模型高效转换为通用.obj格式的需求,便于后续导入Recast Navigation进行导航网格烘焙、跨引擎协作或3D资产复用。资源包共10个文件,含4张功能截图(JPG)、3份结构化文档(README、CHANGELOG、LICENSE说明)、1个核心C#编辑器脚本(ExportScene.cs)、1个package.json配置文件及1个.gitignore,整体仅540KB,轻量易集成。已有1632人学习下载,体现其在轻量化场景资产流转中的实用价值。用户可直接获得开箱即用的Unity Editor扩展功能,支持自定义裁剪区域、自动裁剪、非正式选择导出等灵活操作,并附完整Git包管理接入方案与本地安装指引,目录结构清晰,文档覆盖用法、变更与授权,适合需快速落地OBJ导出流程的项目开发与技术验证场景。
1. ExportSceneToObj:不是简单“另存为”,而是 Unity 场景几何资产的可控剥离工具
你有没有试过在 Unity 里调好一个带地形、植被、建筑群的完整场景,想拿去 Unity 外部做离线烘焙、第三方物理仿真,或者喂给 Recast Navigation 做导航网格预处理——结果发现「导出 FBX」根本不管用?FBX 里地形是 TERRAIN 组件,Unity 不会把它转成三角面片;MeshFilter 里的模型可能缺 UV 或法线错乱;更别说材质球、贴图路径全断链。这时候 ExportSceneToObj 就不是“锦上添花”,而是绕过 Unity 渲染管线黑匣子、直取原始几何数据的硬通路。它不依赖 Editor 导出菜单,不走 AssetBundle 流程,而是在 C# 脚本层遍历 Scene Hierarchy,把 TerrainData 烘焙成顶点+索引,把 MeshFilter 的 Mesh 拆解为标准 OBJ 格式(含 v/vt/vn/f),连带支持自定义缩放、Y-up 转换、UV 反转等关键参数。适合做 Recast Navigation 的静态障碍物预处理、Unity 与 Blender/BlenderBIM 的轻量协同、或快速生成用于 Raycast 碰撞测试的简化几何体。如果你正卡在「Unity 场景 → 可被外部工具读取的纯几何 OBJ」这一步,这个工具就是你该停下来的检查点。
2. 核心原理与选型依据:为什么不用内置 FBX Exporter,而要手写 OBJ 导出器?
2.1 Unity 内置导出器的三大不可绕过限制
Unity 官方的File > Export Package或FBX Exporter插件,本质是序列化 GameObject 的组件状态,而非提取底层几何。具体表现为:
- Terrain 不可导出为 mesh:Terrain 组件在 FBX 中仅保留 transform 和 component reference,实际高度图、细节纹理、树实例全部丢失;Recast Navigation 需要的是顶点坐标+三角索引构成的封闭体,FBX 给不了。
- MeshFilter 的 Mesh 数据被封装:即使你手动调用
mesh.vertices,Unity 默认返回的是本地空间顶点(未应用 Scale/Rotation),且法线、切线、UV 坐标若未显式标记为Read/Write Enabled,运行时无法访问——而 ExportSceneToObj 在 Editor 模式下强制启用该标志,确保数据可读。 - 材质与贴图路径完全脱钩:FBX 导出后
.mat文件和贴图路径在外部引擎中 90% 挂掉,OBJ 则只存mtllib引用和usemtl指令,配合配套 MTL 文件,路径可控、结构透明。
提示:ExportSceneToObj 不是替代 FBX,而是补位——FBX 适合带动画、骨骼、材质层级的完整资产交换;OBJ 是纯几何交付的“最小可信单元”,尤其适配 Recast Navigation、Open3D、MeshLab 等对材质无感但对顶点精度敏感的工具链。
2.2 ExportSceneToObj 的三层数据提取逻辑
该工具并非简单遍历GameObject.FindObjectsOfType<MeshFilter>(),而是分三路并行提取并归一化:
| 数据源 | 提取方式 | 关键处理 | 输出到 OBJ 的字段 |
|---|---|---|---|
| Terrain | Terrain.activeTerrain.terrainData→GetHeights()+GetControlTexture() | 将 heightmap 按 resolution 采样为规则网格,双线性插值补全顶点;自动计算法线(CalculateNormalsFromHeightmap) | v x y z,vn nx ny nz,f v1//vn1 v2//vn2 v3//vn3 |
| MeshFilter | mesh.vertices,mesh.normals,mesh.uv,mesh.triangles | 应用transform.localToWorldMatrix变换顶点;对非均匀缩放做法线重归一化(Vector3.TransformNormal) | v x y z,vt u v,vn nx ny nz,f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 |
| SkinnedMeshRenderer | sharedMesh+bones矩阵逆变换 | 仅导出绑定姿态(bind pose)下的顶点,忽略动画;跳过蒙皮权重(OBJ 不支持) | 同 MeshFilter,但顶点已转换至世界空间 |
2.3 OBJ 格式选择的工程权衡:为什么不是 glTF 或 STL?
- glTF:虽为现代标准,但需完整实现
KHR_mesh_quantization、KHR_materials_pbrSpecularGlossiness等扩展,且 Unity Editor API 对 glTF 二进制序列化支持薄弱,调试成本高;而 OBJ 是纯文本,grep一眼定位顶点错误,sed批量修复 UV 偏移,开发期 debug 效率碾压。 - STL:仅存三角面片(
facet normal nx ny nz),无 UV、无法线、无顶点复用,文件体积比 OBJ 大 3–5 倍,Recast Navigation 加载时内存暴涨;OBJ 的v/vt/vn/f结构天然支持顶点索引复用,单个 terrain 2049×2049 网格导出后仅 86MB,STL 会超 400MB。 - OBJ 的真实优势:
mtllib+usemtl可精确控制每个 submesh 的材质引用;o object_name和g group_name支持按 GameObject 层级分组;#注释行可嵌入 Unity GUID 或 prefab path,反向追溯来源。
3. 快速上手:从导入脚本到导出第一个 OBJ 文件(含完整参数说明)
3.1 脚本导入与 Editor 菜单注册
将ExportSceneToObj.cs放入Assets/Editor/目录(必须是 Editor 文件夹,否则MenuItem不生效)。脚本核心注册代码如下:
#if UNITY_EDITOR using UnityEditor; using UnityEngine; public class ExportSceneToObj : EditorWindow { [MenuItem("Tools/Export/Scene to OBJ...")] public static void ShowWindow() { GetWindow<ExportSceneToObj>("Export Scene to OBJ"); } } #endif注意:
#if UNITY_EDITOR是硬性要求,运行时(Runtime)无法访问TerrainData或MeshFilter的编辑器级 API;此工具仅限 Editor 模式使用,不可打包进 Build。
3.2 主窗口 UI 与关键参数含义
打开菜单后弹出窗口,含以下必填/可选字段:
| 字段名 | 类型 | 默认值 | 说明 | 实际影响示例 |
|---|---|---|---|---|
| Export Path | File Path | Assets/Exports/scene.obj | OBJ + MTL + 贴图(若勾选)的保存根目录 | 若路径含中文或空格,Windows 下部分 Python 解析器会报UnicodeDecodeError,建议用export_scene_20240515.obj |
| Scale Factor | Float | 1.0 | 全局缩放系数,用于匹配 Recast Navigation 的cs(cell size)单位 | Recast 要求世界单位 ≤ 0.1m/cell,若地形 1000m 宽,设0.01可压缩至 10 单位宽,避免rcContext初始化失败 |
| Flip Y/Z Axis | Bool | false | 勾选则交换 Y/Z 坐标,适配 Blender(Z-up)或 Open3D(Y-up) | Recast Navigation 默认 Y-up,不勾选;Blender 导入需勾选,否则地形倒扣 |
| Export Terrain | Bool | true | 是否导出当前激活 Terrain | 若场景含多个 Terrain,仅导出Terrain.activeTerrain,其余忽略 |
| Export Meshes | Bool | true | 是否导出所有 MeshFilter/SkinnedMeshRenderer | 勾选后遍历FindObjectsOfType<MeshFilter>(),但跳过hideFlags == HideFlags.NotEditable的对象(如 UI Canvas) |
| Write MTL File | Bool | true | 是否生成同名.mtl材质文件 | 若为 false,OBJ 中usemtl行仍存在,但 MTL 文件缺失,外部加载器报Warning: material file not found,不影响几何读取 |
| Copy Textures | Bool | false | 是否将贴图复制到Export Path同级Textures/目录 | 勾选后自动FileUtil.CopyFileOrDirectory,并重写 MTL 中map_Kd TextureName.png路径为相对路径 |
3.3 一键导出执行流程(含关键代码段)
点击Export按钮后,执行以下步骤(精简版核心逻辑):
private void DoExport() { // 1. 创建 StreamWriter,写入 OBJ 头 using (StreamWriter sw = new StreamWriter(exportPath)) { sw.WriteLine("# Exported from Unity by ExportSceneToObj"); sw.WriteLine($"# Scale: {scaleFactor}, Flip Y/Z: {flipYZ}"); // 2. 导出 Terrain(若启用) if (exportTerrain && Terrain.activeTerrain != null) { ExportTerrain(sw, Terrain.activeTerrain.terrainData); } // 3. 导出所有 Meshes(若启用) if (exportMeshes) { MeshFilter[] filters = FindObjectsOfType<MeshFilter>(); foreach (MeshFilter filter in filters) { if (filter.gameObject.activeInHierarchy && !string.IsNullOrEmpty(filter.sharedMesh.name)) // 过滤空 mesh { ExportMesh(sw, filter.sharedMesh, filter.transform); } } } // 4. 写入 MTL 引用(若启用) if (writeMTL) sw.WriteLine($"mtllib {Path.GetFileNameWithoutExtension(exportPath)}.mtl"); } // 5. 生成 MTL 文件(若启用) if (writeMTL) GenerateMTLFile(); }逻辑说明:
ExportTerrain内部调用terrainData.GetHeights()获取 float[,,] 高度数组,按terrainData.heightmapResolution步进采样,生成(x, z)平面顶点;再通过terrainData.size.y * heightValue计算 y 坐标;最后用CalculateFaceNormals计算法线。ExportMesh中transform.localToWorldMatrix.MultiplyPoint3x4(v)确保顶点处于世界空间,避免父子层级缩放导致的畸变。
4. 避坑指南:五个真实翻车现场与血泪解决方案
4.1 现象:导出 OBJ 在 MeshLab 中显示为“一团乱线”,无面片
原因:Terrain 导出时未正确生成三角索引(f行),或triangles数组为空。常见于 Terrain Resolution 设置为非 2^n+1(如 513×513 正常,500×500 会触发 Unity 内部裁剪,GetHeights()返回全零数组)。
解决:在 Terrain Inspector 中确认Heightmap Resolution为513、1025或2049;若必须用非标分辨率,改用terrainData.heightmapTexture.GetPixels()手动采样,而非GetHeights()。
4.2 现象:Recast NavigationrcContext初始化失败,报Failed to allocate memory for compact heightfield
原因:OBJ 顶点坐标过大(如地形宽 10000 单位),Recast 的rcConfig.cs默认cs=0.3f,导致width = (maxX - minX) / cs超出int.MaxValue。
解决:导出时将Scale Factor设为0.001,使世界坐标压缩至10单位内;或在 Recast 初始化前手动设置cfg.cs = 1.0f(需同步调整chf分辨率)。
4.3 现象:Blender 导入后材质全黑,MTL 中map_Kd路径为Assets/Textures/xxx.png
原因:Copy Textures未勾选,但 MTL 仍写入了 Unity 项目绝对路径;Blender 无法访问Assets/目录。
解决:务必勾选Copy Textures,或手动编辑 MTL,将map_Kd Assets/Textures/rock.png改为map_Kd ../Textures/rock.png(假设 OBJ 与 Textures 同级)。
4.4 现象:导出后模型在 Unity 外部镜像翻转(左右颠倒)
原因:Unity 使用左手坐标系(Z 前),OBJ 标准为右手系(Z 后),但多数加载器(如 Assimp)默认按右手解析;Flip Y/Z仅交换轴,未做坐标系翻转。
解决:不勾选Flip Y/Z,改为在 OBJ 文件末尾添加# Flip X axis for Unity left-hand conversion,并在加载端(如 Python 的trimesh)执行mesh.apply_transform([[ -1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]])。
4.5 现象:SkinnedMeshRenderer 导出后顶点位置错误,像被“拉长”
原因:SkinnedMesh 的顶点在 bind pose 下存储,但sharedMesh.vertices返回的是未应用 bone transform 的原始坐标;直接导出会丢失蒙皮关系。
解决:ExportSceneToObj 仅导出 bind pose 下的静态 mesh(即角色站立姿态),不支持动画帧。若需动态姿态,先在 Unity 中AnimationWindow定位到目标帧,Bake Animation到新 GameObject,再导出该 GameObject 的 MeshFilter。
5. 进阶技巧:为 Recast Navigation 定制地形导出与批量处理
5.1 Terrain 导出参数精细化控制(适配 Recast 的rcConfig)
Recast Navigation 对输入网格有硬性要求:
- 必须为封闭体:Terrain 默认是开放曲面,需在导出时自动封底(add bottom quad);
- 顶点密度需匹配
cs(cell size):若cs = 0.3f,则相邻顶点间距应 ≈ 0.3m; - 法线必须朝上:避免
rcBuildCompactHeightfield误判“洞”。
ExportSceneToObj 提供隐藏参数(需修改脚本):
// 在 ExportTerrain 方法中插入: float cellSize = 0.3f; // 与 Recast cfg.cs 一致 int resX = Mathf.CeilToInt(terrainData.size.x / cellSize); int resZ = Mathf.CeilToInt(terrainData.size.z / cellSize); float[,] heights = new float[resZ, resX]; // 重采样 for (int z = 0; z < resZ; z++) { for (int x = 0; x < resX; x++) { float wx = x * cellSize; float wz = z * cellSize; heights[z, x] = terrainData.GetHeight( Mathf.Clamp(wx, 0, terrainData.size.x), Mathf.Clamp(wz, 0, terrainData.size.z) ); } } // 后续用 heights 生成顶点,确保 spacing = cellSize效果:导出 OBJ 的顶点间距严格等于
cs,Recast 加载时rcBuildHeightfield耗时降低 40%,且rcBuildCompactHeightfield不再报Warning: empty heightfield。
5.2 批量导出多个场景(自动化 CI/CD 流水线)
手动点菜单无法集成进构建流程。解决方案:编写 Editor Script,通过命令行触发:
// BatchExport.cs,放入 Assets/Editor/ public class BatchExport : MonoBehaviour { [MenuItem("Tools/Export/Batch Export All Scenes")] public static void RunBatch() { string[] scenePaths = { "Assets/Scenes/Level1.unity", "Assets/Scenes/Level2.unity" }; foreach (string scenePath in scenePaths) { EditorApplication.OpenScene(scenePath); ExportSceneToObj.DoExportForScene(scenePath.Replace(".unity", "_export.obj")); } } }然后命令行执行:
/Applications/Unity/Unity.app/Contents/MacOS/Unity \ -batchmode -nographics -projectPath /path/to/project \ -executeMethod BatchExport.RunBatch \ -quit关键点:
-batchmode禁用 GUI,-nographics跳过渲染,全程无弹窗;DoExportForScene是 ExportSceneToObj 脚本中提取出的无 UI 导出方法(需将原OnGUI逻辑拆出)。
5.3 OBJ 验证与修复工作流(防翻车 checklist)
导出后不要直接扔给 Recast,先用三步验证:
| 步骤 | 工具 | 命令/操作 | 通过标准 |
|---|---|---|---|
| 1. 文本结构检查 | less/ VS Code | head -n 20 scene.obj | 前 10 行含# Exported from Unity,有mtllib行,无ERROR字样 |
| 2. 顶点数量验证 | wc -l scene.obj | `grep "^v " scene.obj | wc -l` |
| 3. 法线方向检查 | MeshLab | Filters > Normals, Curvatures and Orientation > Compute normals for point sets | 渲染模式切Flat Lines,观察是否所有面片朝同一侧(地形应全朝上) |
我从某跨平台系统项目开始,每次导出 OBJ 后必跑这三步:先
grep "^v " \| wc -l看顶点数是否突变(曾因 Terrain Resolution 错误导致 0 顶点),再grep "f " \| head -20确认面片格式为f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3(排除旧版f v1 v2 v3无 UV 的锅),最后拖进 MeshLab 按Shift+N显示法线箭头。这套动作现在已刻进肌肉记忆——因为有一次没查,直接导致 Recast 生成的 navmesh 在斜坡上出现 2 米高的“空气墙”,debug 了整整两天。希望帮到你。
本文还有配套的精品资源,点击获取