查看原文
其他

WebGL大场景性能优化

江明 字节前端 ByteFE 2024-03-30

一、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 框架/引擎

  1. PIXI.js
    demo:https://pixijs.com/gallery/

Pixi.js 使用 WebGL,是一个超快的 HTML5 2D 渲染引擎。作为一个 Javascript 的 2D 渲染器,Pixi.js 的目标是提供一个快速的、轻量级而且是兼任所有设备的 2D 库。Pixi 渲染器可以开发者享受到硬件加速,但并不需要了解 WebGL。

  1. Phaser.js
    demo:https://phaser.io/examples

Phaser 是一个开源的桌面和移动 HTML5 2D 游戏开发框架,支持 JavaScript 和 TypeScript。高性能: 快速、免费、易于维护。一方面,开发者可以直接通过 Koding 平台上的 VM 开发系统进行代码编写及预览。另一方面,也可以在支持 Canvas 的浏览器中直接安装 Phaser 来进行游戏开发。

  1. two.js
    demo:https://two.js.org/examples/

two.js 是一个二维的绘图 API,用于较新的 Web 浏览器,可基于不同上下文绘制,包括 svg、canvas 和 webgl。

3D 框架/引擎

  1. Three.js
    demo:https://threejs.org/examples/#webgl_animation_keyframes

当下最流行的 WebGL 库, 轻量级,容易使用,很多 webgl 库都是基于它来构建。

  1. Babylon.js
    demo:https://www.babylonjs.com/community/

Babylon.js 是一个使用 HTML5 和 WebGL 构建 3D 游戏的 JavaScript 框架。

  1. Cesium.js
    demo:https://sandcastle.cesium.com/

Cesium 是一款开源的基于 JavaScript 的 3D 地图框架。Cesium 能够跨平台、跨浏览器支持绝大多数的浏览器和移动端浏览器;使用 WebGL 进行 3D 图形展示。可应用于三维数字地球, 数据可视化, 创建虚拟场景等功能。

三、大场景案例分析

场景分类

  1. Gis 主场景
    https://sandcastle.cesium.com/?src=3D%20Tiles%20Photogrammetry%20Classification.html
  2. 3D 室内漫游场景
    https://dashboard.archilogic.com/scene/!7defaa68-4293-446c-b55a-d731d3506047/viewer
  3. 2D 室内编辑场景
    https://dashboard.archilogic.com/scene/!7defaa68-4293-446c-b55a-d731d3506047/viewer

各场景痛点

Gis 主场景

  1. Gis 数据老旧与当前场景现实情况不一致,需要引入当前场景的最新数据,数据量大。
  2. 中心场景效果要求较高(光照,阴影,反射等),对中心模型的处理很多。
  3. 主场景的需要还原真实环境(实时天气,整个中心场景的统筹看板),对接数据过大。

3D 室内漫游场景

  1. 1 比 1 精确还原场景,对数据精确度要求很高,同时还需要附加一些效果。
  2. IOT/IBMS 数据挂接,实时状态显示及控制。

2D 室内编辑场景

  1. 场景内圆形、扇形等体块很多,场景三角面很多。
  2. 💡 WebGL 基本绘图元素只有点、线、三角形,如下图中的这种复杂图形,它也是由三角形构成。
  3. 编辑场景中拆分、新增、测量、合并、辅助线等操作很多,吸附、碰撞检测等计算量很大。
  4. 场景中构件种类很多,不同种类的渲染呈现方式也不同,支持的操作功能亦不相同。

性能问题总结

  1. 模型数据过大及挂接数据较多导致的请求过多过慢问题
  2. 片元处理及计算过多
  3. draw call 次数过多
  4. 场景复杂三角面过多
  5. 节点吸附、标签避障计算影响性能

四、解决方案

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。

LZ77LZ78是亚伯拉罕·蓝波与杰可布·立夫在 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 年发明。[摘自维基百科]

字符空格aefhimnstloprux
频率7443222222111111
编码1110100001101101010000111001010110110110010011010011110000011110010

我们可以看到,Huffman 树的建立方法就保证了,出现次数多的符号,得到的 Huffman 编码位数少,出现次数少的符号,得到的 Huffman 编码位数多。读文件,统计每个符号的出现次数。根据每个符号的出现次数,建立 Huffman 树,得到每个符号的 Huffman 编码。将每个符号的出现次数的信息保存在压缩文件中,将文件中的每个符号替换成它的 Huffman 编码,并输出。

优化前后对比

优化前:约 4MB
优化后:约 800KB

2. 片元处理及计算过多

片元处理优化(减少需要处理的片元数目)

  1. 控制绘制顺序 为了最大限度地避免 overdraw,一个重要的优化策略就是控制绘制顺序。由于深度测试的存在,如果我们可以保证物体都是从前往后绘制的,那么就可以很大程度上减少 overdraw。这是因为,在后面绘制的物体由于无法通过深度测试,因此,就不会再进行后面的渲染处理。
  2. 时刻警惕透明物体 由于透明度物体处理时,会导致一些硬件优化策略失效(比如说遮挡剔除,把看不到物体的顶点剔除)。对于透明度混合技术,需要关闭深度写入,所以要保持正确的渲染顺序。透明度测试没有关闭深度测试,但由于它的实现使用了 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 (尽管responseXMLchannel属性总是为空)。一旦创建, 一个 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
页面加载时间8s3s
用户体验节点吸附操作会有明显卡顿页面操作流畅,用户体验提升

六、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 的发展前景,但是前景好不等于市场就一定会选择,非常受制于现有生态。


点击上方关注 · 我们下期再见 

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存