OBJ文件动态载入Unity中的一个算法小问题

FBX、OBJ等格式的3D文件,拖入Unity工程时,都经历了一个内部处理过程以转换成Unity能使用的格式。

通常游戏项目的模型文件都是作为静态资源打包进安装包的。但假设面临在程序运行时,灵活动态获取模型文件,并马上使用的需要,该怎么办呢?

Unity在这块并没有做很好的支持,不要和我说什么AssetBundle,那个也是要预先在Editor里手工处理好才能下载使用的。我是指直接丢过来一个OBJ,程序该如何解析?

 

基本的原理是:利用Unity Mesh类,准备好顶点、法线、面等信息数据,写入Mesh.vertices、Mesh.normals、Mesh.trangles等即可。

这方面有很多老外已经写好了示例工程,网上搜搜都能很容易获取。

(2018.9.12 补充说明:

鉴于经常有人在评论中询问具体的demo和代码,我把找到的参考资料链接贴于此。

OBJ文件格式详解

解析OBJ模型并将其加载到Unity3D场景中

Loading 3d models at runtime in Unity3d

GitHub Project unity-obj-loader)

新发现的两个带代码的文章:

unity 加载obj文件的方法

Obj格式解析以及在Unity3D下导入测试

 

但以上不是这篇文章的重点。

 

故事开始在我下载到一个老外的工具工程后,研读了下他的代码,发现其存在两个内部缺陷:

1.对于顶点数超出65000个的情况,没有做很好的处理,仅仅是报错。

        而我要处理的模型有16万顶点,33万面。

2.直接将faces的vertices依次写入Mesh.vertices,然后trangles直接写成{1,2,3,4,5,6,......n}这样的顺序整形数组。

        熟悉3D渲染的应该都知道,模型的顶点索引是记录一份Vector3[],而面的表示只是用前面的顶点索引信息记录一份int[],节省内存空间。作者这种偷懒的解析法,造成了约6倍的重复顶点数,进一步对性能产生压力。

 

为了消除vertices中的重复数据,并重新映射trangles,我一开始写了这么一个算法:

 

List refinedVertexesList = new List();  //新顶点索引容器
List refinedNormalsList = new List();
List trangles = new List();
for (int j = 0; j < tvertices.Length; j++)
{
    if (!refinedVertexesList.Contains(tvertices[j]))
    {
        refinedVertexesList.Add(tvertices[j]);
        refinedNormalsList.Add(tnormals[j]);
        trangles.Add(refinedVertexesList.Count - 1);
    }
    else
    {
        trangles.Add(refinedVertexesList.IndexOf(tvertices[j]));
    }
}

 

大概的思想就是:

新建一个顶点索引列表,

遍历面队列中记录的每个顶点,

假如没加入新索引,就加入,然后修正面index,

假如已经加入了,就只是修正面index。

 

结果一运行,发现程序卡在那了,经测试,整个运算过程处理一个模型要十五分钟左右。。。

 

主要是List的Contains和IndexOf效率都极低,处理6万个顶点,一趟下来就是6万*6万=36亿的次数量级。

然后我试了各种其他容器,Dictionary的问题在于Add极慢,而HashSet虽然Add和Contains都快,但没法得到索引。

 

最后灵机一动,回归最原始的数组,然后就搞定了:

 

List refinedVertexesList = new List();  //新顶点索引容器
List refinedNormalsList = new List();
int[] trangles = new int[od.allFaces.Count];
{
    int[] vis = new int[vertices.Count];
    for (int k = 0; k < od.allFaces.Count; k++) //给总顶点索引中此object faces用到的vertexes做标记
    {
        vis[od.allFaces[k].vi] = 1;
    }

    int[] newVis = new int[vertices.Count];     //用于记录原index对应的新index
    for (int k = 0; k < vis.Length; k++)
    {
        if (vis[k] != 0)
        {
            refinedVertexesList.Add(vertices[k]);
            refinedNormalsList.Add(normals[k]);
            newVis[k] = refinedVertexesList.Count - 1;
        }
    }

    for (int k = 0; k < od.allFaces.Count; k++) //更新faces索引
    {
        trangles[k] = newVis[od.allFaces[k].vi];
    }
}

当然肯定有更刁钻的优化算法,但这个已经将耗时控制在0.1s以下,符合需求了。

 

原理是建立两个最大容量的数组,一个用于记录顶点是否被使用,一个用于记录新旧索引的映射关系。过程中得到新的一份顶点索引和面信息,就ok了。

如此用几十K的内存空间,换到十几分钟的CPU计算时间。

 

算法优化的本质,是操作元素的数量在2万-6万间变动,那么直接按6万的量来开辟空间,多费一点内存,省下了高速Contains()类容器的Add()时间。

所以没事乱用些不合适的容器,是个很傻的行为。

你可能感兴趣的:(数据结构与算法,Unity,C#)