关于Obj文件格式介绍与Unity加载Obj文件代码参考
以下是一个典型的obj文件内容:
# 这是一个 OBJ 文件的示例
v 0.0 0.0 0.0
v 1.0 0.0 0.0
v 1.0 1.0 0.0
v 0.0 1.0 0.0
v 0.0 0.0 1.0
v 1.0 0.0 1.0
v 1.0 1.0 1.0
v 0.0 1.0 1.0vt 0.0 0.0
vt 1.0 0.0
vt 1.0 1.0
vt 0.0 1.0vn 0.0 0.0 -1.0
vn 0.0 0.0 1.0
vn 0.0 -1.0 0.0
vn 0.0 1.0 0.0
vn -1.0 0.0 0.0
vn 1.0 0.0 0.0f 1/1/1 2/2/2 3/3/3
f 1/1/1 3/3/3 4/4/4
f 5/1/2 6/2/2 7/3/2
f 5/1/2 7/3/2 8/4/2
f 1/1/1 5/1/2 6/2/2
f 1/1/1 6/2/2 2/2/2
f 2/2/2 6/2/2 7/3/2
f 2/2/2 7/3/2 3/3/3
f 3/3/3 7/3/2 8/4/2
f 3/3/3 8/4/2 4/4/4
f 4/4/4 8/4/2 5/1/2
f 4/4/4 5/1/2 1/1/1
v开头的行表示顶点坐标
vt开头的行表示uv坐标
vn开头的行表示法线
f开头的行表示三种索引,用斜杠分隔开,顶点/UV/法线,每个f开头的对应三组,每组的第一个整数是顶点索引,第二个是UV索引,第三组是法线索引,以第三个f开头的行为例,这个面的顶点索引是5、6、7,UV索引是1、2、3,法线索引是2、2、2。
-------------------------------重要的分割线----------------------------------------------------------------
这里必须强调的是,obj文件的索引是从1开始的,不是0!!!!!!
-------------------------------重要的分割线----------------------------------------------------------------
当然obj文件还包含一些其它内容,暂不做介绍。
以下是一个Unity发布WebGL后加载obj文件的参考:
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;public class ObjModelLoadManager : MonoBehaviour
{public static ObjModelLoadManager instance;void Awake(){instance = this;}[SerializeField]Transform modelRoot;//[SerializeField]Material matObj;double offsetX = 0;double offsetY = 0;double offsetZ = 0;float scale = 1;Color matColor = Color.gray;bool addMode = true;void Start(){SceneLoader.instance.AddActBeforeActiveNewScene(delegate { ClearObjModel(); });}public void SetLoadObjModelGlobalParam(string json){if (json == null || json.Length == 0){Debug.Log("EngineLog:TunnelGlobalParameter is empty.");return;}LoadObjModelGlobalParam param = JsonUtility.FromJson<LoadObjModelGlobalParam>(json);if (param == null){Debug.Log("EngineLog:Parse TunnelGlobalParameter failed.");return;}offsetX = param.offsetX;offsetY = param.offsetY;offsetZ = param.offsetZ;scale = param.scale;addMode = param.addMode;SetLoadObjModelColor(param.htmlColor);}public void SetLoadObjModelColor(string htmlColor){if (ColorUtility.TryParseHtmlString(htmlColor, out Color color)){matColor = color;}}public void LoadObjModel(string json){LoadObjModelInfo objLoadInfo = JsonUtility.FromJson<LoadObjModelInfo>(json);if (objLoadInfo == null){Debug.Log("EngineLog:Parse ObjLoadInfo failed.");return;}Color color = matColor;LoadFun.instance.LoadBuffer(objLoadInfo.url, delegate (byte[] buf) { OnLoadedBuf(buf, objLoadInfo.id, color); });}void OnLoadedBuf(byte[] buf, string id, Color color){if (!addMode){ClearObjModel();}MemoryStream mStream = new(buf);StreamReader sr = new(mStream);List<Vector3> listVert = new() { Vector3.zero };List<Vector2> listUV = new() { Vector2.zero };List<Vector3> listNormal = new() { Vector3.up };List<int> listTriangle = new();string line;while ((line = sr.ReadLine()) != null){if (line.StartsWith("v ")){AddVertex(line);}else if (line.StartsWith("vt ")){AddUV(line);}else if (line.StartsWith("vn ")){AddNormal(line);}else if (line.StartsWith("f ")){AddTriangles(line);}}Mesh mesh = new Mesh();mesh.name = id;mesh.vertices = listVert.ToArray();mesh.uv = listUV.ToArray();mesh.normals = listNormal.ToArray();mesh.triangles = listTriangle.ToArray();GameObject obj = new(id);obj.transform.SetParent(modelRoot);//obj.layer = LayerMask.NameToLayer("SelObj");MeshFilter filter = obj.AddComponent<MeshFilter>();filter.mesh = mesh;MeshRenderer meshRender = obj.AddComponent<MeshRenderer>();Material material = Instantiate(matObj);material.color = color;meshRender.material = material;obj.AddComponent<MeshCollider>();SelObj selObj = obj.AddComponent<SelObj>();selObj.id = id;selObj.SetShowName(id);SelObjManager.instance.AddSelObj(selObj);void AddVertex(string line){bool scaleEnabled = !Mathf.Approximately(scale, 1);string[] parts = line.Substring(2).Split(' ', StringSplitOptions.RemoveEmptyEntries);if (parts.Length >= 3 &&double.TryParse(parts[0], out double x) &&double.TryParse(parts[1], out double y) &&double.TryParse(parts[2], out double z)){if (scaleEnabled){x *= scale;y *= scale;z *= -scale;}x += offsetX;y += offsetY;z += offsetZ;listVert.Add(new Vector3((float)x, (float)y, (float)z));}}void AddUV(string line){string[] parts = line.Substring(3).Split(' ', StringSplitOptions.RemoveEmptyEntries);if (parts.Length >= 2 &&float.TryParse(parts[0], out float u) &&float.TryParse(parts[1], out float v)){listUV.Add(new Vector2(u, v));}}void AddNormal(string line){string[] parts = line.Substring(3).Split(' ', StringSplitOptions.RemoveEmptyEntries);if (parts.Length >= 3 &&float.TryParse(parts[0], out float x) &&float.TryParse(parts[1], out float y) &&float.TryParse(parts[2], out float z)){listNormal.Add(new Vector3(x, y, z));}}void AddTriangles(string line){string[] parts = line.Substring(2).Split(' ', StringSplitOptions.RemoveEmptyEntries);foreach (var part in parts){string[] indices = part.Split('/'); // 每个part可能是形如 "v/vt/vn"if (indices.Length >= 1 && int.TryParse(indices[0], out int vertexIndex)){listTriangle.Add(vertexIndex);}}}}public void ClearObjModel(){//清理原来的模型。for (int i = 0; i < modelRoot.childCount; i++){Destroy(modelRoot.GetChild(i).gameObject);}}
}#region JsonClass[Serializable]
public class LoadObjModelGlobalParam
{public double offsetX = 0;public double offsetY = 0;public double offsetZ = 0;public float scale = 1;public string htmlColor = "#888888";public bool addMode = true;
}[Serializable]
public class LoadObjModelInfo
{public string url;public string id;
}
#endregion
其中的offset系列参数是考虑到模型可能距离坐标原点较远,坐标值可能很大,所以用double来解析每个坐标值,然后用户可以整体偏移模型的值,让模型处于坐标原点附近,这时候double转成float精度好很多,scale参数是为了改变模型的大小,这里在使用中是为了调整单位,比如这个obj文件是基于厘米单位的,但是Unity中是米为单位,这时需要把scale设置为0.01,颜色用string表示的htmlColor主要是为了页面使用方便,其值类似"#ff8800"。
下面的代码中,每个列表都首先添加了一个值,然后再添加obj文件中的内容。
List<Vector3> listVert = new() { Vector3.zero };
List<Vector2> listUV = new() { Vector2.zero };
List<Vector3> listNormal = new() { Vector3.up };
这么做是因为obj文件的索引从1开始,不是从0开始,这个添加的值就是为了占据0这个索引位置,让可使用的内容从1开始,这是个投机取巧的办法,你当然可以不这么做,而是把obj的索引的值每个都减1,这样结果是一样的,只是个人觉得这样运算量比较大。
这里SetLoadObjModelColor和LoadObjModel方法经常会配合使用,就是先设置一个颜色,然后加载一个模型,这样这个加载的模型就使用了这个颜色,在代码编写时应该注意的是LoadFun.instance.LoadBuffer方法是一个异步操作,考虑到连续交替执行SetLoadObjModelColor和LoadObjModel方法的时候,在模型文件加载完成并设置颜色的时候,可能SetLoadObjModelColor已经被执行了好几次,模型获得的颜色可能是最后一次的颜色,这里每次加载模型的时候都是用一个临时变量先获取matColor,然后把这个临时变量传递给LoadFun.instance.LoadBuffer方法里面的委托,而不是在加载完成之后再去获取matColor。
这个原理是什么呢,我也说不清楚,编程多了,有直觉,哈哈。