2015-11-23 10:19:54 qinyuanpei 阅读数 24369
版权声明:本文由秦元培创作和发表,采用署名(BY)-非商业性使用(NC)-相同方式共享(SA)国际许可协议进行许可,转载请注明作者及出处,本文作者为秦元培,本文标题为解析OBJ模型并将其加载到Unity3D场景中,本文链接为http://qinyuanpei.com/2015/11/15/deep-learning-of-3d-model-file-format-of-obj/.

  各位朋友,大家好,欢迎大家关注我的博客,我是秦元培,我的博客地址是http://qinyuanpei.com。今天想和大家交流的是解析obj模型并将其加载到Unity3D场景中,虽然我们知道Unity3D是可以直接导入OBJ模型的,可是有时候我们并不能保证我们目标客户知道如何使用Unity3D的这套制作流程,可能对方最终提供给我们的就是一个模型文件而已,所以这个在这里做这个尝试想想还是蛮有趣的呢,既然如此,我们就选择在所有3D模型格式中最为简单的OBJ模型来一起探讨这个问题吧!

关于OBJ模型

  OBJ格式是一种3D模型文件格式,是由Alias|Wavefront公司为3D建模和动画软件 “Advanced Visualizer”开发的一种标准,适合用于3D软件模型之间的互相转换。和FBX、Max这种内部私有格式不同,OBJ模型文件是一种文本文件,我们可以直接使用记事本等软件打开进行编辑和查看,因此我们这里选择OBJ模型主要是基于它开放和标准这两个特点。需要说明的是,OBJ文件是一种3D模型文件,它主要支持多边形模型(三个点以上的面)。OBJ模型支持法线和贴图坐标,可是因为它本身并不记录动画、材质特性、贴图路径、动力学及粒子等信息,所以我们在游戏开发中基本看不到这种模型格式的,所以我们这里做下简单研究就好。

OBJ模型解读

  因为OBJ模型文件是一个文本文件,所以我们可以使用记事本等软件打开它来对它的文件结构进行下了解。首先OBJ文件没有头文件,如果你曾经尝试解析过mp3文件的ID3v1/ID3v2标签就应该知道它是根据mp3文件的开头或者末尾的若干字节来判断这些标签信息的,而在OBJ文件中是没有类似这样的头文件的。OBJ文件是由一行行由关键字、空格和文本字符组成的文本文件,通过关键字我们就可以知道这一行的文本表示的是什么数据。例如:

# Blender v2.76 (sub 0) OBJ File: ''

#关键字表示一个注释行,通过这个注释信息我们可以知道这个OBJ模型是由Blender2.76版本导出的。再比如:

mtllib liumengli.mtl

mtllib关键字则表示当前模型对应的材质库(.mtl)文件名称,每个OBJ模型文件都会有这样一个对应和它同名的.mtl文件,在这个文件中记录了材质相关的信息,稍后我们说到材质的时候会详细说说这个文件的格式,因为它和OBJ文件一样是一个文件文件。再比如:

usemtl Material__33

usemtl关键字则表示从当前行到下一个usemtl关键字所在行间的全部网格结构都使用其对应的材质,通过这个材质名称我们可以在.obj文件对应的.mtl文件中找到它的材质定义,这个我们在讲到材质部分的时候会详细说。

  好了,目前我们要做的工作室解析.obj文件然后创建网格进而可以使其显示在Unity3D场景中,在这里我们要重点关注的关键字有:
* v即Vertex,表示一个顶点的局部坐标系中的坐标,通常有三个分量,因为这里讨论的是三角面。例如:

v  1.5202 14.9252 -1.1004
  • vn即Vertex Normal,表示法线,注意到这些向量都是单位向量,所以我们可以认为三维软件在导出模型的时候已经做好了相关的标准化工作。
vn 0.8361 -0.0976 0.5399
  • vt即Vertex Texture,表示纹理坐标,就是我们熟悉的UV坐标啦,显然UV是个2D坐标,有两个分量。
vt -0.5623 0.4822 1.0000
  • f即face,这是一个真正描述面的关键字,通常它后面有三个索引结构,每个索引结构由顶点索引、法线索引和纹理坐标索引三部分构成。例如:
f 256/303/637 257/304/638 258/305/639 

以上这些关键字对我们解析.obj文件来说已经完全足够了,如果大家想对这些细节有更为深入的了解,可以参考这里这里

OBJ模型的读取

  OBJ模型的读取涉及到网格部分的读取和材质部分的读取两个部分,其中网格部分的读取难点在于当模型存在多个材质的时候,需要将模型分为若干个子物体,然后分别为这些子物体添加材质。可是不幸的是到目前为止,博主并没有找到一种行之有效的方法来对这些网格进行分类,所以这里我们假定模型是一个整体且共享同一种材质和一张贴图。如果大家找到了更好的解决方案,请记得告诉我,再次谢谢大家!

网格部分

  在网格读取这部分,因为我们已经假设所有的面构成一个物体,因此我们可以先将OBJ文件内的文本按照换行符来进行分割,然后再按照关键字去判断每一行的数据类型并进行相应的处理就可以了。读取OBJ模型的基本流程是:
* 读取顶点、法线、UV以及三角面
* 将三角面合并为四边面
* 根据索引重新计算顶点、法线、UV数组

读取顶点、法线、UV以及三角面

  首先我们来看第一步的代码实现:

/// <summary>
/// 从一个文本化后的.obj文件中加载模型
/// </summary>
public ObjMesh LoadFromObj(string objText)
{
    if(objText.Length <= 0) 
        return null;
    //v这一行前面是两个空格后面是一个空格
    objText=objText.Replace("  ", " ");

    //将文本化后的obj文件内容按行分割
    string[] allLines = objText.Split('\n');
    foreach(string line in allLines)
    {
        //将每一行按空格分割
        string[] chars = line.Split(' ');
        //根据第一个字符来判断数据的类型
        switch(chars[0])
        {
             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 "f":
              //处理面
              GetTriangleList(chars);
                break;
       }
 }

在这段代码中,我们首先将文本化的.obj文件按照换行符分割成字符串数组allLines,然后再对每一行按照空格分隔成字符串数组chars,这样我们就可以通过该数组的第一个元素chars[0]来判断当前行中的数据类型。这样我们将每一行的文本读取完后,所有的数据都被存储到了其相对应的列表中。其中,vertexArrayList存储顶点信息、normalArrayList存储法线信息、uvArrayList存储UV坐标。至此,我们完成第一部分中的顶点、法线和UV的读取。

  这里可以注意到我们在开始对文本化的.obj文件的内容有1次替换操作,这是因为在3dsMax中导出的.obj文件关键字v这一行中v后面的第一处空格位置是有2个空格,而我们在处理的时候是按照空格来分割每一行的内容的,这样chars[1]就会变成一个空字符串,显然这不符合我们的初衷,所以这里就需要对字符串进行这样一个操作,希望大家在解析的过程中注意,好吧,我承认我想吐槽3dsMax了,我不明白同一家公司的3dsMax和Maya为什么不能互相转换,我不明白3dsMax导出.obj文件的时候要做这样奇葩的设定,我更不明白为什么有开源、免费、轻巧的Blender都不去用非要每次都去安装容量动辄上G的盗版软件和不知道会不会变成下一个GhostXXXX的注册机,我更加不能容忍的是封闭的FBX格式和用起来就如同自虐的FBX SDK。

  好了,吐槽结束,我们接下来来看看三角面是如何读取的。三角面的读取定义在GetTriangleList()方法中,因此三角面的读取实际上首先需要将每一行文本按照空格进行分割,然后再将每一个元素按照/分割,这样就可以依次得到顶点索引、法线索引和UV索引。在某些情况下法线索引可能不存在,所以在处理的过程中需要对其进行处理。

/// <summary>
/// 获取面列表.
/// </summary>
/// <param name="chars">Chars.</param>
private void GetTriangleList(string[] chars)
{
   List<Vector3> indexVectorList = new List<Vector3>();
   List<Vector3> triangleList = new List<Vector3>();

   for(int i = 1; i < chars.Length;++i )
   {
       //将每一行按照空格分割后从第一个元素开始
       //按照/继续分割可依次获得顶点索引、法线索引和UV索引
       string[] indexs = chars[i].Split('/');
       Vector3 indexVector = new Vector3(0, 0);
       //顶点索引
       indexVector.x = ConvertToInt(indexs[0]);
       //法线索引
       if(indexs.Length > 1){
          if(indexs[1] != "")
             indexVector.y = ConvertToInt(indexs[1]);
       }
       //UV索引
       if(indexs.Length > 2){
          if(indexs[2] != "")
              indexVector.z = ConvertToInt(indexs[2]);
       }

       //将索引向量加入列表中
       indexVectorList.Add(indexVector);
   }

   //这里需要研究研究
   for(int j = 1; j < indexVectorList.Count - 1; ++j)
   {
       //按照0,1,2这样的方式来组成面
       triangleList.Add(indexVectorList[0]);
       triangleList.Add(indexVectorList[j]);
       triangleList.Add(indexVectorList[j + 1]);
   }

   //添加到索引列表
   foreach(Vector3 item in triangleList)
   {
      faceVertexNormalUV.Add(item);
   }
}

在这里,我们首先使用一个索引向量列表indexVectorList存储每一行的索引向量。这里的索引向量是指由顶点索引、法线索引和UV索引分别构成Vector3的三个分量,这样做的好处是我们可以节省重新去定义数据机构的时间。好了,我们把所有的索引向量读取完后,按照0、1、2这样的方式组成三角面,这里可能是.obj文件本身定义的一种方式,我们暂且按照这样的方式来处理。最后,全部的三角面会被读取到faceVertexNormalUV列表中,它表示的是每个三角面的顶点、法线和UV的索引向量,是一个List类型的变量。

将三角面合并为四边面

  现在我们读取到的是三角面,接下来我们需要将它们合并成四边面,合并的原理是判断它们是否在同一个面上。如果两个点的顶点索引相同则表明它们是同一个点,如果两个点的法线索引相同则表明它们在同一个面上。好了,我们来看定义的一个方法Combine():

/// <summary>
/// 合并三角面
/// </summary>
private void Combine()
{
   //使用一个字典来存储要合并的索引信息
   Dictionary<int, ArrayList> toCambineList = new Dictionary<int,ArrayList>();
   for(int i = 0; i < faceVertexNormalUV.Count; i++)
   {
       if(faceVertexNormalUV[i] != Vector3.zero)
       {
           //相同索引的列表
           ArrayList SameIndexList = new ArrayList();
           SameIndexList.Add(i);
           for(int j = 0; j < faceVertexNormalUV.Count; j++)
           {
               if(faceVertexNormalUV[j]!=Vector3.zero)
               {
                  if(i != j)
                  {
                     //如果顶点索引和法线索引相同,说明它们在一个面上
                     Vector3 iTemp = (Vector3)faceVertexNormalUV[i];
                     Vector3 jTemp = (Vector3)faceVertexNormalUV[j];
                     if(iTemp.x == jTemp.x && iTemp.y == jTemp.y)
                     {
                        //将索引相同索引列表然后将其重置为零向量
                        //PS:这是个危险的地方,如果某个索引信息为Vector3.Zero
                        //就会被忽略过去,可是貌似到目前为止没有发现为Vector3.Zero的情况
                        SameIndexList.Add(j);
                        faceVertexNormalUV[j]=Vector3.zero;
                     }
                   }
               }
           }
           //用一个索引来作为字典的键名,这样它可以代替对应列表内所有索引
           toCambineList.Add(i, SameIndexList);
       }
    }
 }

在这里我们使用了一个字典来存储合并后的四边面,这个字典的键名为这一组三角面共同的索引,因为大家都是用同一个索引,因此它可以代替那些被合并的三角面的索引,这样合并以后的四边面列表中元素的个数就是实际的网格中的面数个数,因为如果采用三角面的话,这个面数会比现在的面数还要多,这意味着这样会带来更多的性能上的消耗。这里可能不大好理解,大家可以将博主这里的表达方式换成自己能够理解的方式。佛曰不可说,遇到这种博主自己都说不明白的地方,博主就只能请大家多多担待了。好了,接下来要做的是重新计算顶点、法线和UV数组。可能大家会比较疑惑,这部分内容我们在第一步不是就已经读取出来了嘛,怎么这里又要重新计算了呢?哈哈,且听我慢慢道来!

根据索引重新计算顶点、法线、UV数组

  虽然我们在第一步就读取到了这些坐标数据,可是当我们合并三角面以后,就会出现大量的无用的点,为什么无用呢,因为它被合并到四边面里了,这样我们原来读取的这些坐标数据就变得不适用了。那怎么办呢?在第三步中我们合并四边面的时候已经用一个字典保存了合并后的索引信息,这就相当于我们已经知道哪些是合并前的索引,哪些是合并后的索引,这个时候我们只要根据索引重新为数组赋值即可:

//初始化各个数组
this.VertexArray = new Vector3[toCambineList.Count];
this.UVArray = new Vector2[toCambineList.Count];
this.NormalArray = new Vector3[toCambineList.Count];
this.TriangleArray = new int[faceVertexNormalUV.Count];

//定义遍历字典的计数器
int count = 0;

//遍历词典
foreach(KeyValuePair<int,ArrayList> IndexTtem in toCambineList)
{
    //根据索引给面数组赋值
    foreach(int item in IndexTtem.Value)
    {
        TriangleArray[item] = count;
    }

    //当前的顶点、UV、法线索引信息
    Vector3 VectorTemp = (Vector3)faceVertexNormalUV[IndexTtem.Key];

    //给顶点数组赋值
    VertexArray[count] = (Vector3)vertexArrayList[(int)VectorTemp.x - 1];

    //给UV数组赋值
    if(uvArrayList.Count > 0)
    {
       Vector3 tVec =(Vector3)uvArrayList[(int)VectorTemp.y - 1];
       UVArray[count] = new Vector2(tVec.x, tVec.y);
    }

            //给法线数组赋值
            if(normalArrayList.Count > 0)
            {
                NormalArray[count] = (Vector3)normalArrayList[(int)VectorTemp.z - 1];
            }

            count++;
        }

这样我们就读取到了合并后的坐标信息,通过顶点、法线、UV、面等信息我们现在就可以生成网格了。这部分我们暂且不着急,因为这基本上属于最后整合到Unity3D中步骤了。好了,为了方便大家理解,我已经完整的项目上传到Github,大家可以通过这里了解完整的项目。

材质部分

  材质这块儿的解析主要集中在.mtl文件中,和.obj文件类似,它同样是一个文本文件、同样采用关键字、空格、文本字符这样的结构来表示数据,因此我们可以借鉴.obj文件的读取。例如:

newmtl Material

newmtl关键字表示从当前行到下一个newmtl关键字所在行间都表示该关键字所对应的材质,这里的Material即表示材质的名称,它和.obj文件中的usemtl关键字相对应,因此我们给模型添加材质的过程本质上是从.obj文件中读取网格,然后找到其对应的材质名称,然后在.mtl文件中找到对应的材质定义,并根据定义来生成材质。目前已知的关键字有:

Ka 0.5880 0.5880 0.5880

Ka关键字表示环境反射的RGB数值。

Kd 0.640000 0.640000 0.640000

Kd关键字表示漫反射的RGB数值。

Ks 0.500000 0.500000 0.500000

Ks关键字表示镜面反射的RGB数值。

map_Ka E:\学习资料\Unity3D技术\Unity3D素材\柳梦璃\Texture\1df2eaa0.dds

map_Ka关键字表示环境反射的纹理贴图,注意到这里使用的是绝对路径,显然我们在读取模型的时候不会将贴图放在这样一个固定的路径,因此我们这里初步的想法读取贴图的文件名而非贴图的完整路径,考虑到我们在Unity3D中一般使用PNG格式的贴图,因此这里需要对路径进行处理。

map_Kd E:\学习资料\Unity3D技术\Unity3D素材\柳梦璃\Texture\1df2eaa0.dds

map_Kd关键字表示漫反射的纹理贴图,和环境反射的纹理贴图是类似地,这里就不再说了。此外还有其它的关键字,初步可以推断出的结论是它和3dsMax中材质编辑器里的定义特别地相似,感兴趣的朋友可以进一步去研究。可是现在就有一个新的问题了,怎样将这些参数和Unity3D里的材质关联起来呢?我们知道Unity3D里的材质是是由着色器和贴图两部分组成的,博主对Shader并不是很熟悉,因此这里确实有些说不清楚了。博主感觉对OBJ文件来说,其实使用Diffuse就完全足够了,所以这里对材质部分的研究我们点到为止,不打算做代码上的实现。如果不考虑这些参数的话,我们要做的就是通过WWW或者Resource将贴图加载进来,然后赋值给我们通过代码创建的Shader即可。而对于.obj文件来说,无论是通过Resource、WWW或者是IO流,只要我们拿到了这个文件中的内容就可以使用本文中的方式加载进来,因为我们假定的是读取只有一种材质的模型。有朋友可能要问,那如果有多种材质怎么办呢?答案是在.mtl问价中获取到所有贴图的名称,然后再到程序指定的路径去读取贴图,分别为其创建不同的材质,可是这些材质要怎么附加到它对应的物体上呢?这个目前博主没有找到解决的方法,所以此事暂且作罢吧!

在Unity3D中加载obj模型

  下面我们以一个简单的例子来展示今天研究的成果,我们将从.obj文件中读取出一个简单的模型并将其加载到场景中。好了,我们一起来看代码:

if(!File.Exists("D:\\cube.obj"))
    Debug.Log("请确认obj模型文件是否存在!");

StreamReader reader = new StreamReader("D:\\cube.obj",Encoding.Default);
string content = reader.ReadToEnd();
reader.Close();

ObjMesh objInstace = new ObjMesh();
objInstace = objInstace.LoadFromObj(content);

Mesh mesh = new Mesh();
mesh.vertices = objInstace.VertexArray;
mesh.triangles = objInstace.TriangleArray;
if(objInstace.UVArray.Length > 0)
    mesh.uv = objInstace.UVArray;
if(objInstace.NormalArray.Length>0)
    mesh.normals = objInstace.NormalArray;
mesh.RecalculateBounds();

GameObject go = new GameObject();
MeshFilter meshFilter = go.AddComponent<MeshFilter>();
meshFilter.mesh = mesh;

MeshRenderer meshRenderer = go.AddComponent<MeshRenderer>();

这里没有处理材质,所以读取出来就是这个样子的,哈哈!

最终效果,这是一个悲伤的故事

材质大家可以尝试用代码去创建一个材质,然后在给一张贴图,这个玩玩就好,哈哈!好了,今天的内容就是这样子了,希望大家喜欢,为了写这篇文章我都怀疑我是不是有拖延症啊!

2014-12-31 09:42:21 xiaomuzi0802 阅读数 7468

公司项目的需求,需要动态的从本地导入FBX模型于是花了一天时间翻墙找到了一点可怜的资料。

1、这个可以动态加载OBJ模型,这个的话unity自带的有这个函数,当然OBJ模型是不带贴图的,对于一些场景是无法创建的。

http://download.csdn.net/detail/xiaomuzi0802/8316215

2、这个比较实用的,可以动态载入多种模型,特别是FBX模型,在创建场景时候很好用,不过这个是试用版本,贴图会有问题,如果要更好的还是推荐买正版

http://download.csdn.net/detail/xiaomuzi0802/8316229

3、我同时还想让它加载到场景之后自动做成prefabs,这样就节省了每次加载的时间,但是最终也没找到方法

因为总要用到using UnityEditor,这个编辑类只能在unity中使用,导出就报错,最终我也没找到解决办法

using UnityEngine;
using System.Collections;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class creatPrefabs : MonoBehaviour {

	//public GameObject kk;
	// Use this for initialization
	void Start () {
		GameObject kk = GameObject.Find ("Cube");
		string  localPath= "Assets/StreamingAssets/"+ kk.name + ".prefab";
		Debug.Log (localPath);
		/*if (AssetDatabase.LoadAssetAtPath(localPath,  GameObject) ){
			if (PrefabUtility.DisplayDialog("Are you sure?",
			                                "The prefab already exists. Do you want to overwrite it?",
			                                "Yes",
			                                "No"))
				c/*ateNew(kk, localPath);
		} else {*/
			createNew(kk, localPath);
		//}
		
	}
	static void createNew( GameObject obj,string  localPath) {
	Object  prefab  = PrefabUtility.CreateEmptyPrefab(localPath);
		PrefabUtility.ReplacePrefab(obj, prefab);
	AssetDatabase.Refresh();
	
	DestroyImmediate(obj);
		GameObject clone = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
}

	// Update is called once per frame
	void Update () {

	}
}

希望有大神来指导

2017-01-17 00:16:38 qq_26651375 阅读数 3397

导入模型、贴图和材质

导入资源包文件

1.新建Unity3d项目SpaceShoot。

2.从Asset Store下载SpaceShoot资源包。https://www.assetstore.unity3d.com/cn/#!/content/13866


3.在浏览器中点击【在Unity中打开】按钮,会跳转到Unity3d软件界面。


4.点击下载,同意协议,当下载完毕后,弹出导入资源包界面。选择【Import】。


5.导入完成,在Project栏出现多个目录,_Completed-Assets目录下为已经完成的Demo,其他文件夹为资源文件夹,双击Scenes文件夹下的Done_Main场景,即可打开该场景,点击运行,游戏Demo运行。


6.依次点击File->New Scene,创建一个新场景,然后再次点击File->Save Scene或者【Ctrl+S】保存场景。在弹出的对话窗中新建文件夹“_Scene_Self”,打开文件夹,文件名Main,单击【保存】按钮。


7.此时可看到Project视图中文件夹”_Scene_Self”和空场景文件“Main”。


9.依次单击菜单项File->Build Setting->PlayerSetting。取消Default Is Full Screen的勾选,然后依次设置Width为400,Height为600。


10.这时在Game窗口中,可以看到Standalone模式下运行窗口的尺寸为400 x 600。


创建飞船对象

1.从Project视图下Asset/Models拖动模型文件vehicle_playerShip到Hierarchy视图,重命名为Player,按图重置Transform组件。

2.添加Rigidbody(刚体)组件:在Hierarchy视图中选择Player,在右侧的Inspector视图中点击Add Component->Physics->Rigidbody。刚体的作用是提供作用力,受到力的作用。另外取消Use Gravity的勾选,否则开始游戏,飞船将受到重力的作用而下坠。


3.添加Mesh Collider组件:选择Player,在Inspector视图中点击Add Component->Physics->Mesh Collider(碰撞体)。作用是让飞船可以玉其他物体产生碰撞,并触发碰撞后的事件(比如销毁等)。还需勾选Convex和Is Trigger,从而将Mesh Collider设置为触发器。

4.添加飞船尾部的粒子效果:在Project/Prefads/VFX/Engine下的预制体engine_player拖动到Hierarchy视图下的Player上,使其成为Player的子对象,并重置Transform组件,其Position的Z为-0.8。


设置摄像机参数

1.在Hierarchy视图中选中Main Camera,将其Transform组件的Rotation设置为(90,0,0),使摄像机处于俯视视角。调整Position(0,10,5),此时飞船处于Game窗口下方。

2.设置投影方式(Projection)为正交投影(Orthographic),并设置size为10,Clear Flags改为SolidColor,Background设为黑色,此时飞船处于合适的位置。



添加背景图片

1.单击GameObject->3DObject->Quad,创建一个平面,重命名Background,重置Transform,移出Mesh Collider,背景不需要碰撞体。此时平面垂直飞船,看不到,须设置其Transform组件的Rotation为(90,0,0),绕X轴逆时针旋转90度。

2.为Background添加纹理图片。将Assets/Textures目录下的tile_nebla_green_dff拖动到Background上。注意图片宽高比是1:2,放大平面时须遵守该比例,不然会失真。

3.选中Background,将Shader改为Unlit/Texture。

4.改变Background大小,Transform组件的Scale为(15,30,0)时,基本充满窗口。此时,飞船与Background重叠,将Background的Transform组件的Position的Y设为-10,使飞船处于Background上方。

添加粒子效果

从Project视图中Assets/VFX/Starfield目录下,拖动预制体Starfield到Hierarchy视图中,Transform组件默认不便,运行游戏,繁星点点。

2016-01-08 09:03:09 andyhebear 阅读数 2721

今天想和大家交流的是解析obj模型并将其加载到Unity3D场景中,虽然我们知道Unity3D是可以直接导入OBJ模型的,可是有时候我们并不能保证我们目标客户知道如何使用Unity3D的这套制作流程,可能对方最终提供给我们的就是一个模型文件而已,所以这个在这里做这个尝试想想还是蛮有趣的呢,既然如此,我们就选择在所有3D模型格式中最为简单的OBJ模型来一起探讨这个问题吧!

关于OBJ模型

  OBJ格式是一种3D模型文件格式,是由Alias|Wavefront公司为3D建模和动画软件 “Advanced Visualizer”开发的一种标准,适合用于3D软件模型之间的互相转换。和FBX、Max这种内部私有格式不同,OBJ模型文件是一种文本文件,我们可以直接使用记事本等软件打开进行编辑和查看,因此我们这里选择OBJ模型主要是基于它开放和标准这两个特点。需要说明的是,OBJ文件是一种3D模型文件,它主要支持多边形模型(三个点以上的面)。OBJ模型支持法线和贴图坐标,可是因为它本身并不记录动画、材质特性、贴图路径、动力学及粒子等信息,所以我们在游戏开发中基本看不到这种模型格式的,所以我们这里做下简单研究就好。

OBJ模型解读

  因为OBJ模型文件是一个文本文件,所以我们可以使用记事本等软件打开它来对它的文件结构进行下了解。首先OBJ文件没有头文件,如果你曾经尝试解析过mp3文件的ID3v1/ID3v2标签就应该知道它是根据mp3文件的开头或者末尾的若干字节来判断这些标签信息的,而在OBJ文件中是没有类似这样的头文件的。OBJ文件是由一行行由关键字、空格和文本字符组成的文本文件,通过关键字我们就可以知道这一行的文本表示的是什么数据。例如:

# Blender v2.76 (sub 0) OBJ File: ''
  • 1

#关键字表示一个注释行,通过这个注释信息我们可以知道这个OBJ模型是由Blender2.76版本导出的。再比如:

mtllib liumengli.mtl
  • 1

mtllib关键字则表示当前模型对应的材质库(.mtl)文件名称,每个OBJ模型文件都会有这样一个对应和它同名的.mtl文件,在这个文件中记录了材质相关的信息,稍后我们说到材质的时候会详细说说这个文件的格式,因为它和OBJ文件一样是一个文件文件。再比如:

usemtl Material__33
  • 1

usemtl关键字则表示从当前行到下一个usemtl关键字所在行间的全部网格结构都使用其对应的材质,通过这个材质名称我们可以在.obj文件对应的.mtl文件中找到它的材质定义,这个我们在讲到材质部分的时候会详细说。

  好了,目前我们要做的工作室解析.obj文件然后创建网格进而可以使其显示在Unity3D场景中,在这里我们要重点关注的关键字有: 
* v即Vertex,表示一个顶点的局部坐标系中的坐标,通常有三个分量,因为这里讨论的是三角面。例如:

v  1.5202 14.9252 -1.1004
  • 1
  • vn即Vertex Normal,表示法线,注意到这些向量都是单位向量,所以我们可以认为三维软件在导出模型的时候已经做好了相关的标准化工作。
vn 0.8361 -0.0976 0.5399
  • 1
  • vt即Vertex Texture,表示纹理坐标,就是我们熟悉的UV坐标啦,显然UV是个2D坐标,有两个分量。
vt -0.5623 0.4822 1.0000
  • 1
  • f即face,这是一个真正描述面的关键字,通常它后面有三个索引结构,每个索引结构由顶点索引、法线索引和纹理坐标索引三部分构成。例如:
f 256/303/637 257/304/638 258/305/639 
  • 1

以上这些关键字对我们解析.obj文件来说已经完全足够了,如果大家想对这些细节有更为深入的了解,可以参考这里这里

OBJ模型的读取

  OBJ模型的读取涉及到网格部分的读取和材质部分的读取两个部分,其中网格部分的读取难点在于当模型存在多个材质的时候,需要将模型分为若干个子物体,然后分别为这些子物体添加材质。可是不幸的是到目前为止,博主并没有找到一种行之有效的方法来对这些网格进行分类,所以这里我们假定模型是一个整体且共享同一种材质和一张贴图。如果大家找到了更好的解决方案,请记得告诉我,再次谢谢大家!

网格部分

  在网格读取这部分,因为我们已经假设所有的面构成一个物体,因此我们可以先将OBJ文件内的文本按照换行符来进行分割,然后再按照关键字去判断每一行的数据类型并进行相应的处理就可以了。读取OBJ模型的基本流程是: 
* 读取顶点、法线、UV以及三角面 
* 将三角面合并为四边面 
* 根据索引重新计算顶点、法线、UV数组

读取顶点、法线、UV以及三角面

  首先我们来看第一步的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/// <summary>
/// 从一个文本化后的.obj文件中加载模型
/// </summary>
public ObjMesh LoadFromObj(string objText)
{
if(objText.Length <= 0)
return null;
//v这一行前面是两个空格后面是一个空格
objText=objText.Replace("  ", " ");
 
//将文本化后的obj文件内容按行分割
string[] allLines = objText.Split('\n');
foreach(string line in allLines)
{
//将每一行按空格分割
string[] chars = line.Split(' ');
//根据第一个字符来判断数据的类型
switch(chars[0])
{
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 "f":
//处理面
GetTriangleList(chars);
break;
}
}

在这段代码中,我们首先将文本化的.obj文件按照换行符分割成字符串数组allLines,然后再对每一行按照空格分隔成字符串数组chars,这样我们就可以通过该数组的第一个元素chars[0]来判断当前行中的数据类型。这样我们将每一行的文本读取完后,所有的数据都被存储到了其相对应的列表中。其中,vertexArrayList存储顶点信息、normalArrayList存储法线信息、uvArrayList存储UV坐标。至此,我们完成第一部分中的顶点、法线和UV的读取。

  这里可以注意到我们在开始对文本化的.obj文件的内容有1次替换操作,这是因为在3dsMax中导出的.obj文件关键字v这一行中v后面的第一处空格位置是有2个空格,而我们在处理的时候是按照空格来分割每一行的内容的,这样chars[1]就会变成一个空字符串,显然这不符合我们的初衷,所以这里就需要对字符串进行这样一个操作,希望大家在解析的过程中注意,好吧,我承认我想吐槽3dsMax了,我不明白同一家公司的3dsMax和Maya为什么不能互相转换,我不明白3dsMax导出.obj文件的时候要做这样奇葩的设定,我更不明白为什么有开源、免费、轻巧的Blender都不去用非要每次都去安装容量动辄上G的盗版软件和不知道会不会变成下一个GhostXXXX的注册机,我更加不能容忍的是封闭的FBX格式和用起来就如同自虐的FBX SDK。

  好了,吐槽结束,我们接下来来看看三角面是如何读取的。三角面的读取定义在GetTriangleList()方法中,因此三角面的读取实际上首先需要将每一行文本按照空格进行分割,然后再将每一个元素按照/分割,这样就可以依次得到顶点索引、法线索引和UV索引。在某些情况下法线索引可能不存在,所以在处理的过程中需要对其进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// <summary>
/// 获取面列表.
/// </summary>
/// <param name="chars">Chars.</param>
private void GetTriangleList(string[] chars)
{
List<Vector3> indexVectorList = new List<Vector3>();
List<Vector3> triangleList = new List<Vector3>();
 
for(int i = 1; i < chars.Length;++i )
{
//将每一行按照空格分割后从第一个元素开始
//按照/继续分割可依次获得顶点索引、法线索引和UV索引
string[] indexs = chars[i].Split('/');
Vector3 indexVector = new Vector3(0, 0);
//顶点索引
indexVector.x = ConvertToInt(indexs[0]);
//法线索引
if(indexs.Length > 1){
if(indexs[1] != "")
indexVector.y = ConvertToInt(indexs[1]);
}
//UV索引
if(indexs.Length > 2){
if(indexs[2] != "")
indexVector.z = ConvertToInt(indexs[2]);
}
 
//将索引向量加入列表中
indexVectorList.Add(indexVector);
}
 
//这里需要研究研究
for(int j = 1; j < indexVectorList.Count - 1; ++j)
{
//按照0,1,2这样的方式来组成面
triangleList.Add(indexVectorList[0]);
triangleList.Add(indexVectorList[j]);
triangleList.Add(indexVectorList[j + 1]);
}
 
//添加到索引列表
foreach(Vector3 item in triangleList)
{
faceVertexNormalUV.Add(item);
}
}

在这里,我们首先使用一个索引向量列表indexVectorList存储每一行的索引向量。这里的索引向量是指由顶点索引、法线索引和UV索引分别构成Vector3的三个分量,这样做的好处是我们可以节省重新去定义数据机构的时间。好了,我们把所有的索引向量读取完后,按照0、1、2这样的方式组成三角面,这里可能是.obj文件本身定义的一种方式,我们暂且按照这样的方式来处理。最后,全部的三角面会被读取到faceVertexNormalUV列表中,它表示的是每个三角面的顶点、法线和UV的索引向量,是一个List类型的变量。

将三角面合并为四边面

  现在我们读取到的是三角面,接下来我们需要将它们合并成四边面,合并的原理是判断它们是否在同一个面上。如果两个点的顶点索引相同则表明它们是同一个点,如果两个点的法线索引相同则表明它们在同一个面上。好了,我们来看定义的一个方法Combine():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/// 合并三角面
/// </summary>
private void Combine()
{
//使用一个字典来存储要合并的索引信息
Dictionary<int, ArrayList> toCambineList = new Dictionary<int,ArrayList>();
for(int i = 0; i < faceVertexNormalUV.Count; i++)
{
if(faceVertexNormalUV[i] != Vector3.zero)
{
//相同索引的列表
ArrayList SameIndexList = new ArrayList();
SameIndexList.Add(i);
for(int j = 0; j < faceVertexNormalUV.Count; j++)
{
if(faceVertexNormalUV[j]!=Vector3.zero)
{
if(i != j)
{
//如果顶点索引和法线索引相同,说明它们在一个面上
Vector3 iTemp = (Vector3)faceVertexNormalUV[i];
Vector3 jTemp = (Vector3)faceVertexNormalUV[j];
if(iTemp.x == jTemp.x && iTemp.y == jTemp.y)
{
//将索引相同索引列表然后将其重置为零向量
//PS:这是个危险的地方,如果某个索引信息为Vector3.Zero
//就会被忽略过去,可是貌似到目前为止没有发现为Vector3.Zero的情况
SameIndexList.Add(j);
faceVertexNormalUV[j]=Vector3.zero;
}
}
}
}
//用一个索引来作为字典的键名,这样它可以代替对应列表内所有索引
toCambineList.Add(i, SameIndexList);
}
}
}

在这里我们使用了一个字典来存储合并后的四边面,这个字典的键名为这一组三角面共同的索引,因为大家都是用同一个索引,因此它可以代替那些被合并的三角面的索引,这样合并以后的四边面列表中元素的个数就是实际的网格中的面数个数,因为如果采用三角面的话,这个面数会比现在的面数还要多,这意味着这样会带来更多的性能上的消耗。这里可能不大好理解,大家可以将博主这里的表达方式换成自己能够理解的方式。佛曰不可说,遇到这种博主自己都说不明白的地方,博主就只能请大家多多担待了。好了,接下来要做的是重新计算顶点、法线和UV数组。可能大家会比较疑惑,这部分内容我们在第一步不是就已经读取出来了嘛,怎么这里又要重新计算了呢?哈哈,且听我慢慢道来!

根据索引重新计算顶点、法线、UV数组

  虽然我们在第一步就读取到了这些坐标数据,可是当我们合并三角面以后,就会出现大量的无用的点,为什么无用呢,因为它被合并到四边面里了,这样我们原来读取的这些坐标数据就变得不适用了。那怎么办呢?在第三步中我们合并四边面的时候已经用一个字典保存了合并后的索引信息,这就相当于我们已经知道哪些是合并前的索引,哪些是合并后的索引,这个时候我们只要根据索引重新为数组赋值即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//初始化各个数组
this.VertexArray = new Vector3[toCambineList.Count];
this.UVArray = new Vector2[toCambineList.Count];
this.NormalArray = new Vector3[toCambineList.Count];
this.TriangleArray = new int[faceVertexNormalUV.Count];
 
//定义遍历字典的计数器
int count = 0;
 
//遍历词典
foreach(KeyValuePair<int,ArrayList> IndexTtem in toCambineList)
{
//根据索引给面数组赋值
foreach(int item in IndexTtem.Value)
{
TriangleArray[item] = count;
}
 
//当前的顶点、UV、法线索引信息
Vector3 VectorTemp = (Vector3)faceVertexNormalUV[IndexTtem.Key];
 
//给顶点数组赋值
VertexArray[count] = (Vector3)vertexArrayList[(int)VectorTemp.x - 1];
 
//给UV数组赋值
if(uvArrayList.Count > 0)
{
Vector3 tVec =(Vector3)uvArrayList[(int)VectorTemp.y - 1];
UVArray[count] = new Vector2(tVec.x, tVec.y);
}
 
//给法线数组赋值
if(normalArrayList.Count > 0)
{
NormalArray[count] = (Vector3)normalArrayList[(int)VectorTemp.z - 1];
}
 
count++;
}

这样我们就读取到了合并后的坐标信息,通过顶点、法线、UV、面等信息我们现在就可以生成网格了。这部分我们暂且不着急,因为这基本上属于最后整合到Unity3D中步骤了。

材质部分

  材质这块儿的解析主要集中在.mtl文件中,和.obj文件类似,它同样是一个文本文件、同样采用关键字、空格、文本字符这样的结构来表示数据,因此我们可以借鉴.obj文件的读取。例如:

newmtl Material
  • 1

newmtl关键字表示从当前行到下一个newmtl关键字所在行间都表示该关键字所对应的材质,这里的Material即表示材质的名称,它和.obj文件中的usemtl关键字相对应,因此我们给模型添加材质的过程本质上是从.obj文件中读取网格,然后找到其对应的材质名称,然后在.mtl文件中找到对应的材质定义,并根据定义来生成材质。目前已知的关键字有:

Ka 0.5880 0.5880 0.5880
  • 1

Ka关键字表示环境反射的RGB数值。

Kd 0.640000 0.640000 0.640000
  • 1

Kd关键字表示漫反射的RGB数值。

Ks 0.500000 0.500000 0.500000
  • 1

Ks关键字表示镜面反射的RGB数值。

map_Ka E:\学习资料\Unity3D技术\Unity3D素材\柳梦璃\Texture\1df2eaa0.dds
  • 1

map_Ka关键字表示环境反射的纹理贴图,注意到这里使用的是绝对路径,显然我们在读取模型的时候不会将贴图放在这样一个固定的路径,因此我们这里初步的想法读取贴图的文件名而非贴图的完整路径,考虑到我们在Unity3D中一般使用PNG格式的贴图,因此这里需要对路径进行处理。

map_Kd E:\学习资料\Unity3D技术\Unity3D素材\柳梦璃\Texture\1df2eaa0.dds
  • 1

map_Kd关键字表示漫反射的纹理贴图,和环境反射的纹理贴图是类似地,这里就不再说了。此外还有其它的关键字,初步可以推断出的结论是它和3dsMax中材质编辑器里的定义特别地相似,感兴趣的朋友可以进一步去研究。可是现在就有一个新的问题了,怎样将这些参数和Unity3D里的材质关联起来呢?我们知道Unity3D里的材质是是由着色器和贴图两部分组成的,博主对Shader并不是很熟悉,因此这里确实有些说不清楚了。博主感觉对OBJ文件来说,其实使用Diffuse就完全足够了,所以这里对材质部分的研究我们点到为止,不打算做代码上的实现。如果不考虑这些参数的话,我们要做的就是通过WWW或者Resource将贴图加载进来,然后赋值给我们通过代码创建的Shader即可。而对于.obj文件来说,无论是通过Resource、WWW或者是IO流,只要我们拿到了这个文件中的内容就可以使用本文中的方式加载进来,因为我们假定的是读取只有一种材质的模型。有朋友可能要问,那如果有多种材质怎么办呢?答案是在.mtl问价中获取到所有贴图的名称,然后再到程序指定的路径去读取贴图,分别为其创建不同的材质,可是这些材质要怎么附加到它对应的物体上呢?这个目前博主没有找到解决的方法,所以此事暂且作罢吧!

在Unity3D中加载obj模型

  下面我们以一个简单的例子来展示今天研究的成果,我们将从.obj文件中读取出一个简单的模型并将其加载到场景中。好了,我们一起来看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if(!File.Exists("D:\\cube.obj"))
Debug.Log("请确认obj模型文件是否存在!");
 
StreamReader reader = new StreamReader("D:\\cube.obj",Encoding.Default);
string content = reader.ReadToEnd();
reader.Close();
 
ObjMesh objInstace = new ObjMesh();
objInstace = objInstace.LoadFromObj(content);
 
Mesh mesh = new Mesh();
mesh.vertices = objInstace.VertexArray;
mesh.triangles = objInstace.TriangleArray;
if(objInstace.UVArray.Length > 0)
mesh.uv = objInstace.UVArray;
if(objInstace.NormalArray.Length>0)
mesh.normals = objInstace.NormalArray;
mesh.RecalculateBounds();
 
GameObject go = new GameObject();
MeshFilter meshFilter = go.AddComponent<MeshFilter>();
meshFilter.mesh = mesh;
 
MeshRenderer meshRenderer = go.AddComponent<MeshRenderer>();

这里没有处理材质,所以读取出来就是这个样子的,哈哈!

最终效果,这是一个悲伤的故事

材质大家可以尝试用代码去创建一个材质,然后在给一张贴图,这个玩玩就好,哈哈!好了,今天的内容就是这样子了
2016-03-03 18:57:00 weixin_34146410 阅读数 60

目前基本实现了导入,注意只能打开含有单个模型的obj文件

 

四边面模型:

 

全三角面模型(测试单一材质,自动分了下UV):

 

 

这里介绍下obj格式:

obj格式是waveFront推出的一种3D模型格式,可以存放静态模型以及一些诸如曲线的附加信息。

其格式以文本形式存放,所以解析起来比较方便,它的大体格式如下:

# WaveFront *.obj file (generated by CINEMA 4D)

mtllib ./test.mtl

v -100.00000000000000 -100.00000000000000 -100.00000000000000
v -100.00000000000000 100.00000000000000 -100.00000000000000
v 100.00000000000000 -100.00000000000000 -100.00000000000000
v 100.00000000000000 100.00000000000000 -100.00000000000000
v 100.00000000000000 -100.00000000000000 100.00000000000000
v 100.00000000000000 100.00000000000000 100.00000000000000
v -100.00000000000000 -100.00000000000000 100.00000000000000
v -100.00000000000000 100.00000000000000 100.00000000000000
# 8 vertices

vn 0.00000000000000 0.00000000000000 -1.00000000000000
vn 1.00000000000000 0.00000000000000 0.00000000000000
vn 0.00000000000000 0.00000000000000 1.00000000000000
vn -1.00000000000000 0.00000000000000 0.00000000000000
vn 0.00000000000000 1.00000000000000 0.00000000000000
vn 0.00000000000000 -1.00000000000000 0.00000000000000
# 6 normals

vt 0.00000000000000 -1.00000000000000 0.00000000000000
vt 0.00000000000000 -0.00000000000000 0.00000000000000
vt 1.00000000000000 -0.00000000000000 0.00000000000000
vt 1.00000000000000 -1.00000000000000 0.00000000000000
# 4 texture coordinates

o Cube
usemtl default
f 1/4/1 2/3/1 4/2/1 3/1/1
f 3/4/2 4/3/2 6/2/2 5/1/2
f 5/4/3 6/3/3 8/2/3 7/1/3
f 7/4/4 8/3/4 2/2/4 1/1/4
f 2/4/5 8/3/5 6/2/5 4/1/5
f 7/4/6 1/3/6 3/2/6 5/1/6
Cube(Obj)

 

常用类型:

#开头表示注释

v表示顶点

vn表示法线,可以共用法线

vt表示uv坐标

f表示一个面,比如参数1/4/1,表示顶点索引/UV索引/法线索引

 

下面贴一下工具类的代码:

using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;

namespace Hont
{
    public class ObjFormatAnalyzer
    {
        public struct Vector
        {
            public float X;
            public float Y;
            public float Z;
        }

        public struct FacePoint
        {
            public int VertexIndex;
            public int TextureIndex;
            public int NormalIndex;
        }

        public struct Face
        {
            public FacePoint[] Points;
            public bool IsQuad;
        }

        public Vector[] VertexArr;
        public Vector[] VertexNormalArr;
        public Vector[] VertexTextureArr;
        public Face[] FaceArr;


        public void Analyze(string content)
        {
            content = content.Replace('\r', ' ').Replace('\t', ' ');

            var lines = content.Split('\n');
            var vertexList = new List<Vector>();
            var vertexNormalList = new List<Vector>();
            var vertexTextureList = new List<Vector>();
            var faceList = new List<Face>();

            for (int i = 0; i < lines.Length; i++)
            {
                var currentLine = lines[i];

                if (currentLine.Contains("#") || currentLine.Length == 0)
                {
                    continue;
                }

                if (currentLine.Contains("v "))
                {
                    var splitInfo = currentLine.Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                    vertexList.Add(new Vector() { X = float.Parse(splitInfo[1]), Y = float.Parse(splitInfo[2]), Z = float.Parse(splitInfo[3]) });
                }
                else if (currentLine.Contains("vt "))
                {
                    var splitInfo = currentLine.Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                    vertexTextureList.Add(new Vector() { X = float.Parse(splitInfo[1]), Y = float.Parse(splitInfo[2]), Z = float.Parse(splitInfo[3]) });
                }
                else if (currentLine.Contains("vn "))
                {
                    var splitInfo = currentLine.Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                    vertexNormalList.Add(new Vector() { X = float.Parse(splitInfo[1]), Y = float.Parse(splitInfo[2]), Z = float.Parse(splitInfo[3]) });
                }
                else if (currentLine.Contains("f "))
                {
                    var splitInfo = currentLine.Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                    var isQuad = splitInfo.Length > 4;
                    var face1 = splitInfo[1].Split('/');
                    var face2 = splitInfo[2].Split('/');
                    var face3 = splitInfo[3].Split('/');
                    var face4 = isQuad ? splitInfo[4].Split('/') : null;
                    var face = new Face();
                    face.Points = new FacePoint[4];
                    face.Points[0] = new FacePoint() { VertexIndex = int.Parse(face1[0]), TextureIndex = int.Parse(face1[1]), NormalIndex = int.Parse(face1[2]) };
                    face.Points[1] = new FacePoint() { VertexIndex = int.Parse(face2[0]), TextureIndex = int.Parse(face2[1]), NormalIndex = int.Parse(face2[2]) };
                    face.Points[2] = new FacePoint() { VertexIndex = int.Parse(face3[0]), TextureIndex = int.Parse(face3[1]), NormalIndex = int.Parse(face3[2]) };
                    face.Points[3] = isQuad ? new FacePoint() { VertexIndex = int.Parse(face4[0]), TextureIndex = int.Parse(face4[1]), NormalIndex = int.Parse(face4[2]) } : default(FacePoint);
                    face.IsQuad = isQuad;

                    faceList.Add(face);
                }
            }

            VertexArr = vertexList.ToArray();
            VertexNormalArr = vertexNormalList.ToArray();
            VertexTextureArr = vertexTextureList.ToArray();
            FaceArr = faceList.ToArray();
        }
    }
}
ObjFormatAnalyzer

 

工厂类,目前只有输出GameObject:

using UnityEngine;
using System.IO;
using System.Linq;
using System.Collections;
using System.Collections.Generic;

namespace Hont
{
    public static class ObjFormatAnalyzerFactory
    {
        public static GameObject AnalyzeToGameObject(string objFilePath)
        {
            if (!File.Exists(objFilePath)) return null;

            var objFormatAnalyzer = new ObjFormatAnalyzer();

            objFormatAnalyzer.Analyze(File.ReadAllText(objFilePath));

            var go = new GameObject();
            var meshRenderer = go.AddComponent<MeshRenderer>();
            var meshFilter = go.AddComponent<MeshFilter>();

            var mesh = new Mesh();

            var sourceVertexArr = objFormatAnalyzer.VertexArr;
            var sourceUVArr = objFormatAnalyzer.VertexTextureArr;
            var faceArr = objFormatAnalyzer.FaceArr;
            var notQuadFaceArr = objFormatAnalyzer.FaceArr.Where(m => !m.IsQuad).ToArray();
            var quadFaceArr = objFormatAnalyzer.FaceArr.Where(m => m.IsQuad).ToArray();
            var vertexList = new List<Vector3>();
            var uvList = new List<Vector2>();

            var triangles = new int[notQuadFaceArr.Length * 3 + quadFaceArr.Length * 6];
            for (int i = 0, j = 0; i < faceArr.Length; i++)
            {
                var currentFace = faceArr[i];

                triangles[j] = j;
                triangles[j + 1] = j + 1;
                triangles[j + 2] = j + 2;

                var vec = sourceVertexArr[currentFace.Points[0].VertexIndex - 1];
                vertexList.Add(new Vector3(vec.X, vec.Y, vec.Z));

                var uv = sourceUVArr[currentFace.Points[0].TextureIndex - 1];
                uvList.Add(new Vector2(uv.X, uv.Y));

                vec = sourceVertexArr[currentFace.Points[1].VertexIndex - 1];
                vertexList.Add(new Vector3(vec.X, vec.Y, vec.Z));

                uv = sourceUVArr[currentFace.Points[1].TextureIndex - 1];
                uvList.Add(new Vector2(uv.X, uv.Y));

                vec = sourceVertexArr[currentFace.Points[2].VertexIndex - 1];
                vertexList.Add(new Vector3(vec.X, vec.Y, vec.Z));

                uv = sourceUVArr[currentFace.Points[2].TextureIndex - 1];
                uvList.Add(new Vector2(uv.X, uv.Y));

                if (currentFace.IsQuad)
                {
                    triangles[j + 3] = j + 3;
                    triangles[j + 4] = j + 4;
                    triangles[j + 5] = j + 5;
                    j += 3;

                    vec = sourceVertexArr[currentFace.Points[0].VertexIndex - 1];
                    vertexList.Add(new Vector3(vec.X, vec.Y, vec.Z));

                    uv = sourceUVArr[currentFace.Points[0].TextureIndex - 1];
                    uvList.Add(new Vector2(uv.X, uv.Y));

                    vec = sourceVertexArr[currentFace.Points[2].VertexIndex - 1];
                    vertexList.Add(new Vector3(vec.X, vec.Y, vec.Z));

                    uv = sourceUVArr[currentFace.Points[2].TextureIndex - 1];
                    uvList.Add(new Vector2(uv.X, uv.Y));

                    vec = sourceVertexArr[currentFace.Points[3].VertexIndex - 1];
                    vertexList.Add(new Vector3(vec.X, vec.Y, vec.Z));

                    uv = sourceUVArr[currentFace.Points[3].TextureIndex - 1];
                    uvList.Add(new Vector2(uv.X, uv.Y));
                }

                j += 3;
            }

            mesh.vertices = vertexList.ToArray();
            mesh.uv = uvList.ToArray();
            mesh.triangles = triangles;

            meshFilter.mesh = mesh;
            meshRenderer.material = new Material(Shader.Find("Standard"));

            return go;
        }
    }
}
ObjFormatAnalyzerFactory

 

 

测试使用脚本:

public class ObjFormatAnalyzerTest : MonoBehaviour
{
    void Start()
    {
        var go = ObjFormatAnalyzerFactory.AnalyzeToGameObject(@"D:\TestObj\centaur.obj");

        WWW www = new WWW("file:/D:/TestObj/texture.jpg");

        while (!www.isDone) { }

        go.GetComponent<MeshRenderer>().material.mainTexture = www.texture;
    }
}

 

然后挂载运行即可。

目前可能还有点小问题,因为没有实现材质的绑定,所以只能支持单一材质。

unity导入模型无色

阅读数 2321

没有更多推荐了,返回首页