HTML5画布save()和restore()的性能

9

我遇到的问题是,在开发HTML5画布应用程序时,需要使用大量变换(如平移、旋转、缩放),因此需要调用许多context.save()和context.restore()方法。即使只绘制很少的内容,性能也会快速下降(因为在循环中尽可能多地调用save()和restore()方法)。是否有替代方法可以使用这些转换但仍然能够提高性能?谢谢!


如果你只需要重置转换矩阵,那么你只需要使用 ctx.setTransform(1,0,0,1,0,0)。请记住,每次调用 ctx.save() 都会将所有上下文属性堆叠起来,并且只有在调用 ctx.restore() 时才会释放它们,因此请确保对于每个 save(),都有一个相应的 restore() 被调用。 - Kaiido
谢谢您的建议,但我意识到转换本身是昂贵的。这一点我不太理解,因为它们应该被优化,以便能够在许多游戏中频繁地应用。 - TheMAAAN
嗯,转换不应该很耗费时间,我认为你需要向我们展示一个带有 [mcve] 的片段。 - Kaiido
1个回答

24

动画和游戏性能提示。

避免保存和还原状态

使用setTransform可以避免需要进行保存和还原状态。

有很多原因会导致保存和还原状态会导致速度变慢,这取决于当前的GPU和2D上下文状态。如果您将当前的填充和/或描边样式设置为大型图案,或者使用复杂的字体/渐变,或者使用过滤器(如果可用),那么保存和还原过程可能比呈现图像需要更长时间。

当编写动画和游戏时,性能是一切,对我来说,它与精灵计数有关。每帧(60分之一秒)我可以绘制的精灵越多,我就可以添加更多的特效,环境越详细,游戏就越好。

我让状态保持开放式,即我不保持对当前2D上下文状态的详细跟踪。这样我就永远不必使用保存和还原状态。

ctx.setTransform而不是ctx.transform

由于变换函数transform、rotate、scale、translate会使当前变换矩阵相乘,它们很少被使用,因为我不知道变换状态是什么。

为了处理未知状态,我使用setTransform来完全替换当前的变换矩阵。这也允许我在一次调用中设置比例和平移,而无需知道当前状态是什么。

ctx.setTransform(scaleX,0,0,scaleY,posX,posY); // scale and translate in one call

我也可以添加旋转功能,但是用于查找x、y轴向量(setTransform中的前4个数字)的JavaScript代码比旋转慢。

精灵和其渲染

下面是一个扩展的精灵函数。它从精灵表绘制精灵,精灵具有x和y缩放、位置和中心,并且我总是使用alpha来设置透明度。

// image is the image. Must have an array of sprites
// image.sprites = [{x:0,y:0,w:10,h:10},{x:20,y:0,w:30,h:40},....]
// where the position and size of each sprite is kept
// spriteInd is the index of the sprite
// x,y position on sprite center
// cx,cy location of sprite center (I also have that in the sprite list for some situations)
// sx,sy x and y scales
// r rotation in radians
// a alpha value
function drawSprite(image, spriteInd, x, y, cx, cy, sx, sy, r, a){
    var spr = image.sprites[spriteInd];
    var w = spr.w;
    var h = spr.h;
    ctx.setTransform(sx,0,0,sy,x,y); // set scale and position
    ctx.rotate(r);
    ctx.globalAlpha = a;
    ctx.drawImage(image,spr.x,spr.y,w,h,-cx,-cy,w,h); // render the subimage
}
在一般的计算机上,使用该函数可以以完整帧速率渲染1000+精灵。在Firefox(撰写时)中,我使用该函数获得了2000+(精灵是从1024 x 2048精灵表中随机选择的),最大精灵尺寸为256 x 256。
但我有超过15个这样的函数,每个函数都具有实现所需功能的最小功能。如果它从未旋转或缩放(用于UI),则...
function drawSprite(image, spriteInd, x, y, a){
    var spr = image.sprites[spriteInd];
    var w = spr.w;
    var h = spr.h;
    ctx.setTransform(1,0,0,1,x,y); // set scale and position
    ctx.globalAlpha = a;
    ctx.drawImage(image,spr.x,spr.y,w,h,0,0,w,h); // render the subimage
}

或者最简单的玩法精灵、粒子、子弹等。

function drawSprite(image, spriteInd, x, y,s,r,a){
    var spr = image.sprites[spriteInd];
    var w = spr.w;
    var h = spr.h;
    ctx.setTransform(s,0,0,s,x,y); // set scale and position
    ctx.rotate(r);
    ctx.globalAlpha = a;
    ctx.drawImage(image,spr.x,spr.y,w,h,-w/2,-h/2,w,h); // render the subimage
}

如果它是一个背景图片

function drawSprite(image){
    var s = Math.max(image.width / canvasWidth, image.height / canvasHeight); // canvasWidth and height are globals
    ctx.setTransform(s,0,0,s,0,0); // set scale and position
    ctx.globalAlpha = 1;
    ctx.drawImage(image,0,0); // render the subimage
}

常见的情况是游戏场地可以进行缩放、平移和旋转。为此,我维护了一个闭包变换状态(上面的所有全局变量都是封闭的变量,并且是渲染对象的一部分)。

// all coords are relative to the global transfrom
function drawGlobalSprite(image, spriteInd, x, y, cx, cy, sx, sy, r, a){
    var spr = image.sprites[spriteInd];
    var w = spr.w;
    var h = spr.h;
    // m1 to m6 are the global transform
    ctx.setTransform(m1,m2,m3,m4,m5,m6); // set playfield
    ctx.transform(sx,0,0,sy,x,y); // set scale and position
    ctx.rotate(r);
    ctx.globalAlpha = a * globalAlpha; (a real global alpha)
    ctx.drawImage(image,spr.x,spr.y,w,h,-cx,-cy,w,h); // render the subimage
}

以上所有内容都是关于实际游戏精灵渲染速度最快的技术。

一般提示

永远不要使用任何矢量类型的渲染方法(除非您有多余的帧时间),例如填充,描边,filltext,arc,rect,moveTo,lineTo,因为它们会立即减慢速度。如果需要呈现文本,请创建一个离屏画布,渲染一次,然后显示为精灵或图像。

图像尺寸和GPU RAM

创建内容时,始终使用图像尺寸的幂规则。GPU处理大小为2的幂的图像。 (2,4,8,16,32,64,128....)因此,宽度和高度必须是2的幂。即1024 x 512或2048 x 128是很好的大小。

如果您不使用这些大小,2D上下文并不在意,它所做的是将图像扩展到最接近的幂。因此,如果我有一个300x300的图像,为了适应GPU,图像必须扩展到最接近的幂,即512x512。因此,实际内存占用比您能够显示的像素大2.5倍以上。当GPU的本地内存用尽时,它将开始从主板RAM中切换内存,当这种情况发生时,帧速率会下降到无法使用。

确保您调整图像大小以便不浪费RAM,这意味着在撞到RAM壁之前,您可以将更多内容装入游戏中(对于较小的设备而言,RAM墙并不多)。

GC是主要的帧窃取者

最后一项优化是确保GC(垃圾收集器)几乎没有任何工作要做。在主循环中,避免使用new(重复使用对象而不是dereference它并创建另一个),避免从数组中推送和弹出(保持其长度不下降),保持活动项目的单独计数。创建自定义迭代器和推送函数,这些函数具有项目上下文感知性(知道数组项是否活动)。当您推送时,除非没有非活动项,否则不会推送新项,当项目变为非活动状态时,将其留在数组中,并稍后使用它(如果需要)。

有一个简单的策略,我称之为快速堆栈,超出了此答案的范围,但可以处理1000多个短暂的游戏对象,而不会产生任何GC负载。一些更好的游戏引擎使用类似的方法(池化数组提供非活动项目的池)。

GC应该不到您的游戏活动的5%,如果没有,请查找您在哪里不必要地创建和dereferencing。


谢谢,这提供了很多见解。然而,我正在创建的游戏(这可能听起来很奇怪),与其他游戏不同,使用小精灵很少,大多数使用路径(moveTo、arcTo、lineTo等)和stroke方法以及shadowBlur等属性进行发光效果和其他效果。换句话说,我的游戏主要需要本地功能,而不是精灵或精灵表。因此,为了获得更强的发光效果,我会重新绘制路径并用设置为某个值的shadowBlur多次描边,基本上做这些事情。我的性能下降实际上与此有关。 - TheMAAAN
@TheMAAAN 绝大部分的图像都是使用SVG源图像和渲染脚本完成的。您可以根据设备适当的分辨率在客户端上按需呈现精灵表。这也允许您跳过低内存和刷新率设备的动画帧。如果需要颜色变化,您可以预先渲染3个RGB精灵并使用组合轻化混合它们。渐变、发光、阴影等都是低分辨率的16*16像素,并且具有双线性过滤的模糊效果。与矢量相比,位图渲染提供了如此多的性能优势,值得一试。 - Blindman67
那么你的意思是所有的缩放都是使用SVG完成的,因此质量不会受到影响,然后在实际游戏中呈现为位图?你说的渐变、发光和低分辨率是什么意思? - TheMAAAN
1
你所有函数的最后一行中使用的 ctx.drawSprite() 方法是什么?这应该是 ctx.drawImage() 吗? - nnnnnn
1
@camillo777 我们设置的变换矩阵被应用于在画布上绘制的内容,而不是画布本身发生变换。 - Blindman67
显示剩余10条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接