1. 项目缘起:一个让人头疼的BIM大文件问题
干工程建筑的同行应该都遇到过这种场景:项目上BIM模型动辄几个GB,Revit导出的模型文件、Navisworks缓存、无人机倾斜摄影点云,稍微精细一点的模型直接奔着10GB去了。以前的做法是让同事用U盘拷,或者通过文件服务器FTP下载,但在国企工程公司的局域网环境里,这事儿没那么简单。
我们单位最近做了一套基于Web的工程管理系统,其中一个核心功能是在线预览BIM模型。给领导汇报的时候,模型加载体验直接决定了这个项目能不能顺利推进。最开始大家想着局域网带宽大,直接把整个模型文件一次性下发不就行了?实测下来发现完全不是那么回事——模型文件一多,并发请求一上来,交换机扛不住,其他业务系统也跟着卡。更重要的是,浏览器在加载大文件时内存占用飙升,用户的普通办公电脑根本撑不住。
后来我琢磨出一条出路:把文件按照目录结构做分片传输。我在这个项目里基于JS实现了BIM模型大文件的目录结构分片传输方案,实测下来效果不错,解决了文件传输慢、内存占用高、预览卡顿这几个核心痛点。这篇文章把整个思路、实现细节和踩过的坑完整记录下来,给同样在国企工程单位做Web应用的兄弟参考。
这个方案的适用场景很明确:局域网环境下的BIM模型在线预览和协同管理,尤其是那种模型文件动辄几个GB、需要多人同时访问的场景。它解决的核心问题是:在不改变用户操作习惯、不用安装客户端的的前提下,让浏览器能够快速、稳定地加载和预览超大BIM文件。适合做工程信息化、智慧工地平台、BIM协同管理系统的前后端开发人员参考。
2. 整体设计与技术选型背后的思考
2.1 为什么必须做分片传输,一次性传输到底输在哪
先说结论:不做分片,在大模型场景下几乎必挂。
我们最初的原型版本用的是最传统的思路:前端发一个请求,后端把整个BIM模型文件读出来,通过HTTP响应直接传给前端,前端拿到ArrayBuffer后交给模型解析程序处理。局域网千兆网络下,一个1GB的文件理论传输时间也就10秒上下,听起来很快对吧?
但在真实场景里,问题一个接一个。首先,后端一次性读取整个文件到内存,Java或Node服务进程内存直接飙升几个GB,我们测试时服务端内存占用到80%以上,整个服务器上的其他业务应用都跟着遭殃。其次,前端接收完整个文件后,需要一次性把数据交给WebGL或Three.js解析,浏览器的堆内存直接爆掉,Chrome标签页直接变白屏。最后,多人并发访问时,服务器同时处理多个大文件读取,磁盘IO被打满,传输速度从千兆降到几十兆。
这些问题的根源在于:整个流程假设内存无限大、带宽无限快,但实际上公务电脑的配置、服务器的内存、网络设备的能力都是有上线的。分片传输就是把一次性的、大规模的资源占用,拆成无数次小的、可控的资源占用,用时间换空间,让系统在最普通的硬件条件下也能稳定运行。
2.2 目录结构分片的核心思想
2.2 目录结构分片的核心思想
“分片传输”这个词在网盘、视频上传领域已经很常见了,但BIM模型的分片思路有些不同。我们平时把文件切成1MB或2MB的小块按顺序传输,这是最简单粗暴的切法。但对于BIM模型来说,直接按等大小切分有几个问题:
第一,BIM模型文件往往是复合结构。IFC格式本身有头部区、数据区、映射区;Revit导出的模型包含各种构件族、材质、纹理贴图;倾斜摄影模型是一大批瓦片目录,几百个文件夹,每个文件夹里几十个文件。这种异构数据结构如果被等大小硬切,接收端要完成重组、拉平、再解析的整个过程,中间很容易出现数据断裂。
第二,用户实际需要的东西往往不是整个文件。比如领导只是想看第3号楼的BIM模型,如果做目录级的分片,前端只需要请求3号楼对应的目录树和其中的几何数据,其他楼栋的文件不传输、不加载,加载速度和内存占用都能提升一个量级。
所以我的方案不是把文件当做一个大的二进制流去切,而是先解析出文件的目录结构,形成一个目录树清单,然后以目录节点(或者目录下的单个文件)为传输单位,前端按需请求,后端按目录读取和返回数据。本质上是从“把一个整体文件切开传”变成了“把文件按逻辑结构拆成独立单元分发”。
2.3 技术栈权衡:为什么选了JavaScript而不是原生方案
是不是一定要用JS?当然不是。如果允许安装客户端,用C++或C#写一个专门的模型传输工具,性能肯定更好。但国企工程公司的现实条件限制了很多技术选型:
一是桌面终端管控严格,不能随意安装第三方软件。我们单位所有办公电脑都装了统一的终端安全管理系统,安装新软件要走审批流程,周期长,而且业务科室的电脑不允许装跟业务无关的东西。Web应用是唯一不需要安装、即开即用的形态。
二是Web端技术生态相对成熟。Three.js配合IFC.js、xeokit等开源库,已经能做比较完整的BIM模型解析和渲染。模型数据通过JS在浏览器端解析,用WebGL渲染,这套链路跑通后维护成本低,业务人员不需要接受额外培训。
三是局域网环境的特殊性。既然内网带宽大、延迟低,那就没必要做复杂的压缩传输、断点续传协议,简化的分片方案就够用,不需要引入复杂的P2P协议(比如WebRTC DataChannel),避免增加维护复杂度。
最终定的技术路线是:前端用React + Three.js + xeokit,后端用Node.js + Express,文件系统直接挂在服务器共享磁盘上,通过一个单独的文件服务模块来提供目录结构和分片数据的接口。这个组合在国企的IT审批环境里最友好——全都是开源组件,不需要购买商业授权,部署也简单,一台普通的Windows服务器就能跑起来。
3. 核心细节解析与实操要点
3.1 目录结构解析与索引构建
分片传输的第一步,不是切文件,而是先把BIM模型的目录结构摸清楚。这一步做得好不好,直接决定后续所有逻辑的复杂度。
如果你拿到的是一个以文件夹形式组织的模型(比如ContextCapture生成的倾斜摄影瓦片目录),目录结构天然就存在,直接在服务器端递归遍历目录树生成JSON索引即可。但大部分BIM协作场景下,你拿到的是单个的IFC或RVT文件,这种文件的内部本身就是一种结构化的数据封装,这时单纯的分片思路不同。
我采用的方案是:如果是单文件模型,则预先用解析工具把文件拆解成独立的数据块文件,保存到服务器上一个预处理的目录中。每个数据块对应一个目录节点,目录结构就成了逻辑上的树形结构。举个例子,一个IFC文件可以拆解为:
- 根目录
- header.json(文件头部元数据)
- geometry(几何数据)
- building1.json
- building2.json
- property(属性数据)
- element-001.json
- element-002.json
- texture(纹理贴图)
- materials.json
- images/
这样处理后,每个独立的块都是一个可单独传输的最小单元。前端需要哪个块,就请求哪个块。这个预处理过程只需要做一次,之后每次访问直接复用索引文件即可。
目录索引的信息结构建议这样设计:
{ "modelId": "building-3", "version": "2.3.1", "totalBlocks": 245, "totalSize": 3542177280, "blockList": [ { "id": "root-header", "path": "header.json", "size": 24576, "type": "meta" }, { "id": "geo-building-1", "path": "geometry/building1.json", "size": 10240000, "type": "geometry", "dependencies": ["root-header", "prop-element-001"] }, { "id": "tex-main", "path": "texture/images/main.jpg", "size": 204800, "type": "image", "compress": true } ] }在实际操作中,预处理时间也需要在预期内。我们有一栋约40万平方米的公建项目全套BIM模型,拆解出300多个数据块,处理时间大概10分钟。这个预处理建议做成后台任务,模型上传后自动触发,处理完成后在数据库中标记状态,前端检测到可用模型后再显示预览入口。
3.2 前端分片调度与并发控制
前端拿到目录结构后,怎么按顺序请求这些数据块,是一个很容易被忽视但影响很大的环节。
最笨的方式是一个一个顺序加载,优点是稳定,但问题是慢。300个数据块,每个数据块100ms请求时间,串行加载就是30秒,这还没算解析的时间。如果一股脑儿全部并发请求,浏览器同时发起几百个请求,后端和网络设备压力大不说,浏览器本身也会因为连接数限制出现问题——Chrome对同一域名的并发连接数默认是6个,超过的请求会被排队,这不叫并发,叫自欺欺人。
所以我在前端写了一个分片调度器,核心逻辑是三个部分:
第一,轨道数控制。维护一个并发池,同时最多发起4个请求。这个数字不是随便拍的——经过实测,在千兆局域网环境下,4个并发请求可以让单个请求速度处于最优和CPU占用合理之间,同时给浏览器其他页面的请求留有余量。
第二,优先级排序。目录树中有依赖关系,比如渲染一个楼栋的几何模型时需要先有构件ID列表,就得让头文件和高优先级数据块先下载。调度器根据数据块的依赖信息和用户当前视角(通过相机视野范围判断需要加载哪些区域),动态调整请求队列。视角内的模型数据块优先于视野外数据块。
第三,失败重试与超时控制。局域网相对稳定,但服务器偶尔会有磁盘繁忙或网络闪断,分片请求失败率不是零。我的策略是:单个分片请求超时时间设为10秒,连续失败3次后,把这个数据块移到队尾,等最后再尝试。如果同一个分片重试超过3次,就弹窗提示用户刷新页面。这个逻辑保证了即便服务器繁忙,传输仍然有兜底机制。
class ChunkScheduler { constructor({ concurrency = 4, timeout = 10000, retry = 3 } = {}) { this.concurrency = concurrency; this.timeout = timeout; this.retry = retry; this.queue = []; this.activeCount = 0; this.failedMap = new Map(); } addChunk(chunk) { // 根据优先级插入队列(优先级高的放前面) const index = this.queue.findIndex(item => item.priority < chunk.priority); if (index === -1) { this.queue.push(chunk); } else { this.queue.splice(index, 0, chunk); } this.processQueue(); } async processQueue() { while (this.activeCount < this.concurrency && this.queue.length > 0) { const chunk = this.queue.shift(); this.activeCount++; this.fetchChunk(chunk) .finally(() => { this.activeCount--; this.processQueue(); }); } } async fetchChunk(chunk) { const task = fetch(`/api/model/chunk?modelId=${chunk.modelId}&blockId=${chunk.id}`, { signal: AbortSignal.timeout(this.timeout) }).then(res => { if (!res.ok) throw new Error('chunk fetch failed'); return res.arrayBuffer(); }); try { const buffer = await task; this.onChunkLoaded(chunk, buffer); this.failedMap.delete(chunk.id); } catch (e) { const failCount = (this.failedMap.get(chunk.id) || 0) + 1; if (failCount >= this.retry) { this.onChunkFailed(chunk, e); } else { this.failedMap.set(chunk.id, failCount); this.addChunk(chunk); // 重新入队,降低优先级 } } } }注意:调度器只是负责传输,数据的解析和渲染需要等数据块全部到位。所以我在调度器回调里维护一个map,存已加载的块,每一层的依赖项都到位后,才触发渲染层的加载事件。
3.3 服务端读取与流式返回
3.3 服务端读取与流式返回
前端调度器写好之后,服务端接口其实是相对简单的一块。但简单归简单,仍然有一些关键点值得单独说说。
后端接口只需要做两件事:一是根据modelId返回目录索引,二是根据blockId读取对应的数据块文件并返回响应。数据块文件是预处理阶段生成在服务器磁盘上的,接口按路径找到文件后通过流方式返回。
这里需要特别注意:所有文件读取必须用流式,而不能一次性读入内存。Node.js的fs.readFile会把整个文件加载进内存,如果多个人同时请求大纹理图片,服务器内存很快见底。用createReadStream创建可读流,pipe到HTTP响应,Node底层会处理好背压(backpressure)问题,根据网络状态自动调节读取速度。实测下来,用流式读取比直接readFile在同一台低配服务器上内存占用降低了约70%。
另外,建议在响应头上加上Cache-Control头。分片数据块是不可变的,一旦模型生成后就不会再改动,所以可以放心让浏览器缓存。
app.get('/api/model/chunk', (req, res) => { const { modelId, blockId } = req.query; const chunkPath = getChunkPath(modelId, blockId); if (!chunkPath || !fs.existsSync(chunkPath)) { return res.status(404).json({ error: 'chunk not found' }); } // 先发送ETag用于缓存校验 const stat = fs.statSync(chunkPath); const etag = getETag(modelId, blockId, stat.mtimeMs); if (req.headers['if-none-match'] === etag) { return res.status(304).end(); } res.setHeader('ETag', etag); res.setHeader('Cache-Control', 'public, max-age=31536000'); res.setHeader('Content-Type', getContentTypeByPath(chunkPath)); res.setHeader('Content-Length', stat.size); const stream = fs.createReadStream(chunkPath); stream.on('error', () => { if (!res.headersSent) { res.status(500).json({ error: 'read error' }); } else { res.end(); } }); stream.pipe(res); });注意:接口必须校验请求来源,因为局域网部署的Web应用很容易被其他页面直接请求接口。我做法是让网关统一加一个内部Vary头的校验,非网关请求一律杀掉。
4. 实操过程与核心环节实现
4.1 预处理服务的完整实现
这一节把整个流程串起来,从模型上传到可以预览,你到底要经过哪些步骤。
第一步:上传模型。用户在Web管理界面上传BIM模型文件,这里先不做特殊处理,文件保存在临时目录,同时往数据库写入一条记录,状态为“待处理”。
第二步:调用预处理服务。后端收到上传完成事件后,把任务丢进队列。预处理服务是一段独立的Node进程,它负责拆解模型。具体拆解方式因文件类型而异:
对于IFC文件,我会用web-ifc库读取其内部的实体数据,把实体(Elements)、几何(Geometry)、属性(Property)分别抽取出来,存成若干JSON文件。抽取时可以按楼层、按单体建筑、按构件类型分组,这样目录树直接和业务语义挂钩。比如一个学校模型,目录树可以做成:
model/ ├── meta.json // 模型全局元数据:坐标系、单位、版本 ├── building-a/ │ ├── meta.json │ ├── floors/ │ │ ├── f1.geom.json // 一层几何数据 │ │ ├── f2.geom.json │ │ └── f1.prop.json // 一层属性数据 │ └── f2.prop.json ├── building-b/ │ └── ... └── assets/ ├── texture-1.jpg └── material.json对于倾斜摄影模型(Tile模型),本身已经是瓦片目录结构,预处理就简单得多。只需要扫描目录,把每个瓦片文件的信息记录到索引JSON中,并把瓦片之间的父子依赖关系计算出来。这一步不需要复制或移动文件,索引直接指向原始瓦片文件的路径即可。
第三步:生成索引并持久化。预处理完成后,把索引JSON写入数据库或直接存为文件。我推荐直接存为JSON文件,因为后面接口读取这个文件比查数据库更快,成本也更低。
第四步:通知前端刷新状态。前端轮询模型状态接口,发现状态变为“可用”,就展示“在线预览”按钮。
我实际开发中遇到一个坑:预处理服务如果直接在Web应用进程里做,模型文件大的时候会阻塞其他业务的请求。后来我把预处理做成了一个独立的进程(通过child_process.fork启动),让它从头到尾自己跑,跑完写一个flag文件,Web应用看到flag文件存在就判断为处理完成。这个方案简单且不影响主进程的响应性能。
4.2 前端加载时序与渲染条
4.2 前端加载时序与渲染调优
数据块到位后,剩下最重要的事就是如何高效解析并驱动渲染引擎把模型画出来。
我用的渲染库是Three.js,配合IFC.js来解析几何数据。加载的节奏需要和渲染进度紧密配合,不能等所有分片都下载完才开始渲染,那样首次出图时间会很长。我的方式是采用“逐块处理、边下载边渲染”的策略。
具体展开说:当前端拿到一个几何数据块后,不急着直接丢给Three.js解析,而是先放入一个解析队列。解析队列做得比较保守——一次同时解析一个块。因为Web Worker虽然能减少对主线程的阻塞,但硬件条件差的办公电脑上,过度使用Worker反而容易把CPU吃光。实测下来,单线程逐个解析,在渲染过程中保持每一帧不掉帧到15FPS以下,是可以接受的。
每个几何块解析完成之后,我把它添加到一个的Object3D节点下,然后把这个节点挂到场景图中。Three.js对于新增对象的处理是自动的,它会触发一次渲染循环,不需要人工干预。但有一点需要特别说明:频繁新增节点会导致渲染开销波动,如果一口气挂上去几十个几何体,帧率会突然掉到个位数。为了平滑过渡,我做了个“每帧最多挂两个节点”的限制,让渲染速率始终可控。
另外,内存复用很重要。Three.js里的BufferGeometry在模型被移除或层级切换时,不会自动释放GPU显存。尤其是拼合的几何体,每个构件都有自己的一份顶点数据缓存,长时间切换楼层浏览会导致显存持续上涨。我在楼层切换时用一个显存回收函数,把当前楼层所有geometry的dispose和material的dispose全部调用一遍,再创建新楼层的模型。
function disposeModelGroup(group) { group.traverse((child) => { if (child.isMesh) { child.geometry?.dispose(); if (Array.isArray(child.material)) { child.material.forEach(m => m.dispose()); } else { child.material?.dispose(); } } }); scene.remove(group); renderer.renderLists.dispose(); }4.3 关键参数的计算与确认
这里把我在项目中用到的几个关键参数列一下,并解释它们的计算口径。
第一个参数是并发数。我最初设置的是6(跟随浏览器限制上限),但在压力测试中发现,当模型数据块中存在大量小文件时(比如纹理图片,每个只有几十KB),4个并发和6个并发的总耗时差距很小,但6个并发下服务器CPU占用率明显高出一截。后来我把并发数调整为4,同时在调度器中加入动态可调参数,通过query参数可以临时调高到6或8做紧急加速。
第二个参数是数据块大小。我自己定的基准是单个数据块不超过10MB,尽量维持在2到5MB。这个区间很平衡:块太大,单个请求耗时变长,进度条不流畅;块太小,请求数量增加,HTTP握手开销和响应头开销占比上升。对于IFC几何数据,按楼栋或区域划分后,单块通常在1到8MB之间,符合预期。
第三个参数是超时时间。单块10秒超时,这是根据局域网最慢盘(机械硬盘)大文件的顺序读取速度估算的。一块5MB文件,从磁盘读出来到网卡发完,在最差的情况下也就2秒左右;给10秒的余量已经算是非常大的缓冲了。如果超过10秒还没有返回,大概率是服务端卡死或者网络设备出了问题,这时候重试的意义不大,应该做整请求失败处理。
第四个参数是缓存策略。前面提到ETag + Cache-Control一年。这里要强调一点:BIM模型如果发生变更,版本号必须跟着变。我在模型ID上拼上版本号,比如building-3-v2,这样新版本文件的URL不会命中旧缓存,同时旧版本的缓存也不用主动清理,自然过期。
关于参数的选择,最靠谱的方式还是实测:搭一个测试环境,用你们单位模型库里的代表模型(体积、块数、文件类型要接近真实情况),分别在并发4、6、8、12下跑一遍加载流程,记录总耗时、服务器CPU峰值、失败率。拿数据说话,比任何经验公式都靠谱。
5. 常见问题与排查技巧实录
5.1 加载进度卡在99%不动了
这个问题我在开发阶段遇到得最多。现象是模型加载进度条走到99%后迟迟不完成,控制台没有任何报错,网络面板显示一些请求状态为pending。
排查思路从服务端日志开始。因为这个现象的本质是“少了一个或几个分片没有加载成功”。我写了一个诊断接口,可以根据模型ID和会话标识返回该模型所有分片的加载状态,是已加载、加载中、还是未请求。通过对比日志里记录的已加载分片列表和索引JSON里的全部分片列表,很快就能找到缺失的分片ID。
缺失的原因一般有两种:一种是调度器按依赖顺序加载,某个分片的重试次数用尽后标记了失败,但渲染层没有感知到,进度条的完成条件只统计了已加载成功数,没有统计失败数。另一种是服务端某次读取文件时发生超时,请求已经断开,但客户端调度器误认为成功了(这个坑主要是响应头中Content-Length大文件读取时偶发异常导致的)。
解决方法是双管齐下:渲染层监听“所有依赖分片全部就绪”事件来触发出图,而不是“进度数值到100”触发;调度器对已标记失败的分片提供一次“强制重新加载”操作,用户点击重试按钮后重新发起请求。
5.2 服务端内存上涨后不再回落
压测时发现的另一个典型问题:虽然使用流式读取后内存峰值大幅下降,但在高并发场景下,Node进程的内存呈阶梯式上涨,最终稳定在高位。
查了堆快照之后,发现主要占内存的是一些不再需要的大字符串——Express的response对象在数据发送完后没有及时被GC回收。虽然后端代码里限制了并发数,但HTTP Keep-Alive连接长时间挂着,老旧的response对象无法被释放。
解决方案是给响应加上writehead、end回调的清理逻辑,同时周期性重启问题严重的Node进程。不过最有效的方法是减少不必要的文件元数据处理,直接在分片接口中省掉stat调用,因为预处理阶段已经把size记录到了索引JSON里,接口直接查索引即可,不需要实时读取文件系统属性。
5.3 浏览器段时间无操作后模型灰了
这是WebGL应用常见的坑,不只是分片方案特有的。浏览器标签页在后台长时间挂起后,GPU上下文会丢失,回到前台时所有模型mesh都会消失变成灰色。
解决办法是监听WebGL的contextlost和contextrestored事件。更为彻底的办法是:页面进入后台时主动暂停渲染循环并把场景图序列化保存,回到前台时将场景重新构建。但序列化BIM场景的数据量太大,不现实。我用了一个折中方案:保存当前视角(相机位置、目标点),回到前台检测到contextlost后调用renderer.forceContextRestore,并重新创建渲染器绑定所有场景对象。
这个问题在设计阶段就要留有预案,特别是那种把模型浏览器挂在后台、过半小时切回来看的办公场景,很常见。
5.4 局域网多用户同时加载,服务端响应变慢
最后说一个发生在真实项目环境的案例。某天项目组同事反馈,早上九点上班时集体打开模型,大家都加载得很慢,有些甚至直接失败。
排查时先看了服务端日志:节点进程没有卡死,但磁盘读取延迟明显增大,单块响应从几十毫秒飙到2秒以上。进一步排查磁盘,发现模型预处理后的数据块和公司其他文件共享存储在同一个服务器磁盘上,早高峰时其他人对该磁盘的读写冲突严重。
解决思路有两个层面:一是在存储上把模型数据单独放到一块独立磁盘(最好是固态硬盘),避免和其他业务抢IO;二是对分片接口做限流,服务端通过一个简单的令牌桶,限制同一时刻最多处理10个数据块读取请求,多出来的请求直接返回503,客户端收到503后把任务放入延迟队列重试。这个方案虽然在一定程度上增加了请求次数,但保证每个请求都能在1秒内完成,整体体验反而更稳定。
避坑提示:千万别把数据块直接放在系统盘C盘上,也不要把模型数据和数据库文件放同一块盘,否则IO瓶颈早晚会反咬一口。
6. 效果评估与经验沉淀
方案实施完成后,我在内部做了一轮完整的验证。选取了一个约2.8GB的复杂公建BIM模型,拆解后共有372个数据块。在千兆局域网条件下,首次浏览(无缓存)从点击预览到模型完整出图,耗时约42秒;二次浏览(完全命中缓存)大约8秒即可出图。相比之前一次性传输需要接近2分钟且浏览器容易白屏的表现,体验提升非常明显。
更关键的是稳定性。持续一周的内网多用户并发压测,20人同时在线浏览各栋楼模型,服务端内存峰值控制在1.5GB以内,没有出现一次服务崩溃或模型加载失败的情况。这个结果在国企工程公司现有的服务器配置(一台CPU 8核、32GB内存的通用服务器)上算是相当理想了。
最后总结几条个人经验:
- 分片方案最大的价值不在于快,而在于可控。它把不可预期的一次大任务变成了可预期、可监控、可重试的一堆小任务,这让后续的运维和优化都变得简单。
- 目录结构的语义化处理比分片本身更值得投入精力。分片粒度与业务语义对齐之后,加载策略(按楼层加载、按区域加载)都顺理成章地实现了。
- 前端调度器是整个系统的核心,但它在编码层面非常容易实现。真正难的是各种边界情况的判断:超时、重试、失败、依赖不满足,每一个边界情况都要考虑清楚,项目的可靠度取决于这些边角逻辑,而不是主流程。
- 不要把技术复杂化。有人建议我引入消息队列、Redis缓存来做分片状态管理,实际用下来,Node进程内的内存状态加一个简易的持久化日志完全够用,少依赖中间件,维护成本更低。
这套方案目前已经在我们多个工程项目的BIM模型管理中稳定运行。后续准备扩展的方向有两个:一是增加按视角动态加载的LOD策略,先传整体轮廓,相机拉近时再加载细节分片;二是将分片协议标准化,让不同模型平台的预处理工具都能输出同一格式的目录索引。这些方向都建立在现有的分片传输框架之上,骨架不出问题,上层加东西就只是时间问题了。