关于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。

        这个原理是什么呢,我也说不清楚,编程多了,有直觉,哈哈。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值