前言
DORAVIS 可视化大屏编辑器,是植根于浏览器的可视化平台。我们不难发现,DORAVIS 的众多图表中,有多种实现方案。如,基于 ECharts 二次开发的 BI 图表,有基于 Mapbox/leaflet 等实现的地理图表,以及根据业务需要制作的基于 Dom 的内容、视频、图片等组件。以 ECharts 为例,我们知道它的底层是基于 zrender 的。因此,ECharts 也可以自由的切换渲染引擎。默认地,ECharts 使用的是 Canvas 作为渲染器。如果你希望看到节点结构,你还可以使用下面的语句,用SVG作为渲染器。
我们把上面的方式总结一下,得到绘制浏览器图表的方式,大概有如下四种:Dom 方式、SVG 方式、Canvas2d 方式以及 WebGL 方式。
本次分享我们抛开具体的图表库,分别用这些方式的原始方案来讲解简单的绘图原理。并分析它们的优势和难点。经过本次分享,你可能在使用封装库时候,会更加了解底层的细节。
DOM 方式
我们认为这就是普通的 HTML+CSS 方式。很多同学可能认为一般 dom 不能做可视化图表。这是一种偏见。可能我们平时用各种 charts 库有关,我们认为 charts 必然和传统的 HTML 有区别。这实际是一种偏见。借用一下可视化大名鼎鼎的 D3.js 的理念。可视化图表,一方面需要解决数据的组织方法,一方面需要解决底层的渲染方式。D3.js 重点关注的是数据的组织形式,而具体的渲染交由具体的渲染底层。那这里面说的dom方式,实际也就是一种渲染底层。
我们来看第一个例子:https://jsbin.com/kexamup/edit?html,css,output
柱形图使用 dom 还是非常容易达成的。而且,使用这种方式,可以比较方便的利用成熟的事件系统进行对交互的控制。比如鼠标事件、tips等等。
不过,用这种方式的缺点也是比较明显的。
1、性能。对于一般的可视化图表,比复杂的网页元素要简单的多。而基于 html 渲染引擎的绘制,还会考虑 html、css 的解析、元素位置的安排,元素变化引起的重排重绘等等。这对于可视化图表来讲,我们对于布局的需求并不复杂,调用这个渲染引擎有些过于铺张,而且大部分的工作对于我们要完成的事情是空耗的性能。
2、dom 对于非矩形元素支持复杂。举例,如果我们需要在图表上表现圆形、椭圆形、直线、不规则多边形等等,传统 dom 实现将变得比较复杂。像饼图、折线图这种,单用 dom 元素模拟就显得比较啰嗦。即便实现起来,也非常不直观。
SVG 方式
SVG是一种以XML语法为基础的图像格式。SVG有如下特点。
1、图片是矢量图(内连位图除外),可缩放不失真。
2、有线、圆弧、矩形、多边形、圆形等直接的元素。
3、可以用 img 元素的 src 属性加载。
4、可内连在 html 中。
5、部分 CSS 可对 SVG 元素起作用。
我们来看第二个例子:https://jsbin.com/homajes/edit?html,output
用 SVG 方式绘制图表一方面可以使用html中的事件、样式等便利工具,另一方面,也简化了不规则图形的编写工作。
我们来看用 SVG 写的一个层次关系图的例子:https://jsbin.com/kasexen/edit?html,js,output
我们看到,SVG 作为跟 DOM 类似的做法,事件系统是完备的,可以很方便应用。很多绘图的库也使用 SVG 作为默认的绘图渲染引擎。比如 @antv/x6,d3 等等。
但是,性能依旧是一个值得关注的问题。虽然,在SVG中,内部元素的布局比HTML做了适度的简化,但是如果我们要绘制的图形非常复杂,元素节点的数量就会非常多,这会大大增加渲染和重绘所需时间。
由于 SVG 也是图片的一种,所以也可以放在 Canvas 上。而 Canvas 一般会比SVG有更高的性能。可以结合双方的优点来使用。
Canvas方式
Canvas 是大名鼎鼎的 HTML5 引入的一个标签。Canvas 绘图一般分为以下几个步骤:
1.Canvas 在浏览器上创造一个空白的画布,通过提供渲染上下文,赋予我们绘制内容的能力。
2.调用渲染上下文,设置各种属性,然后调用绘图指令完成输出,就能在画布上呈现图形。
这里我们定义了一个 canvas 元素。
下面这个代码我们用来获取canvas的上下文:
然后我们就能直接调用绘图API进行绘图了:
这里(https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial )是 MDN 列出的 canvas 的绘图 API 。
我们同样看看canvas实现的层次关系图:https://jsbin.com/hixoxex/edit?html,js,output
Canvas 直接把绘图 API 提供出来。这样给开发者的能力更多。同时,对于图形本身的封装以及事件等处理没有做更多的封装。它不需要进行上下文、布局等。单论绘图这一动作,是比 Dom 以及 SVG 要快的。
Canvas 画布大小有限制,不同的浏览器不同,最新的 Chrome 下应该是不超过 16384 X 16384 ,单个宽高不超过 32767 像素,一般的可视化大屏足够用了。检测设备的 Canvas 大小可以用这个项目:https://github.com/jhildenbiddle/canvas-size
Canvas 要比 SVG 和 dom 的更底层。所以需要一些额外的工作。有时候也可以结合 SVG 和 Canvas 的优点。有很多库对 Canvas 做了不同程度的封装,如:https://github.com/fabricjs/fabric.js https://github.com/pixijs/pixijs 等等。
在 Chrome 上 canvas 的底层封装的是一个超快的图像引擎叫skia。这个搞过Flutter的同学对它可能会比较了解。
WebGL 方式
理论上,Canvas2D 已经足够快了。不过当以下几种情况,我们就需要更底层的技术了:
1.要绘制的图形数量非常多,而且位置、形态都有变化,这种情况即使用 Canvas2D 依然有性能问题。
2.对较大图像的细节做像素处理,如实现物体的光影、流体效果和一些复杂的像素滤镜。由于这些效果往往要精准地改变一个图像全局或局部区域的所有像素点,要计算的像素点数量非常的多(一般是数十万甚至上百万数量级的)。这时,即使采用 Canvas2D 操作,也会达到性能瓶颈。
3.绘制 3D 物体。
浏览器给出了一个基于浏览器的底层接口:WebGL。WebGL迄今发展不过 10 多年。但是已经经历了两代。是对 OpenGL 接口的封装,并进行了一定程度的取舍,给浏览器直接面对 GPU 接口的机会。
我们的 Dom、SVG、Canvas 其实也都是对 GPU 程序的各种封装。只不过 WebGL 让我们直面底层的接口。
那有 CPU 为什么还要引入 GPU 呢?一方面,CPU 在整个机器中个数较少,成本较高,GPU 成本较低,数量较多;另一方面,由于 GPU 个数多,适合做大量重复、较为简单的计算。图像渲染恰好就是 GPU 的良好适用场景。
CPU 和 GPU 都属于处理单元,但是结构不同。形象点来说,CPU 就像个大的工业管道,等待处理的任务就像是依次通过这个管道的货物。一条 CPU 流水线串行处理这些任务的速度,取决于 CPU(管道)的处理能力。
这样的结构用来处理大型任务是足够的,但是要处理图像应用就不太合适了。这是因为,处理图像应用,实际上就是在处理计算图片上的每一个像素点的颜色和其他信息。每处理一个像素点就相当于完成了一个简单的任务,而一个图片应用又是由成千上万个像素点组成的,所以,我们需要在同一时间处理成千上万个小任务。
GPU 是由大量的小型处理单元构成的,它可能远远没有 CPU 那么强大,但胜在数量众多,可以保证每个单元处理一个简单的任务。即使我们要处理一张 800 * 600 大小的图片,GPU 也可以保证这 48 万个像素点分别对应一个小单元,这样我们就可以同时对每个像素点进行计算了。
“启动太阳轨道计算软件‘Three-Bodyl.0’!”牛顿声嘶力竭地发令,“启动计算主控!加载差分模块!加载有限元模块!加载谱方法模块……调入初始条件参数!计算启动!!”主板上波光粼粼,显示阵列上的各色标志此起彼伏地闪动,人列计算机开始了漫长的计算。“真是很有意思。”秦始皇手指壮观的计算机说,“每个人如此简单的行为,竟产生了如此复杂的大东西!”-- 刘慈欣 《三体》
我们来看一下,WebGL如何绘制一个三角形的例子:https://jsbin.com/minigiz/edit?html,js,output
理论上,绘制一个WebGL的图形需要以下几步:
1.创建 WebGL 上下文
2.创建 WebGL 程序(WebGL Program)
3.将数据存入缓冲区
4.将缓冲区数据读取到 GPU
5.GPU 执行 WebGL 程序,输出结果
我们看到,上面的着色器分为两种,一种是确定顶点数据的顶点着色器,一种是片元着色器。
顶点着色器和片元着色器是职责清晰、依次运行的。
可以把顶点着色器理解为处理顶点的 GPU 程序代码。它可以改变顶点的信息(如顶点的坐标、法线方向、材质等等),从而改变我们绘制出来的图形的形状或者大小等等。顶点处理完成之后,WebGL 就会根据顶点和绘图模式指定的图元,计算出需要着色的像素点,然后对它们执行片元着色器程序。简单来说,就是对指定图元中的像素点着色。
WebGL 从顶点着色器和图元提取像素点给片元着色器执行代码的过程,就是生成光栅信息的过程,也叫光栅化过程。所以,片元着色器的作用,就是处理光栅化后的像素信息。
由于定点、片元是依次执行的,所以,我们可以定义变量,把它传给片元着色器。
我们把上一个例子加工一下,实现一个带有渐变的三角形。https://jsbin.com/dedubos/edit?html,js,output
从工程上看,如果把 WebGL 程序也归到前端的话,我们前端传统的三件套 HTML、CSS 和 Javascript 。也就变为了 4 件套,加上 Shader,也就是着色器。
WebGL 由于偏底层,所以,比其他图形系统的使用稍显复杂。但是在精细图像控制以及 3d 上是一个比较好的选择。当然,对于 WebGL 是有一定的库做封装的。如 Three.JS、Babylon.js 等等。
WebGL 其实没有把所有的 GPU 计算功能都给 Web 浏览器,比如通用计算 GPU 。同时,CPU 到 GPU 会有一定的传输带宽。传输相对于计算是更加耗时的操作。
这张图片表示了 WebGL 渲染图片的流程。
其他
WebGPU
2017 年开始正式提案的 WebGPU ,目标就是取代现有的 WebGL ,当然这是一个漫长的过程,WebGL 和 WebGPU 注定需要共存一段时间而且很多概念还是相通的。
WebGPU 目前(2023.02)在浏览器届还没有完全支持。即便如 Chrome 也需要在金丝雀版本上也要打开最新的 flag ,才能使用。据传闻今年 4 月份会出正式版本。
WebGPU 一方面重新定义了 Shader 语言。一方面,把引入 WebGPU 控制权和 Canvas 分离,也就把计算同主线程解耦。从而在 WebWorker 以及 WebAssembly 中也可以使用了。也使 GPU 的并行计算能力充分释放,不再完全绑定在“绘图”这一任务上。一个热门的方向叫 GPGPU ,也即 GPU 通用计算,现在正在逐步引入到 Web 上。以后在网页上挖矿和做大数据训练也许即将成为可能。
WebGPU 也可以替代 WebGL 做相关的工作,而且能够更快更统一的运行。
WebRTC
可视化渲染引擎虚幻,为了解决 3d 物体与多种终端互操作,引入了像素流技术。将控制流上行,将预置的像素流下传。借助 WebRTC 实现信息的低延迟传输。3D 游戏引擎将 cpu/ 显卡计算好的像素流通过 DP/HTMI 系统总线直接传导至显示器,这样看来,所有的过程发生在同一台电脑上;但基于 WebRTC 的像素流技术让视频的计算和显示发生在由计算机网络相连的不同设备上,这种情况下,一台机器运行引擎,另一台机器显示画面。除此之外还有一个重要区别:由于计算机网络的带宽远小于数据总线,还要保证网络安全,像素流在机器间的传输必须经过压缩和加密,这无疑给该项技术增加了许多难度,好在,WebRTC 本身就支持媒体流的压缩和加密,这也是虚幻引擎选择 WebRTC 的原因。
参考资料
https://time.geekbang.org/column/article/252076 https://blog.csdn.net/u013850277/article/details/103746615 https://juejin.cn/post/7090921455893872647