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()时间。
所以没事乱用些不合适的容器,是个很傻的行为。