unity2018.4.2f1
vs2017
最近项目需求,需要实现动态读物外部obj模型,并加载到场景中,研究了好几天,终于实现了,在此做个记录。
1、首先随便找个.obj模型,带贴图,我的资源截图如下:
.mtl文件是负责记录模型与贴图的对应关系
obj问价与mtl文件均可以用记事本打开,查看内部数据
obj文件截图:
mtllib Tifa.mtl 记录当前obj文件对应的mtl文件的名称(一个mtl文件可以包含多个材质)
v:模型顶点数据
vt:模型顶点纹理坐标数据
vn:模型顶点法线数据
usemtl diss_00.png 表示当前模型分组使用的材质,每一个模型分组以usemtl 开始(一个模型存在有多个子物体的情况)
比如我这个:
mtl文件截图:
newmtl diss_00.png:表示当前的材质名称
illum:照明度(0-10)
kd:当前材质的散射光
ka:当前材质的环境光
ks:当前材质的镜面光
ke:当前材质的散射光
Ns:材质的光亮度
map_Kd:当前材质对应的贴图名称
2、根据模型文本文件分析,编写模型数据结构,直接上代码
ObjPart:obj文件数据结构
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjPart
{
/// <summary>
/// 材质名称
/// </summary>
public string strMatName;
/// <summary>
/// UV坐标数组
/// </summary>
public List<Vector2> listUV; //vt
/// <summary>
/// 法线数组
/// </summary>
public List<Vector3> listNormal;//vn
/// <summary>
/// 切线数组
/// </summary>
public List<Vector4> listTangent;
/// <summary>
/// 顶点数组
/// </summary>
public List<Vector3> listVertex; //v
/// <summary>
/// 面数组 面索引
/// </summary>
public List<int> listTriangle;
public ObjPart()
{
strMatName = "";
listUV = new List<Vector2>();
listNormal = new List<Vector3>();
listVertex = new List<Vector3>();
listTriangle = new List<int>();
listTangent = new List<Vector4>();
}
}
ObjMatItem:mtl文件数据结构
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//mtl数据结构(记录的是材质对应的贴图)
public class ObjMatItem
{
/// <summary>
/// 材质名称
/// </summary>
public string strMatName;
public int illum;
public Vector3 Kd;
public Vector3 Ka;
public Vector3 Tf;
public Vector2 widthHeight;
public string map_Kd;
public float Ni;
public ObjMatItem()
{
strMatName = "";
illum = -1;
Kd = new Vector3(0.0f, 0.0f, 0.0f);
Ka = new Vector3(0.0f, 0.0f, 0.0f);
Tf = new Vector3(0.0f, 0.0f, 0.0f);
map_Kd = "";
Ni = 1.0f;
widthHeight.x = 0.0f;
widthHeight.y = 0.0f;
}
}
ObjModel:模型数据结构
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 完整模型=网格+材质
/// </summary>
public class ObjModel
{
public List<ObjPart> objParts; //网格
public List<ObjMatItem> ObjMats; //材质
}
3、从文本文件中加载obj数据以及mtl数据
ObjMesh:负责从.obj文件中加载数据
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
/// <summary>
/// 负责从.obj文件中加载数据
/// </summary>
public class ObjMesh
{
/// <summary>
/// UV坐标列表
/// </summary>
private List<Vector3> uvArrayList;
/// <summary>
/// 法线列表
/// </summary>
private List<Vector3> normalArrayList;
/// <summary>
/// 顶点列表
/// </summary>
private List<Vector3> vertexArrayList;
public List<ObjPart> listObjParts;
public List<ObjMatItem> listObjMats;
public string _strMatPath { get; set; }
public string _strObjPath { get; set; }
public string _strObjName { get; set; }
/// <summary>
/// 构造函数
/// </summary>
public ObjMesh()
{
//初始化列表
uvArrayList = new List<Vector3>();
normalArrayList = new List<Vector3>();
vertexArrayList = new List<Vector3>();
listObjParts = new List<ObjPart>();
listObjMats = new List<ObjMatItem>();
_strMatPath = _strObjName = "";
}
/// <summary>
/// 从一个文本化后的.obj文件中加载模型
/// </summary>
public ObjMesh LoadFromObj(string strObjPath)
{
_strObjPath = strObjPath;
_strObjName = strObjPath;
_strObjName = _strObjName.Replace("/", "");
//读取内容
if (!File.Exists(strObjPath)) return null;
StreamReader reader = new StreamReader(strObjPath, System.Text.Encoding.Default);
string objText = reader.ReadToEnd();
reader.Close();
if (objText.Length <= 0)
return null;
//v这一行在3dsMax中导出的.obj文件
// 前面是两个空格后面是一个空格
objText = objText.Replace(" ", " ");
//将文本化后的obj文件内容按行分割
string[] allLines = objText.Split('\n');
foreach (string line in allLines)
{
//将每一行按空格分割
char[] charsToTrim = { ' ' };
string[] chars = line.TrimEnd('\r').TrimStart(' ').Split(charsToTrim, StringSplitOptions.RemoveEmptyEntries);
if (chars.Length <= 0)
{
continue;
}
//根据第一个字符来判断数据的类型
switch (chars[0])
{
case "mtllib":
_strMatPath = _strObjPath.Substring(0, _strObjPath.LastIndexOf('/') + 1) + chars[1];
break;
case "v":
//处理顶点
this.vertexArrayList.Add(new Vector3(
-(ConvertToFloat(chars[1])),
ConvertToFloat(chars[2]),
ConvertToFloat(chars[3]))
);
break;
case "vn":
//处理法线
this.normalArrayList.Add(new Vector3(
-ConvertToFloat(chars[1]),
ConvertToFloat(chars[2]),
ConvertToFloat(chars[3]))
);
break;
case "vt":
//处理UV
this.uvArrayList.Add(new Vector3(
ConvertToFloat(chars[1]),
ConvertToFloat(chars[2]))
);
break;
case "usemtl":
ObjPart objPart = new ObjPart();
objPart.strMatName = chars[1];//材质名称
listObjParts.Add(objPart);
break;
case "f":
//处理面
GetTriangleList(chars);
break;
}
}
//获取mtl文件路径
// string mtlFilePath = strObjPath.Replace(".obj", ".mtl");
if (_strMatPath != "")
{
LoadMat(_strMatPath);
}
return this;
}
private void LoadMat(string strmtlPath)
{
//从mtl文件中加载材质
listObjMats = ObjMaterial.Instance.LoadFormMtl(strmtlPath);
}
/// <summary>
/// 获取面列表.
/// </summary>
/// <param name="chars">Chars.</param>
///
private List<Vector3> indexVectorList = new List<Vector3>();
private Vector3 indexVector = new Vector3(0, 0);
private void GetTriangleList(string[] chars)
{
indexVectorList.Clear();
for (int i = 1; i < chars.Length; ++i)
{
//将每一行按照空格分割后从第一个元素开始
//按照/继续分割可依次获得顶点索引、法线索引和UV索引
string[] indexs = chars[i].Split('/');
Vector3 vertex = (Vector3)vertexArrayList[ConvertToInt(indexs[0]) - 1];
listObjParts[listObjParts.Count - 1].listVertex.Add(vertex);
indexVector = new Vector3(0, 0);
//UV索引
if (indexs.Length > 1)
{
if (indexs[1] != "")
indexVector.y = ConvertToInt(indexs[1]);
}
//法线索引
if (indexs.Length > 2)
{
if (indexs[2] != "")
indexVector.z = ConvertToInt(indexs[2]);
}
//给UV数组赋值
if (uvArrayList.Count > 0 && indexVector.y > 0.01)
{
Vector3 tVec = (Vector3)uvArrayList[(int)indexVector.y - 1];
listObjParts[listObjParts.Count - 1].listUV.Add(new Vector2(tVec.x, tVec.y));
}
//给法线数组赋值
if (normalArrayList.Count > 0 && indexVector.z > 0.01)
{
Vector3 nVec = (Vector3)normalArrayList[(int)indexVector.z - 1];
listObjParts[listObjParts.Count - 1].listNormal.Add(nVec);
}
//将索引向量加入列表中
indexVectorList.Add(indexVector);
}
//面索引
int nCount = listObjParts[listObjParts.Count - 1].listVertex.Count - indexVectorList.Count; //nCount==0 3 6 9 indexVectorList.Count=3 三角形面
//Debug.Log(indexVectorList.Count);
for (int j = 1; j < indexVectorList.Count - 1; ++j)
{
//按照0,1,2这样的方式来组成面
listObjParts[listObjParts.Count - 1].listTriangle.Add(nCount);
listObjParts[listObjParts.Count - 1].listTriangle.Add(nCount + j);
listObjParts[listObjParts.Count - 1].listTriangle.Add(nCount + j + 1);
}
}
/// <summary>
/// 将一个字符串转换为浮点类型
/// </summary>
/// <param name="s">待转换的字符串</param>
/// <returns></returns>
private float ConvertToFloat(string s)
{
//return (float)System.Convert.ToDouble(s,CultureInfo.InvariantCulture);
float fValue = 0.0f;
try
{
fValue = (float)Convert.ToDouble(s);
}
catch (Exception ex)
{
Debug.LogError("数据[" + s + "]转换失败! " + ex.Message);
}
return fValue;
}
/// <summary>
/// 将一个字符串转化为整型 /// </summary>
/// <returns>待转换的字符串</returns>
/// <param name="s"></param>
private int ConvertToInt(string s)
{
//return System.Convert.ToInt32(s, CultureInfo.InvariantCulture);
int nValue = 0;
try
{
nValue = Convert.ToInt32(s);
}
catch (Exception ex)
{
Debug.LogError("数据[" + s + "]转换失败! " + ex.Message);
}
return nValue;
}
}
ObjMaterial:负责从.mtl中加载数据
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
/// <summary>
/// 负责从.mtl中加载数据
/// </summary>
public class ObjMaterial
{
/// <summary>
/// 当前实例
/// </summary>
private static ObjMaterial instance=new ObjMaterial();
public static ObjMaterial Instance
{
get
{
if (instance == null)
instance = new ObjMaterial();//GameObject.FindObjectOfType<ObjMaterial>();
return instance;
}
}
/// <summary>
/// 从一个文本化后的mtl文件加载一组材质
/// </summary>
/// <param name="mtlText">文本化的mtl文件</param>
/// <param name="texturePath">贴图文件夹路径</param>
public List<ObjMatItem> LoadFormMtl(string strMtlText)
{
List<ObjMatItem> listObjMats = new List<ObjMatItem>();
DirectoryInfo mtlParent=Directory.GetParent(Settings.ObjPath);
if (!File.Exists(mtlParent + "\\" + strMtlText)) return null;
Stream mtlStream = new FileStream(mtlParent+"\\"+ strMtlText,FileMode.Open);//mtl文件与obj文件处于同一级目录下
StreamReader reader = new StreamReader(mtlStream);
string mtlText = reader.ReadToEnd();
reader.Close();
if (mtlText == "")
return listObjMats;
//将文本化后的内容按行分割
string[] allLines = mtlText.Split('\n');
foreach (string line in allLines)
{
//按照空格分割每一行的内容
string[] chars = line.TrimEnd('\r').TrimStart(' ').Split(' ');
switch (chars[0])
{
case "newmtl":
//处理材质名
ObjMatItem matItem = new ObjMatItem();
matItem.strMatName = chars[1];
listObjMats.Add(matItem);
//根据贴图创建材质球
break;
case "Ka":
listObjMats[listObjMats.Count - 1].Ka = new Vector3(
ConvertToFloat(chars[1]),
ConvertToFloat(chars[2]),
ConvertToFloat(chars[3])
);
break;
case "Kd":
//处理漫反射
listObjMats[listObjMats.Count - 1].Kd = new Vector3(
ConvertToFloat(chars[1]),
ConvertToFloat(chars[2]),
ConvertToFloat(chars[3])
);
break;
case "Ks":
//暂时仅考虑漫反射
break;
case "Ke":
//Todo
break;
case "Tf":
//处理漫反射
listObjMats[listObjMats.Count - 1].Tf = new Vector3(
ConvertToFloat(chars[1]),
ConvertToFloat(chars[2]),
ConvertToFloat(chars[3])
);
break;
case "Ni":
listObjMats[listObjMats.Count - 1].Ni = ConvertToFloat(chars[1]);
break;
case "e":
//Todo
break;
case "illum":
listObjMats[listObjMats.Count - 1].illum = Convert.ToInt32(chars[1]);
break;
case "map_Ka":
//暂时仅考虑漫反射
break;
case "map_Kd":
//处理漫反射贴图
string textureName = chars[1].Substring(chars[1].LastIndexOf("\\") + 1, chars[1].Length - chars[1].LastIndexOf("\\") - 1);
listObjMats[listObjMats.Count - 1].map_Kd = textureName;
break;
case "map_Ks":
//暂时仅考虑漫反射
break;
}
}
GameObject.Find("Canvas").GetComponent<LoadModel>().CreatMaterial(listObjMats);
return listObjMats;
}
/// <summary>
/// 将一个字符串转换为浮点类型
/// </summary>
/// <param name="s">待转换的字符串</param>
/// <returns></returns>
private float ConvertToFloat(string s)
{
return System.Convert.ToSingle(s);
}
}
Settings:记录数据
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Settings
{
public static string ObjPath=null; //模型路径
public static Dictionary<string, Material> ModelMaterialList = new Dictionary<string, Material>();//材质球存储 key:材质名称 value:材质球
}
4、开始使用获得的文件数据,在场景中加载模型
LoadModel:创建模型(该脚本挂载到canvas上,由按钮触发LoadModelEvent函数 开始创建模型)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using System;
using System.IO;
public class LoadModel : MonoBehaviour
{
private List<ObjMesh> listObjModel = new List<ObjMesh>();
private List<ObjMatItem> listObjMats = new List<ObjMatItem>();
private List<ObjPart> listObjParts = new List<ObjPart>();
private GameObject goParent;
public GameObject _gameObj; //模型父节点
private bool isDone = false; //true:材质球全部创建完成后 给模型贴材质
//按钮事件处理
public void LoadModelEvent()
{
string strObjPath = "C:\\Users\\123\\Desktop\\obj__001\\123\\飞马\\飞马 .obj";
Settings.ObjPath = strObjPath;
ObjMesh objInstace = new ObjMesh();
objInstace = objInstace.LoadFromObj(strObjPath);
if (objInstace == null) return;
listObjModel.Add(objInstace);
listObjParts = objInstace.listObjParts;//模型
Debug.Log("Parts:" + listObjParts.Count);
listObjMats = objInstace.listObjMats; //材质
if(listObjMats!=null)
Debug.Log("Mats:" + listObjMats.Count);
string strGameName = strObjPath;
strGameName = strGameName.Replace("/", "");
string[] names = strGameName.Split('\\');
strGameName = names[names.Length - 1];
goParent = new GameObject(strGameName); //模型资源名称
StartCoroutine(WaitLoadMaterialTexture());
}
IEnumerator WaitLoadMaterialTexture()
{
while (!isDone&& listObjMats!=null) //等待材质创建完成
{
yield return null;
}
//计算网格
int i = 0;
foreach (ObjPart part in listObjParts)
{
++i;
Mesh mesh = new Mesh();
mesh.vertices = part.listVertex.ToArray();//顶点
//mesh.triangles = part.listTriangle.ToArray();
mesh.triangles= ResetTriangles(part.listTriangle.ToArray());//修改三角形面 翻转三角形面
if (part.listUV.Count > 0)
{
mesh.uv = part.listUV.ToArray();
}
if (part.listNormal.Count > 0)
{
mesh.normals = part.listNormal.ToArray();
}
mesh.tangents = part.listTangent.ToArray(); //切线
mesh.RecalculateBounds();
// mesh.RecalculateNormals(); //法线
//生成物体
GameObject go = new GameObject(part.strMatName + i.ToString());
// ==go.AddComponent<ObjDestroy>();
MeshFilter meshFilter = go.AddComponent<MeshFilter>();
meshFilter.mesh = mesh;
//parts里面存储的有材质名称 根据材质名称生成材质球
MeshRenderer render = go.AddComponent<MeshRenderer>();
RenderAddMaterials(go, part);
go.transform.SetParent(goParent.transform);
}
goParent.transform.SetParent(_gameObj.transform);
}
//翻转法线
private Vector3[] FlipNormals(Vector3[] normals)
{
Vector3[] res=new Vector3[normals.Length];
for (int i = 0; i < normals.Length; i++)
{
normals[i] = -normals[i];
}
return normals;
}
//翻转三角形面片
private int[] ResetTriangles(int[] triangles)
{
for (int i = 0; i < triangles.Length; i+=3)
{
int t = triangles[i];
triangles[i] = triangles[i + 2];
triangles[i + 2] = t;
}
return triangles;
}
private void RenderAddMaterials(GameObject go, ObjPart part)
{
//给模型添加材质球
if (Settings.ModelMaterialList.ContainsKey(part.strMatName))
{
go.GetComponent<MeshRenderer>().material = Settings.ModelMaterialList[part.strMatName];
}
}
//创建所有需要的材质球(mtl文件与obj文件读取完成后,调用)
public void CreatMaterial(List<ObjMatItem> matList)
{
int nowIndex = 0;
foreach (ObjMatItem item in matList)
{
++nowIndex;
string textureName = item.map_Kd;//贴图名称
string texturePath = Directory.GetParent(Settings.ObjPath).FullName + "\\" + textureName;
StartCoroutine(LoadTexture(item, nowIndex, matList.Count));
}
}
/// <summary>
/// 加载贴图
/// </summary>
IEnumerator LoadTexture(ObjMatItem item, int nowIndex, int maxIndex)
{
string texturePath = Directory.GetParent(Settings.ObjPath).FullName + "\\" + item.map_Kd; //图片路径
string matName = item.strMatName;//材质名称
if (!File.Exists(texturePath)) { yield break; } //终止协成
UnityWebRequest request = new UnityWebRequest(texturePath);
DownloadHandlerTexture tex = new DownloadHandlerTexture(true);
request.downloadHandler = tex;
yield return request.SendWebRequest();
if (!request.isNetworkError)
{
Texture2D texture = tex.texture; //下载的东西不用时要清空 否则内存占用会越来越多TODO
Material material = new Material(Shader.Find("Standard"));
material.mainTexture = texture;
Settings.ModelMaterialList.Add(matName, material);
if (nowIndex == maxIndex)
{
isDone = true;
}
}
else
{
Debug.LogError("load texture failed!");
}
}
}
5、实现过程中遇到的困难
首先脚本数据结构以及文件读取都是从网上找的,然后结合自己的实际需求稍作修改。
比如mtl文件、obj文件以及贴图都在同一路径,所以读取路径拼接方式全部都改了;
模型材质加载是自己加上去的,根据mtl文件创建对应的材质球,存储起来,模型的子节点根据材质名称选择贴哪个材质;
其中最大的问题是模型网格面反了,导致贴上贴图后模型看起来是透明的,问了老大,说可能是法线反了,改了没效果,就百度下mesh的知识,看了下网格加载部分的代码,注意到加载顶点以及法线时vector3的第一个数据取反了,如下图所示:
所以面反了,根据这个将mesh中的triangles三角形面翻转,最后完美实现加载。
有想法的可以留言,时间长了我估计有可能记不清了,就不能都回复了