WebGL(Web 图形库)是一个 JavaScript API,可在任何兼容的 Web 浏览器中渲染高性能的交互式 3D 和 2D 图形,而无需使用插件。WebGL 通过引入一个与 OpenGL ES 2.0 非常一致的 API 来做到这一点,该 API 可以在 HTML5
<canvas>
元素中使用。这种一致性使 API 可以利用用户设备提供的硬件图形加速。[摘自 MDN]本质:一个用来创建 2D/3D 图形网页的 JavaScript API
Pixi.js 使用 WebGL,是一个超快的 HTML5 2D 渲染引擎。作为一个 Javascript 的 2D 渲染器,Pixi.js 的目标是提供一个快速的、轻量级而且是兼任所有设备的 2D 库。Pixi 渲染器可以开发者享受到硬件加速,但并不需要了解 WebGL。
Phaser 是一个开源的桌面和移动 HTML5 2D 游戏开发框架,支持 JavaScript 和 TypeScript。高性能: 快速、免费、易于维护。一方面,开发者可以直接通过 Koding 平台上的 VM 开发系统进行代码编写及预览。另一方面,也可以在支持 Canvas 的浏览器中直接安装 Phaser 来进行游戏开发。
two.js 是一个二维的绘图 API,用于较新的 Web 浏览器,可基于不同上下文绘制,包括 svg、canvas 和 webgl。
当下最流行的 WebGL 库, 轻量级,容易使用,很多 webgl 库都是基于它来构建。
Babylon.js 是一个使用 HTML5 和 WebGL 构建 3D 游戏的 JavaScript 框架。
Cesium 是一款开源的基于 JavaScript 的 3D 地图框架。Cesium 能够跨平台、跨浏览器支持绝大多数的浏览器和移动端浏览器;使用 WebGL 进行 3D 图形展示。可应用于三维数字地球, 数据可视化, 创建虚拟场景等功能。
Gis 主场景
3D 室内漫游场景
2D 室内编辑场景
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
处理方式:控制绘制顺序,同时先对不透明物体从前往后排序后绘制,再绘制透明物体,进行混合。
减少实时光照和阴影,可以使用光照贴图代替实时计算。
简单理解就是将使用同一个材质的物体一起处理 们之间的不同就是顶点数据的差别。如果使用不同材质,但要使用批处理,也可以将这些纹理合并到同一张大纹理称为图集,再使用不同的采样坐标对纹理采样即可。如果需要微小的不同,使用顶点颜色数据来储存。批处理是 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 是指模型的网格,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));
优点:性能不会有损失。因为将所有的的网格合并成为了一个,性能将大大的增加。
缺点:组能够对每个单独的个体进行操作,而合并网格后则失去对每个对象的单独控制。想要移动、旋转或缩放某个方块是不可能的。
尽可能减少模型中三角形面片的数目,需要美工人员的帮助。
这种将顶点一分为多的原因主要有两个:一个是为了分离纹理坐标(uv splits),另一个是为了产生平滑的边界(smoothing splits)。它们的本质,其实都是因为对于 GPU 来说,顶点的每一个属性和顶点之间必须是一对一的关系。而分离纹理坐标,是因为建模时一个顶点的纹理坐标有多个。例如,对于一个立方体,它的 6 个面之间虽然使用了一些相同的顶点,但在不同面上,同一个顶点的纹理坐标可能并不相同。对于 GPU 来说,这是不可理解的,因此,它必须把这个顶点拆分成多个具有不同纹理坐标的顶点。而平滑边界也是类似的,不同的是,此时一个顶点可能会对应多个法线信息或切线信息。这通常是因为我们要决定一个边是一条硬边(hard edge)还是一条平滑边。
ThreeJS 实现了 LOD 模型。这种技术的原理是,当一个物体离摄像机很远时,模型上的很多细节是无法被察觉到的。因此,LOD 允许当对象逐渐远离摄像机时,减少模型上的面片数量,从而提高性能。
OpenGL 允许检查所有正面朝向(Front facing)观察者的面,并渲染它们,而丢弃所有背面朝向(Back facing)的面,这样就节约了我们很多片段着色器的命令(它们很昂贵!)。我们必须告诉 OpenGL 我们使用的哪个面是正面,哪个面是反面。OpenGL 使用一种聪明的手段解决这个问题——分析顶点数据的连接顺序(Winding order)。
默认情况下,逆时针的顶点连接顺序被定义为三角形的正面。WebGL 也是这样,具体设置代码为:
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.FRONT_AND_BACK);
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 开发很难招,尤其是一个专家级别的,基本都靠相互内推、挖墙脚。
个人:WebGL 企业很少,基本就那几家,近几年相关企业明显增多。
但是 WebGL 开发的工资是真的高:
WebGPU 是一套基于浏览器的图形 API,浏览器封装了现代图形 API(Dx12、Vulkan、Metal),提供给 Web 3D 程序员,为 Web 释放了更多的 GPU 硬件的功能。WebGPU 不是 WebGL 的延续,也不对标 OpenGL,而是下一代全新的基于 Web 的图形 API。
目前 WebGPU 虽然还未正式发布,但已经比较成熟了,也有相关的 Demo 可供学习。
chrome://flags/#enable-unsafe-webgpu
Safari → Preferences → Advanced → Develop menu→ Experimental Features → WebGPU
提到 WebGPU 的未来,更应该考虑的是 Web 生态和 Native 生态的问题。尤其是面向未来,面向元宇宙时代。我们到底应该怎么选择我们 3D 场景,是通过浏览器 Web 来实现?还是选择桌面端 Native 来实现呢?
我想不同的人,不同的企业,都会有不同的答案。我个人是非常看好 WebGPU 的发展前景,但是前景好不等于市场就一定会选择,非常受制于现有生态。
点击上方关注 · 我们下期再见