Unity导出模型为Obj文件

  • 资源链接
  • 下载导入
  • 代码纪要
  • 使用方式
  • 参考链接


资源链接

原插件代码中只有MeshFilter的Obj导出代码;由于项目需求,需要将SkinnedMeshRenderer导出为Obj文件,故在原代码的基础上,扩展出了SkinnedMeshRenderer的Obj导出功能。对原代码有兴趣的可以自行进入参考链接2
下载链接:修改后MeshFilter和SkinnedMeshRenderer的Obj导出功能

下载导入

将下载的插件导入项目之后,就是文件夹Editor下的3个C#脚本文件。如下图,

unity导出iOS工程 bundle资源文件 unity导出项目文件_unity

代码纪要

下面对代码进行简单分析纪要:

  1. 总共3个脚本分别为抽象泛型父类EditorObjExporter<T, R>MeshFilter的子类EditorObjExporter_MeshFilterSkinnedMeshRenderer的子类EditorObjExporter_SkinnedMeshRendererMeshFilterSkinnedMeshRenderer的整个过程代码都一样只有在获取Mesh网格和Material[]材质时有所不同。
  2. 由于是Unity的工具栏程序,所以大部分为static静态函数,尤其是3个触发函数:ExportSelectionToSeparateExportWholeSelectionToSingleExportEachSelectionToSingle
  3. 结合前两条,脚本之所以使用单例设计模式。是因为这个功能唯一的非静态函数string MeshToString(MeshFilter t, Dictionary<string, ObjMaterial> materialList),用于子类分别获取Mesh网格和Material[]材质,所以该函数使用泛型类继承的方式;同时父类的static void MeshToFile(T t, string folder, string filename)static void MeshesToFile(T[] ts, string folder, string filename)两个静态函数都需要调用该方法,但因为MeshToString是非静态函数,使用单例对象调用是最方便简单的。
    该功能,如果还有什么更加好的解决方案,欢迎探讨。示例代码:
sw.Write(GetInstance().MeshToString(t, materialList));//静态类中使用单例对象调用函数
  1. 3个触发函数的基本流程相同:先通过Selection.GetTransforms(SelectionMode.Editable | SelectionMode.ExcludePrefab);函数获取当前选择的Transform对象数组;再根据泛型的每个子类不同的需求遍历该数组,并获取对应的组件;最后根据获取的组件数组保存成Obj文件。示例代码:
protected 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[] components = selection[i].GetComponentsInChildren(typeof(T));
        for (int m = 0; m < components.Length; m++)
        {
           exportedObjects++;
           MeshToFile((T)components[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!", "");
}
  1. 先通过MeshToString函数将组件中获取的Mesh网格转换成String字符串:先顶点数据循环,再法线数据循环,然后是UV数据循环,最后材质数据循环的同时储存材质对象字典。函数代码:
protected static string MeshToString(Mesh mesh, Material[] materials, string name, Transform transform, Dictionary<string, ObjMaterial> materialList)
{
    StringBuilder sb = new StringBuilder();
    sb.Append("g ").Append(name).Append("\n");//数据起始
    foreach (Vector3 lv in mesh.vertices)
    {//顶点数据循环输入
        Vector3 wv = 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 mesh.normals)
    {//法线数据循环输入
        Vector3 wv = 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 mesh.uv)
    {//UV数据循环输入
        sb.Append(string.Format("vt {0} {1}\n", v.x, v.y));
    }
    for (int material = 0; material < mesh.subMeshCount; material++)
    {//材质数据循环输入
        sb.Append("\n");//空格
        sb.Append("usemtl ").Append(materials[material].name).Append("\n");
        sb.Append("usemap ").Append(materials[material].name).Append("\n");
        try
        {//See if this material is already in the materiallist.看看这个字典是否已经在字典中
            ObjMaterial objMaterial = new ObjMaterial
            {
                name = materials[material].name
            };
            if (materials[material].mainTexture)
                objMaterial.textureName = AssetDatabase.GetAssetPath(materials[material].mainTexture);//另一种方式EditorUtility.GetAssetPath(mats[material].mainTexture)
            else
                objMaterial.textureName = null;
            materialList.Add(objMaterial.name, objMaterial);
        }
        catch (ArgumentException)
        {//已经在字典中
         //Already in the dictionary
        }
        int[] triangles = mesh.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 += mesh.vertices.Length;
    normalOffset += mesh.normals.Length;
    uvOffset += mesh.uv.Length;
    return sb.ToString();
}
  1. MeshToFileMeshesToFile函数是将Mesh网格转换的String字符串保存到文件中,其中的最后一步MaterialsToFile是遍历材质对象字典保存到文件中。
  2. 最后,因为工具栏的出发函数必须为静态函数,故子类都新建了静态函数用于“包装”父类的相应静态函数,并使用MenuItem修饰。示例:
[MenuItem("Custom/Export Obj/MeshFilter/导出所有选择的网格过滤器以分离的Obj形式")]//Export all MeshFilters in selection to separate Objs
protected static void ExportSelectionToSeparate_MF()
{
    ExportSelectionToSeparate();
}

使用方式

unity导出iOS工程 bundle资源文件 unity导出项目文件_数据_02

  1. 在场景中选择模型对象。
  2. 在工具栏选择Custom—>Export Obj,再根据所选择的模型对象所使用的Mesh组件选择对应的选项:MeshFilterSkinnedMeshRenderer
  3. 再根据自己的需求选择导出的模式:
  • 所有网格分别输出到一个Obj文件;
  • 输出选择的网格合体到一个Obj文件;
  • 输出每个选择的模型到单一Obj文件。
  1. 之后弹出对话框,即为成功。
  2. 项目文件夹下会出现一个名为ExportedObj的文件夹,其中便是保存下来的Obj文件以及对应的png图片。可以双击查看是否正确。