WebGL大场景性能优化
一、WebGL 是什么
WebGL(Web 图形库)是一个 JavaScript API,可在任何兼容的 Web 浏览器中渲染高性能的交互式 3D 和 2D 图形,而无需使用插件。WebGL 通过引入一个与 OpenGL ES 2.0 非常一致的 API 来做到这一点,该 API 可以在 HTML5
<canvas>
元素中使用。这种一致性使 API 可以利用用户设备提供的硬件图形加速。[摘自 MDN]本质:一个用来创建 2D/3D 图形网页的 JavaScript API
二、WebGL 能做什么
2D 框架/引擎
PIXI.js
demo:https://pixijs.com/gallery/
Pixi.js 使用 WebGL,是一个超快的 HTML5 2D 渲染引擎。作为一个 Javascript 的 2D 渲染器,Pixi.js 的目标是提供一个快速的、轻量级而且是兼任所有设备的 2D 库。Pixi 渲染器可以开发者享受到硬件加速,但并不需要了解 WebGL。
Phaser.js
demo:https://phaser.io/examples
Phaser 是一个开源的桌面和移动 HTML5 2D 游戏开发框架,支持 JavaScript 和 TypeScript。高性能: 快速、免费、易于维护。一方面,开发者可以直接通过 Koding 平台上的 VM 开发系统进行代码编写及预览。另一方面,也可以在支持 Canvas 的浏览器中直接安装 Phaser 来进行游戏开发。
two.js
demo:https://two.js.org/examples/
two.js 是一个二维的绘图 API,用于较新的 Web 浏览器,可基于不同上下文绘制,包括 svg、canvas 和 webgl。
3D 框架/引擎
Three.js
demo:https://threejs.org/examples/#webgl_animation_keyframes
当下最流行的 WebGL 库, 轻量级,容易使用,很多 webgl 库都是基于它来构建。
Babylon.js
demo:https://www.babylonjs.com/community/
Babylon.js 是一个使用 HTML5 和 WebGL 构建 3D 游戏的 JavaScript 框架。
Cesium.js
demo:https://sandcastle.cesium.com/
Cesium 是一款开源的基于 JavaScript 的 3D 地图框架。Cesium 能够跨平台、跨浏览器支持绝大多数的浏览器和移动端浏览器;使用 WebGL 进行 3D 图形展示。可应用于三维数字地球, 数据可视化, 创建虚拟场景等功能。
三、大场景案例分析
场景分类
Gis 主场景
https://sandcastle.cesium.com/?src=3D%20Tiles%20Photogrammetry%20Classification.html3D 室内漫游场景
https://dashboard.archilogic.com/scene/!7defaa68-4293-446c-b55a-d731d3506047/viewer2D 室内编辑场景
https://dashboard.archilogic.com/scene/!7defaa68-4293-446c-b55a-d731d3506047/viewer
各场景痛点
Gis 主场景
Gis 数据老旧与当前场景现实情况不一致,需要引入当前场景的最新数据,数据量大。 中心场景效果要求较高(光照,阴影,反射等),对中心模型的处理很多。 主场景的需要还原真实环境(实时天气,整个中心场景的统筹看板),对接数据过大。
3D 室内漫游场景
1 比 1 精确还原场景,对数据精确度要求很高,同时还需要附加一些效果。 IOT/IBMS 数据挂接,实时状态显示及控制。
2D 室内编辑场景
场景内圆形、扇形等体块很多,场景三角面很多。 💡 WebGL 基本绘图元素只有点、线、三角形,如下图中的这种复杂图形,它也是由三角形构成。 编辑场景中拆分、新增、测量、合并、辅助线等操作很多,吸附、碰撞检测等计算量很大。 场景中构件种类很多,不同种类的渲染呈现方式也不同,支持的操作功能亦不相同。
性能问题总结
模型数据过大及挂接数据较多导致的请求过多过慢问题 片元处理及计算过多 draw call 次数过多 场景复杂三角面过多 节点吸附、标签避障计算影响性能
四、解决方案
1. 自定义 GeoJSON 数据结构及大数据压缩方案
GeoJSON 是一种开放标准的地理空间数据交换格式,可表示简单的地理要素及其非空间属性。GeoJSON 以 JavaScript 对象表示法 (JSON) 为基础,是对各种地理数据结构进行编码时所采用的格式。该格式使用地理坐标参考系(世界大地测量系统 1984),并且以十进制度作为单位。
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point", // 点
"coordinates": [102.0, 0.5]
},
"properties": {
"prop0": "value0"
},
},
{
"type": "Feature",
"geometry": {
"type": "LineString", // 线段
"coordinates": [
[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]
]
},
"properties": {
"prop0": "value0",
"prop1": 0.0
},
},
{
"type": "Feature",
"geometry": {
"type": "Polygon", // 面
"coordinates": [
[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0],
[100.0, 1.0], [100.0, 0.0]]
]
},
"properties": {
"prop0": "value0",
"prop1": { "this": "that" }
},
},
...
]
}
由于每个场景中需要显示的元素很多。导致每个楼层的 GeoJSON 的数据过大(大约每层 4MB),这样通过 http 传输就会很慢,再加上还要请求其他的挂接数据,导致页面在渲染前耗费的时间就占用了页面所有时间的一半。
思路
从 GZip 来的灵感 => Deflate 算法 => LZ77 + Huffman。
LZ77与LZ78是亚伯拉罕·蓝波与杰可布·立夫在 1977 年以及 1978 年发表的论文中的两个无损数据压缩算法。这两个算法是大多数 LZ 算法变体,如 LZW、LZSS 以及其它一些压缩算法的基础。与最小冗余编码器或者行程长度编码器不同,这两个都是基于字典的编码器。LZ77 是“滑动窗”压缩算法,这个算法后来被证明等同于 LZ78 中首次出现的显式字典编码技术。
下面我们来举一个例子。
有一份数据文件的内容如下
https://www.bytedance.com https://sso.bytedance.com其中有些部分的内容,前面已经出现过了,下面用()括起来的部分就是相同的部分。
https://www.bytedance.com (https://)sso(.bytedance.com)我们使用 (两者之间的距离,相同内容的长度) 这样一对信息,来替换后一块内容。
https://www.bytedance.com (26,8)sso(26,14)(26,8) 中,26 为相同内容块与当前位置之间的距离,8 为相同内容的长度。
(23,4) 中,26 为相同内容块与当前位置之间的距离,14 为相同内容的长度。
由于(两者之间的距离,相同内容的长度)这一对信息的大小,小于被替换内容的大小,所以文件得到了压缩。
霍夫曼编码(英语:Huffman Coding),又译为哈夫曼编码、赫夫曼编码,是一种用于无损数据压缩的熵编码(权编码)算法。由美国电脑科学家大卫·霍夫曼(David Albert Huffman)在 1952 年发明。[摘自维基百科]
字符 | 空格 | a | e | f | h | i | m | n | s | t | l | o | p | r | u | x |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
频率 | 7 | 4 | 4 | 3 | 2 | 2 | 2 | 2 | 2 | 2 | 1 | 1 | 1 | 1 | 1 | 1 |
编码 | 111 | 010 | 000 | 1101 | 1010 | 1000 | 0111 | 0010 | 1011 | 0110 | 11001 | 00110 | 10011 | 11000 | 00111 | 10010 |
我们可以看到,Huffman 树的建立方法就保证了,出现次数多的符号,得到的 Huffman 编码位数少,出现次数少的符号,得到的 Huffman 编码位数多。读文件,统计每个符号的出现次数。根据每个符号的出现次数,建立 Huffman 树,得到每个符号的 Huffman 编码。将每个符号的出现次数的信息保存在压缩文件中,将文件中的每个符号替换成它的 Huffman 编码,并输出。
优化前后对比
优化前:约 4MB
优化后:约 800KB
2. 片元处理及计算过多
片元处理优化(减少需要处理的片元数目)
控制绘制顺序 为了最大限度地避免 overdraw,一个重要的优化策略就是控制绘制顺序。由于深度测试的存在,如果我们可以保证物体都是从前往后绘制的,那么就可以很大程度上减少 overdraw。这是因为,在后面绘制的物体由于无法通过深度测试,因此,就不会再进行后面的渲染处理。 时刻警惕透明物体 由于透明度物体处理时,会导致一些硬件优化策略失效(比如说遮挡剔除,把看不到物体的顶点剔除)。对于透明度混合技术,需要关闭深度写入,所以要保持正确的渲染顺序。透明度测试没有关闭深度测试,但由于它的实现使用了 discard 或 clip 操作。也就是说,只要在执行了所有的片元着色器后,GPU 才知道哪些片元会被真正渲染到屏幕上,这样,原先那些可以减少 overdraw 的优化就都无效了。
处理方式:控制绘制顺序,同时先对不透明物体从前往后排序后绘制,再绘制透明物体,进行混合。
减少片元计算
减少实时光照和阴影,可以使用光照贴图代替实时计算。
3. draw call 次数过多
使用批处理
简单理解就是将使用同一个材质的物体一起处理 们之间的不同就是顶点数据的差别。如果使用不同材质,但要使用批处理,也可以将这些纹理合并到同一张大纹理称为图集,再使用不同的采样坐标对纹理采样即可。如果需要微小的不同,使用顶点颜色数据来储存。批处理是 OpenGL 的概念,WebGL 有实例绘制,告诉 GPU 使用共享模型来绘制每一个实例,也是一次绘制多个物体。
在 WebGL2.0 绘制方法是:
gl.drawArraysInstanced(mode,first,count,instanceCount);
gl.drawElementsInstanced(mode, count, type, offset, instanceCount);
下面这个方法调整实例位置:
gl.vertexAttribDivisor(index,divisor);
WebGL1.0 可以使用扩展:
var ext = gl.getExtension('ANGLE_instanced_arrays');
ext.drawArraysInstancedANGLE();
ext.drawElementsInstancedANGLE();
在 ThreeJS 中使用 InstanceMesh 或 InstanceGeometry。
Mesh 合并
Mesh 网格 ,Mesh 是指模型的网格,3D 模型是由多边形拼接而成,而多边形实际上是由多个三角形拼接而成的。所以一个 3D 模型的表面是由多个彼此相连的三角面构成。三维空间中,构成这些三角面的点以及三角形的边的集合就是 Mesh。
为了尽量降低 drawcall,即调用 mesh 绘制的次数。因为一次性处理一个大 mesh 比多次绘制小 mesh 要快得多。从而达到性能优化
在 ThreeJS 中使用 THREE.Geometry 对象的上面的 merge 方法。
举个 🌰:当我们使用普通组的情况,绘制 20000 个立方体,帧率在 15 帧左右,如果我们选择合并以后,再绘制两万,就会发现,我们可以轻松的渲染 20000 个立方体,而且没有性能的损失。合并的代码如下:
var geometry = new THREE.Geometry();
for (var i = 0; i < 20000; i++) {
var cube = addCube(); // 新调用立方体方法
cube.updateMatrix();
geometry.merge(cube.geometry, cube.matrix); // 合并mesh
}
scene.add(new THREE.Mesh(geometry, cubeMaterial));
和组的优缺点对比
优点:性能不会有损失。因为将所有的的网格合并成为了一个,性能将大大的增加。
缺点:组能够对每个单独的个体进行操作,而合并网格后则失去对每个对象的单独控制。想要移动、旋转或缩放某个方块是不可能的。
4. 场景复杂三角面过多
优化几何体
尽可能减少模型中三角形面片的数目,需要美工人员的帮助。
这种将顶点一分为多的原因主要有两个:一个是为了分离纹理坐标(uv splits),另一个是为了产生平滑的边界(smoothing splits)。它们的本质,其实都是因为对于 GPU 来说,顶点的每一个属性和顶点之间必须是一对一的关系。而分离纹理坐标,是因为建模时一个顶点的纹理坐标有多个。例如,对于一个立方体,它的 6 个面之间虽然使用了一些相同的顶点,但在不同面上,同一个顶点的纹理坐标可能并不相同。对于 GPU 来说,这是不可理解的,因此,它必须把这个顶点拆分成多个具有不同纹理坐标的顶点。而平滑边界也是类似的,不同的是,此时一个顶点可能会对应多个法线信息或切线信息。这通常是因为我们要决定一个边是一条硬边(hard edge)还是一条平滑边。
模型的 LOD 技术(多细节层次)
ThreeJS 实现了 LOD 模型。这种技术的原理是,当一个物体离摄像机很远时,模型上的很多细节是无法被察觉到的。因此,LOD 允许当对象逐渐远离摄像机时,减少模型上的面片数量,从而提高性能。
面剔除技术
OpenGL 允许检查所有正面朝向(Front facing)观察者的面,并渲染它们,而丢弃所有背面朝向(Back facing)的面,这样就节约了我们很多片段着色器的命令(它们很昂贵!)。我们必须告诉 OpenGL 我们使用的哪个面是正面,哪个面是反面。OpenGL 使用一种聪明的手段解决这个问题——分析顶点数据的连接顺序(Winding order)。
默认情况下,逆时针的顶点连接顺序被定义为三角形的正面。WebGL 也是这样,具体设置代码为:
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.FRONT_AND_BACK);
5. 节点吸附、标签避障计算影响性能
利用 Web Workers 进行复杂计算
Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,他们可以使用XMLHttpRequest
执行 I/O (尽管responseXML
和channel
属性总是为空)。一旦创建, 一个 worker 可以将消息发送到创建它的 JavaScript 代码,通过将消息发布到该代码指定的事件处理程序(反之亦然)。[摘自 MDN]
// filter-sprites-worker.js
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = handleFilterSprites(e.data); // 处理避障结果函数
console.log('Posting message back to main script');
postMessage(workerResult);
}
class view3D {
constructor() {
this.initView();
this.initEvents();
this.initWorkers();
}
// 初始化视图
initView() {
// ...
}
// 初始化事件
initEvents() {
// ...
}
// 相机改变
cameraChange() {
this.autoFiterSprites();
}
// 初始化worker
initWorkers() {
this.calcWorker = new Worker('filter-sprites-worker.js');
this.calcWorker.onmessage = this.onmessage;
}
// 处理worker结果回调
onmessage(e) {
console.log(e, 'Message received from worker');
}
// 进行标签自动避障计算
autoFiterSprites() {
// 标签组及相机参数传入worker计算
this.calcWorker.postMessage([spritesGruops, this.camera]);
}
// 场景销毁函数
disposed() {
this.removeEvents();
this.calcWorker.terminate();
// ...
}
}
优缺点对比
优点:主线程运行的同时 worker 线程也在运行,相互不干扰,在 worker 线程运行结束后把结果返回给主线程。这样做的好处是主线程可以把计算密集型或高延迟的任务交给 worker 线程执行,这样主线程就会变得轻松,不会被阻塞或拖慢。
缺点:worker 一旦新建,就会一直运行,不会被主线程的活动打断,这样有利于随时响应主线程的通性,但是也会造成资源的浪费,所以不应过度使用,用完注意关闭。或者说:如果 worker 无实例引用,该 worker 空闲后立即会被关闭;如果 worker 实列引用不为 0,该 worker 空闲也不会被关闭。
五、优化前后对比
优化前 | 优化后 | |
---|---|---|
数据大小 | 约 4MB | 约 800KB |
页面加载时间 | 8s | 3s |
用户体验 | 节点吸附操作会有明显卡顿 | 页面操作流畅,用户体验提升 |
六、WebGL 总结
WebGL 给 Web 图形开发者打开了一扇新的大门,浏览器可以不借助插件做炫酷三维效果。 WebGL 对前端开发来说入门比较简单,丰富的框架和引擎便于上手和搭建需求效果。 WebGL 进阶和深入还有很长的路要走,实时渲染技术与图形学知识、计算几何知识都需要有涉猎。
目前市场上的 WebGL 开发现状:
企业:WebGL 开发很难招,尤其是一个专家级别的,基本都靠相互内推、挖墙脚。
个人:WebGL 企业很少,基本就那几家,近几年相关企业明显增多。
但是 WebGL 开发的工资是真的高:
七、WebGPU
WebGPU 是什么
WebGPU 是一套基于浏览器的图形 API,浏览器封装了现代图形 API(Dx12、Vulkan、Metal),提供给 Web 3D 程序员,为 Web 释放了更多的 GPU 硬件的功能。WebGPU 不是 WebGL 的延续,也不对标 OpenGL,而是下一代全新的基于 Web 的图形 API。
WebGPU 的优点
更好地多线程支持 compute shader 支持 作为标准各大浏览器都会支持 ....
WebGPU 尝鲜
目前 WebGPU 虽然还未正式发布,但已经比较成熟了,也有相关的 Demo 可供学习。
对于 Chrome:
下载 Chrome Canary:https://www.google.com/intl/zh-CN/chrome/canary/ 打开 chrome://flags/#enable-unsafe-webgpu
对于 Safari:
下载 Safari Technology Preview:https://developer.apple.com/safari/technology-preview/ 选中 Safari → Preferences → Advanced → Develop menu→ Experimental Features → WebGPU
WebGPU 的未来
提到 WebGPU 的未来,更应该考虑的是 Web 生态和 Native 生态的问题。尤其是面向未来,面向元宇宙时代。我们到底应该怎么选择我们 3D 场景,是通过浏览器 Web 来实现?还是选择桌面端 Native 来实现呢?
我想不同的人,不同的企业,都会有不同的答案。我个人是非常看好 WebGPU 的发展前景,但是前景好不等于市场就一定会选择,非常受制于现有生态。
点击上方关注 · 我们下期再见