前言
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 frame
void 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_EDITOR
mesh = meshFilters[i].sharedMesh;
mats = meshFilters[i].gameObject.GetComponent<MeshRenderer>().sharedMaterials;
#else
mesh = meshFilters[i].mesh;
mats = meshFilters[i].gameObject.GetComponent<MeshRenderer>().materials;
#endif
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(outputPath));
exportedMatList.Add(mats[j].name);
}
#if UNITY_EDITOR
UnityEditor.EditorUtility.DisplayProgressBar("导出Obj", mesh.name + ":" + i + "/" + meshCount, i * 1.0f / meshCount);
#endif
ExportMeshToObj(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_EDITOR
mesh = skinnedMeshRenderers[i].sharedMesh;
mats = skinnedMeshRenderers[i].sharedMaterials;
#else
mesh = meshFilters[i].mesh;
mats = meshFilters[i].materials;
#endif
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(outputPath));
exportedMatList.Add(mats[j].name);
}
#if UNITY_EDITOR
UnityEditor.EditorUtility.DisplayProgressBar("导出Obj", mesh.name + ":" + (i+meshFilters.Length) + "/" + meshCount, i * 1.0f / meshCount);
#endif
ExportMeshToObj(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_EDITOR
mesh = meshFilters[i].sharedMesh;
mats = meshFilters[i].gameObject.GetComponent<MeshRenderer>().sharedMaterials;
#else
mesh = meshFilters[i].mesh;
mats = meshFilters[i].gameObject.GetComponent<MeshRenderer>().materials;
#endif
List<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_EDITOR
UnityEditor.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_EDITOR
mesh = skinnedMeshRenderers[i].sharedMesh;
mats = skinnedMeshRenderers[i].sharedMaterials;
#else
mesh = meshFilters[i].mesh;
mats = meshFilters[i].
materials;
#endif
List<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_EDITOR
UnityEditor.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>();
//计算重复的顶点法线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);
}
}
}
//将不重复的顶点法线uv写出到obj
foreach (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)
{
//既有法线也有uv
if (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");
}
//有法线没uv
else 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");
}
//既没法线也没uv
else if(!hasNormal && !hasUV)
{
sw.Write("f " + (verticesIndex2 + 1 + currentIndex));
sw.Write(" " + (verticesIndex1 + 1 + currentIndex));
sw.Write(" " + (verticesIndex3 + 1 + currentIndex) + "\n");
}
}
else
{
//既有法线也有uv
if (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");
}
//有法线没uv
else 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");
}
//既没法线也没uv
else 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_EDITOR
string 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
}
}