Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canvas 滤镜的性能优化 #17

Open
akira-cn opened this issue Aug 23, 2019 · 1 comment
Open

Canvas 滤镜的性能优化 #17

akira-cn opened this issue Aug 23, 2019 · 1 comment

Comments

@akira-cn
Copy link
Owner

最近几天没有及时更新,是因为这几天在忙一个项目mesh.js,这个项目是一个基于Canvas2D和WebGL的跨平台图形系统,提供底层的高性能API,同时也将是未来新版SpriteJS的底层渲染引擎。

这个项目目前主要API已经全部实现,在整理文档、撰写测试的收尾阶段,有兴趣的同学可以关注它。今天主要讲我们在开发过程中有针对地做的性能优化中的一个小点:Canvas的滤镜优化。

我们知道Canvas是支持滤镜的,几乎支持所有与CSS3滤镜一致的滤镜效果,包括:

  • url
  • blur
  • brightness
  • contrast
  • drop-shadow
  • grayscale
  • hue-rotate
  • invert
  • opacity
  • saturate
  • sepia

要使用这些滤镜也很简单,直接给context设置filter属性即可:

const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');

context.filter = 'blur(5px)';
context.fillStyle = 'blue';
context.beginPath();
context.arc(80, 80, 50, 0, Math.PI * 2);
context.fill();

这样就绘制出一个带有模糊滤镜的圆形。

👉🏻 但是,滤镜是一种比较消耗性能的操作,尤其是类似于blur,drop-shadow这样的滤镜,更是消耗性能。如果我们要绘制多个图形,应用同样的blur滤镜,就会明显感到性能的消耗。

我们可以通过例子对比一下:

假设我们要在画布上绘制并刷新200个随机的蓝色小圆点,代码也比较简单:

const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');

const count = 200;

// context.filter = 'blur(5px)';
context.fillStyle = 'blue';

function render(context) {
  context.clearRect(0, 0, 512, 512);
  for(let i = 0; i < count; i++) {
    const pos = [Math.random() * 512, Math.random() * 512];
    context.beginPath();
    context.arc(...pos, 10, 0, Math.PI * 2);
    context.fill();
  }
}

render(context);

requestAnimationFrame(function update() {
  render(context);
  requestAnimationFrame(update);
});

上面的代码是不带滤镜的效果,可以看到在普通的笔记本电脑的浏览器上,帧率也能轻松达到60fps。

如果我们加一个blur滤镜,把上面代码注释掉的代码的注释去掉让它生效

context.filter = 'blur(5px)';

看到帧率在我的电脑上已经下降到15帧一下,而且电脑风扇猛转。

所以如果我们要绘制多个相同滤镜的图形,普通的设置滤镜的方法,会导致性能开销特别大。那么除了谨慎使用滤镜外,有没有什么办法优化性能呢?

实际上,对这个case,因为连续绘制相同滤镜的图形,我们可以考虑将滤镜绘制合并起来,具体做法是:

  1. 先绘制不带滤镜的图片到一个干净的缓冲canvas上
  2. 将滤镜设置到我们要绘制的目标canvas上
  3. 用drawImage将图片从缓冲canvas绘制到目标canvas上
  4. 将滤镜设置从目标canvas上取消(以继续绘制其他内容,如果有的话)

我们看一下修改后的代码:

const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');

const count = 200;

const tempCanvas = new OffscreenCanvas(canvas.width, canvas.height);
const tempContext = tempCanvas.getContext('2d');

tempContext.fillStyle = 'blue';

function render(context, tempContext) {
  tempContext.clearRect(0, 0, 512, 512);
  for(let i = 0; i < count; i++) {
    const pos = [Math.random() * 512, Math.random() * 512];
    tempContext.beginPath();
    tempContext.arc(...pos, 10, 0, Math.PI * 2);
    tempContext.fill();
  }
  context.clearRect(0, 0, 512, 512);
  context.filter = 'blur(5px)';
  context.drawImage(tempContext.canvas, 0, 0);
  context.filter = 'none';
}

render(context, tempContext);

requestAnimationFrame(function update() {
  render(context, tempContext);
  requestAnimationFrame(update);
});

在上面的代码里,我们创建了一个用来绘制不带滤镜的图片的OffscreenCanvas,然后先将图形绘制到这个tempCanvas上,绘制完成后,再将tempCanvas整个图像以带滤镜的方式绘制到原canvas上,这样,我们把多次计算滤镜的操作合并到了1次,从而大大提升了性能。

我们看到,这么做之后,帧率回到了60fps。

当然,这种做法实际上只是一种近似绘制,严格上来说,两种绘制是有区别的,但是在blur滤镜上基本是没有问题的,而在其他个别滤镜,例如drop-shadow滤镜,实际上是有问题的,我们用两种写法看一下:

const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');


context.filter = 'drop-shadow(5px 5px)';


function render(context) {
  context.clearRect(0, 0, 512, 512);
  context.fillStyle = 'blue';
  context.beginPath();
  context.rect(100, 100, 100, 100);
  context.fill();
  context.fillStyle = 'red';
  context.beginPath();
  context.rect(50, 50, 100, 100);
  context.fill();
}

render(context);

const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');


const tempCanvas = new OffscreenCanvas(canvas.width, canvas.height);
const tempContext = tempCanvas.getContext('2d');

function render(context, tempContext) {
  tempContext.clearRect(0, 0, 512, 512);
  tempContext.fillStyle = 'blue';
  tempContext.beginPath();
  tempContext.rect(100, 100, 100, 100);
  tempContext.fill();
  tempContext.fillStyle = 'red';
  tempContext.beginPath();
  tempContext.rect(50, 50, 100, 100);
  tempContext.fill();
  context.clearRect(0, 0, 512, 512);
  context.filter = 'drop-shadow(5px 5px)';
  context.drawImage(tempContext.canvas, 0, 0);
  context.filter = 'none';
}

render(context, tempContext);

可以看到两个渲染结果并不一样,这是显然的,drop-shadow这样的滤镜,合并渲染会使得两个图形重叠的交界不会形成阴影,所以这种情况,如果要获得正确的效果,那就只能牺牲性能,不能合并滤镜了。

以上是今天讨论的内容,关于Canvas的滤镜优化,还有什么问题,欢迎在issue中讨论。

在后续文章中,我们有机会进一步讨论在WebGL上基于Shader实现的滤镜和性能的优化。

@huyong007
Copy link

滤镜如果通过getImageData之后对数组进行操作,并且canvas放大缩小过程中也会进行滤镜操作,非常耗时,这个有优化建议吗?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants