web 2d 渲染引擎基础

前言

早期的浏览器并没有提供 2d 绘图功能,许多用到绘图的地方往往使用flash完成。随着 html5标准的普及,svg/canvas在浏览器中基本不存在兼容性问题了,在node,小程序环境也能使用canvas绘图。

各种基于2d绘图的应用也开始涌现出来, 比如像echarts,g2这样的可视化库, processon的流程图, 甚至连谷歌文档也改用canvas实现了。本文介绍 2d 绘图的基础知识以及一些性能优化手段。

基础技术

目前常见的web 2d绘图使用的底层技术主要是canvas和svg,  大多数2d渲染引擎都是基于这两种技术,如zrender、paperjs、raphael.js。webgl主要用于3d渲染, 2d是 3d的一个特例,所以webgl也可以用来绘制2d图形,如pixijs。

这几种技术各有优缺点,一般来说对于偏静态的场景如图表库、图可视化,使用canvas/svg差点不大。在数据量较大或动画较多的场景,使用canvas在性能上会更有优势。webgl除了性能优势外,还能做出很多canvas/svg没有的特效。

svg

svg是基于 xml 的2d矢量绘图技术,具有分辨率无关的优点,缩放时不会模糊。除了提供基本的绘图外,svg还内置了动画和丰富的滤镜元素。由于svg是基于dom的,也可以很好地与react, vue等框架结合。

<svg width= 300  height= 300 >
    <circle cx= 50  cy= 50  r= 10  fill= #000  stroke= red  stroke-width= 1  />
</svg>
复制代码

canvas

canvas 画布是html5中新增的元素, 用于提供绘图接口。和 svg 相比, canvas对像素的掌握更灵活,而且没有dom,对于大量数据的渲染,性能相比svg更好一些。

// 用canvas画一个圆
const ctx = canvas.getContext('2d')
ctx.beginPath()
ctx.arc(50, 50, 10, 0, Math.PI * 2)
ctx.fillStyle = '#000'
ctx.strokeStyle = 'red'
ctx.fill()
ctx.stroke()
复制代码

webgl

使用webgl绘制 2d 图形通常做法是将 2d 图形使用三角剖分,再使用webgl绘制三角形。简单几何形状如直线、圆、矩形的三角剖分比较容易, 对于复杂形状尤其是带孔洞的三角剖分,则要麻烦的多。

还有一种基于SDF(有向距离场)的方案来绘制一些常见几何形状。这种方案定义点在区域边界外部为正,边界上为0,边界内为负。通过sdf函数,可以判断像素点是否在图形内。

圆的sdf函数

float myCircleSDF( in vec2 p, in float r ) 
{
    return length(p)-r;
}
复制代码

图形绘制

在 svg 中除了直接提供常见的图形元素如rect, circle, polygon等形状外,还提供了一个万能的path元素,通过path指令完成任何形状的绘制。canvas也提供了类似 path 的 api, 可以和svg path实现互相转换,进而实现svg/canvas双引擎的支持。

canvas svg path
moveTo m, M
lineTo L, l
beizerCurveTo c, C, S, s
quadraticCurveTo Q q T t
arc & ellipse A, a
closePath z, Z
rect

svg path 绘制一个三角形

<path d= M 0,0L100,100L0,100Z  />
复制代码

使用canvas实现

ctx.beginPath()
ctx.moveTo(0,0)
ctx.lineTo(100, 100)
ctx.lineTo(0, 100)
ctx.closePath()
复制代码

此外现代浏览器支持使用Path2D对象生成绘制路径

const path = new Path2D('M 0,0L100,100L0,100Z');
ctx.fill(path);
复制代码

事件交互

如果使用的是 svg 绘图技术,可以像普通dom元素一样监听各种事件。 而canvas 只提供了一个画布,无法直接获取事件坐标所在的图形,需要用户自己判断拾取,常见的拾取方式有几何拾取和像素拾取。此外,canvas 2d 提供了isPointInPath api 用于判断点是否在路径内。

几何拾取

几何拾取是直接基于数学方法判断点的坐标是否在图形内。比如点是否在圆内的,可以计算点到圆心的距离是否小于圆的半径。

function isPointInCirlce(cx, cy, r, x, y) {
    return (x -cx ) ** 2 + (y - cy) ** 2 < r ** 2
}
复制代码

对于大部分简单的形状,都可以使用数学方法计算出来,性能也是各种方法里最好的。对于复杂的形状,尤其是包含曲线的形状,数学计算的难度也很大。

像素拾取

像素拾取的实现原理比较简单,将所有图形在离屏canvas上重新绘制一遍,绘制时使用图形的编号生成索引颜色。然后使用canvas 提供的getImageData获取(x, y)处的颜色,即可获取该点对应的图形。像素拾取由于重新将图形绘制了一遍,当图形较多时,绘制开销较大,性能不如纯几何拾取的效果好。

OKee在实践中综合使用了以上两种拾取方式。对于大部分简单的形状,使用数学计算。无法使用数学计算的复杂形状,再使用像素拾取。整体拾取性能可以支持数十万图元的交互。

动画

动画是根据人眼的视觉停留效果,通过更改图形的位置、颜色等属性,形成动画效果。浏览器提供了requestAnimationFrame接口,通过使用该接口绘制下一帧。

根据动效的不同,将动画分成以下两类

  • 属性动画,位置、旋转、颜色等属性进行插值
  • 路径动画,沿着一条路径运动

  • 变形动画,形状随着时间变化

动画插值使用的缓动函数,可以参考Tween.js提供的,如二次函数、三次曲线。

性能优化

渲染性能优化

  • 尽可能减少canvas上下文调用,如相同样式的图形只设置一次上下文
// 绘制1000个相同样式的矩形, 只设置一次上下文
ctx.save()
ctx.strokeStyle = color
ctx.lineWidth = 1
for (let i = 0; i < 1000; i++) {
    ctx.strokeRect(x, y, width, height)
}
ctx.restore
复制代码
  • 视口之外的图形不参与绘制

有许多场景只有部分元素在可视范围内, 视口外不可见的元素就可以跳过绘制

  • 只绘制变化的部分(脏矩形技术)

当屏幕上的元素发生变化只局限于一个区域时, 可以擦除这个区域的像素, 并重绘与这个区域重叠的图形, 而不用全屏重绘.

  • 对于不变和变动的图形分层渲染或使用cacheAsBitMap缓存技术
  • 大批量的数据分成多帧渲染

比如图表中大量的散点, 如果同时渲染需要等待数秒才能绘制完毕. 可以每帧渲染一部分, 直至将所有图形绘制完.

  • 使用多线程和离屏canvas分块瓦片渲染

将canvas分成多个瓦片, 在多个work线程中使用OffscreenCanvas渲染完后再渲染到对应的瓦片区域, 这种优化手段在地图应用中比较常见, 地图数据本身就是瓦片式存储的.

拾取性能优化

  • 使用包围盒加快判断

先判断点是否在其包围盒内, 如果不在包围盒内, 则直接排除. 如果在, 则使用更精确的几何或像素拾取判断.

  • 缓存图形的变换矩阵、逆矩阵

在拾取过程中, 需要反复计算图形的矩阵和逆矩阵. 缓存后可以减少计算量.

  • 使用R-tree等空间索引加速拾取过程

使用空间换取时间,通过建立R-tree索引, 加快拾取过程.

关于我们

OKee Design 创立于2019年,面向场景繁多、诉求复杂的企业级产品设计,旨在打造一套逻辑清晰、扩展性强大的设计系统。通过足够全面、通用、美观、灵活的内容,为各类产品设计提供强有力的解决方案及指导规则。

github组织: github.com/oceanengine

参考

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享