当前位置: 首页 > 科技观察

Canvas性能优化:脏矩形渲染

时间:2023-03-17 11:49:12 科技观察

大家好,我是前端西瓜哥。在使用Canvas作为图形编辑器时,我们需要维护自己的图形树来保存图形的信息,定义元素之间的关系。我们改变画布中的某个图形来更新画布,最简单的方法就是清空画布,然后根据图形树重新绘制所有图形,在图形较少的情况下是没有问题的。但是如果图形很多,画的时候可能会卡住。那么,有什么办法可以优化吗?是的,脏矩形渲染。画布应该如何更新?这里我们假设一个场景,在画布上的随机位置绘制了大量的绿球,然后在最上面一层绘制了一个红球。现在我们想让红球随着光标移动,下面的绿球保持不变。我们如何更新它?首先我们先不考虑Canvas的分层方式,因为我这里为了方便使用了一个比较简单的场景。实际场景会比较复杂,通常是用光标选中一个元素并拖动,这就涉及到图形拾取的实现,元素会在任意层级。这里为了重点更新,去掉了这些不相关的点。OK,回到正题,想想怎么更新?一个容易想到的解决方案是更新整个体积,当鼠标移动时重新绘制所有的球。前面说了,当球数少的时候这不是问题,但是如果图形数逐渐增加,达到一定数量,就会出现GPU瓶颈,出现丢帧现象。因为很多图形是在很短的时间内画出来的。另一种解决方法是本文主题用脏矩形渲染,本质上是局部重绘。在讲解脏矩形渲染原理之前,我们先了解几个概念。脏矩形:改变图形的物理信息后,需要重新渲染的矩形区域通常是目标图形的当前帧和下一帧组成的边界框。边界框:包围图形的最小矩形。通常用作低成本的碰撞检测。因为矩形的碰撞检测算法简单高效,而复杂图形的碰撞检测复杂低效。脏矩形渲染简单来说就是计算两帧变化的目标图形生成的边界框(脏矩形),清空该区域,然后重绘该区域内所有与脏矩形相交的图形。对于红球移动的场景,具体逻辑是:计算当前帧和下一帧红球形成的边界框,这个边界框就是脏矩形。遍历绿球的物理信息,计算它们的边界框,取出与脏矩形相交的绿球。清空脏矩形区域。将dirtyrectangle设置为裁剪区域,这样就只能在dirtyrectangle中绘制。绿球按顺序抽,红球最后抽。顺序是为了保证层级是正确的。与全图相比,部分绘制可以有效减少需要绘制的图形数量,减少对GPU绘图指令的调用,从而提高渲染性能。这里还有一个优化点,就是减少遍历图形的数量,可以通过使用四叉树碰撞检测来优化。具体读者可以自行上网搜索,稍后我会写一篇文章进行说明。脏矩形渲染的具体实现可以看这个在线demo:https://codesandbox.io/s/1jr5lj。其中有如下一段代码,可以通过注释和反注释来选择“全局渲染”或者“脏矩形渲染”。canvas.addEventListener("mousemove",(e)=>{constx=e.clientX;consty=e.clientY;//重新渲染所有(性能不佳)//ctx.clearRect(0,0,canvasWidth,canvasHeight);//drawGreenBalls(greenBalls);//drawRedBall(x,y);//部分重新渲染(良好的性能)partRender(x,y);});另外,可以通过greenBallCount变量设置绿球的数量,测试性能上限。然后说说涉及到的一些简单的算法,可以在我的github项目中找到:https://github.com/F-star/graphics-algorithm。TypeScript类型:导出接口IPoint{x:number;y:数字;}导出接口IRect{x:数字;y:数字;宽度:数字;height:number;}/***IRect数组,数组长度必须大于等于1*/exporttypeINoEmptyArray=[T,...T[]];exporttypeIBox=IRect;exportinterfaceICircle{x:数字;y:数字;radius:number;}(1)找到多个圆形状组成的边界框该算法用于两帧红球构成的边界框,即脏矩形。并计算绿球的边界框。/***多个圆形状组成的包围盒*/exportfunctiongetCircleBBox(...circles:INoEmptyArray):IBox{//TODO:constrects:IRect[]=circles.map((circle)=>{const{x,y,radius}=圆;constd=radius*2;return{x:x-radius,y:y-radius,width:d,height:d,};});returngetRectBBox(...(rectsasINoEmptyArray));}/***多个形状组合的包围盒*/exportfunctiongetRectBBox(...rects:INoEmptyArray):IBox{constfirst=矩形[0];让x=first.x;让y=first.y;让x2=x+first.width;让y2=y+first.height;for(leti=1,len=rects.length;ix2){x2=_x2;}const_y2=rect.y+rect.height;如果(_y2>y2){y2=_y2;}}返回{x,y,宽度:x2-x,height:y2-y,};}(2)多个矩形是否相交(碰撞)该算法用于找出与脏矩形碰撞的绿球/***矩形是否相交*/导出函数isRectIntersect(rect1:IRect,rect2:IRect){return(rect1.x<=rect2.x+rect2.width&&rect1.x+rect1.width>=rect2.x&&rect1.y<=rect2.y+rect2.height&&rect1.height+rect1.y>=rect2.y);}(3)计算特定范围内的随机坐标,生成大量随机绿球。函数getRandPos(w,h,offset){函数getRandInt(min,max){min=Math.floor(min);max=Math.ceil(max);返回Math.floor(Math.random()*(max-min+1)+min);}constx=getRandInt(0+offset,w-offset);consty=getRandInt(0+offset,h-offset);return{x,y};}性能测试主要是看fps。我们先开启浏览器的fps监控。然后选择此项以打开fps监控。以3300个绿球为例,快速移动光标,使红球不断变换位置。对于我的设备,测试结果如下。在使用dirtyrectangle渲染的情况下,除了一开始就初始化所有必要的渲染外,fps可以稳定在满帧数59.4,没有波动(不同显示器的fullFPS不同)。后来改成30000,结果还是稳定在59.4。主要原因是两个移动框形成的脏矩形太小,所以重绘的图形数量其实并不多。如果脏矩形变大,渲染性能会下降。当脏矩形变成画布大小时,实际上退化为全局渲染。全局渲染降到37.8fps,还是3300的情况,最后的脏矩形渲染其实是局部渲染。它找到图形将更改并更新的区域(脏矩形)。在这个区域之外,它保持不变。找到与脏矩形相交的所有图形,并在该区域更新它们。