# 三维模型前端加载优化及存储
目标:在web浏览器中快速加载模型并流畅使用
# 模型加载问题
问题描述:Threejs开发项目加载的模型,可能会比较大;模型三角形面数越多,一方面是threejs渲染模型的性能下降,另一方面是加载模型的时间比较长,影响体验。因为三维模型文件往往比较大,所以Web3D项目相比较普通的前端web项目,需要加载文件体积比较大,需要花费的时间自然比较多,加载时间比较长的情下,对于用户来说体验肯定不太好。

# 解决方案
# 模型
# 模型精简
模型和贴图数据是整个加载过程中最为“重量级”的数据。因此,我们应当从建模阶段就定好规范,在保证外观效果的前提下,尽量使用较为精简(面数较少)的模型。一般3D美术导出模型的时候,会进行减面操作,并导出模型的法线贴图,比如你只是加载一个机械零件模型(非批量),可以让3D美术进行减面然后导出法线贴图,这样的话在不影响曲面显示质量前提下,减少模型三角形面数,一方面可以降低模型文件大小提高网络传输性能,另一方面可以提高threejs渲染模型的渲染性能。
# 使用二进制格式
使用不同文件格式,文件的大小会有所不同,如果同一个模型,导出二进制.fbx大小要比文本格式的.obj文件要小1~2倍。常见的GLTF格式可以是文本格式,也可以使二进制格式,为了更好的传输性能可以选择二进制格式,.glTF打包转化为.glb二进制文件。
# 非二进制模型压缩
对于一些纯字符编码的模型,如 obj,dae 等,在服务端开启 gzip 压缩,可以带来较好的压缩比。
# 图片压缩
很多时候贴图文件往往大于模型。贴图尺寸也应该根据需要选取,不应该过大,一般最好不要超过 4k,保持 1024 或者 2048 较好。贴图也最好使用 2 的 n 次幂的尺寸。
对于常见的贴图一般使用 jpg、png、tga 等格式,但是这部分格式加载完毕后,png/jpg 仍需要全部转码为纹理(texture)才能开始渲染,而具有相同尺寸的贴图纹理 GPU 占用内存大小相同,故压缩后的 png/jpg 对于渲染过程并没有优化。庆幸的是许多设备都有可直接用于渲染的 GPU 压缩纹理(compress texture)格式,压缩纹理可比由 png 直接转换的纹理减少 5 倍或以上的大小。如果直接提供压缩纹理格式,则不需要进行 png 的转码过程且可大大减少纹理内存。但由于 GPU 芯片提供商太多,设备的压缩纹理格式多种多样(例如安卓设备常用格式是 ETC1/ETC2,苹果设备是 PVRTC…)
2019 五月份,Binomial 公司和 google 联合推出了 Basis Universal 压缩 GPU 纹理技术,Basis Universal 支持多种常用的压缩纹理格式,将 png 转换为 basis 文件后,大小与 jpg 格式差不多,但在 GPU 上比 png/jpg 小 6-8 倍。在保持 GPU 性能效率的同时,提升 Web、桌面端和移动应用程序中图像传输的性能。此版本填补了图形压缩生态系统中的一个关键技术空白,同时也补充了 Draco 几何压缩的部分早期工作。


# 前端
# 加载进度条
因为Web3D项目加载时间相对普通web页面时间比较长,如果用户一直等着,web页面没有什么反应,可能会关掉,这种情况下,可以在web页面放置一个进度条实时显示模型文件加载进度。
如何获得三维模型文件的加载进度可以查看threejs文档关于FileLoader类的介绍,至于web进度条,可以通过普通HTML和CSS代码去实现,然后和threejs加载进度数据进行绑定即可。
# 动态分批加载
如果一个场景中,有多个网格模型模型,比如室内设计效果展示,里面有沙发、椅子、电视等三维模型,这时候把这些模型分别单独建立一个文件,threejs可以按照一定的顺序分别先后加载这些单独的网格模型文件,然后插入到场景中。这样的话,用户可以以最快速度查看到场景中的部分模型,不用一直等待,没有什么反应,用户体验更好。
# 分包流式加载
对于大的场景,特别是 BIM 或者 GIS 场景,如果把所有模型全部加装完成了再展示,可能会让用户等待时间过久。所以,可以采取分包流式加载的方式,每个包加载一部分的模型,加载解析完成后即丢到渲染主线程中进行渲染。这种方式大家之前经常看到的是百度地图的数据的分片加载。
想要达到这样的效果,一般会使用 worker 进行数据的加载和解析处理,这样保证了加载的线程和主线程是隔离的,不会产生加载解析模型过程中产生的 UI 卡顿。同时,如果我们想要达到并行加载和解析的效果,可以开多个 worker 进行多线程的同时加载解析工作。
# 使用LOD
LOD(Level of Details)技术指的是将场景中的模型按不同精度分为 N 套,按照模型与相机的距离远近,动态切换模型的精度,距离相机较近的模型采用精细模型展示,而距离相机较远的模型使用较为粗糙的模型进行展示。对于大的场景,使用 LOD 技术也是一个有效提升帧率的手段,可以有效减少整个场景中的渲染三角面数。
使用 LOD 技术一个非常重要的问题是如何生成多套不同精度的模型。一般来说有一下几个方案:
一是在建模软件中,使用减面工具,直接生成多套模型,这部分一般是美术建模人员来完成。虽然程序少了很多事儿,但是通用性较差,自动化程度较低。
二是选用开源的减面方案进行程序减面。一般来说可以选取一些 QEM 算法减面,使用程序进行减面。但是一般这类算法都有一些局限,比如贴图问题,破面问题。需要程序不断调试,找到比较合适的参数。
https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification
三就是使用商业的 sdk 进行减面。一般常用的是 simplygon、instlod 等都是比较成熟的商用减面工具,也广泛的使用在游戏等行业。
另外,很多游戏引擎也自带一些减面工具,比如 u3d、ue4 等都支持 LOD 减面。
three.js LOD参考:
https://threejs.org/docs/#api/en/objects/LOD
http://www.yanhuangxueyuan.com/doc/Three.js/Lod.html
# 使用缓存
优化完了首次加载的耗时操作,我们可以继续优化加载,采用各种缓存技术是也常见的加载优化方法,其中包括使用 cdn 文件服务,浏览器文件缓存、indexeddb 前端缓存等。
# 使用 indexedDB
因为模型数据变化其实不大,所以再首次加载的过程中,我们可以使用 indexedDB 做前端的数据缓存。indexedDB 具有良好的查询性能,超大的存储空间(理论上磁盘剩余空间的一半左右),以及支持二进制存储等优良特性,比较适合做前端模型以及贴图数据的缓存。
在使用了 indexeddb 做前端缓存之后,可以做到首次加载之后,对大场景的数据进行秒级加载的超高性能 ,大大提升了用户频繁打开大场景的操作体验。
因此,有些 Webgl 引擎(例如 Babylon.js )已经在引擎级别对 indexedDB 缓存做了支持。开发者只需要简单配置,即可打开缓存,优化加载体验。
# 渲染帧率优化
在加载优化完成之后,我们继续聊聊如何进行大场景的渲染优化。如果主要性能瓶颈在 GPU 渲染部分,那么我们就需要仔细看看到底是什么造成的渲染瓶颈。我们可从以下方案入手,看看是否已经使用相关优化手段。总的来说,目的是尽量减少 drawcall(opengl state 切换带来的性能损耗),减少向 GPU 提交的数据量(带宽压力)。
# 各种剔除 Culling
为了达到尽量减少 DrawCall 的目的,应当尽量的做好剔除工作,将最少的数据提交给 GPU 进行渲染。一般常见的剔除方式有视椎体剔除,背面剔除、遮挡剔除等。
# 视椎体剔除(Frustum Culling)
一般是指只有在视椎体内的物体才能被渲染出来,不在视椎体内的物体将被剔除不作渲染。这也比较符合我们一般的视觉逻辑,不在可见范围内的物体,渲染了也看不见,纯粹属于性能浪费。这部分的剔除,大部分的引擎都已经自带了,而且都是默认开启的。如果需要自己实现,算法也比较简单,一般是遍历视椎体的 6 个面,算出物体的中心到面的最小距离(带正负方向的)与包围球的半径做比较,如果小于半径,就表示在外面。

# 背面渲染剔除(Backface Culling)
一般来讲,渲染引擎大多会开启背面剔除。原生 webgl 中使用 gl.enable(gl.CULL_FACE); 来开启背面剔除。在 threejs 中可以使用 material 的 side 属性指定 front 进行单面渲染。
# 遮挡剔除(Occlusion Culling)
遮挡剔除是指在相机剔除后,在视野范围内仍然有许多物体直接有遮挡关系的,不需要进行渲染,虽然 gpu 有深度测试,会将有遮挡的物体进行剔除,但是我们仍然希望在提交 GPU 之前对遮挡关系进行判断,提前剔除掉一些东西,减少渲染压力。
webgl2 OcclusionQuery 遮挡查询
https://github.com/tsherif/webgl2examples/blob/master/occlusion.html (opens new window)

# 合批 batch
除了剔除以外,合批次提交也能提高渲染性能,原因也非常的简单,就是合批后提交 GPU 的 DrawCall 减少了。对于合批,我们应该遵循以下原则。
首先是材质相同的进行 batch,材质不同的无法进行 batch。一个 batch 其实就是一个 drawcall,对应的其实一种材质,不同种材质效果需要使用不同的 shader 实现所以无法实现合批展示。
其次 batch 的定点格式限制问题。因为 webgl 的 index 数据使用的 short 类型,所以最好不要超过 65535 个顶点索引。但是,由于 webgl 的扩展 OES_element_index_uint 已经有了非常良好的兼容性(99%兼容),所以其实你已经可以使用超过 65535 个顶点 index。
stackoverflow 关于顶点限制的讨论: https://stackoverflow.com/questions/4998278/is-there-a-limit-of-vertices-in-webgl (opens new window)
如果你使用的 threejs,可以选择使用 geometry 的 merge 方法在前端进行合并 batch,或者手工进行顶点 loop 循环合并顶点,其实原理差不多。
# instance
instance 实例化其实也是渲染优化中常用的技术,特别适合那些外观一致大量重复的渲染。比如小树组成的森林,一个发布会场景中的大量椅子等等。在 instance 渲染的时候,我们不需要传入大量的顶点数据(只需要传入每个 instance 的 matrix 数据),而是共享一份顶点数据,这样可以大大降低显存的使用率,降低显存带宽。
在 webgl 中我们使用的是ANGLE_instanced_arrays扩展来实现 instance 渲染。但是值得注意的是,instance 的使用所带来一些额外处理,比如单个物体的选择操作等问题。
# 内存管理优化
对于重型 webgl 应用,特别是 BIM、GIS 场景,有时候,我们需要加载多个大型模型,例如 cesium 还需要能够支持整个地球数据的加载。这就需要我们对内存有着较好的控制。
在不适用对象的时候,及时销毁对象,释放 JS 内存。同时对于模型数据,大量的顶点、法线等等 buffer 数据也是非常占用内存的。可以在 js 推送完数据之后,将这部分数据从内存中释放掉,从而降低 JS 的内存压力。对于 v8 引擎来说 32 位的 JS heap 最多能到 3.8G 左右,如果不及时释放内存,很容易内存爆掉,导致浏览器 crash。
# 交互操作优化
除了加载,渲染等方面,普通的交互操作在大场景的条件下,也容易带来非常大的挑战。交互操作中一个常见的问题就是模型拾取。一般普通的三维场景中,常用的是射线拾取的方式,遍历场景中的模型,进行模型的相交测试。但是由于场景非常大,可能会导致整个遍历非常耗时。所以要对拾取进行优化。常见的优化方式有使用 GPU 拾取、以及使用八叉树进行加速遍历等方式。
gpu 拾取 一般做法是给每个 mesh 一种颜色 然后渲染绘制一遍,在鼠标点所在的位置调用 readPixel 读取像素颜色,根据颜色与模型的对应关系,反推当前拾取到的颜色对应的 mesh。
八叉树优化 一般是使用八叉树的数据结构,将整个场景中的模型放入八叉树的不同 cell 中,由于八叉树类似空间范围内的二分查找,所以能够非常迅速将查找范围落在最终需要遍历的模型上。从而达到加速模型场景遍历的目的。对于八叉树的实现网上有很多的版本,一般使用的是稀松八叉树。
前端优化参考文档:
https://zhuanlan.zhihu.com/p/154425898
https://www.cnblogs.com/liaocheng/p/9512817.html
# 后端
后端主要是为了给前端提供模型和数据,以及对大量模型数据和GIS数据进行组织和存储。
# 使用 CDN
对于加载来说,除了文件本身大小因素,我们不得不考虑的一点就是文件所放置的服务器带宽问题。如果模型场景都放在一台服务器上,加载过程中必定会对服务器的带宽带来一定压力。可以使用 cdn 做静态资源的文件服务。(使用NGINX开启静态文件gzip压缩)
# 文件存储
针对单个文件很大的模型数据可以使用Minio、HDFS进行分布式存储,提高文件的访问速度。
针对模型文件不大,但数量多的情况,可以使用mongodb的gridFS进行分布式文件存储,比如三维模型的切片文件(3dtitles)。
MongoDB的文件存储方式可看成一个大的哈希表,其中文件名作为键,文件内容作为值被保存。它可将一个大文件分割成多个较小的文件。当BIM+GIS集成过程中形成的各种基础信息存储到MongoDB数据库后会被分割成许多块,每块仅保存一个主题空间的数据。这为有效保存较大的文件对象提供可能,如遥感影像、全景地图和视频等。
MongoDB数据库可管理百亿级的三维切片数据,并具有高并发能力,响应时间达到秒级,适用于大规模数字城市三维场景的应用。将BIM+GIS集成数据存储在MongoDB中,可实现三维切片数据的分布式存储,为智慧城市基础大数据的管理提供技术支撑。
shapeFile文件等矢量GIS数据使用PostgreSQL+PostGIS进行存储。
# 使用网关
使用微服务网关,进行负载均衡,提高前端数据接口访问的并发量。
其它参考:
https://mp.weixin.qq.com/s/Qexw7HOl1JpCFDbjPgSfqQ
https://mp.weixin.qq.com/s/iM2XPnOrOstbGrKWnKRHcQ
# three.js案例总结
# 太阳光影变化
https://threejs.org/examples/?q=sun#webgl_shaders_sky