├── .gitignore ├── CHANGELOG.md ├── Documentation └── screenshot │ ├── export.jpg │ ├── part.jpg │ ├── scene.jpg │ └── select.jpg ├── Editor └── ExportScene.cs ├── LICENSE.md ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.vs 2 | 3 | *.meta 4 | 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | * (2019.10.29) 增加`Package Manager`包安装模式支持 4 | -------------------------------------------------------------------------------- /Documentation/screenshot/export.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monitor1394/ExportSceneToObj/30e2f0b45af8d077bada22dceb9a5ec712a94302/Documentation/screenshot/export.jpg -------------------------------------------------------------------------------- /Documentation/screenshot/part.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monitor1394/ExportSceneToObj/30e2f0b45af8d077bada22dceb9a5ec712a94302/Documentation/screenshot/part.jpg -------------------------------------------------------------------------------- /Documentation/screenshot/scene.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monitor1394/ExportSceneToObj/30e2f0b45af8d077bada22dceb9a5ec712a94302/Documentation/screenshot/scene.jpg -------------------------------------------------------------------------------- /Documentation/screenshot/select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monitor1394/ExportSceneToObj/30e2f0b45af8d077bada22dceb9a5ec712a94302/Documentation/screenshot/select.jpg -------------------------------------------------------------------------------- /Editor/ExportScene.cs: -------------------------------------------------------------------------------- 1 | /******************************************/ 2 | /* */ 3 | /* Copyright (c) 2018 monitor1394 */ 4 | /* https://github.com/monitor1394 */ 5 | /* */ 6 | /******************************************/ 7 | 8 | using System.IO; 9 | using System.Text; 10 | using UnityEditor; 11 | using UnityEngine; 12 | using UnityEngine.SceneManagement; 13 | 14 | public class ExportScene : EditorWindow 15 | { 16 | private const string CUT_LB_OBJ_PATH = "export/bound_lb"; 17 | private const string CUT_RT_OBJ_PATH = "export/bound_rt"; 18 | 19 | private static float autoCutMinX = 1000; 20 | private static float autoCutMaxX = 0; 21 | private static float autoCutMinY = 1000; 22 | private static float autoCutMaxY = 0; 23 | 24 | private static float cutMinX = 0; 25 | private static float cutMaxX = 0; 26 | private static float cutMinY = 0; 27 | private static float cutMaxY = 0; 28 | 29 | private static long startTime = 0; 30 | private static int totalCount = 0; 31 | private static int count = 0; 32 | private static int counter = 0; 33 | private static int progressUpdateInterval = 10000; 34 | 35 | [MenuItem("ExportScene/ExportSceneToObj")] 36 | [MenuItem("GameObject/ExportScene/ExportSceneToObj")] 37 | public static void Export() 38 | { 39 | ExportSceneToObj(false); 40 | } 41 | 42 | [MenuItem("ExportScene/ExportSceneToObj(AutoCut)")] 43 | [MenuItem("GameObject/ExportScene/ExportSceneToObj(AutoCut)")] 44 | public static void ExportAutoCut() 45 | { 46 | ExportSceneToObj(true); 47 | } 48 | 49 | [MenuItem("ExportScene/ExportSelectedObj")] 50 | [MenuItem("GameObject/ExportScene/ExportSelectedObj", priority = 44)] 51 | public static void ExportObj() 52 | { 53 | GameObject selectObj = Selection.activeGameObject; 54 | if (selectObj == null) 55 | { 56 | Debug.LogWarning("Select a GameObject"); 57 | return; 58 | } 59 | string path = GetSavePath(false, selectObj); 60 | if (string.IsNullOrEmpty(path)) return; 61 | 62 | Terrain terrain = selectObj.GetComponent(); 63 | MeshFilter[] mfs = selectObj.GetComponentsInChildren(); 64 | SkinnedMeshRenderer[] smrs = selectObj.GetComponentsInChildren(); 65 | Debug.Log(mfs.Length + "," + smrs.Length); 66 | ExportSceneToObj(path, terrain, mfs, smrs, false, false); 67 | } 68 | 69 | public static void ExportSceneToObj(bool autoCut) 70 | { 71 | string path = GetSavePath(autoCut, null); 72 | if (string.IsNullOrEmpty(path)) return; 73 | Terrain terrain = UnityEngine.Object.FindObjectOfType(); 74 | MeshFilter[] mfs = UnityEngine.Object.FindObjectsOfType(); 75 | SkinnedMeshRenderer[] smrs = UnityEngine.Object.FindObjectsOfType(); 76 | ExportSceneToObj(path, terrain, mfs, smrs, autoCut, true); 77 | } 78 | 79 | public static void ExportSceneToObj(string path, Terrain terrain, MeshFilter[] mfs, 80 | SkinnedMeshRenderer[] smrs, bool autoCut, bool needCheckRect) 81 | { 82 | int vertexOffset = 0; 83 | string title = "export GameObject to .obj ..."; 84 | StreamWriter writer = new StreamWriter(path); 85 | 86 | startTime = GetMsTime(); 87 | UpdateCutRect(autoCut); 88 | counter = count = 0; 89 | progressUpdateInterval = 5; 90 | totalCount = (mfs.Length + smrs.Length) / progressUpdateInterval; 91 | foreach (var mf in mfs) 92 | { 93 | UpdateProgress(title); 94 | if (mf.GetComponent() != null && 95 | (!needCheckRect || (needCheckRect && IsInCutRect(mf.gameObject)))) 96 | { 97 | ExportMeshToObj(mf.gameObject, mf.sharedMesh, ref writer, ref vertexOffset); 98 | } 99 | } 100 | foreach (var smr in smrs) 101 | { 102 | UpdateProgress(title); 103 | if (!needCheckRect || (needCheckRect && IsInCutRect(smr.gameObject))) 104 | { 105 | ExportMeshToObj(smr.gameObject, smr.sharedMesh, ref writer, ref vertexOffset); 106 | } 107 | } 108 | if (terrain) 109 | { 110 | ExportTerrianToObj(terrain.terrainData, terrain.GetPosition(), 111 | ref writer, ref vertexOffset, autoCut); 112 | } 113 | writer.Close(); 114 | EditorUtility.ClearProgressBar(); 115 | 116 | long endTime = GetMsTime(); 117 | float time = (float)(endTime - startTime) / 1000; 118 | Debug.Log("Export SUCCESS:" + path); 119 | Debug.Log("Export Time:" + time + "s"); 120 | OpenDir(path); 121 | } 122 | 123 | private static void OpenDir(string path) 124 | { 125 | DirectoryInfo dir = Directory.GetParent(path); 126 | int index = path.LastIndexOf("/"); 127 | OpenCmd("explorer.exe", dir.FullName); 128 | } 129 | 130 | private static void OpenCmd(string cmd, string args) 131 | { 132 | System.Diagnostics.Process.Start(cmd, args); 133 | } 134 | 135 | private static string GetSavePath(bool autoCut, GameObject selectObject) 136 | { 137 | string dataPath = Application.dataPath; 138 | string dir = dataPath.Substring(0, dataPath.LastIndexOf("/")); 139 | string sceneName = SceneManager.GetActiveScene().name; 140 | string defaultName = ""; 141 | if (selectObject == null) 142 | { 143 | defaultName = (autoCut ? sceneName + "(autoCut)" : sceneName); 144 | } 145 | else 146 | { 147 | defaultName = (autoCut ? selectObject.name + "(autoCut)" : selectObject.name); 148 | } 149 | return EditorUtility.SaveFilePanel("Export .obj file", dir, defaultName, "obj"); 150 | } 151 | 152 | private static long GetMsTime() 153 | { 154 | return System.DateTime.Now.Ticks / 10000; 155 | //return (System.DateTime.Now.ToUniversalTime().Ticks - 621355968000000000) / 10000; 156 | } 157 | 158 | private static void UpdateCutRect(bool autoCut) 159 | { 160 | cutMinX = cutMaxX = cutMinY = cutMaxY = 0; 161 | if (!autoCut) 162 | { 163 | Vector3 lbPos = GetObjPos(CUT_LB_OBJ_PATH); 164 | Vector3 rtPos = GetObjPos(CUT_RT_OBJ_PATH); 165 | cutMinX = lbPos.x; 166 | cutMaxX = rtPos.x; 167 | cutMinY = lbPos.z; 168 | cutMaxY = rtPos.z; 169 | } 170 | } 171 | 172 | private static void UpdateAutoCutRect(Vector3 v) 173 | { 174 | if (v.x < autoCutMinX) autoCutMinX = v.x; 175 | if (v.x > autoCutMaxX) autoCutMaxX = v.x; 176 | if (v.z < autoCutMinY) autoCutMinY = v.z; 177 | if (v.z > autoCutMaxY) autoCutMaxY = v.z; 178 | } 179 | 180 | private static bool IsInCutRect(GameObject obj) 181 | { 182 | if (cutMinX == 0 && cutMaxX == 0 && cutMinY == 0 && cutMaxY == 0) return true; 183 | Vector3 pos = obj.transform.position; 184 | if (pos.x >= cutMinX && pos.x <= cutMaxX && pos.z >= cutMinY && pos.z <= cutMaxY) 185 | return true; 186 | else 187 | return false; 188 | } 189 | 190 | private static void ExportMeshToObj(GameObject obj, Mesh mesh, ref StreamWriter writer, ref int vertexOffset) 191 | { 192 | Quaternion r = obj.transform.localRotation; 193 | StringBuilder sb = new StringBuilder(); 194 | foreach (Vector3 vertice in mesh.vertices) 195 | { 196 | Vector3 v = obj.transform.TransformPoint(vertice); 197 | UpdateAutoCutRect(v); 198 | sb.AppendFormat("v {0} {1} {2}\n", -v.x, v.y, v.z); 199 | } 200 | foreach (Vector3 nn in mesh.normals) 201 | { 202 | Vector3 v = r * nn; 203 | sb.AppendFormat("vn {0} {1} {2}\n", -v.x, -v.y, v.z); 204 | } 205 | foreach (Vector3 v in mesh.uv) 206 | { 207 | sb.AppendFormat("vt {0} {1}\n", v.x, v.y); 208 | } 209 | for (int i = 0; i < mesh.subMeshCount; i++) 210 | { 211 | int[] triangles = mesh.GetTriangles(i); 212 | for (int j = 0; j < triangles.Length; j += 3) 213 | { 214 | sb.AppendFormat("f {1} {0} {2}\n", 215 | triangles[j] + 1 + vertexOffset, 216 | triangles[j + 1] + 1 + vertexOffset, 217 | triangles[j + 2] + 1 + vertexOffset); 218 | } 219 | } 220 | vertexOffset += mesh.vertices.Length; 221 | writer.Write(sb.ToString()); 222 | } 223 | 224 | private static void ExportTerrianToObj(TerrainData terrain, Vector3 terrainPos, 225 | ref StreamWriter writer, ref int vertexOffset, bool autoCut) 226 | { 227 | int tw = terrain.heightmapWidth; 228 | int th = terrain.heightmapHeight; 229 | 230 | Vector3 meshScale = terrain.size; 231 | meshScale = new Vector3(meshScale.x / (tw - 1), meshScale.y, meshScale.z / (th - 1)); 232 | Vector2 uvScale = new Vector2(1.0f / (tw - 1), 1.0f / (th - 1)); 233 | 234 | Vector2 terrainBoundLB, terrainBoundRT; 235 | if (autoCut) 236 | { 237 | terrainBoundLB = GetTerrainBoundPos(new Vector3(autoCutMinX, 0, autoCutMinY), terrain, terrainPos); 238 | terrainBoundRT = GetTerrainBoundPos(new Vector3(autoCutMaxX, 0, autoCutMaxY), terrain, terrainPos); 239 | } 240 | else 241 | { 242 | terrainBoundLB = GetTerrainBoundPos(CUT_LB_OBJ_PATH, terrain, terrainPos); 243 | terrainBoundRT = GetTerrainBoundPos(CUT_RT_OBJ_PATH, terrain, terrainPos); 244 | } 245 | 246 | int bw = (int)(terrainBoundRT.x - terrainBoundLB.x); 247 | int bh = (int)(terrainBoundRT.y - terrainBoundLB.y); 248 | 249 | int w = bh != 0 && bh < th ? bh : th; 250 | int h = bw != 0 && bw < tw ? bw : tw; 251 | 252 | int startX = (int)terrainBoundLB.y; 253 | int startY = (int)terrainBoundLB.x; 254 | if (startX < 0) startX = 0; 255 | if (startY < 0) startY = 0; 256 | 257 | Debug.Log(string.Format("Terrian:tw={0},th={1},sw={2},sh={3},startX={4},startY={5}", 258 | tw, th, bw, bh, startX, startY)); 259 | 260 | float[,] tData = terrain.GetHeights(0, 0, tw, th); 261 | Vector3[] tVertices = new Vector3[w * h]; 262 | Vector2[] tUV = new Vector2[w * h]; 263 | 264 | int[] tPolys = new int[(w - 1) * (h - 1) * 6]; 265 | 266 | for (int y = 0; y < h; y++) 267 | { 268 | for (int x = 0; x < w; x++) 269 | { 270 | Vector3 pos = new Vector3(-(startY + y), tData[startX + x, startY + y], (startX + x)); 271 | tVertices[y * w + x] = Vector3.Scale(meshScale, pos) + terrainPos; 272 | tUV[y * w + x] = Vector2.Scale(new Vector2(x, y), uvScale); 273 | } 274 | } 275 | int index = 0; 276 | for (int y = 0; y < h - 1; y++) 277 | { 278 | for (int x = 0; x < w - 1; x++) 279 | { 280 | tPolys[index++] = (y * w) + x; 281 | tPolys[index++] = ((y + 1) * w) + x; 282 | tPolys[index++] = (y * w) + x + 1; 283 | tPolys[index++] = ((y + 1) * w) + x; 284 | tPolys[index++] = ((y + 1) * w) + x + 1; 285 | tPolys[index++] = (y * w) + x + 1; 286 | } 287 | } 288 | count = counter = 0; 289 | progressUpdateInterval = 10000; 290 | totalCount = (tVertices.Length + tUV.Length + tPolys.Length / 3) / progressUpdateInterval; 291 | string title = "export Terrain to .obj ..."; 292 | for (int i = 0; i < tVertices.Length; i++) 293 | { 294 | UpdateProgress(title); 295 | StringBuilder sb = new StringBuilder(22); 296 | sb.AppendFormat("v {0} {1} {2}\n", tVertices[i].x, tVertices[i].y, tVertices[i].z); 297 | writer.Write(sb.ToString()); 298 | } 299 | for (int i = 0; i < tUV.Length; i++) 300 | { 301 | UpdateProgress(title); 302 | StringBuilder sb = new StringBuilder(20); 303 | sb.AppendFormat("vt {0} {1}\n", tUV[i].x, tUV[i].y); 304 | writer.Write(sb.ToString()); 305 | } 306 | for (int i = 0; i < tPolys.Length; i += 3) 307 | { 308 | UpdateProgress(title); 309 | int x = tPolys[i] + 1 + vertexOffset; ; 310 | int y = tPolys[i + 1] + 1 + vertexOffset; 311 | int z = tPolys[i + 2] + 1 + vertexOffset; 312 | StringBuilder sb = new StringBuilder(30); 313 | sb.AppendFormat("f {0} {1} {2}\n", x, y, z); 314 | writer.Write(sb.ToString()); 315 | } 316 | vertexOffset += tVertices.Length; 317 | } 318 | 319 | private static Vector2 GetTerrainBoundPos(string path, TerrainData terrain, Vector3 terrainPos) 320 | { 321 | var go = GameObject.Find(path); 322 | if (go) 323 | { 324 | Vector3 pos = go.transform.position; 325 | return GetTerrainBoundPos(pos, terrain, terrainPos); 326 | } 327 | return Vector2.zero; 328 | } 329 | 330 | private static Vector2 GetTerrainBoundPos(Vector3 worldPos, TerrainData terrain, Vector3 terrainPos) 331 | { 332 | Vector3 tpos = worldPos - terrainPos; 333 | return new Vector2((int)(tpos.x / terrain.size.x * terrain.heightmapWidth), 334 | (int)(tpos.z / terrain.size.z * terrain.heightmapHeight)); 335 | } 336 | 337 | private static Vector3 GetObjPos(string path) 338 | { 339 | var go = GameObject.Find(path); 340 | if (go) 341 | { 342 | return go.transform.position; 343 | } 344 | return Vector3.zero; 345 | } 346 | 347 | private static void UpdateProgress(string title) 348 | { 349 | if (counter++ == progressUpdateInterval) 350 | { 351 | counter = 0; 352 | float process = Mathf.InverseLerp(0, totalCount, ++count); 353 | long currTime = GetMsTime(); 354 | float sec = ((float)(currTime - startTime)) / 1000; 355 | string text = string.Format("{0}/{1}({2:f2} sec.)", count, totalCount, sec); 356 | EditorUtility.DisplayProgressBar(title, text, process); 357 | } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 monitor1394 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | # ExportSceneToObj 3 | 4 | 一款用于导出场景(包括`GameObject`和`Terrian`)或`.fbx`模型到`.obj`文件的`Unity`插件。 5 | 6 | ## 功能 7 | 8 | * 支持导出物件和地形 9 | * 支持自定义裁剪区域 10 | * 支持自动裁剪功能 11 | * 支持单个选择导出 12 | * 支持导出`.fbx`模型 13 | 14 | ## 截图 15 | 16 | 17 | 18 | 19 | 20 | 21 | ## 用法 22 | 23 | * 通过下载源码或`unitypackage`包导入到你的项目中(放在`Assets`目录下)。如果你是`2018.3`及以上版本,可通过`Package Manager`的`Git`来导入包(也可以下载后本地安装包): 24 | 25 | 1. 打开`Packages`目录下的`manifest.json`文件,在`dependencies`下加入: 26 | ``` json 27 | "com.monitor1394.exportscenetoobj": "https://github.com/monitor1394/ExportSceneToObj.git", 28 | ``` 29 | 2. 回到`Unity`,可能会花1分钟左右进行下载和编译,成功后就可以开始使用了。 30 | 3. 如果要删除`ExportSceneToObj`,删除掉1步骤所加的内容即可。 31 | 4. 如果要更新`ExportSceneToObj`,删除`manifest.json`文件的`lock`下的`com.monitor1394.exportscenetoobj`相关内容即会从新下载编译。 32 | 33 | * 如果要自定义裁剪区域的话,场景中增加空`GameObject`用于表示裁剪区域(需要左下角和右上角两个空`GameObject`),并修改代码中`CUT_LB_OBJ_PATH`和`CUT_RT_OBJ_PATH`为对应的路径 34 | * 在`Unity`的菜单栏上有`ExportScene`菜单即可 35 | * 怎么单独导出`.fbx`模型? 36 | 1. 将`.fbx`拖到场景中 37 | 2. 在`Hierarchy`试图中选中`fbx`的`GameObject`,右键执行`ExportScene` --> `ExportSelectedObj`单独导出即可 38 | 39 | ## 其他 40 | 41 | 1. 目前判断物件是否在裁剪区域只是判断物件的坐标是否在区域内,还没有实现物件边界裁剪。 42 | 2. 只有包含`MeshFilter`、`SkinnedMeshRenderer`、`Terrian`的物件才会被导出。 43 | 44 | ## 问题 45 | 46 | 1. 为什么将脚本放入项目中后菜单栏还是看不到`ExportScene`菜单项? 47 | 答:脚本文件放到正确的目录,同时要检查是否有其他脚本有报错没有编译通过,有报错时先要处理报错。 48 | 49 | 2. 为什么导出的`obj`文件在`Maya`等`3D`软件中显示正常,但在`3d Max`显示异常? 50 | 答:`3d Max`导入设置中勾选`Import as single mesh`选项。 51 | 52 | ## 参考 53 | 54 | 1. [ExportOBJ](http://wiki.unity3d.com/index.php?title=ExportOBJ) 55 | 2. [TerrainObjExporter](http://wiki.unity3d.com/index.php?title=TerrainObjExporter) 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.monitor1394.exportscenetoobj", 3 | "displayName": "Export Scene To Obj", 4 | "version": "1.0.0", 5 | "unity": "2018.3", 6 | "description": "Export scene (including objects and terrain ) or fbx to .obj file for Unity.", 7 | "keywords": [ 8 | "unity", 9 | "export", 10 | "scene", 11 | "terrain", 12 | "obj", 13 | "fbx" 14 | ], 15 | "category": "editor", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/monitor1394/ExportSceneToObj.git" 19 | }, 20 | "author": "monitor1394", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/monitor1394/ExportSceneToObj/issues" 24 | }, 25 | "homepage": "https://github.com/monitor1394/ExportSceneToObj" 26 | } --------------------------------------------------------------------------------