文章目录

  • 前言
  • 一、部分细节
    • 1.镜像
    • 2.压缩存储
  • 二、测试效果
    • 1.编辑器非运行环境
    • 2.编辑器运行环境
  • 局限性
  • 完整代码

前言

obj格式是一种通用的3D模型格式,也是unity支持的模型格式之一。obj具体格式介绍可以去某度看看,有不少。本篇重点是在unity编辑器中运行状态下和非运行状态下将场景中的物体导出为obj。


一、部分细节

1.镜像

也就是坐标手系变换,unity是使用左手坐标系的,而标准obj是右手坐标系,所以unity在导入obj后会自动将obj模型镜像。在导出时笔者也加上了这个功能,不然按默认的导出obj后再放入unity中就会发现两个模型镜像了。其实变换手系原理很简单,如下图所示(图片来源),固定两条轴------把两条轴重叠-------就会发现另外一条轴是相反的,比如把Y轴和Z轴对齐,此时只要把X轴的值取反就可以达到镜像的效果。

//写出顶点
for (int i = 0; i < vertices.Length; i++)
{Vector3 worldPos = trans.TransformPoint(vertices[i]);//顶点镜像if (exchangeCoordinate) worldPos.x *= -1;sw.Write("v " + worldPos.x + " " + worldPos.y + " " + worldPos.z + "\n");
}
sw.Write("\n");//写出法线
if(normals.Length == vertices.Length)
{hasNormal = true;for (int i = 0; i < normals.Length; i++){Vector3 worldNormal = trans.TransformDirection(normals[i]);//法线镜像if (exchangeCoordinate) worldNormal.x *= -1;sw.Write("vn " + worldNormal.x + " " + worldNormal.y + " " + worldNormal.z + "\n");}sw.Write("\n");
}

2.压缩存储

所谓压缩存储,其实就是利用obj三角片面的索引特性将指向相同的内容使用同一个索引,也就是重用。其实unity里面的基本几何体都是没有重用的,如下图,一个正方体应该只有8个点,12个三角面片,但图中显示的却是24个顶点。

将cube直接不压缩导出来后确实有24个,但是可以明显看到有不少点坐标是相同的,特别是uv信息,相同的更多,这种方式有个特点,就是顶点有多少个,法线和uv就有多少个(某些可能没有法线或uv的模型除外),从后面的三角面片信息也可以看出来,顶点/法线/UV 的索引都是相同的。


再看下添加重用后导出的obj数据,明显少了很多数据,此时顶点是真的只有8个了,然后看下三角面片中顶点,法线和uv的索引不尽相同。

不过这时如果把这个压缩后的obj再次导入unity就会发现一个神奇的事,如下图,显示的顶点数又变成24了,具体原因可以看下这篇博客

要实现压缩存储其实不难,就是先遍历一遍,把相同的数据用一个代替就可以了,这里用字典来存储单一的数据

//保存相同的 顶点/法线/UV 对应的唯一索引
Dictionary<Vector3, int> verticesDic = new Dictionary<Vector3, int>();
Dictionary<Vector3, int> normalDic = new Dictionary<Vector3, int>();
Dictionary<Vector2, int> uvDic = new Dictionary<Vector2, int>();
//计算重复的顶点法线uv
for (int i = 0; i < vertices.Length; i++)
{if (!verticesDic.ContainsKey(vertices[i]))verticesDic.Add(vertices[i], verticesDic.Count);}
}
if(normals.Length  == vertices.Length)
{hasNormal = true;for (int i = 0; i < normals.Length; i++){if (!normalDic.ContainsKey(normals[i])){normalDic.Add(normals[i], normalDic.Count);}}
}if(uvs.Length == vertices.Length )
{hasUV = true;for (int i = 0; i < uvs.Length; i++){if (!uvDic.ContainsKey(uvs[i])){uvDic.Add(uvs[i], uvDic.Count);}}
}

写出数据部分有点长,可以看下下面的完整代码部分。


二、测试效果

测试模型来源于AssetStore中的unity-chan!

1.编辑器非运行环境

测试脚本

/****************************************************文件:ExportObjExample.cs作者:TKB邮箱: 544726237@qq.com日期:2021/7/24 23:42:59功能:Nothing
*****************************************************/
using UnityEngine;
using System.IO;namespace TLib
{public class ExportObjExample{#if UNITY_EDITOR//将选中的模型及其子物体导出到一个obj中[UnityEditor.MenuItem("Tools/导出obj",false)]private static void OnClickExportObj(){GameObject go = UnityEditor.Selection.activeObject as GameObject;Exporter.ExportObj(go, Application.dataPath + "/Export/"+ go.name+".obj",true,true);UnityEditor.AssetDatabase.Refresh();}//将选中的物体及其子对象分别导出为obj[UnityEditor.MenuItem("Tools/导出objs", false)]private static void OnClickExportObj1(){GameObject go = UnityEditor.Selection.activeObject as GameObject;Exporter.ExportObjs(go, Application.dataPath + "/Export");UnityEditor.AssetDatabase.Refresh();}
#endif}
}

既可以导出MeshRendererer(右边方块组成的)也可以导出SkinnedMeshRenderer(左边)。眼尖的同学可能看到了导出来的chan脸上的腮红有点问题,显示效果也有差距,这其实是因为chan使用的shader是自定义的,不是标准材质。

2.编辑器运行环境

测试代码

using UnityEngine;
using TLib;public class RunTimeExport : MonoBehaviour
{public GameObject go;// Update is called once per framevoid Update(){if (Input.GetKeyUp(KeyCode.A)){if (go != null){Exporter.ExportObj(go, Application.dataPath + "/Export/" + go.name + ".obj");}}}
}

注意,运行时如果是带动画的需要先把动画脚本禁用掉,不然一些节点位置可能会发生错乱
禁用动画:

导出的效果截图:

不禁用动画时,可以看到脸部节点已经错乱了:

局限性

  • 只支持unity标准材质,或者漫反射颜色与贴图属性名跟标准材质相同的自定义材质
  • 材质只有漫反射颜色、透明度和漫反射贴图导出
  • 上面提到的带动画导出时节点位置可能会错乱
  • 导出的模型名请别用中文,目前导出的mtl文件的名字将会与模型名保持一致,如果mtl文件名含有中文,不少3D软件无法识别材质信息,包括unity,可以看下笔者的另一篇博客

完整代码

/****************************************************文件:Exporter.cs作者:TKB邮箱: 544726237@qq.com日期:2021/7/24 23:9:12功能:导出obj(如果有贴图,仅在编辑器模式下才支持)
*****************************************************/using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;namespace TLib
{public class Exporter{//保存同名次数static Dictionary<string, int> meshNameCountDic = new Dictionary<string, int>();#region 导出obj/// <summary>/// 导出GameObject及其子对象为一个obj/// </summary>/// <param name="go">要导出的GameObject</param>/// <param name="outputPath">导出的obj完整路径,如 Application.dataPath+"/temp.obj"</param>/// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系(标准obj),默认为true</param>/// <param name="compress">是否要压缩存储,默认为true</param>public static void ExportObj(GameObject go, string outputPath, bool exchangeCoordinate = true,bool compress = true){if (!go) return;meshNameCountDic.Clear();if (!Directory.Exists(Path.GetDirectoryName(outputPath))){Directory.CreateDirectory(Path.GetDirectoryName(outputPath));}if (File.Exists(outputPath)){try{File.Delete(outputPath);Debug.LogWarning("该路径已存在同名文件,已删除!" + outputPath);}catch (Exception e){Debug.LogError(e + "该路径已存在同名文件并且删除失败!" + outputPath);return;}}MeshFilter[] meshFilters = go.GetComponentsInChildren<MeshFilter>();SkinnedMeshRenderer[] skinnedMeshRenderers = go.GetComponentsInChildren<SkinnedMeshRenderer>();int meshCount = meshFilters.Length + skinnedMeshRenderers.Length;List<string> exportedMatList = new List<string>();//保存已经导出的材质名字 FileStream meshFS=null;StreamWriter meshSW=null;try{meshFS = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);meshSW = new StreamWriter(meshFS, Encoding.UTF8);string matPath = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath) + ".mtl");StringBuilder sb = new StringBuilder();int currentIndex = 0;int currentNormalIndex = 0;int currentUVIndex = 0;meshSW.Write("#Export by TLib from Unity3D\n");meshSW.Write("#Time : "+DateTime.Now+"\n");meshSW.Write("\nmtllib " + Path.GetFileNameWithoutExtension(outputPath) + ".mtl\n\n");for (int i = 0; i < meshFilters.Length; i++){Mesh mesh;Material[] mats;
#if UNITY_EDITORmesh = meshFilters[i].sharedMesh;mats = meshFilters[i].gameObject.GetComponent<MeshRenderer>().sharedMaterials;
#elsemesh = meshFilters[i].mesh;mats = meshFilters[i].gameObject.GetComponent<MeshRenderer>().materials;
#endiffor (int j = 0; j < mats.Length; j++){//某些材质没有设置或者丢失if (mats[j] == null){Material mat = new Material(Shader.Find("Standard"));mat.name = mesh.name + "_" + i + "_" + j;mats[j] = mat;}if (exportedMatList.Contains(mats[j].name)) continue;ExportMaterialToObj(mats[j], sb, Path.GetDirectoryName(outputPath));exportedMatList.Add(mats[j].name);}
#if UNITY_EDITORUnityEditor.EditorUtility.DisplayProgressBar("导出Obj", mesh.name + ":" + i + "/" + meshCount, i * 1.0f / meshCount);
#endifExportMeshToObj(meshFilters[i].transform, mesh, meshSW, mats, ref currentIndex, ref currentNormalIndex, ref currentUVIndex, exchangeCoordinate,compress);}for (int i = 0; i < skinnedMeshRenderers.Length; i++){Mesh mesh;Material[] mats;
#if UNITY_EDITORmesh = skinnedMeshRenderers[i].sharedMesh;mats = skinnedMeshRenderers[i].sharedMaterials;
#elsemesh = meshFilters[i].mesh;mats = meshFilters[i].materials;
#endiffor (int j = 0; j < mats.Length; j++){//某些材质没有设置或者丢失if (mats[j] == null){Material mat = new Material(Shader.Find("Standard"));mat.name = mesh.name + "_" + i + "_" + j;mats[j] = mat;}if (exportedMatList.Contains(mats[j].name)) continue;ExportMaterialToObj(mats[j], sb, Path.GetDirectoryName(outputPath));exportedMatList.Add(mats[j].name);}
#if UNITY_EDITORUnityEditor.EditorUtility.DisplayProgressBar("导出Obj", mesh.name + ":" + (i+meshFilters.Length) + "/" + meshCount, i * 1.0f / meshCount);
#endifExportMeshToObj(skinnedMeshRenderers[i].transform, mesh, meshSW, mats, ref currentIndex, ref currentNormalIndex, ref currentUVIndex, exchangeCoordinate,compress);}meshSW.Close();meshFS.Close();File.WriteAllText(matPath, sb.ToString());}catch (Exception e){Debug.LogError(e);if (meshSW!=null) meshSW.Close();if (meshFS != null) meshFS.Close();}finally{exportedMatList.Clear();UnityEditor.EditorUtility.ClearProgressBar();}}/// <summary>/// 导出Transform及其子对象为一个obj/// </summary>/// <param name="trans">待导出的Transform</param>/// <param name="outputPath">导出的obj完整路径,如 Application.dataPath+"/temp.obj"</param>/// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系(标准obj),默认为true</param>/// <param name="compress">是否要压缩存储,默认为true</param>public static void ExportObj(Transform trans, string outputPath, bool exchangeCoordinate = true,bool compress = true){ExportObj(trans.gameObject, outputPath, exchangeCoordinate,compress);}/// <summary>/// 导出GameObject及其子对象为多个obj,每个mesh对应一个obj/// </summary>/// <param name="go">要导出的GameObject</param>/// <param name="outputDir">将obj导出到哪个文件夹</param>/// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系(标准obj),默认为true</param>/// <param name="compress">是否要压缩存储,默认为true</param>public static void ExportObjs(GameObject go, string outputDir, bool exchangeCoordinate = true, bool compress = true){if (!Directory.Exists(outputDir)) Directory.CreateDirectory(outputDir);MeshFilter[] meshFilters = go.GetComponentsInChildren<MeshFilter>();SkinnedMeshRenderer[] skinnedMeshRenderers = go.GetComponentsInChildren<SkinnedMeshRenderer>();int meshCount = meshFilters.Length + skinnedMeshRenderers.Length;Dictionary<string, int> meshNameDic = new Dictionary<string, int>();int currentIndex = 0;int currentNormalIndex = 0;int currentUVIndex = 0;for (int i = 0; i < meshFilters.Length; i++){try{string name = meshFilters[i].gameObject.name;if (meshNameDic.ContainsKey(name)){meshNameDic[name]++;name += meshNameDic[name];}else meshNameDic.Add(name, 0);string objPath = Path.Combine(outputDir, name + ".obj");FileStream meshFS = new FileStream(objPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);StreamWriter meshSW = new StreamWriter(meshFS, Encoding.UTF8);string matPath = Path.Combine(outputDir, name + ".mtl");StringBuilder sb = new StringBuilder();meshNameCountDic.Clear();meshSW.Write("# Export by TLib\n# " + DateTime.Now.ToString("yyyy-MM-dd hh-mm-ss") + "\n");meshSW.Write("\nmtllib " + name + ".mtl\n\n");Mesh mesh;Material[] mats;
#if UNITY_EDITORmesh = meshFilters[i].sharedMesh;mats = meshFilters[i].gameObject.GetComponent<MeshRenderer>().sharedMaterials;
#elsemesh = meshFilters[i].mesh;mats = meshFilters[i].gameObject.GetComponent<MeshRenderer>().materials;
#endifList<string> exportedMatList = new List<string>();//保存已经导出的材质名字 for (int j = 0; j < mats.Length; j++){//某些材质没有设置或者丢失if (mats[j] == null){Material mat = new Material(Shader.Find("Standard"));mat.name = mesh.name + "_" + i + "_" + j;mats[j] = mat;}if (exportedMatList.Contains(mats[j].name)) continue;ExportMaterialToObj(mats[j], sb, Path.GetDirectoryName(objPath));exportedMatList.Add(mats[j].name);}
#if UNITY_EDITORUnityEditor.EditorUtility.DisplayProgressBar("导出Obj", mesh.name + ":" + i + "/" + meshCount, i * 1.0f / meshCount);
#endif//分别导出obj时,每导出一个obj都要重置这些索引变量currentIndex = 0;currentNormalIndex = 0;currentUVIndex = 0;ExportMeshToObj(meshFilters[i].transform, mesh, meshSW, mats, ref currentIndex, ref currentNormalIndex, ref currentUVIndex, exchangeCoordinate,compress);File.WriteAllText(matPath, sb.ToString());meshSW.Close();meshFS.Close();}catch (Exception e){Debug.Log(e);}}for (int i = 0; i < skinnedMeshRenderers.Length; i++){try{string name = skinnedMeshRenderers[i].gameObject.name;if (meshNameDic.ContainsKey(name)){name += meshNameDic[name];meshNameDic[name]++;}else meshNameDic.Add(name, 1);string objPath = Path.Combine(outputDir, name + ".obj");FileStream meshFS = new FileStream(objPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);StreamWriter meshSW = new StreamWriter(meshFS, Encoding.UTF8);string matPath = Path.Combine(outputDir, name + ".mtl");StringBuilder sb = new StringBuilder();meshNameCountDic.Clear();meshSW.Write("# Export by TLib\n# " + DateTime.Now.ToString("yyyy-MM-dd hh-mm-ss") + "\n");meshSW.Write("\nmtllib " + name + ".mtl\n\n");Mesh mesh;Material[] mats;
#if UNITY_EDITORmesh = skinnedMeshRenderers[i].sharedMesh;mats = skinnedMeshRenderers[i].sharedMaterials;
#elsemesh = meshFilters[i].mesh;mats = meshFilters[i].
materials;
#endifList<string> exportedMatList = new List<string>();//保存已经导出的材质名字 for (int j = 0; j < mats.Length; j++){//某些材质没有设置或者丢失if (mats[j] == null){Material mat = new Material(Shader.Find("Standard"));mat.name = mesh.name + "_" + i + "_" + j;mats[j] = mat;}if (exportedMatList.Contains(mats[j].name)) continue;ExportMaterialToObj(mats[j], sb, Path.GetDirectoryName(objPath));exportedMatList.Add(mats[j].name);}
#if UNITY_EDITORUnityEditor.EditorUtility.DisplayProgressBar("导出Obj", mesh.name + ":" + (i + meshFilters.Length) + "/" + meshCount, i * 1.0f / meshCount);
#endif//分别导出obj时,每导出一个obj都要重置这些索引变量currentIndex = 0;currentNormalIndex = 0;currentUVIndex = 0;ExportMeshToObj(skinnedMeshRenderers[i].transform, mesh, meshSW, mats, ref currentIndex, ref currentNormalIndex, ref currentUVIndex, exchangeCoordinate,compress);File.WriteAllText(matPath, sb.ToString());meshSW.Close();meshFS.Close();}catch (Exception e){Debug.Log(e);}}UnityEditor.EditorUtility.ClearProgressBar();}/// <summary>/// 导出Transform及其子对象为多个obj,每个mesh对应一个obj/// </summary>/// <param name="trans">待导出的Transform</param>/// <param name="outputDir">将obj导出到哪个文件夹</param>/// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系(标准obj),默认为true</param>/// <param name="compress">是否要压缩存储,默认为true</param>public static void ExportObjs(Transform trans, string outputDir, bool exchangeCoordinate = true,bool compress = true){ExportObjs(trans.gameObject, outputDir, exchangeCoordinate,compress);}/// <summary>/// 将mesh数据导出obj,用指定的StreamWrite写出/// </summary>/// <param name="trans">mesh对应的Transform,用于将顶点转换到世界空间</param>/// <param name="mesh">待导出的mesh</param>/// <param name="sw">输出流,使用这个输出流导出obj</param>/// <param name="materialName">这个mesh对应的材质名</param>/// <param name="currentIndex">到这个mesh为止前面导出了多少个顶点,用于索引偏移</param>/// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系(标准obj)</param>private static void ExportMeshToObj(Transform trans, Mesh mesh, StreamWriter sw, Material[] materials, ref int currentIndex, ref int currentNormalIndex, ref int currentUVIndex, bool exchangeCoordinate,bool compress){Vector3[] vertices = mesh.vertices;Vector3[] normals = mesh.normals;Vector2[] uvs = mesh.uv;bool hasNormal = false;bool hasUV = false;//mesh名字处理string name = mesh.name;if (meshNameCountDic.ContainsKey(name)){string tempName = name;name = name + "_" + meshNameCountDic[tempName];meshNameCountDic[tempName]++;}else{meshNameCountDic.Add(name, 1);}if (compress){//保存相同的 顶点/法线/UV 对应的唯一索引Dictionary<Vector3, int> verticesDic = new Dictionary<Vector3, int>();Dictionary<Vector3, int> normalDic = new Dictionary<Vector3, int>();Dictionary<Vector2, int> uvDic = new Dictionary<Vector2, int>();//计算重复的顶点法线uvfor (int i = 0; i < vertices.Length; i++){if (!verticesDic.ContainsKey(vertices[i])){verticesDic.Add(vertices[i], verticesDic.Count);}}if(normals.Length  == vertices.Length){hasNormal = true;for (int i = 0; i < normals.Length; i++){if (!normalDic.ContainsKey(normals[i])){normalDic.Add(normals[i], normalDic.Count);}}}if(uvs.Length == vertices.Length ){hasUV = true;for (int i = 0; i < uvs.Length; i++){if (!uvDic.ContainsKey(uvs[i])){uvDic.Add(uvs[i], uvDic.Count);}}}//将不重复的顶点法线uv写出到objforeach (Vector3 item in verticesDic.Keys){//变换到世界空间Vector3 worldPos = trans.TransformPoint(item);//如果需要变换手系if (exchangeCoordinate) worldPos.x *= -1;//写出sw.Write("v " + worldPos.x + " " + worldPos.y + " " + worldPos.z + "\n");}sw.Write("\n");if(hasNormal){foreach (Vector3 item in normalDic.Keys){//变换到世界空间Vector3 worldNormal = trans.TransformDirection(item);//如果需要变换手系if (exchangeCoordinate) worldNormal.x *= -1;//写出sw.Write("vn " + worldNormal.x + " " + worldNormal.y + " " + worldNormal.z + "\n");}sw.Write("\n");}if (hasUV){foreach (Vector2 item in uvDic.Keys){sw.Write("vt " + item.x + " " + item.y + " 0.0\n");}sw.Write("\n");}for (int k = 0; k < mesh.subMeshCount; k++){if (mesh.subMeshCount == 1){sw.Write("\ng " + name + "\n");}else{sw.Write("\ng " + name + "_" + k + "\n");}sw.Write("usemtl " + materials[k].name + "\n");int[] tris = mesh.GetIndices(k);for (int i = 0; i < tris.Length / 3; i++){int verticesIndex1 = 0, verticesIndex2 = 0, verticesIndex3 = 0, normalIndex1 = 0, normalIndex2 = 0, normalIndex3 = 0, uvIndex1 = 0, uvIndex2 = 0, uvIndex3 = 0;try{Vector3 curPos1 = vertices[tris[i * 3]];Vector3 curPos2 = vertices[tris[i * 3 + 1]];Vector3 curPos3 = vertices[tris[i * 3 + 2]];verticesIndex1 = verticesDic[curPos1];verticesIndex2 = verticesDic[curPos2];verticesIndex3 = verticesDic[curPos3];}catch(Exception e){Debug.Log("缺少顶点重用信息,索引为:" + tris[i * 3] + "\n" + e);}if (hasNormal){try{Vector3 curNormal1 = normals[tris[i * 3]];Vector3 curNormal2 = normals[tris[i * 3 + 1]];Vector3 curNormal3 = normals[tris[i * 3 + 2]];normalIndex1 = normalDic[curNormal1];normalIndex2 = normalDic[curNormal2];normalIndex3 = normalDic[curNormal3];}catch (Exception e){Debug.Log("缺少法线重用信息,索引为:" + tris[i * 3] + "\n" + e);}}if (hasUV){try{Vector2 curUv1 = uvs[tris[i * 3]];Vector2 curUv2 = uvs[tris[i * 3 + 1]];Vector2 curUv3 = uvs[tris[i * 3 + 2]];uvIndex1 = uvDic[curUv1];uvIndex2 = uvDic[curUv2];uvIndex3 = uvDic[curUv3];}catch(Exception e){Debug.Log("缺少UV重用信息,索引为:" + tris[i * 3] + "\n" + e);}}if (exchangeCoordinate){//既有法线也有uvif (hasNormal && hasUV){sw.Write("f " + (verticesIndex2 + 1 + currentIndex) + "/" + (uvIndex2 + 1 + currentUVIndex) + "/" + (normalIndex2 + 1 + currentNormalIndex));sw.Write(" " + (verticesIndex1 + 1 + currentIndex) + "/" + (uvIndex1 + 1 + currentUVIndex) + "/" + (normalIndex1 + 1 + currentNormalIndex));sw.Write(" " + (verticesIndex3 + 1 + currentIndex) + "/" + (uvIndex3 + 1 + currentUVIndex) + "/" + (normalIndex3 + 1 + currentNormalIndex) + "\n");}//有uv没法线else if(!hasNormal && hasUV){sw.Write("f " + (verticesIndex2 + 1 + currentIndex) + "/" + (uvIndex2 + 1 + currentUVIndex));sw.Write(" " + (verticesIndex1 + 1 + currentIndex) + "/" + (uvIndex1 + 1 + currentUVIndex));sw.Write(" " + (verticesIndex3 + 1 + currentIndex) + "/" + (uvIndex3 + 1 + currentUVIndex) + "\n");}//有法线没uvelse if(hasNormal && !hasUV){sw.Write("f " + (verticesIndex2 + 1 + currentIndex) + "//" + (normalIndex2 + 1 + currentNormalIndex));sw.Write(" " + (verticesIndex1 + 1 + currentIndex) + "//" + (normalIndex1 + 1 + currentNormalIndex));sw.Write(" " + (verticesIndex3 + 1 + currentIndex) + "//" + (normalIndex3 + 1 + currentNormalIndex) + "\n");}//既没法线也没uvelse if(!hasNormal && !hasUV){sw.Write("f " + (verticesIndex2 + 1 + currentIndex));sw.Write(" " + (verticesIndex1 + 1 + currentIndex));sw.Write(" " + (verticesIndex3 + 1 + currentIndex) + "\n");}}else{//既有法线也有uvif (hasNormal && hasUV){sw.Write("f " + (verticesIndex1 + 1 + currentIndex) + "/" + (uvIndex1 + 1 + currentUVIndex) + "/" + (normalIndex1 + 1 + currentNormalIndex));sw.Write(" " + (verticesIndex2 + 1 + currentIndex) + "/" + (uvIndex2 + 1 + currentUVIndex) + "/" + (normalIndex2 + 1 + currentNormalIndex));sw.Write(" " + (verticesIndex3 + 1 + currentIndex) + "/" + (uvIndex3 + 1 + currentUVIndex) + "/" + (normalIndex3 + 1 + currentNormalIndex) + "\n");}//有uv没法线else if (!hasNormal && hasUV){sw.Write("f " + (verticesIndex1 + 1 + currentIndex) + "/" + (uvIndex1 + 1 + currentUVIndex));sw.Write(" " + (verticesIndex2 + 1 + currentIndex) + "/" + (uvIndex2 + 1 + currentUVIndex));sw.Write(" " + (verticesIndex3 + 1 + currentIndex) + "/" + (uvIndex3 + 1 + currentUVIndex) + "\n");}//有法线没uvelse if (hasNormal && !hasUV){sw.Write("f " + (verticesIndex1 + 1 + currentIndex) + "//" + (normalIndex1 + 1 + currentNormalIndex));sw.Write(" " + (verticesIndex2 + 1 + currentIndex) + "//" + (normalIndex2 + 1 + currentNormalIndex));sw.Write(" " + (verticesIndex3 + 1 + currentIndex) + "//" + (normalIndex3 + 1 + currentNormalIndex) + "\n");}//既没法线也没uvelse if (!hasNormal && !hasUV){sw.Write("f " + (verticesIndex1 + 1 + currentIndex));sw.Write(" " + (verticesIndex2 + 1 + currentIndex));sw.Write(" " + (verticesIndex3 + 1 + currentIndex) + "\n");}}}}sw.Write("\n");currentIndex += verticesDic.Count;currentNormalIndex += normalDic.Count;currentUVIndex += uvDic.Count;verticesDic.Clear();normalDic.Clear();uvDic.Clear();}else{for (int i = 0; i < vertices.Length; i++){Vector3 worldPos = trans.TransformPoint(vertices[i]);//顶点镜像if (exchangeCoordinate) worldPos.x *= -1;sw.Write("v " + worldPos.x + " " + worldPos.y + " " + worldPos.z + "\n");}sw.Write("\n");if(normals.Length == vertices.Length){hasNormal = true;for (int i = 0; i < normals.Length; i++){Vector3 worldNormal = trans.TransformDirection(normals[i]);//法线镜像if (exchangeCoordinate) worldNormal.x *= -1;sw.Write("vn " + worldNormal.x + " " + worldNormal.y + " " + worldNormal.z + "\n");}sw.Write("\n");}if(uvs.Length == vertices.Length){hasUV = true;for (int i = 0; i < uvs.Length; i++){sw.Write("vt " + uvs[i].x + " " + uvs[i].y + "\n");}sw.Write("\n");}for (int k = 0; k < mesh.subMeshCount; k++){if (mesh.subMeshCount == 1){sw.Write("\ng " + mesh.name + "\n");}else{sw.Write("\ng " + mesh.name + "_" + k + "\n");}sw.Write("usemtl " + materials[k].name + "\n");int[] tris = mesh.GetIndices(k);for (int i = 0; i < tris.Length / 3; i++){if (exchangeCoordinate){if(hasNormal && hasUV){sw.Write("f " + (tris[i * 3 + 1] + 1 + currentIndex) + "/" + (tris[i * 3 + 1] + 1 + currentUVIndex) + "/" + (tris[i * 3 + 1] + 1 + currentNormalIndex));sw.Write(" " + (tris[i * 3] + 1 + currentIndex) + "/" + (tris[i * 3] + 1 + currentUVIndex) + "/" + (tris[i * 3] + 1 + currentNormalIndex));sw.Write(" " + (tris[i * 3 + 2] + 1 + currentIndex) + "/" + (tris[i * 3 + 2] + 1 + currentUVIndex) + "/" + (tris[i * 3 + 2] + 1 + currentNormalIndex) + "\n");}else if(!hasNormal && hasUV){sw.Write("f " + (tris[i * 3 + 1] + 1 + currentIndex) + "/" + (tris[i * 3 + 1] + 1 + currentUVIndex));sw.Write(" " + (tris[i * 3] + 1 + currentIndex) + "/" + (tris[i * 3] + 1 + currentUVIndex));sw.Write(" " + (tris[i * 3 + 2] + 1 + currentIndex) + "/" + (tris[i * 3 + 2] + 1 + currentUVIndex) + "\n");}else if(hasNormal && !hasUV){sw.Write("f " + (tris[i * 3 + 1] + 1 + currentIndex) + "//" + (tris[i * 3 + 1] + 1 + currentNormalIndex));sw.Write(" " + (tris[i * 3] + 1 + currentIndex) + "//" + (tris[i * 3] + 1 + currentNormalIndex));sw.Write(" " + (tris[i * 3 + 2] + 1 + currentIndex) + "//" + (tris[i * 3 + 2] + 1 + currentNormalIndex) + "\n");}else if(!hasNormal && !hasUV){sw.Write("f " + (tris[i * 3 + 1] + 1 + currentIndex));sw.Write(" " + (tris[i * 3] + 1 + currentIndex));sw.Write(" " + (tris[i * 3 + 2] + 1 + currentIndex) + "\n");}}else{if (hasNormal && hasUV){sw.Write("f " + (tris[i * 3] + 1 + currentIndex) + "/" + (tris[i * 3] + 1 + currentUVIndex) + "/" + (tris[i * 3] + 1 + currentNormalIndex));sw.Write(" " + (tris[i * 3 + 1] + 1 + currentIndex) + "/" + (tris[i * 3 + 1] + 1 + currentUVIndex) + "/" + (tris[i * 3 + 1] + 1 + currentNormalIndex));sw.Write(" " + (tris[i * 3 + 2] + 1 + currentIndex) + "/" + (tris[i * 3 + 2] + 1 + currentUVIndex) + "/" + (tris[i * 3 + 2] + 1 + currentNormalIndex) + "\n");}else if (!hasNormal && hasUV){sw.Write("f " + (tris[i * 3] + 1 + currentIndex) + "/" + (tris[i * 3] + 1 + currentUVIndex));sw.Write(" " + (tris[i * 3 + 1] + 1 + currentIndex) + "/" + (tris[i * 3 + 1] + 1 + currentUVIndex));sw.Write(" " + (tris[i * 3 + 2] + 1 + currentIndex) + "/" + (tris[i * 3 + 2] + 1 + currentUVIndex) + "\n");}else if (hasNormal && !hasUV){sw.Write("f " + (tris[i * 3] + 1 + currentIndex) + "//" + (tris[i * 3] + 1 + currentNormalIndex));sw.Write(" " + (tris[i * 3 + 1] + 1 + currentIndex) + "//" + (tris[i * 3 + 1] + 1 + currentNormalIndex));sw.Write(" " + (tris[i * 3 + 2] + 1 + currentIndex) + "//" + (tris[i * 3 + 2] + 1 + currentNormalIndex) + "\n");}else if (!hasNormal && !hasUV){sw.Write("f " + (tris[i * 3] + 1 + currentIndex));sw.Write(" " + (tris[i * 3 + 1] + 1 + currentIndex));sw.Write(" " + (tris[i * 3 + 2] + 1 + currentIndex) + "\n");}}}}currentIndex += vertices.Length;currentNormalIndex += normals.Length;currentUVIndex += uvs.Length;}}/// <summary>/// 写出材质信息,贴图仅在编辑器模式可用/// </summary>/// <param name="mat">要写出的材质</param>/// <param name="sb">StringBuilder,用于写出材质</param>/// <param name="outputDir">如果有贴图,需要将贴图复制到obj的生成路径上</param>private static void ExportMaterialToObj(Material mat, StringBuilder sb, string outputDir){sb.Append("\nnewmtl " + mat.name + "\n");//漫反射颜色sb.Append("Kd " + mat.color.r + " " + mat.color.g + " " + mat.color.b + "\n");sb.Append("d " + mat.color.a + "\n"); //透明度if (mat.mainTexture){#if UNITY_EDITORstring path = UnityEditor.AssetDatabase.GetAssetPath(mat.mainTexture);sb.Append("map_Kd " + Path.GetFileName(path) + "\n");File.Copy(path, Path.Combine(outputDir, Path.GetFileName(path)), true);
#endif}}#endregion}
}

Unity 导出obj模型相关推荐

  1. unity导出.obj模型文件

    unity导出.obj模型文件 最近使用realworldterrain生成真实地形遇到一个问题,就是该地形的坐标轴没有在中心位置,这样在旋转缩放操作时候就有各种问题,效果不好,于是想到先导出该地形为 ...

  2. Unity 导出 obj, fbx

    本文转自本人简书,原文链接: https://www.jianshu.com/p/6b7e36f70be3 2020-05-05 项目需要用Unity导出3D物体为obj或fbx格式,从而导入其他软件 ...

  3. unity导出fbx模型_Unity批量合并Animation工具/根据已有的Animation文件批量生成带FBX动画工具...

    由于本人现有项目的项目素材大部分都需要继续沿用旧项目的模型与动画,但在接受旧模型动画的时候发现,模型动画由于外包已经丢失了3dmax的源文件,只剩下了一堆AnimationCilp(.anim)文件与 ...

  4. unity导出fbx模型_ARTBOOK艺书专栏:Fbx导出杂谈

    我经常说,看人三维基本功扎实不扎实,可以直接看他导出到引擎正确不正确,可以在非常短时间内看出对三维制作工具和引擎的基本理解. 以我的观察,国内从业者可以说在这方面百分之九十不合格, 首先是mesh轴向 ...

  5. 【H5 3D应用开发】Blender 制作导出Obj模型带纹理到three.js(二)

    工具: Bender2.7.8.0  +  three.js忘记了多少了 QQ:453738784 1.首先正常打开一个Blender  我们看到一个正方形 选择编辑模式后 选中你要添加纹理的面   ...

  6. 3ds Max导出带贴图的obj模型

    先找一块大理石地板的贴图 接着在3ds Max中画一个最简单的立方体,要薄一点像地板 点击菜单栏中的渲染,选择精简材质编辑器 在材质编辑器面板里随便点一个球,然后按照下图所示的1和2步骤分别点击漫反射 ...

  7. Unity导出模型为Obj文件

    Unity导出模型为Obj文件 资源链接 下载导入 代码纪要 使用方式 参考链接 资源链接 原插件代码中只有MeshFilter的Obj导出代码:由于项目需求,需要将SkinnedMeshRender ...

  8. Unity地形导出为.obj模型

    我在Uniyt 3D中创建的真实地形想保存为模型以备以后使用,经过在网上艰辛的搜索(呵呵...),终于找到一个方法,经过实验验证,绝对真实可靠!有图有真相! 先上代码(O(∩_∩)O哈哈~). 源代码 ...

  9. unity 批量导入模型工具_零基础的Unity图形学笔记3:使用多模型UV与优化模型导出...

    前文所说,贴图多UV,直接命名对应贴图就可以. 模型的多套UV,则需要在3DMAX里编辑. 这篇文章主要解决两个问题: 如何正确使用多模型UV? 从3DMAX导出,到shader使用 如何优化模型导出 ...

  10. zbrush导入obj模型不显示_ZBrush中如何导入和导出OBJ文件—ZBrush教程

    原标题:ZBrush中如何导入和导出OBJ文件-ZBrush教程 ZBrush中如何导入和导出OBJ文件 ZBrush软件中对于文件的导出与储存格式是多样的.OBJ格式是如何导入和导出ZBrush的, ...

最新文章

  1. 国民认证科技有限公司助力构建我国可信网络空间
  2. php获取表单信息的代码_php 表单数据的获取代码
  3. Html5 冒泡排序演示
  4. 图片饱和度_摄影后期完全调色指南(三):饱和度与自然饱和度有什么区别?...
  5. 【Head First 设计模式】-装饰者模式读后总结
  6. 数据库实验3 数据库的单表查询
  7. Identity Server 4 - Hybrid Flow - MVC客户端身份验证
  8. SQL在线格式化工具
  9. 1 使用WPE工具分析游戏网络封包
  10. linux锐捷认证成功无法上网,如何修复win7系统锐捷认证成功但是却无法上网的操作教程...
  11. 某度文库付费文档下载,实测可用~
  12. Talib技术因子详解(七)
  13. Django框架零基础入门
  14. Android 利用重力感应调整手机模式
  15. 安装AAE v11.x Control Room简易教程
  16. java中isolate时间_Flutter 92: 图解 Dart 单线程实现异步处理之 Isolate (一)
  17. Java韩顺平02变量
  18. Flutter网络请求
  19. 微信小程序自定义选中样式打小勾
  20. 国际贸易术语解释通则(CIF 成本、保险费加运费(……指定目的港))

热门文章

  1. JAVA 多线程并发
  2. 推荐一个图片在线生成链接的网站
  3. QT入门-可视化UI设计
  4. mongos、nanomsg、zeroMQ简述和go-mongos使用实例
  5. python 串口助手 简书_pySerial 串口工具简介
  6. coap 返回版本信息_CoAP协议浅析
  7. ppp协议c语言,ppp协议是用于拨号上网和路由器之间通信的点到点通信协议,是属于(1)协议,它不具有(2)的功能。( - 信管网...
  8. 系列课程 ElasticSearch 之第 9 篇 —— ELK (ElasticSearch、Logstash、Kibana)分布式日志收集和查看(完结)
  9. 9.20残差网络 ResNet
  10. 同花顺公式转python_【转】 同花顺系统公式编写教程及函数用法基础(一)