简介:面向需要为Unity项目加入运行时模型导入能力的开发者,这份源码工程基于TriLib 2.3.7插件,使用Unity 2021.3.27 Standard Render Pipeline构建,实现了运行中动态选择模型、加载并预览的功能,且可替换当前场景中的已有模型。TriLib本身支持Windows、Mac、Linux、Android、WebGL等平台,可解析FBX、OBJ、GLTF2、STL、ZIP等常见格式,也兼容Standard/URP/HDRP渲染管线,因此这套工程也可作为模型查看器、关卡编辑器、AR/VR可视化及游戏内模型热更新的基础框架。压缩包共879个文件,约26.37MB,包含145个DLL、94个C#脚本、35个Asset配置、18个材质与18个Shader,另有示例FBX/OBJ模型、UI图集及字体资源,工程目录划分清楚。当前已有267人学习下载。通过完整工程,能快速掌握TriLib的接入流程、UI交互逻辑和模型加载释放细节,并基于现有代码扩展出符合自身需求的模型管理模块。 做Unity开发这些年,遇到“运行时加载3D模型”这个需求的次数实在太多了。不管是做游戏Mod工具、自定义角色穿戴,还是给美术团队做资源预览器、给客户展示产品配置效果,都绕不开同一个问题:程序运行之后,用户选中一个外部fbx或obj文件,场景里就得出现这个模型。这个需求看着简单,背后牵扯的却是文件选择、格式解析、Mesh和Material生成、模型卸载、内存回收一连串问题。这个基于TriLib插件的Unity3d C#源码工程,解决的就是这么一件事:在游戏运行时把外部3D模型文件顺利导进场景,并且能用、能删、能替换。
这个工程源码最实用的点在于,它把TriLib的加载流程完整跑通了一遍,从用户选文件到模型出现在场景指定节点下,中间所有环节都有现成代码可以抄。适合三类人参考:一是游戏项目的Mod功能开发,二是工具型产品需要内置模型预览能力,三是刚接触TriLib、想少踩坑的Unity开发者。下面我把这个工程的实现思路、核心代码和踩坑记录都拆开讲清楚。
1. 为什么需要运行时模型导入:场景与痛点拆解
1.1 三个典型场景:Mod工具、编辑器、产品配置
先说游戏Mod场景。很多PC游戏允许玩家往游戏里塞自定义模型,比如赛车游戏换车皮、沙盒游戏加道具。这种需求如果靠Unity编辑器预先把模型打进AssetBundle,那玩家每换一个模型都要我重新打一次包,根本不现实。运行时导入模型是唯一的出路,玩家把文件往指定目录一扔,游戏启动后扫描目录,加载进场景,这事儿就成了。
再说编辑器类工具。我给美术团队做过一个简易的模型预览工具:美术把fbx导出来,拖进工具窗口,立刻看到模型在标准环境下的效果,用来检查比例、法线、材质是否正常。这种工具本质上是Unity工程,但它要打开的是美术随便放在哪个目录下的文件,文件路径在运行前根本不可知,不用运行时导入完全没法做。
最后是产品配置场景。比如家具软装App,用户选一个沙发模型,要能拖进自己家的户型图里看效果。模型文件可以放在本地磁盘或服务器上,运行时下载或读取后加载。这种场景对加载速度、模型精度、材质还原度都有要求,TriLib这类插件在这方面的表现比Unity自带的Resources路径灵活太多。
1.2 为什么不用AssetBundle和原生路径
有人会问:Unity不是有AssetBundle吗?把模型打进AB包,运行时加载不就行了?
理论上可以,但实际操作会很难受。AssetBundle的打包流程依赖编辑器,你得先在Unity里指定要打的资源,生成AB文件,再发布到服务器或打进安装包。问题是:你根本不知道玩家会在运行时给你一个什么样的模型文件。玩家的fbx可能是网上随便下载的,也可能是自己用Blender导出的,这个文件在你打包时根本不存在,AssetBundle从机制上就堵死了这条路。
也有人问:那我自己写解析器,直接读fbx、obj文件不就行了?说实话,obj格式还勉强可以试试,fbx格式本身就是个复杂的二进制容器,里面涵盖网格、UV、法线、骨骼、动画、材质引用,业余时间写个完整解析器基本等于重新造一个行业级工具,投入产出比极低。与其自己造轮子,不如用经过大量项目验证的第三方插件。TriLib做的就是这件事:把fbx、obj、gltf、glb、stl、ply等多种格式统一解析成Unity的Mesh、Material、AnimationClip和GameObject结构,而且把异步加载、资源分配、碰撞体生成这些坑都处理好了。
2. 选型TriLib背后的逻辑:技术对比与功能边界
2.1 方案对比:自己解析和现成插件
我整理一下市面上做运行时模型导入的几个方案,大家对照着看就明白了。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Unity AssetBundle | 官方支持、性能好 | 依赖编辑器打包流程、无法处理运行时任意文件 | 预知资源清单的常规资源加载 |
| 自己解析obj/fbx | 零成本、无插件依赖 | fbX解析极复杂、开发周期长、格式支持不全 | obj格式简单场景、学习研究 |
| UnityWebRequest加载服务器文件 | 能拉远程文件 | 拿到文件后仍需解析成Mesh/材质,等同绕路 | 配合TriLib做远程下载再导入 |
| TriLib插件 | 多格式支持、异步API成熟、资源管理完善 | 需要购买授权许可 | 运行时导入外部模型的最佳通用解 |
不难看出,TriLib在“运行时加载用户任意模型文件”这个垂直场景里几乎没有对手。它的价值不是省了写Loader那几百行代码,而是省掉了格式解析这种极其耗费精力的底层工作。
2.2 TriLib的核心能力和边界
TriLib支持几十种主流3D格式,最常见的fbx、obj、gltf、glb、blend、stl、ply、3ds、dae都在支持列表里。它支持网格、材质、纹理、骨骼动画、BlendShape、LOD这些常规3D资产信息,还有一个很重要的能力:自动生成碰撞体。这对游戏场景里的物理交互很关键,模型加载进来就能直接参与碰撞检测,不用再手动挂Collider。
平台方面,PC、Mac、Linux、iOS、Android、WebGL都支持,开发者可以用同一套API处理多平台模型导入。
边界也要说清楚。TriLib对fbx的格式版本兼容不是无限覆盖的,过于老或者过于特殊的fbx版本偶尔会出现解析失败。另外,某些复杂的着色器节点、特殊贴图属性,TriLib导入后可能退化成基础材质,这是格式转换工具的通病,不单是TriLib的问题。理解了它能做什么、不能做什么,后面写代码的时候心里就有底了。
3. 核心功能拆解:从文件选择到模型挂接
3.1 文件选择:不止是弹窗那么简单
模型加载的第一步是拿到文件路径。TriLib自带的AssetBrowserPicker组件封装了文件选择窗口,最方便的做法是直接用它的UI预制体。但实际工程里,我更推荐用StandaloneFileBrowser这类第三方文件选择库,然后在拿到路径后调用TriLib的加载API,这样UI风格跟自己的项目更统一,交互上也不会被插件绑死。
要注意的是移动端权限问题。Android 6.0以上运行时读写存储需要动态申请权限,iOS虽然没有文件系统层面的路径选择,但用户通过Files App选择文件后,拿到的是沙盒内的临时访问URL,需要先复制到可访问目录再加载。这部分代码要写在文件选择之前,否则会出现选完文件却读不到的诡异问题。
我给的工程源码里,文件选择的逻辑放在一个独立脚本里,方便替换和复用。如果你不需要UI弹窗,想直接加载某个固定目录下的模型文件,跳过选择步骤直接调用加载API即可,核心逻辑不受影响。
3.2 模型加载:异步永远比同步更稳
TriLib提供同步加载AssetLoader.LoadModelFromFile和异步加载AssetLoader.LoadModelFromFileAsync两个入口。同步加载用法简单直接,模型越大、顶点越多,卡顿越严重。一个动辄几十MB的fbx文件,同步加载可能造成好几秒的画面冻结,玩家直接以为游戏崩溃了。
所以实际工程里,凡是外部文件加载一律用异步。异步API的基本用法是这样:
using TriLib; // 加载选项:可以配置是否自动生成碰撞体、是否加载骨骼动画等 AssetLoaderOptions assetLoaderOptions = AssetLoaderOptions.CreateInstance(); assetLoaderOptions.AutoLoadColliders = true; // 异步加载模型,加载完成后回调处理 AssetLoader.LoadModelFromFileAsync(filePath, assetLoaderOptions, OnModelLoaded, OnModelLoadProgress, OnModelLoadError);回调函数的要点是根节点处理。TriLib加载完成后,会把整个模型包装在一个根GameObject下返回,你需要决定这个根节点放哪儿。如果只是单纯预览,挂到场景根目录就行;如果要跟随某个角色或挂在UI界面上,就得设置父节点。
3.3 模型加载选项的参数细节
TriLib的AssetLoaderOptions里有几个参数是必须认真调的,不调好后面会有各种奇怪问题。
AutoLoadColliders控制是否自动生成碰撞体。需要物理交互就打开,纯预览就关掉,打开会增加多余的物理计算。AutoPlayAnimation针对带骨骼动画的模型,打开后加载完会自动播放默认动画,编辑器预览场景可以打开,游戏内使用建议关闭,避免动画状态不受控。ScaleMode控制模型缩放策略,默认是按文件内单位直接映射到Unity单位,很多3D软件导出的模型跟Unity的单位比例不一致,建议配置为适配Unity单位,否则模型可能偏大或偏小。
这些参数组合起来,基本决定了模型加载之后“是什么样”。工程源码里我对每个参数都写了注释,方便大家按需调整。
4. 实操过程:源码工程搭建与关键实现
4.1 工程准备与插件导入
做这个工程前,先确认Unity版本。MyTriLib库对Unity版本有一定要求,建议使用Unity 2019 LTS以上的版本,最好用2021或2022,API兼容性更好。创建新工程时不需要额外配置渲染管线,内置渲染管线和URP都能跑,如果要支持WebGL平台,记得在Player Settings里勾选对应模块。
接着把TriLib插件包导入工程。我用的版本是TriLib 2.x。导入后确认插件目录下有TriLib的ASMDEF文件,这样你的脚本才能正常引用using TriLib命名空间。如果工程采用Assembly Definition组织代码,记得给自己的程序集添加TriLib程序集引用,否则编译时会报找不到类型的错误。
4.2 核心代码实现:一个可以直接跑的Demo
我写的示例Demo提供了一个加载模型的最简流程,核心包括三部分:初始化加载选项、调用文件选择器、执行异步加载。下面是核心代码的骨架,省略了UI绑定的部分:
using UnityEngine; using TriLib; using SFB; public class RuntimeModelLoader : MonoBehaviour { [Header("模型挂载的父节点")] public Transform modelParent; [Header("加载完成后自动适配相机")] public bool autoFrameCamera = true; private AssetLoaderOptions _options; void Start() { _options = AssetLoaderOptions.CreateInstance(); _options.AutoLoadColliders = false; _options.MarkModelsAsStatic = false; _options.ScaleMode = ScaleMode.Normal; } public void OnButtonClickLoadModel() { #if UNITY_ANDROID && !UNITY_EDITOR // Android 需要先申请存储权限,再打开文件选择器 PermissionHelper.RequestStoragePermission(() => { OpenFilePanel(); }); #else OpenFilePanel(); #endif } private void OpenFilePanel() { var extensions = new[] { new ExtensionFilter("3D Model Files", "fbx", "obj", "gltf", "glb", "blend", "stl", "ply", "3ds", "dae"), new ExtensionFilter("All Files", "*") }; var paths = StandaloneFileBrowser.OpenFilePanel("选择3D模型文件", "", extensions, false); if (paths.Length > 0) { LoadModel(paths[0]); } } private void LoadModel(string filePath) { AssetLoader.LoadModelFromFileAsync(filePath, _options, OnModelLoaded, OnModelProgress, OnModelError); } private void OnModelLoaded(AssetLoaderContext context) { GameObject loadedModel = context.LoadedGameObject; if (loadedModel == null) return; if (modelParent != null) { loadedModel.transform.SetParent(modelParent, false); } if (autoFrameCamera) { CameraFrameHelper.FrameObject(Camera.main, loadedModel); } } private void OnModelProgress(AssetLoaderContext context, float progress) { // 进度回调,可用于更新UI进度条 } private void OnModelError(AssetLoaderContext context) { Debug.LogError($"模型加载失败: {context.Error}"); } }这段代码能跑通整个流程。大家要注意modelParent这个字段,它决定了模型加载完挂在哪里。如果希望模型出现在世界坐标原点,保持modelParent为空即可,TriLib加载出的模型本身就在世界原点。
4.3 资源管理与销毁:防止内存持续上涨
模型加载不是加载完就万事大吉,重复加载和退出场景时的资源管理才是真正的考验。我见过很多项目,模型加载功能做完了,但在场景间切换几次后内存暴涨,这个问题就出在只销毁了GameObject、没有卸载TriLib创建的原生资源上。
TriLib的处理逻辑是:加载模型时会创建Mesh、Material、Texture、AnimationClip等Unity原生资源。如果你只是Destroy(context.LoadedGameObject),这些资源依然留在内存里,因为它们在场景中已经没有引用,但Unity的引用计数机制还没触发卸载回收。
正确做法是先销毁GameObject,再调用AssetLoader.UnloadModel并配合Resources.UnloadUnusedAssets:
public void UnloadModel(GameObject modelGo) { if (modelGo == null) return; // 通过TriLib上下文卸载模型资源 AssetLoader.UnloadModel(modelGo); Destroy(modelGo); // 触发Unity资源垃圾回收 Resources.UnloadUnusedAssets(); System.GC.Collect(); }这里有一个注意点:AssetLoader.UnloadModel要放在Destroy之前调用,因为UnloadModel内部会根据GameObject上的引用关系释放Mesh、Material等资源。先Destroy的话,引用关系已经断了,TriLib就拿不到需要释放的资源列表了。
5. 常见问题与排查技巧实录
5.1 典型问题速查表
我做了这么久的模型加载功能,积累了一些高频问题的排查经验,整理成表格供大家参考。
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 加载完成后模型全黑 | 模型法线数据缺失或朝向问题 | 检查模型源文件的法线设置,关闭TriLib的AutoComputeNormals再重新加载 |
| 模型位置偏移严重 | 源文件单位比例与Unity不一致 | 设置ScaleMode为FitInBounds或手动配置UnitScale |
| 材质贴图丢失只有一个默认材质 | fbx引用的外部贴图路径失效 | 把模型文件和贴图放在同一目录,或通过OnMaterialsLoaded回调手动重新指定贴图 |
| Android加载报权限错误 | 未申请运行时存储权限 | 在打开文件选择器前申请READ_EXTERNAL_STORAGE权限 |
| 加载大模型时网络请求卡死 | 误用同步加载且文件过大 | 改用LoadModelFromFileAsync,并监控进度回调 |
| 模型动画不播放 | 没有配置动画加载选项 | 开启AutoPlayAnimation,确认模型包含动画Clip |
5.2 材质发黑与法线问题的排查细节
材质发黑是反馈率最高的问题。很多fbx模型源文件是在Blender或3ds Max里做的,导出时法线方向没处理好,TriLib导入后就会出现单面显示正常、双面显示全黑的情况。
排查方法是把相机转到模型背面,如果背面能看到、正面全黑,就是法线朝向问题。解决办法有两种:一是修改源文件,在建模软件里翻转法线再导出;二是在TriLib加载选项中开启AutoComputeNormals,让插件自动重算法线。第二个办法省事,但会丢失模型原有的法线细节,对要求高的模型建议还是从源文件下手。
还有一类发黑是贴图通道问题。很多PBR材质的金属度和粗糙度贴图用的是单独通道,TriLib导入后如果不能识别,材质看起来就不太对劲。这时候需要检查模型导入后的材质球参数,确认Metallic和Smoothness贴图是否正确赋值。
5.3 内存与性能优化经验
模型文件动辄几十MB,加载过程对内存的冲击很大。针对这一点,我总结了几个实际好用的优化策略。
用完即弃是基本原则。如果模型只用于临时预览,预览结束后立刻走上面说的卸载流程。模型缓存策略方面,如果同一个模型会被反复加载,可以把它加载后的GameObject缓存下来,下次需要时直接复用拷贝而不是重新解析文件。TriLib每次从文件重新解析都会新建一份资源,相同模型加载十次就是十份资源,缓存能省下大量内存。
还有一个容易被忽略的点:加载时避免一次处理太多模型。用户连续快速点击加载多个模型时,建议做任务队列,一次只允许一个模型在解析,解析完成再加下一个。TriLib底层虽然有多线程处理,但JSON解析和资源生成阶段依然吃主线程,同时处理多个大模型会造成明显卡顿。
5.4 TriLib版本与Unity版本兼容性
最后提一下版本匹配问题。TriLib有几个大版本,老版本对Unity 2020以下支持更好,新版本在Unity 2021以上兼容性更稳。如果你在导入插件后脚本报编译错误,大概率是程序集引用没配置好。检查一下Project Settings里的Assembly Definition References,找到TriLib的三个ASMDEF文件,把引用加到自己脚本所在的程序集里,编译错误基本就能消失。
另外,TriLib有些特性依赖Unity的Job System和Burst编译器,如果工程把这俩模块裁剪了,加载性能会明显下降。所以Player Settings里的Scripting Backend推荐选择IL2CPP,并在Installing Packages时保留Job System相关依赖。
写在最后
聊到这儿,我想把TriLib运行时加载模型做得比较顺的方法总结成一句话:先搞清楚模型来源和格式范围,再针对性地调加载选项,最后把卸载和缓存设计好。这个顺序不能反过来,否则就是加载一时爽、优化火葬场。
分享一个我个人习惯的小技巧:给TriLib的加载回调写日志时,不要只记录OnModelLoaded和OnModelError,把OnModelProgress的进度也打出来。模型解析阶段偶尔会有假死现象,如果你的UI层能看到进度还在走,心里就不慌,调试时也能区分是“加载中”还是“真卡死”。
后面我打算在这个工程基础上扩展两个功能:一是把模型缩略图预览加上,文件选择之前先在UI里看到模型长什么样;二是接入网络下载加载,让模型文件可以直接从服务器拉取。到时候再写一篇详细的扩展教程。
本文还有配套的精品资源,点击获取