[Unity] 유니티에서 조합한 모델링을 OBJ로 내보내기

Filed under tutorials, unity | Comments Off

(Exporting Unity Mesh Into OBJ Format)

게임 레벨 제작은 일반적으로 더미 작업을 거친다. 최종적으로 사용될 애셋이 만들어지기 전에, 낮은 비용으로 쉽고 빠르게 게임 플레이를 확인하기 위해서다. 언리얼 엔진의 경우는 브러쉬를 이용해서 이런 작업을 처리할 수 있다. 유니티의 경우는 에디터에서 제공하는 기본 도형(Cube, Plane, Cylinder…)을 이용해서 처리할 수 있다. 그런데 여기 한가지 한계가 있다. 유니티는 다양한 포맷의 메쉬 데이터를 불러올 수 있다. 그러나 반대로 유니티에서 만든 메쉬를 밖으로 가져나갈 방법이 기본적으로 없다. 예를 들어, 레벨 디자이너가 정교하게 수치를 맞춘 더미 레벨을 만들어도, 배경 아티스트가 외부 패키지에서 템플릿으로 사용하기 위해 그 데이터를 가지고 나갈 방법이 없다는 얘기다. 바로 이럴 경우 다음에 소개하는 스크립트를 사용하면 유니티에서 조합한 모델링 데이터를 OBJ 포맷으로 내보낼 수 있다.

우선 아래 스크립트를 프로젝트의 Editor 폴더에 저장한다(Editor Class를 사용한 스크립트는 기본적으로 Asset/Editor 폴더에 있어야 한다).


/*
Based on ObjExporter.cs, this "wrapper" lets you export to .OBJ directly from the editor menu.

This should be put in your "Editor"-folder. Use by selecting the objects you want to export, and select
the appropriate menu item from "Custom->Export". Exported models are put in a folder called
"ExportedObj" in the root of your Unity-project. Textures should also be copied and placed in the
same folder.
N.B. there may be a bug so if the custom option doesn't come up refer to this thread http://answers.unity3d.com/questions/317951/how-to-use-editorobjexporter-obj-saving-script-fro.html */

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System;

struct ObjMaterial
{
	public string name;
	public string textureName;
}

public class EditorObjExporter : ScriptableObject
{
	private static int vertexOffset = 0;
	private static int normalOffset = 0;
	private static int uvOffset = 0;

	//User should probably be able to change this. It is currently left as an excercise for
	//the reader.
	private static string targetFolder = "ExportedObj";

    private static string MeshToString(MeshFilter mf, Dictionary<string, ObjMaterial> materialList)
    {
        Mesh m = mf.sharedMesh;
        Material[] mats = mf.renderer.sharedMaterials;

        StringBuilder sb = new StringBuilder();

        sb.Append("g ").Append(mf.name).Append("\n");
        foreach(Vector3 lv in m.vertices)
        {
        	Vector3 wv = mf.transform.TransformPoint(lv);

        	//This is sort of ugly - inverting x-component since we're in
        	//a different coordinate system than "everyone" is "used to".
            sb.Append(string.Format("v {0} {1} {2}\n",-wv.x,wv.y,wv.z));
        }
        sb.Append("\n");

        foreach(Vector3 lv in m.normals)
        {
        	Vector3 wv = mf.transform.TransformDirection(lv);

            sb.Append(string.Format("vn {0} {1} {2}\n",-wv.x,wv.y,wv.z));
        }
        sb.Append("\n");

        foreach(Vector3 v in m.uv)
        {
            sb.Append(string.Format("vt {0} {1}\n",v.x,v.y));
        }

        for (int material=0; material < m.subMeshCount; material ++) {
            sb.Append("\n");
            sb.Append("usemtl ").Append(mats[material].name).Append("\n");
            sb.Append("usemap ").Append(mats[material].name).Append("\n");

            //See if this material is already in the materiallist.
            try
       		{
          		ObjMaterial objMaterial = new ObjMaterial();

          		objMaterial.name = mats[material].name;

          		if (mats[material].mainTexture)
          			objMaterial.textureName = EditorUtility.GetAssetPath(mats[material].mainTexture);
          		else
          			objMaterial.textureName = null;

          		materialList.Add(objMaterial.name, objMaterial);
        	}
        	catch (ArgumentException)
        	{
            	//Already in the dictionary
        	}

            int[] triangles = m.GetTriangles(material);
            for (int i=0;i<triangles.Length;i+=3)
            {
            	//Because we inverted the x-component, we also needed to alter the triangle winding.
                sb.Append(string.Format("f {1}/{1}/{1} {0}/{0}/{0} {2}/{2}/{2}\n",
                    triangles[i]+1 + vertexOffset, triangles[i+1]+1 + normalOffset, triangles[i+2]+1 + uvOffset));
            }
        }

        vertexOffset += m.vertices.Length;
        normalOffset += m.normals.Length;
        uvOffset += m.uv.Length;

        return sb.ToString();
    }

    private static void Clear()
    {
    	vertexOffset = 0;
    	normalOffset = 0;
    	uvOffset = 0;
    }

   	private static Dictionary<string, ObjMaterial> PrepareFileWrite()
   	{
   		Clear();

    	return new Dictionary<string, ObjMaterial>();
   	}

   	private static void MaterialsToFile(Dictionary<string, ObjMaterial> materialList, string folder, string filename)
   	{
   		using (StreamWriter sw = new StreamWriter(folder + "/" + filename + ".mtl"))
        {
        	foreach( KeyValuePair<string, ObjMaterial> kvp in materialList )
        	{
        		sw.Write("\n");
        		sw.Write("newmtl {0}\n", kvp.Key);
        		sw.Write("Ka  0.6 0.6 0.6\n");
				sw.Write("Kd  0.6 0.6 0.6\n");
				sw.Write("Ks  0.9 0.9 0.9\n");
				sw.Write("d  1.0\n");
				sw.Write("Ns  0.0\n");
				sw.Write("illum 2\n");

				if (kvp.Value.textureName != null)
				{
					string destinationFile = kvp.Value.textureName;

					int stripIndex = destinationFile.LastIndexOf('/');//FIXME: Should be Path.PathSeparator;

       				if (stripIndex >= 0)
            			destinationFile = destinationFile.Substring(stripIndex + 1).Trim();

            		string relativeFile = destinationFile;

            		destinationFile = folder + "/" + destinationFile;

					Debug.Log("Copying texture from " + kvp.Value.textureName + " to " + destinationFile);

					try
					{
						//Copy the source file
						File.Copy(kvp.Value.textureName, destinationFile);
					}
					catch
					{

					}

					sw.Write("map_Kd {0}", relativeFile);
				}

				sw.Write("\n\n\n");
        	}
        }
   	}

    private static void MeshToFile(MeshFilter mf, string folder, string filename)
    {
    	Dictionary<string, ObjMaterial> materialList = PrepareFileWrite();

        using (StreamWriter sw = new StreamWriter(folder +"/" + filename + ".obj"))
        {
        	sw.Write("mtllib ./" + filename + ".mtl\n");

            sw.Write(MeshToString(mf, materialList));
        }

        MaterialsToFile(materialList, folder, filename);
    }

    private static void MeshesToFile(MeshFilter[] mf, string folder, string filename)
    {
    	Dictionary<string, ObjMaterial> materialList = PrepareFileWrite();

        using (StreamWriter sw = new StreamWriter(folder +"/" + filename + ".obj"))
        {
        	sw.Write("mtllib ./" + filename + ".mtl\n");

        	for (int i = 0; i < mf.Length; i++)
        	{
            	sw.Write(MeshToString(mf[i], materialList));
            }
        }

        MaterialsToFile(materialList, folder, filename);
    }

    private static bool CreateTargetFolder()
    {
    	try
    	{
    		System.IO.Directory.CreateDirectory(targetFolder);
    	}
    	catch
    	{
    		EditorUtility.DisplayDialog("Error!", "Failed to create target folder!", "");
    		return false;
    	}

    	return true;
    }

    [MenuItem ("Custom/Export/Export all MeshFilters in selection to separate OBJs")]
    static void ExportSelectionToSeparate()
    {
    	if (!CreateTargetFolder())
    		return;

        Transform[] selection = Selection.GetTransforms(SelectionMode.Editable | SelectionMode.ExcludePrefab);

        if (selection.Length == 0)
        {
        	EditorUtility.DisplayDialog("No source object selected!", "Please select one or more target objects", "");
        	return;
        }

        int exportedObjects = 0;

       	for (int i = 0; i < selection.Length; i++)
       	{
       		Component[] meshfilter = selection[i].GetComponentsInChildren(typeof(MeshFilter));

       		for (int m = 0; m < meshfilter.Length; m++)        		{        			exportedObjects++;        			MeshToFile((MeshFilter)meshfilter[m], targetFolder, selection[i].name + "_" + i + "_" + m);        		}        	}        	if (exportedObjects > 0)
       		EditorUtility.DisplayDialog("Objects exported", "Exported " + exportedObjects + " objects", "");
       	else
       		EditorUtility.DisplayDialog("Objects not exported", "Make sure at least some of your selected objects have mesh filters!", "");
    }

    [MenuItem ("Custom/Export/Export whole selection to single OBJ")]
    static void ExportWholeSelectionToSingle()
    {
    	if (!CreateTargetFolder())
    		return;

        Transform[] selection = Selection.GetTransforms(SelectionMode.Editable | SelectionMode.ExcludePrefab);

        if (selection.Length == 0)
        {
        	EditorUtility.DisplayDialog("No source object selected!", "Please select one or more target objects", "");
        	return;
        }

        int exportedObjects = 0;

        ArrayList mfList = new ArrayList();

       	for (int i = 0; i < selection.Length; i++)
       	{
       		Component[] meshfilter = selection[i].GetComponentsInChildren(typeof(MeshFilter));

       		for (int m = 0; m < meshfilter.Length; m++)        		{        			exportedObjects++;        			mfList.Add(meshfilter[m]);        		}        	}        	if (exportedObjects > 0)
       	{
       		MeshFilter[] mf = new MeshFilter[mfList.Count];

       		for (int i = 0; i < mfList.Count; i++)        		{        			mf[i] = (MeshFilter)mfList[i];        		}        		string filename = EditorApplication.currentScene + "_" + exportedObjects;        		int stripIndex = filename.LastIndexOf('/');//FIXME: Should be Path.PathSeparator        		if (stripIndex >= 0)
            	filename = filename.Substring(stripIndex + 1).Trim();

       		MeshesToFile(mf, targetFolder, filename);

       		EditorUtility.DisplayDialog("Objects exported", "Exported " + exportedObjects + " objects to " + filename, "");
       	}
       	else
       		EditorUtility.DisplayDialog("Objects not exported", "Make sure at least some of your selected objects have mesh filters!", "");
    }

    [MenuItem ("Custom/Export/Export each selected to single OBJ")]
    static void ExportEachSelectionToSingle()
    {
    	if (!CreateTargetFolder())
    		return;

        Transform[] selection = Selection.GetTransforms(SelectionMode.Editable | SelectionMode.ExcludePrefab);

        if (selection.Length == 0)
        {
        	EditorUtility.DisplayDialog("No source object selected!", "Please select one or more target objects", "");
        	return;
        }

        int exportedObjects = 0;

       	for (int i = 0; i < selection.Length; i++)
       	{
       		Component[] meshfilter = selection[i].GetComponentsInChildren(typeof(MeshFilter));

       		MeshFilter[] mf = new MeshFilter[meshfilter.Length];

       		for (int m = 0; m < meshfilter.Length; m++)        		{        			exportedObjects++;        			mf[m] = (MeshFilter)meshfilter[m];        		}        		MeshesToFile(mf, targetFolder, selection[i].name + "_" + i);        	}        	if (exportedObjects > 0)
       	{
       		EditorUtility.DisplayDialog("Objects exported", "Exported " + exportedObjects + " objects", "");
       	}
       	else
       		EditorUtility.DisplayDialog("Objects not exported", "Make sure at least some of your selected objects have mesh filters!", "");
    }

}

[출처: http://answers.unity3d.com/questions/317951/how-to-use-editorobjexporter-obj-saving-script-fro.html]

유니티에서 Cube를 조합해서 만든 모델링이다. 이 모델링을 OBJ 포맷으로 내보낼 것이다.
obj_01

스크립트가 Editor 폴더에 저장되면 상단 메뉴에 Custom 항목이 추가된다.
obj_02

Scene 뷰에서 내보낼 GameObject를 모두 선택하고, 메뉴에서 ‘Export whole selection to single OBJ’를 선택한다. 선택한 모든 GameObject를 하나의 OBJ 파일로 만들어 주는 옵션이다.

내보내기가 완료되면 다음과 같은 창이 뜬다.
obj_03

탐색기에서 프로젝트가 설치된 곳으로 가면, ExportedObj라는 폴더가 생성돼 있다. OBJ 파일이 저장되는 곳이다. 폴더를 보면 mesh와 material 정보를 담은 OBJ와 MTL파일, 그리고 유니티에서 사용했던 텍스처까지 들어있다.
obj_04

이제 외부 패키지에서 OBJ파일을 불러서 확인한다.
obj_05

메쉬와 텍스처 모두를 제대로 불러온다는 사실을 확인할 수 있다.

Comments are closed.