关于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.0
vt 0.0 0.0
vt 1.0 0.0
vt 1.0 1.0
vt 0.0 1.0
vn 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.0
f 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已经被执行了好几次,模型获得的颜色可能是最后一次的颜色,这里每次vi加载模型的时候都是用一个临时变量先获取matColor,然后把这个临时变量传递给LoadFun.instance.LoadBuffer方法里面的委托,而不是在加载完成之后再去获取matColor。
这个原理是什么呢,我也说不清楚,编程多了,有直觉,哈哈。