three.js使用Instanced Draw+Frustum Cull+LOD来渲染大场景(开源)
大家好,本文使用three.js实现了渲染大场景,在移动端也有较好的性能,并给出了代码,分析了关键点,感谢大家~
关键词:three.js、Instanced Draw、大场景、LOD、Frustum Cull、优化、Web3D、WebGL、开源
代码:Github
我正在承接Web3D数字孪生项目,具体介绍可看承接各种Web3D业务
加QQ群交流:106047770
需求描述
数字孪生项目通常需要渲染超大场景,比如智慧城市就需要渲染一片城市区域,甚至整个城市
渲染大场景所需要的技术点包括:
- Instanced Draw
一个Draw Call批量渲染物体,物体可以有不同的Transform、Color - Frustum Cull
剔除视椎体外的物体 - Occlusion Cull
剔除被遮挡的物体(WebGL1不支持) - LOD
根据物体到相机的距离,显示不同Level的物体。越远的越粗糙,越近的越细致 - GPU Driven Pipeline
把前几个优化都放到GPU中来做,并且物体可以有更多差异(需要WebGPU) - Space Partition
使用Octree、BVH、BSP等加速结构来划分场景,在Cull、碰撞检测、Ray Picking等操作时查询加速结构而不是遍历所有物体,从而提高性能 - Multi Thread Render
开一个渲染线程来渲染 - AssetBundle、Stream Load
划分为多个场景包,动态、流式加载
本文使用Instanced Draw+Frustum Cull+LOD来渲染大场景,最终实现效果演示:
场景总三角面数是千万级,总物体数量是1万(PC端)/5000(移动端),动态物体数量是800(PC端)/400(移动端)
其中,树使用了Instanced Draw+LOD,白色立方体使用了Instanced Draw
可以把相机拉进、拉远,可看到不同Level的树
移动相机,可看到红框内的Triangles在变化(大概在几十万到几百万),这是Frustum Cull后的三角面数
在5年前的中配手机上,FPS可达15左右
下面开始实现各个关键点,给出实现的思路:
Instanced Draw
比如要将克隆的1000个Mesh改为Instanced的,则保留它们作为source,并创建一个InstancedMesh,count设为1000,写入1000个Mesh的世界矩阵;然后隐藏source,显示InstancedMesh
之所以保留source,是因为可以用它们来做碰撞检测、Ray Picking等单个Mesh的操作
值得注意的是物体可能是多材质的Object3D(如树),所以要将其中的每个Mesh拆分到一组Instanced Draw中。举例来说,如果树有个3个材质(即3个子Mesh),则需要创建3个InstancedMesh,然后将所有树的第一个材质的Mesh对应到第一个InstancedMesh中,其余的以此类推
Frustum Cull
如上图所示,实现Instanced Draw+Frustum Cull的原理是将要剔除的物体移到InstancedMesh的instanceMatrix的最后,并将count减1
值得注意的是要将three.js默认的对单个Object3D的frustum cull关闭(即将source的frustumCulled设为false)
另外,我们直接遍历所有的待剔除物体来进行Fustum Cull检测,没有使用Octree等加速结构
相关的讨论请参考:Linear search vs Octree (Frustum cull)
LOD
如上图所示,假设车有3个Level的LOD层级,我们希望离相机越远,显示越高的Level(Geometry、Material越简单)
我们需要创建3个InstanceMesh,分别对应不同的Level,如下图所示:
参考资料
InstancedMesh2 - Easy handling and frustum culling
What is a more straightforward way to do instance culling?
Linear search vs Octree (Frustum cull)
LOD with Instancing and Multi-Material Meshes
Three.js InstancedMesh performance optimizations - DevLog 10