前言
- 写这篇文章的初衷是为了记录做进做的一个工程中遇到的一个关于匹配字节的效率问题
为了将.obj格式的文件读入到Unity中 其包含的步骤包含3个过程:
1、读取obj文件
2、将读到的值存储下来
3、赋值给gameobject 并作渲染 - 其中第一步耗时很少 对于一个12.2M(561606行数据)的文件,其读取时间仅仅为500ms(即0.5s),但是readalllines()函数读取到的字符串需要在赋值之前做一个处理 网上所查到的资料有几种处理方法:Indexof()、split()将字符串分割为多个子字符串。
- 但是以上提及到的方法效率非常慢 c++有一个读取固定格式的方法,效率很高:fscanf()函数 读取并进行赋值 整个流程大概在15s以内,因此我想到在C#中重写fscanf()函数来提高读取文件的速度,这就需要用到正则表达式来进行字符匹配。
- obj文件长什么样?


1.正则表达式
字符 | 描述 |
---|
\cx | 匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。 |
\f | 匹配一个换页符。等价于 \x0c 和 \cL。 |
\n | 匹配一个换行符。等价于 \x0a 和 \cJ。 |
\r | 匹配一个回车符。等价于 \x0d 和 \cM。 |
\s | 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。 |
\S | 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。 |
\t | 匹配一个制表符。等价于 \x09 和 \cI。 |
\v | 匹配一个垂直制表符。等价于 \x0b 和 \cK。 |
特殊字符 | 描述 |
---|
$ | 匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 ‘\n’ 或 ‘\r’。要匹配 $ 字符本身,请使用 $。 |
( ) | 标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用 ( 和 )。 |
* | 匹配前面的子表达式零次或多次。要匹配 * 字符,请使用 *。 |
+ | 匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 +。 |
. | 匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 . 。 |
[ | 标记一个中括号表达式的开始。要匹配 [,请使用 [。 |
? | 匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 ?。 |
\ | 将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, ‘n’ 匹配字符 ‘n’。’\n’ 匹配换行符。序列 ‘\’ 匹配 “”,而 ‘(’ 则匹配 “(”。 |
^ | 匹配输入字符串的开始位置,除非在方括号表达式中使用,当该符号在方括号表达式中使用时,表示不接受该方括号表达式中的字符集合。要匹配 ^ 字符本身,请使用 ^。 |
{ | 标记限定符表达式的开始。要匹配 {,请使用 {。 |
竖线(这里打不出来) | 指明两项之间的一个选择。要匹配 |
1.1 怎么写
匹配项 | 正则表达式 |
---|
String | [\w\d\S]+ |
Int16 | -[0-9]+ |
UInt16 | [0-9]+ |
Int32 | -[0-9]+ |
UInt32 | [0-9]+ |
Int64 | -[0-9]+ |
UInt64 | [0-9]+ |
Single | [-] |
Double | [-] |
Boolean | true |
Byte | [0-9]{1,3} |
SByte | -[0-9]{1,3} |
Char | [\w\S]{1} |
Decimal | [-] |
- 注意:
正则表达式最好根据自己的需求进行写 以上仅作为参考
比如:上面的Double的正则表达式无法匹配这样的数:328.501
我自己的匹配正则表达式为:-?([0-9]{1,4}[.]?[0-9]{0,5})
可以把自己写的正则表达式在这个网站进行测试:正则表达式在线测试工具
1.2 对应的代码
- 选择读取的文件
public void SelectFile()
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "obj文件(*.obj)|*.obj";
ofd.Title = "选择待手术的对象";
ofd.InitialDirectory = "file://" + UnityEngine.Application.dataPath;
if (ofd.ShowDialog() == DialogResult.OK)
{
Debug.Log(ofd.FileName);
int start_ReadAllLines = System.Environment.TickCount;
string[] lines = File.ReadAllLines(ofd.FileName);
Debug.Log("读取文件耗时:" + (System.Environment.TickCount-start_ReadAllLines));
GameObject o = OBJCreate(lines);
o.GetComponent<Transform>().position =new Vector3(0.5f,-0.5f,-0.5f);
if (o.AddComponent<Interactable>() != null)
Debug.Log("Interactable has loaded");
if (o.AddComponent<Choosable>() != null)
Debug.Log("Choosable has loaded");
}
}
static GameObject OBJCreate(string[] lines)
{
Debug.Log("OBJCreate begin~");
GameObject o = new GameObject();
o.AddComponent<MeshCollider>();
o.AddComponent<MeshFilter>();
o.GetComponent<MeshFilter>().mesh.Clear();
o.AddComponent<MeshRenderer>();
List<Vector3> vertexList = new List<Vector3>();
List<int> TrianglesList = new List<int>();
Scanf s = new Scanf();
int start_Regex = System.Environment.TickCount;
for (int i = 0; i < lines.Length; i++)
{
var currentLine = lines[i];
MatchCollection matches;
if (currentLine[0]=='#' || currentLine.Length == 0)
{
Debug.Log("#");
continue;
}
if (currentLine[0] == 'v')
{
Debug.Log("V");
matches = s.Scan(currentLine, 1);
vertexList.Add(new Vector3((float)Double.Parse(matches[0].Value), (float)Double.Parse(matches[1].Value), (float)Double.Parse(matches[2].Value)));
Debug.Log(vertexList[vertexList.Count - 1].x +" "+ vertexList[vertexList.Count - 1].y + " " + vertexList[vertexList.Count - 1].z);
else if (currentLine[0] == 'f')
{
Debug.Log("f");
matches = s.Scan(currentLine, 0);
for (int j = 0; j < 3; j++)
{
int temp = (Int32.Parse(matches[j].Value) - 1);
Debug.Log("三角索引:"+temp);
if (temp > vertexList.Count)
Debug.Log("越界节点:" + temp);
TrianglesList.Add(temp);
}
}
}
Debug.Log("匹配耗时:" + (System.Environment.TickCount - start_Regex));
Debug.Log("vertexList.Count:" + vertexList.Count);
Debug.Log("TrianglesList.Count:" + TrianglesList.Count);
Debug.Log("赋值");
Debug.Log("1");
Debug.Log("2");
Debug.Log("3");
Debug.Log("4");
o.GetComponent<MeshFilter>().mesh.vertices = vertexList.ToArray();
Debug.Log("5");
o.GetComponent<MeshFilter>().mesh.triangles = TrianglesList.ToArray();
Debug.Log("正则匹配耗时:" + (System.Environment.TickCount - start_Regex));
Debug.Log("创建完毕");
return o;
}
1.3 速度对比
- split()方法读取一个356KB的文件需要12641ms 这个函数仅需要5000ms 相当于速度提升一倍 但是这个方法还存在问题 当读取12.2M的文件时报错 以为马上开组会 所以还没来得及去找出bug 因为这个速率也是非常慢的 用户等待时间太长 因此不得不推荐下面这个插件 这部分bug会在后面进行补充待续!!
2.OBJ导入插件 Trilib
2.1 简介
- 这是一个专用于导入3D模型的插件 速度非常快
支持在运行状态下动态导入模型的插件,支持obj,fbx,stl等格式的模型
2.2 使用
2.2.1 界面展示
- 提供9个现成的Demo场景 (暂时只用过第一个)

载入的界面
下面这个是导入的一个30MB的露骨OBJ文件,耗时小于5s

2.2.2 使用
- 只需要做两个简单修改 导入界面无需更改 但如果需要将其导入其他场景 需要在脚本"AssetLoaderWindows"的GoToSceneButtonClick()函数中修改为要导入的场景。

- 根据上述步骤 即可实现简单的模型导入场景,但是导入模型存在大小不合适 不能抓取等功能,需再进行调整
- 调整物体(完整版代码)
void Start () {
GameObject go = GameObject.Find("LoadedObject");
Transform g = go.transform.GetChild(0);
g.SetParent(null);
while (g.GetComponent<MeshFilter>().mesh.bounds.size.x * g.transform.localScale.x > 1.5 || g.GetComponent<MeshFilter>().mesh.bounds.size.y * g.transform.localScale.y > 1.5 || g.GetComponent<MeshFilter>().mesh.bounds.size.z * g.transform.localScale.z > 1.5)
{
Debug.Log("ok");
g.transform.localScale = g.transform.localScale / 10;
}
Debug.Log("0");
GameObject defaultobject = GameObject.Find("defaultobject");
if (defaultobject != null)
Debug.Log(defaultobject);
defaultobject.AddComponent<MeshCollider>();
defaultobject.AddComponent<Interactable>();
defaultobject.AddComponent<Choosable>();
defaultobject.tag = "Objects";
defaultobject.name = "Created in "+System.DateTime.Now.ToShortTimeString();
}
2.3 导入的模型全黑
- 简单表现为模型全为黑色,导致根本看不清楚细节
- 如图所示(头骨):

2.31 问题所在