在画布上绘制 10,000 个对象 javascript

IT技术 javascript html performance canvas drawimage
2021-03-03 15:48:21

我需要在画布上绘制超过 10,000 张图像(32x32 像素),但绘制超过 2000 张,性能非常差。

这是一个小例子:

对象结构 {position:0}

for(var nObject = 0; nObject < objects.length; nObject++){
    ctx.save();
    ctx.translate(coords.x,coords.y);
    ctx.rotate(objects[nObject].position/100);
    ctx.translate(radio,0);
    ctx.drawImage(img,0,0);
    ctx.restore();
    objects[nObject].position++;
}

使用此代码,我对坐标周围的图像进行了转换。

你有什么建议来提高性能?

更新:

我尝试分层但性能变差

http://jsfiddle.net/72nCX/3/

5个回答

我可以给你 10,000,但有两个主要缺点。

  1. 您可能会注意到图像并不完全尊重透明度,可以修复……但这超出了本答案的范围。

  2. 您将不得不使用数学来进行任何类型的转换,因为标准画布转换矩阵不能应用于 ImageData

现场演示

代码和方法说明

因此,要使用画布和大量对象获得最快的性能,您需要使用ImageData这基本上是在每个像素级别访问画布元素,并允许您做各种很酷的事情。我使用了两种主要方法。

此外,这里还有一个不错的教程,可以对其进行一些介绍,以帮助您更好地理解。

所以我首先为图像创建了一个临时画布

imgToDraw.onload = function () {
    // In memory canvas
    imageCanvas = document.createElement("canvas"),
    iCtx = imageCanvas.getContext("2d");

    // set the canvas to the size of the image
    imageCanvas.width = this.width;
    imageCanvas.height = this.height;

    // draw the image onto the canvas
    iCtx.drawImage(this, 0, 0);

    // get the ImageData for the image.
    imageData = iCtx.getImageData(0, 0, this.width, this.height);
    // get the pixel component data from the image Data.
    imagePixData = imageData.data;

    // store our width and height so we can reference it faster.
    imgWidth = this.width;
    imgHeight = this.height;

    draw();
};

Next 是渲染函数中的主要部分

我只是发布相关部分。

// create new Image data. Doing this everytime gets rid of our 
// need to manually clear the canvas since the data is fresh each time
var canvasData = ctx.createImageData(canvas.width, canvas.height),
    // get the pixel data
    cData = canvasData.data;

// Iterate over the image we stored 
for (var w = 0; w < imgWidth; w++) {
    for (var h = 0; h < imgHeight; h++) {
        // make sure the edges of the image are still inside the canvas
        // This also is VERY important for perf reasons
        // you never want to draw outside of the canvas bounds with this method
        if (entity.x + w < width && entity.x + w > 0 &&
            entity.y + h > 0 && entity.y + h < height) {

            // get the position pixel from the image canvas
            var iData = (h * imgWidth + w) * 4;

            // get the position of the data we will write to on our main canvas
            // the values must be whole numbers ~~ is just Math.floor basically
            var pData = (~~ (entity.x + w) + ~~ (entity.y + h) * width) * 4;

            // copy the r/g/b/ and alpha values to our main canvas from 
            // our image canvas data.

            cData[pData] = imagePixData[iData];
            cData[pData + 1] = imagePixData[iData + 1];
            cData[pData + 2] = imagePixData[iData + 2];
            // this is where alpha blending could be applied
            if(cData[pData + 3] < 100){
                cData[pData + 3] = imagePixData[iData + 3];
            }
        }
    }
}

// now put all of that image data we just wrote onto the actual canvas.
ctx.putImageData(canvasData, 0, 0);

主要的收获是,如果您需要在画布上绘制大量您无法使用的对象drawImage,像素操作是您的朋友。

@Loktar,+1 不错的解决方案。我特别喜欢你对 getImageData+putImageData 的有效使用,它经常被误用导致性能下降。
2021-04-24 15:48:21
哇!这真太了不起了!我对 putImageData 和 createImageData 不是特别熟悉,但现在我有一些新东西要学习。
2021-04-28 15:48:21
谢谢@NevinMadhukarK 像画布这样的问题是我最喜欢尝试解决的问题。您的回答也很好,我应该查看 Kinect.js 以了解它们如何处理渲染。
2021-05-05 15:48:21
如果您不关心透明度,则可以Uint32Array通过在ArrayBuffers用于andUint8Clamped数组的底层上创建视图并一次复制 32 位来提高性能。这稍微快一点。cDataImagePixData
2021-05-07 15:48:21
此外,代码受内存限制,因此不是将四个浮点值放在 JS 对象中,而是将所有数据放在 Float64Array 中。把它们放在一起,你可以(在我的笔记本电脑中)将精灵的 nr 增加到 50000,并且仍然更快:jsfiddle.net/vanderZwan/ddg1kpfr/1
2021-05-21 15:48:21

我认为就是你所需要的。

Eric Rowell(KineticJS 的创建者)在这里做了一些压力测试。

他是这样说的:

“创建 10 个图层,每个图层包含 1000 个形状,以创建 10,000 个形状。这极大地提高了性能,因为从图层中删除圆形而不是所有 10,000 个形状时,一次只需绘制 1,000 个形状。”

“请记住,层数过多也会降低性能。我发现使用 10 个层,每个层由 1,000 个形状组成,比使用 500 个形状的 20 层或具有 2,000 个形状的 5 层性能更好。”

更新:您需要运行最适合您的最优化程序的测试用例。示例:10000 个形状可以通过以下任一方式实现:

10000 个形状 * 1 层

5000 个形状 * 2 层

2500个形状* 4层

哪个适合你,选择那个!这取决于您的代码。

是的(1 层上有 1000 个形状)而不是(1 层上有 10000 个形状)的 10 倍。
2021-04-25 15:48:21
@NevinMadhukarK。请注意,KineticJS 示例只是重新定位了对象的 x,y。提问者试图每秒保存+转换+恢复上下文 10,000 次。那次尝试超出了canvas的能力。
2021-04-28 15:48:21
@markE 我想他后来添加了转换功能?我不确定。或者我一定错过了。
2021-05-08 15:48:21
我创建一个测试并告诉你。
2021-05-20 15:48:21
如果我理解正确,最好为每 1,000 个要打印的对象创建一个画布元素?
2021-05-21 15:48:21

您可以执行以下一些步骤来提高性能:

  • 首先摆脱save/ restore- 它们是非常昂贵的调用,可以替换为setTransform
  • 展开循环以在每次迭代中执行更多操作
  • 缓存所有属性

小提琴

循环展开 4 次迭代的示例:

for(var nObject = 0,
        len = objects.length,    // cache these
        x = coords.x,
        y = coords.y; nObject < len; nObject++){

    ctx.setTransform(1,0,0,1, x, y);   // sets absolute transformation
    ctx.rotate(objects[nObject].position*0.01);
    ctx.translate(radio,0);
    ctx.drawImage(imgToDraw,0,0);
    objects[nObject++].position++;

    ctx.setTransform(1,0,0,1,x, y);
    ctx.rotate(objects[nObject].position*0.01);
    ctx.translate(radio,0);
    ctx.drawImage(imgToDraw,0,0);
    objects[nObject++].position++;

    ctx.setTransform(1,0,0,1,x, y);
    ctx.rotate(objects[nObject].position*0.01);
    ctx.translate(radio,0);
    ctx.drawImage(imgToDraw,0,0);
    objects[nObject++].position++;

    ctx.setTransform(1,0,0,1,x, y);
    ctx.rotate(objects[nObject].position*0.01);
    ctx.translate(radio,0);
    ctx.drawImage(imgToDraw,0,0);
    objects[nObject++].position++;
}
ctx.setTransform(1,0,0,1,0,0);  // reset transform for rAF loop

(不过不要指望实时性能)。

虽然,在这么小的区域内绘制 2000 个对象可能有点毫无意义。如果您追求效果,我会建议使用这种方法:

  • 创建一个离屏画布
  • 用上面的方法制作5-8帧并存储为图像
  • 按原样播放那 5-8 张图像,而不是进行所有计算

如果您需要更流畅的外观,只需生成更多帧。您可以根据稍后用作精灵表的单元格将每个帧存储在单个画布中。绘图时,您当然必须注意当前位置是静态的,而不是实际动画时的移动位置。旋转和由此产生的位置是另一个因素。

经过各种测试,我得出以下结论:

  • canvas 没有执行此任务的能力。
  • 分层画布仅在不需要不断重绘静态元素时有助于提高性能。
  • 添加坐标打印限制对渲染有很大帮助。
  • 慢功能的替代方案
  • 不要打印元素最终会被另一个具有更高 z-index 的元素隐藏(正在处理它)。

最终结果是所有贡献的小混合。但需要改进。

用 30,000 个对象进行测试,性能保持在 60/fps。

http://jsfiddle.net/NGn29/1/

        var banPrint = true;
        for(nOverlap = nObject; nOverlap < objects.length; nOverlap++){
            if(
                objects[nOverlap].position == objects[nObject].position
                && nOverlap != nObject
            ){
                banPrint = false;
                break;
            }
        }
Canvas 在一定程度上是有能力的。在某些条件下做起来有点棘手,但可能。
2021-05-17 15:48:21

如果图像不重叠,则生成的图像为 3200x3200 像素,这超出了大多数显示器的显示能力。因此,您可以尝试获取转换图像的边界框并跳过可见区域之外的边界框(即使画布应该已经为您完成了)。

另一个想法是将小图像组合成更大的图像,并将它们作为一个组转换在一起。

如果您想将图像组织成一个环,那么您可以将它们绘制为一个环,将其保存为一个图像,然后旋转“环图像”而不是每个单独的图像。

最后,看看可能比 2D canvasAPI更有效的 WebGL

我喜欢你最后的提示。我会努力实现它。至于 WebGL,我有最后一个选择,但没有忘记。非常感谢你。
2021-04-29 15:48:21
@markE:我的猜测是他会将它们排列在一个正方形中(大多数情况下),这样每个轴或 100 个,而不是 1000 个是 sqrt(10000) 个图像。
2021-04-29 15:48:21
如果旋转环,图像可能看起来有颗粒感。如果是这种情况,请使用 2x-4x 最终分辨率(即渲染 128x128px)渲染中间图像,然后缩小图像。这会给你一个很好的抗锯齿,而且成本很低。
2021-05-07 15:48:21
@AaronDigulla :-) 只是说……32x32 乘以 10,000 张图像是 32,000 x 32,000,而 WebGL 是画布(3D 上下文)。+1 用于将类似的转换和缓存分组为图像。
2021-05-19 15:48:21