STL文件是一种非常简单且实用的三角形网格文件,他只按照三角面片的方式存储了所有的面信息及法矢量,不包含纹理以及其他任何媒体信息,主要存储格式分为:ASCII码格式、二进制格式。
这种文件格式在工业上以及3D打印行业应用非常广泛,但是Unity并不支持这种格式的模型,原因可能是因为STL模型的顶点普遍量级过大,随随便便一个模型都有可能越过了Unity单个网格顶点数不能超过65000的上限,以及他自身在未经过3DMax或maya等的二次加工前提下并不能表现出良好的渲染效果的原因。
综上所述,貌似几乎不会有哪个吃撑了的会想直接将STL模型拿到Unity中开发,毕竟那样的计算量级确实过于庞大了,不过,好吧,看来我就是那个吃撑了的......只不过,目前的效果,导入一个4M的STL模型就花了将近5分钟!对了,到这里我不得不吐槽Unity的某些机制了,我可以理解为Unity将编辑器主线程的资源分配做了极大限制,在不开其他线程的情况下,主线程导入一个4M的STL模型需要约20几分钟,开一个线程来单独处理的话,只需要5分钟左右,但再开10个线程或者100个线程的话(不存在同步锁),处理时间几乎也是5分钟左右,当然100个线程电脑几乎死机,我查了一下这应该是由于进行的是内存计算的原因,多线程并不能优化本地内存计算的效率,不过在子线程中处理运算与在Unity主线程中处理却是有着本质区别的。
好了,废话不多说,进入主题:
首先,我在网上随便下了一个STL模型,因为一直没找到ASCII码格式的,所以暂时只处理二进制格式的文件。
模型原图:
我将他拖到工程中,先重写DefaultAsset的OnInspectorGUI,使他看起来像一个我们已知的文件,DefaultAsset代表的是Unity缺省资源,不可识别的资源,我们在OnInspectorGUI中判断他的格式是否为STL,是的话就可以展示出我们为他定制的界面了。
[CustomEditor(typeof(DefaultAsset))]
public class ImportSTL : Editor
{
public override void OnInspectorGUI()
{
if (AssetDatabase.GetAssetPath(target).IsStl())
{
ShowUI();
}
}
}
在ShowUI中进行一些界面设计,好了,接下来在Project中选中这个STL文件就有比较好的展现效果了,让他看起来更像是一个模型文件(好吧,多余的都是无用功除了装逼以外可以省略)。
我们可以看到这个模型的面数为 Triangles Count:84456,也就是说顶点的总量为3 * 84456 = 25W+,这明显超过了Unity标准单个网格上限65000,那么说我们在读取顶点信息后的一个比较重要的操作就是筛选掉所有位置相同的顶点,现在的话我们只能认为只要是位置相等的顶点都是同一顶点。
STL二进制格式文件,前80个字节为模型名称,之后的4个字节存储的模型三角面数量,在之后就是每50个字节一组的存储着每个三角面的法矢量、三个顶点数据,在这里我们先忽略法矢量。
在子线程中逐一的读取顶点信息,并在存储时判断该顶点是否存在相同值,若存在则证明为相同顶点,不再进行存储,同时因为Unity中的mesh需要用三角面数组来记录顶点组成三角面的规则,也就是说三角面数组中的元素即是指向顶点数组中的元素的索引,所以在发现相同顶点的时候,顶点数组不需要添加重复顶点,但三角面数组需要添加重复三角面,并指向重复顶点在顶点数组中第一次出现的位置:
///
/// 读取顶点信息
///
private void ReadVertex()
{
while (_number < _total)
{
byte[] bytes;
bytes = _binaryReader.ReadBytes(50);
if (bytes.Length < 50)
{
_number += 1;
continue;
}
Vector3 vec1 = new Vector3(BitConverter.ToSingle(bytes, 12), BitConverter.ToSingle(bytes, 16), BitConverter.ToSingle(bytes, 20));
Vector3 vec2 = new Vector3(BitConverter.ToSingle(bytes, 24), BitConverter.ToSingle(bytes, 28), BitConverter.ToSingle(bytes, 32));
Vector3 vec3 = new Vector3(BitConverter.ToSingle(bytes, 36), BitConverter.ToSingle(bytes, 40), BitConverter.ToSingle(bytes, 44));
int tri1 = _vertices.FindIndex(delegate (Vector3 v) { return v == vec1; });
if (tri1 < 0)
{
_vertices.Add(vec1);
tri1 = _vertices.Count - 1;
}
int tri2 = _vertices.FindIndex(delegate (Vector3 v) { return v == vec2; });
if (tri2 < 0)
{
_vertices.Add(vec2);
tri2 = _vertices.Count - 1;
}
int tri3 = _vertices.FindIndex(delegate (Vector3 v) { return v == vec3; });
if (tri3 < 0)
{
_vertices.Add(vec3);
tri3 = _vertices.Count - 1;
}
_triangles.Add(tri1);
_triangles.Add(tri2);
_triangles.Add(tri3);
_number += 1;
}
}
然后是CreateInstance按钮的回调:
///
/// 创建STL模型实例
///
private void CreateInstance()
{
string fullPath = Path.GetFullPath(AssetDatabase.GetAssetPath(target));
_total = int.Parse(_trianglescount);
_number = 0;
_binaryReader = new BinaryReader(File.Open(fullPath, FileMode.Open));
//抛弃前84个字节
_binaryReader.ReadBytes(84);
_vertices = new List();
_triangles = new List();
//读取顶点信息
Thread t = new Thread(ReadVertex);
t.Start();
while (_number < _total)
{
EditorUtility.DisplayProgressBar("读取信息", "正在读取顶点信息(" + _number + "/" + _total + ")......", (float)_number / _total);
}
//创建GameObject
GameObject tem = new GameObject(Path.GetFileNameWithoutExtension(fullPath));
MeshFilter mf = tem.AddComponent();
MeshRenderer mr = tem.AddComponent();
Mesh m = new Mesh();
m.vertices = _vertices.ToArray();
m.triangles = _triangles.ToArray();
m.RecalculateNormals();
mf.mesh = m;
mr.material = new Material(Shader.Find("Standard"));
EditorUtility.ClearProgressBar();
_binaryReader.Close();
Debug.Log(tem.name + ":顶点数量 " + _vertices.Count);
}
到此,我们点击CreateInstance按钮,便可以创建实例在Scene中:
创建完成之后:
切换网格模式,我们可以看到模型的面非常的精细,优化之后的顶点在4W左右,比之导入进来时的25W确实少了不少:
总结一下:8W个面的模型,25W+顶点,每个顶点在读取时都会与已读取顶点逐一比对以判断是否相等,从而筛选重复顶点,这样的话计算量在25W*25W=600亿,对半分之后,以及大部分顶点并未与全部顶点比较,计算量大概也在100亿次!多线程显然也无济于事,如果只是读取25W个顶点的话,只需要1秒钟不到,但筛选掉重复顶点是必须的......呃,总之这个坑实在想不到什么精妙的算法来解决了,下一章再看吧。
大概也就是说,导入STL模型更多的应该只是用于展示效果。