ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity运行时动态加载外部OBJ/FBX模型:原理、实战与优化

Unity运行时动态加载外部OBJ/FBX模型:原理、实战与优化 简介在数字孪生、工业仿真和模型查看器等应用中运行时动态加载外部3D模型是高频刚需。它让模型数据与程序逻辑解耦无需重新打包即可灵活替换资源。然而Runtime环境下加载模型涉及文件解析、Mesh构建、坐标修正和材质绑定等复杂环节直接复用编辑器导入逻辑往往行不通。本文从Unity网格底层原理出发对比了AssetBundle、纯C#解析和Assimp库三条技术路线重点剖析OBJ文本格式的解析细节如索引从1开始、三角形剖分、坐标轴转换以及FBX二进制格式的库集成方案。同时分享了一套经项目验证的完整代码覆盖异步加载、贴图处理、内存管理和性能优化策略帮助开发者避开常见坑点快速构建稳定可靠的运行时模型加载功能。 做数字孪生和工业仿真项目这几年被问得最多的一个需求就是用户想在程序运行的时候自己丢一个FBX或者OBJ文件进来程序不用重启就能把模型显示出来。这个需求听起来不复杂真做起来坑却不少。Unity本身在编辑器里导入模型非常方便拖进Project窗口就行但运行时动态加载外部模型文件完全是另一套逻辑涉及到文件读取、格式解析、Mesh构建、材质绑定、异步加载、内存管理等一系列问题。这篇文章我就把实际项目中踩过的坑和最终验证可行的方案整理出来覆盖OBJ和FBX两种最常见格式从原理到可落地的代码再到排查问题的方法一次讲清楚。1. 需求场景与整体方案选型1.1 什么项目真的需要运行时加载模型先说说什么情况下你会碰到这个需求。最常见的就是数字孪生和工业仿真类项目用户手里有一套CAD导出的模型可能是某个设备、一条产线、甚至一整栋楼系统要求能随时把新模型加进去展示而不是每次都在Unity编辑器里手动导入再打包发布。模型查看器类的工具型应用也是典型的场景用户可以加载本地模型文件到场景里预览、测量、标注。游戏项目里还有一个需求是Mod机制玩家自己制作模型放到指定目录下游戏启动时或者运行时动态加载。另外后台系统上传模型、前端实时展示这种Web端配合Unity的方案也越来越多模型文件可能是用户刚上传的程序需要从服务器拉取再动态加载。这类需求本质上都是同一个技术问题Unity运行时怎么把一个外部的模型文件变成场景里能看的GameObject。这个技术点的核心价值在于它把模型数据和程序逻辑彻底解耦了。程序发布之后不需要因为模型变更而重新打包模型文件作为外部资源灵活替换这在很多业务场景里是刚需。1.2 三条实现路线的对比与取舍我在项目里试过几种方案各有优劣先摆出来对比一下方便你选型。方案原理优点缺点适用场景AssetBundle编辑器里把模型打成AB包运行时加载加载快、支持全部Unity特性动画、材质、LOD需要预处理步骤不支持用户直接拖入FBX/OBJ游戏Mod、内容更新Runtime OBJ解析C#直接解析OBJ文本构建Mesh无需任何预处理代码可控只支持OBJ复杂模型解析慢不支持动画工具类应用、模型查看器、小型模型Assimp库集成用第三方库解析FBX/OBJ等几十种格式格式支持全面FBX也能处理库体积较大需要处理原生库兼容性数字孪生、工业软件、需要兼容多种格式如果你只是想在游戏里加载自己打包好的模型AssetBundle方案绝对首选Unity原生支持坑最少。如果用户要直接丢一个OBJ文件进来纯C#解析是性价比最高的方案因为OBJ格式够简单。但如果要支持FBX这种复杂格式目前Runtime环境下自己从零解析基本不现实FBX是二进制格式不同版本差异极大业内普遍做法是集成Assimp库把解析脏活交给它。我最终在正式项目里采用的是双轨方案OBJ走自研解析器轻量可控FBX走Assimp库保证格式兼容性。这两个方案在后面的实操章节都会给出具体实现思路。2. 模型文件格式的核心差异与加载原理2.1 OBJ格式解析看起来简单细节别忽视OBJ格式是Wavefront公司定义的3D模型文本格式它的核心特点就是人可读。一个简单的OBJ文件长这样# 顶点坐标 v 0.0 0.0 0.0 v 1.0 0.0 0.0 v 1.0 1.0 0.0 v 0.0 1.0 0.0 # 纹理坐标 vt 0.0 0.0 vt 1.0 0.0 vt 1.0 1.0 vt 0.0 1.0 # 法线 vn 0.0 0.0 1.0 # 面定义顶点索引/纹理索引/法线索引 f 1/1/1 2/2/1 3/3/1 f 1/1/1 3/3/1 4/4/1解析OBJ的核心逻辑并不复杂按行读取根据行首的关键字做对应的处理。v开头的是顶点坐标vt是纹理坐标vn是法线f是面索引。但这些细节如果不注意会直接掉坑里第一索引的基准是1不是0。OBJ的索引是从1开始的解析的时候必须做-1操作否则第一个顶点会被丢掉整个模型错位甚至垮掉。第二面里的索引有三种写法。f 1 2 3表示只用了顶点索引f 1/2/3 4/5/6 7/8/9是顶点/纹理/法线三个索引都用f 1//3表示中间纹理索引缺失。解析器必须兼容这几种格式。第三OBJ面不保证是三角形。一个四边形面会写成f 1 2 3 4但Unity的Mesh只接受三角形所以你必须在解析的时候做三角剖分把四边形拆成两个三角形。第四OBJ默认坐标轴是Z轴向上的而Unity是Y轴向上加载进来之后需要做一个旋转修正把整个模型绕X轴旋转-90度。这些细节听起来琐碎但任何一个没处理好模型显示出来就是各种诡异问题。我在项目里看到过同事解析的模型面是乱的、法线是反的、模型躺在地上一动不动基本都是这几个细节出了问题。2.2 FBX格式为什么推荐用现成库FBX的复杂度比OBJ高几个数量级。它是Autodesk的 proprietary 格式虽然官方有SDK但Unity Runtime环境下用起来并不方便。FBX同时存在二进制和ASCII两种版本二进制版本里数据以块chunk的形式组织每个块有类型、长度、属性只有拿到了官方规范才能正确解析。FBX不仅存几何数据还包含动画、骨骼、材质、纹理、灯光、相机、甚至单元设置等一堆东西。就算只是加载几何部分也要处理各种连接关系connections理清楚哪个节点挂在哪层哪个网格归哪个模型。这些逻辑自己写的话工作量不亚于写一个简化版的3D引擎。所以我的建议非常明确Runtime环境加载FBX直接集成AssimpOpen Asset Import Library。这是一个跨平台的开源库支持FBX、OBJ、Collada、glTF等几十种常见格式解析结果会统一成一个数据结构你只需要遍历这个结构转换成Unity的Mesh即可。Assimp库在Unity里集成的常见方式是用DllImport调用原生库最新版也提供了.NET绑定。使用的时候需要注意Assimp解析FBX得到的数据通常也是Z轴向上的同样需要做坐标轴转换。另外Assimp解析出来的Mesh顶点、法线、纹理坐标是分组的不要想当然地认为是交错存储转换的时候要仔细读数据结构。2.3 Unity Mesh的底层逻辑理解了格式之后还需要知道Unity的Mesh是怎么组织的。一句话概括Unity的Mesh就是一堆并行的数组。vertices存储顶点坐标normals存储法线uv存储纹理坐标triangles存储三角形索引。它们通过数组下标一一对应第0个顶点对应第0个法线、第0个UV。这里的坑在于OBJ文件里顶点索引、法线索引、纹理索引三者是独立的。比如顶点有8个、法线有6个面里可能引用了1/2/3这样的组合。Unity的Mesh要求一个顶点对应一组完整的属性所以你的解析器必须对顶点做焊接处理如果两个面引用了同一个顶点索引但不同的法线索引那Unity里必须创建两个独立的顶点。这个过程专业术语叫顶点去重反向操作实际操作中就是用一个字典缓存组合键到自己顶点索引的映射。理解了这一层你会发现一个典型的解析流程就是遍历所有面对每个面的每个顶点组合去字典里找对应的Unity顶点索引找不到就新建一个顶点把位置、法线、UV填进去然后把这个顶点索引加入triangles列表。这样Unity才能正确渲染出模型。3. 完整实操从文件读取到场景展示3.1 本地文件加载的两种姿势先解决怎么拿到文件的问题。运行时加载外部模型文件文件位置通常有两个选择一是放在项目的StreamingAssets目录下二是用户在运行时自己指定路径。StreamingAssets目录在PC平台上会被原样复制到最终发布目录你可以通过Application.streamingAssetsPath拿到它的绝对路径。这个目录适合放默认模型、预置好的资源。这里有一个关键点在PC平台上Application.streamingAssetsPath返回的是正常的文件系统路径直接用File.ReadAllLines就能读但在Android平台上不一样返回的是一个jar包内的路径必须用UnityWebRequest来读取。iOS平台同理。写代码的时候别直接假定自己是PC端。如果用户是运行时在文件对话框里选择模型那需要获取文件路径。Unity编辑器窗口里可以用OpenFilePanel像这样string path EditorUtility.OpenFilePanel(选择模型文件, , fbx;obj);但注意EditorUtility只在编辑器模式下可用发布后的程序要用System.Windows.Forms的OpenFileDialog或者自己写一个简单的文件浏览器UI。PC端也可以用Windows Runtime的FileOpenPicker但需要处理异步和权限问题。我在项目里为了简单是直接用OpenFileDialog做的。拿到路径之后就是读取。OBJ是文本文件用File.ReadAllLines或StreamReader逐行解析。FBX是二进制文件Assimp库里会有对应的读取接口加载。3.2 手写一个OBJ解析器的完整实现现在直接上代码。这个解析器我写在项目里验证过可以处理带纹理坐标、法线的OBJ文件支持四边形自动三角化using System.Collections.Generic; using System.IO; using UnityEngine; public static class ObjParser { public static Mesh ParseFile(string filePath) { if (!File.Exists(filePath)) { Debug.LogError($OBJ文件不存在: {filePath}); return null; } string[] lines File.ReadAllLines(filePath); // 先收集原始数据 ListVector3 positions new ListVector3(); ListVector2 uvs new ListVector2(); ListVector3 normals new ListVector3(); // 用于顶点焊接的字典 Dictionarystring, int vertexIndexMap new Dictionarystring, int(); // 最终Mesh数据 ListVector3 finalVertices new ListVector3(); ListVector3 finalNormals new ListVector3(); ListVector2 finalUvs new ListVector2(); Listint triangles new Listint(); foreach (string rawLine in lines) { string line rawLine.Trim(); if (line.Length 0 || line.StartsWith(#)) continue; string[] parts line.Split( ); switch (parts[0]) { case v: positions.Add(new Vector3( float.Parse(parts[1]), float.Parse(parts[2]), float.Parse(parts[3]) )); break; case vt: uvs.Add(new Vector2( float.Parse(parts[1]), float.Parse(parts[2]) )); break; case vn: normals.Add(new Vector3( float.Parse(parts[1]), float.Parse(parts[2]), float.Parse(parts[3]) )); break; case f: // 每个面的顶点数不固定先收集这一面的顶点组合 Listint faceIndices new Listint(); for (int i 1; i parts.Length; i) { string[] indices parts[i].Split(/); int posIdx int.Parse(indices[0]) - 1; // OBJ索引从1开始 int uvIdx indices.Length 1 indices[1].Length 0 ? int.Parse(indices[1]) - 1 : -1; int normalIdx indices.Length 2 indices[2].Length 0 ? int.Parse(indices[2]) - 1 : -1; string key ${posIdx}|{uvIdx}|{normalIdx}; int vertexIndex; if (!vertexIndexMap.TryGetValue(key, out vertexIndex)) { // 新组合创建Unity顶点 vertexIndex finalVertices.Count; vertexIndexMap[key] vertexIndex; finalVertices.Add(positions[posIdx]); if (uvIdx 0 uvIdx uvs.Count) finalUvs.Add(uvs[uvIdx]); else finalUvs.Add(Vector2.zero); if (normalIdx 0 normalIdx normals.Count) finalNormals.Add(normals[normalIdx]); else finalNormals.Add(Vector3.zero); } faceIndices.Add(vertexIndex); } // 三角剖分假设是凸多边形用扇形剖分 for (int i 1; i faceIndices.Count - 1; i) { triangles.Add(faceIndices[0]); triangles.Add(faceIndices[i]); triangles.Add(faceIndices[i 1]); } break; } } Mesh mesh new Mesh(); mesh.name Path.GetFileNameWithoutExtension(filePath); mesh.SetVertices(finalVertices); mesh.SetNormals(finalNormals); mesh.SetUVs(0, finalUvs); mesh.SetTriangles(triangles, 0); mesh.RecalculateBounds(); return mesh; } }这个解析器的关键逻辑在f分支里。每遇到一个面的一个顶点组合先查字典看这个组合是否已经存在存在就直接复用索引不存在才新建顶点。这样既能保证Mesh顶点数据的紧凑又不会出现三角形索引越界。三角形剖分我用了最简单的扇形剖分就是取第一个顶点为公共顶点把四边形拆成两个三角形、五边形拆成三个三角形。这个算法对凸多边形有效对凹多边形会出问题但实际工程中OBJ模型的面基本以三角形和四边形为主所以够用。如果遇到复杂凹多边形建议用ear clipping算法但性能开销会大不少。注意解析之后记得调一次mesh.RecalculateBounds()否则模型的包围盒不对可能会导致视锥剔除出问题模型明明在屏幕里却不显示。解析完之后一个简单的加载函数就是public static GameObject LoadObjToScene(string filePath) { Mesh mesh ObjParser.ParseFile(filePath); if (mesh null) return null; GameObject go new GameObject(LoadedOBJ); MeshFilter filter go.AddComponentMeshFilter(); MeshRenderer renderer go.AddComponentMeshRenderer(); filter.sharedMesh mesh; renderer.sharedMaterial new Material(Shader.Find(Standard)); // OBJ默认Z轴向上Unity是Y轴向上需要旋转修正 go.transform.rotation Quaternion.Euler(-90f, 0f, 0f); return go; }这里面旋转修正非常重要。如果不做这一步你在3ds Max或Blender里看着正常的模型进到Unity里就会整个躺倒。Blender里默认是Z轴向上导出OBJUnity是Y轴向上差了90度。当然也有部分导出工具会做坐标转换判断标准就是看原始OBJ文件的顶点数据如果模型本身是立着的但加载后躺倒了加上这个旋转修正就好。3.3 使用Assimp库加载FBXFBX的加载我直接说集成Assimp的方案。Assimp在Unity里的集成方式一般有两种一种是把C原生库编译成对应平台的原生插件.dll/.so/.dylib通过[DllImport]调用另一种是直接用C#封装版本的Assimp例如Silk.NET封装的Assimp配合原生库一起使用。流程分几步public static GameObject LoadFbxWithAssimp(string filePath) { // 1. 初始化Assimp上下文 Assimp.AssimpContext importer new Assimp.AssimpContext(); // 2. 导入模型返回场景 Scene scene importer.ImportFile(filePath, PostProcessSteps.Triangulate | PostProcessSteps.GenerateSmoothNormals | PostProcessSteps.JoinIdenticalVertices | PostProcessSteps.FlipUVs); if (scene null || scene.RootNode null) { Debug.LogError(FBX加载失败); return null; } // 3. 递归遍历场景节点树 GameObject root new GameObject(LoadedFBX); ProcessNode(scene.RootNode, scene, root.transform); // 4. 坐标修正 root.transform.rotation Quaternion.Euler(-90f, 0f, 0f); return root; } private static void ProcessNode(Node node, Scene scene, Transform parent) { GameObject go new GameObject(node.Name); go.transform.SetParent(parent, false); // 处理该节点上的Mesh if (node.MeshIndices.Count 0) { foreach (int meshIndex in node.MeshIndices) { Mesh mesh ConvertAssimpMesh(scene.Meshes[meshIndex]); GameObject meshGo new GameObject(Mesh); meshGo.transform.SetParent(go.transform, false); MeshFilter filter meshGo.AddComponentMeshFilter(); MeshRenderer renderer meshGo.AddComponentMeshRenderer(); filter.sharedMesh mesh; renderer.sharedMaterial new Material(Shader.Find(Standard)); } } // 递归处理子节点 foreach (Node child in node.Children) { ProcessNode(child, scene, parent); } }这个递归遍历的逻辑很重要。FBX文件里的节点是有层级结构的一个父节点下面可以有多个子节点每个节点可能携带Mesh也可能只做分组用。必须把整个层级树完整地还原到Unity场景里模型的Transform结构才能保持一致不然模型显示出来可能是散的或者位置错乱的。ConvertAssimpMesh这个函数本质上是把Assimp的Mesh数据拷贝到Unity Mesh里核心逻辑和OBJ解析器类似都是把顶点、法线、UV、三角形索引填进去。Assimp有一个好处它解析完的数据已经做过三角形化和顶点索引统一你不需要自己再做焊接处理。在ImportFile的时候有几个后处理标志是强烈建议用上的Triangulate把多边形面切成三角形省得自己处理三角剖分。GenerateSmoothNormals如果模型没有法线数据让它自动生成平滑法线。JoinIdenticalVertices对有相同属性组合的顶点做焊接减少重复顶点省内存。FlipUVs把UV的V方向翻转很多DCC工具和DirectX的UV坐标习惯是顶部为0的而Unity的UV坐标习惯反过来。这几个标志能让你省掉很多自己动手处理数据的功夫。3.4 材质贴图的配套处理模型加载出来只有白色材质那基本等于没做完。正确的做法是解析模型文件里的材质引用信息把对应的贴图也加载出来。OBJ文件通过mtllib关键字引用一个.mtl材质库文件材质库文件里定义了漫反射贴图map_Kd、法线贴图map_Bump、高光贴图map_Ks等。解析流程是解析OBJ文件时遇到mtllib记录材质文件路径遇到usemtl记录当前面使用哪个材质。然后在场景中同一材质的模型分组要放到同一个节点下共用同一个Unity材质。我在实际项目中做OBJ解析时材质关联做成了两步加载public static Material LoadMtlMaterial(string mtlFilePath, string textureBaseDir) { // 1. 解析mtl文件找到map_Kd对应的纹理路径 string diffuseMapPath null; using (StreamReader reader new StreamReader(mtlFilePath)) { string line; while ((line reader.ReadLine()) ! null) { line line.Trim(); if (line.StartsWith(map_Kd)) { string[] parts line.Split( ); diffuseMapPath parts[1]; break; } } } // 2. 生成标准材质 Material mat new Material(Shader.Find(Standard)); if (diffuseMapPath ! null) { string fullPath Path.Combine(textureBaseDir, diffuseMapPath); if (File.Exists(fullPath)) { byte[] data File.ReadAllBytes(fullPath); Texture2D tex new Texture2D(2, 2); tex.LoadImage(data); // 直接从PNG/JPG字节加载纹理 mat.mainTexture tex; } } return mat; }Texture2D.LoadImage这个方法非常实用它可以直接从PNG或JPG的字节数组解码生成纹理不需要Unity编辑器里导入资产。这也是运行时加载外部模型贴图的关键配套。注意贴图路径的处理有的OBJ的mtl文件里写的是相对路径有的是绝对路径还有的带反斜杠和盘符。实际使用的时候务必统一转换成项目内的资源路径或者StreamingAssets下的绝对路径不然容易踩坑。FBX文件更复杂材质通常内嵌在文件里或者有外部引用Assimp库导出材质信息后你也可以遍历scene.Materials拿贴图路径。FBX的贴图路径可能是相对导出位置这个需要自己处理路径拼接。4. 常见问题与排查技巧4.1 模型加载失败的排查思路我总结了一下运行时加载模型最常见的失败原因就那几条快速定位是关键。症状可能原因排查方法模型完全不显示文件路径错误打印实际路径检查文件是否存在网格数据为空检查解析的Vertices和Triangles数量相机被包围盒裁剪手动设置一次bounds模型显示但不完整法线数据缺失调RecalculateNormals()索引越界检查索引解析是否忘了-1模型位置错乱坐标轴未修正加上绕X轴-90度旋转层级结构丢失确认递归遍历是否完整材质全白贴图未加载检查mtl文件路径和图片文件第一条排查建议永远是在加载后立刻打印关键信息顶点数量、三角形数量、材质数量。如果顶点数为0说明文件解析出了问题如果三角形数为0说明三角剖分没成功。有了这些数字你就能快速判断问题出在哪一层。索引越界这个坑我特别说一下FBX导出或OBJ转换的时候有的工具会生成带非法索引的面比如索引值超过顶点数组长度。解析的时候最好加个边界判断打印警告而不是直接抛异常崩溃。4.2 模型显示异常的典型原因很多人在做完加载后发现模型表面黑一块白一块的这个基本是法线问题。OBJ文件里如果没有vn定义解析器给每个顶点的法线填了Vector3.zero这个法线会导致光照计算结果恒为0模型表面就是黑的。解决办法是解析完没有法线的话调mesh.RecalculateNormals()让Unity根据三角形面自动计算法线。注意RecalculateNormals计算的是面法线的平均软边和硬边不会区分但至少视觉上能接受。再一个是UV方向问题。OBJ的UV坐标和Unity的UV坐标在V轴方向上正好相反如果不处理贴图会上下颠倒。解决方案可以在解析的时候直接把V取反uv.y 1f - uv.y;也可以在材质里设置textureScale new Vector2(1, -1)。我更建议前者因为后者在批处理的时候可能引起其他问题。模型闪烁现象字面意思就是模型表面在视角变化时不停闪动这是z-fighting导致的两个面几乎重叠。出现这个现象一般是模型导出的时候有重复面或者轻微的重叠面。这种问题很难在加载层面解决建议是在3D建模软件里清理一下模型再导出。4.3 性能和内存优化经验最后聊一下性能问题这是运行时加载模型最大的隐患。内存方面最容易被忽略的是Mesh的引用管理。动态加载的模型如果不再使用一定要主动销毁Mesh资源否则会一直躺在内存里。正确姿势是// 销毁模型时不只销毁GameObject还要销毁Mesh Destroy(modelGameObject); Destroy(modelGameObject.GetComponentMeshFilter().sharedMesh);否则临时目录里堆积的模型会缓慢耗尽内存。这个我在长时间运行的数字孪生项目里吃过亏跑了一个星期之后内存涨了四五个GB排查下来就是之前动态加载的Mesh都没有释放。加载性能方面大模型的解析是纯CPU操作可能会卡住主线程。一个5万面的OBJ文件单线程解析大概需要几百毫秒这个卡顿是肉眼可见的。优化方向有两个一是把解析放到后台线程解析完成后在主线程构建Mesh二是解析之前先做预处理降低顶点和面的数量。我在实际项目里用了简单而有效的方式解析逻辑放到ThreadPool里执行解析完成后用UnityMainThreadDispatcher之类的工具切回主线程来创建Mesh和GameObject。因为Mesh的构建和GameObject的创建必须在主线程但文件解析和字符串处理完全可以扔到后台线程。还有个性能优化点这个在网上讨论不多但非常重要mesh.Optimize()方法。在网格创建完成后调用一次Unity会重新组织顶点缓冲和索引缓冲让GPU的缓存命中也更好。实测在某些移动设备上优化后的渲染性能有5%-10%的提升。代价是优化过程本身耗时所以大模型建议只在后台异步做完再切回主线程调用。对于超大模型我的建议是引入LOD细节层次方案。运行时解析完模型之后生成一个降面版本的Mesh作为远距离使用的LOD配合Unity的LODGroup组件使用。降面算法可以用简化版本的顶点聚类或者直接均匀采样。这个方法在数字孪生项目里作用很明显一台几十万面的设备模型放在场景里远近结合的LOD能把帧率稳定在60帧。关于文件格式兼容性最后给大家一个底线建议给用户用的模型导入功能一定要在文档里明确支持范围。OBJ格式虽然简单但不是所有DCC工具导出的OBJ都规范我遇到过一个软件导出的OBJ文件每个面后面都跟着一些奇怪的扩展数据如果不是解析器做了容错处理直接就被干崩了。FBX就更不用说了不同版本、不同软件导出的FBX差异很大生产环境必须把Assimp库固定一个版本并且做好异常捕获不能让一个坏文件把整个应用搞挂。我在实际使用中发现运行时动态加载模型这个功能最考验的不是代码本身的实现而是对各种边界情况的处理。文件损坏、格式变体、坐标轴不一致、贴图路径缺失这些才是真正让你加班到凌晨的东西。建议在项目初期就把异常处理的框架搭好把错误日志打全后面会省很多事。最后再分享一个小技巧解析任何模型之前先读文件的前几百字节判断一下文件类型。OBJ文件前几行必然是v或mtllib开头FBX二进制文件开头有Kaydara FBX Binary的魔数。这个判断能避免扩展名被篡改或者文件被错误解析的问题。模型加载本来就是个脆弱的环节多一道文件头校验就少一个线上事故。本文还有配套的精品资源点击获取
返回列表