如何提高我的HTML5 2D画布的性能?

huangapple go评论69阅读模式
英文:

How can I improve the performance of my HTML5 2D canvas?

问题

我做了一个名为"生态格图游乐场"的应用程序,它可以帮助设计2D格图的生态系统,使用了simplex噪声。算法大致如下:对于每个格图瓦片,我们查看此坐标处的噪声值(湿度和高度),确定瓦片的颜色,然后在画布上渲染它。当存在数百个瓦片时,这非常慢。此外,渲染画布会阻塞主线程,因此UI变得非常烦人。我认为可以通过使用Web Workers 来解决这个问题。然而,这并不会解决我的主要问题:画布渲染似乎很慢。我想知道是否使用threejs可以提高性能?或者也许有更智能的算法可以实现?

英文:

I've made a "biome gridmap playground" app which help to design biomes for a 2D grid map using simplex noise. The algorithm is roughly this: for each grid map tile, we look at the noises values (moisture and height) at this coordinate, determine the color of the tile and then render it on the canvas. It is very slow when there are hundreds of tiles. Also, rendering the canvas blocks the main thread and therefore the UI, which is very annoying. I think this could be solved by using Web Workers. However, it would not fix my main issue: canvas rendering seems to be slow. I'm wondering if using threejs could improve performances? Or maybe there is a smarter algorithm I could implement?

答案1

得分: 1

GPU状态更改

很好的应用程序 "生物网格地图游乐场" 如何提高我的HTML5 2D画布的性能?

通过为每个图块设置 ctx.fillStyle 来更改GPU状态会浪费大量时间。

GPU状态更改是所有使用GPU的应用程序(甚至原生应用程序)减速的主要原因。始终尽量避免GPU状态更改,因为它们是邪恶的。

使用CPU

与其使用2D API来填充图块(gridMap),不如直接使用CPU更改图像像素。

使用选项创建2D API canvas.getContext("2d", {willReadFrequently: true}) 这将禁用2D API的GPU(因为我们将不使用它)

使用 ctx.getImageData 获取像素。数据包含原始像素数据。

然后,您可以直接写入图像数据缓冲区,从而避免在过程中进行所有状态更改。

示例

drawRawMap 为例。

drawRawMap 的详细信息

  • 从您的Git并使用JS而不是TS。
  • 替换两个函数 drawRawMapget2DCanvas
  • 创建工作画布 wCanvas,大小为1,以填充B/W像素
  • 获取 imageData 并使用 Uint32Array 的视图 d32 以每像素进行单一写入。
  • 创建查找表 pxLu 以将原始的0 - 255值转换为灰度像素
  • 绘制像素并将新像素放回工作画布。
  • 获取屏幕画布,并将工作画布按照图块大小缩放到上面。
  • 完成。

这将比现有代码快一个数量级(至少)。

您可以对其他渲染调用采用相同的方法。绘制到工作画布的像素,使用查找表获取像素颜色。使用图块大小1来避免需要设置多个像素,以及在绘制结果到显示画布时按照 tilesize 进行缩放。

// 假设tileSize > 0 && width > 0 && height > 0
// 假设rawMap的行和列与height和width匹配
// 假设sizes rawMap.length === height && rawMap[0 to height - 1].length === width
drawRawMap(name, rawMap, width, height, tilesize) {
    
    // 接下来的4行最好只在需要时执行(例如宽度或高度更改)
    const wCanvas = Object.assign(document.createElement("canvas"), {width, height});  // 创建工作画布
    const wCtx = wCanvas.getContext("2d", {willReadFrequently: true});    
    const imgData = wCtx.getImageData(0, 0, width, height);    
    const d32 = new Uint32Array(imgData.data.buffer); // 获取像素的32位整数视图

    // 接下来的2行最好只执行一次
    const pxLu = new Uint32Array(256);   // 查找灰度像素
    for (let i = 0; i < 255; i ++) { pxLu[i] = 0xFF000000 | (i << 16) | (i << 8) | i; } 
    
    // 将rawMap绘制到32位像素视图d32中
    var idx = 0;
    for (const row of rawMap) {  // 假设行
        for (const val of row) {  // 每列的值
            d32[idx++] = pxLu[(val + 1) * 0.5 * 255 | 0]; // 假设val -1到1转换为0 -255,| 0 强制整数
        }
    }
    wCtx.putImageData(imgData, 0, 0);  // 将像素移动到工作画布
    
    // 将工作画布绘制到显示画布。
    const ctx = this.get2DCanvas(name, width, height, tilesize);
    if (!ctx) { return; /* 致命错误 */ }
    ctx.imageSmoothingEnabled = false;
    ctx.drawImage(wCanvas, 0, 0, width * tilesize, height * tileSize);
    ctx.imageSmoothingEnabled = true;
}


get2DCanvas(name, width, height, tilesize, gap = 0) {
    const canvas = document.getElementById(name);
    canvas.width = width * (tilesize + gap);
    canvas.height = height * (tilesize + gap);
    return canvas.getContext("2d");
}

避免阻塞代码

使用工作线程

您可以通过工作线程获得改进,但由于在离屏画布之间移动数据存在许多注意事项,因此这将会变得复杂。

使用生成器

使用生成器函数,您可以将任务分成部分(例如,按行)。使用 yield 来停止生成器函数的执行(而不会丢失其上下文),并让UI接管。

可以使用计时器显示结果(例如 requestAnimationFrame),因为数据正在创建。计时器只是调用生成器的 next() 来处理下一部分(行)。

如果生成器每部分(yield标记)少于16毫秒,用户将不会体验到任何延迟。

显示进度的生成器的示例

这将防止任务阻塞UI,并允许用户看到解决方案的进度(或等待完成以显示结果)。

噪声

我没有查看您的simplex noise的源代码,以下是对原始的 open simplex noise的一个分支,性能比原始版本有很大提升。

英文:

GPU State changes

Nice APP "biome gridmap playground" 如何提高我的HTML5 2D画布的性能?

You are wasting a lot of time changing GPU state by setting ctx.fillStyle for each tile.

GPU State changes are a major source of slowdown for all apps that use the GPU (even native apps) Always go out of your way to avoid GPU state changes as they are evil.

Use CPU

Rather than use the 2D API to fill tiles (gridMap) change the image pixels directly using the CPU.

Create the 2D API with option canvas.getContext(&quot;2d&quot;, {willReadFrequently: true}) this will disable the GPU for the 2D API (as we will not be using it)

Get the pixels using ctx.getImageData. The data contains the raw pixel data.

You can then write directly to the image data buffer and avoid all state changes in the process.

Example

Using drawRawMap as an example.

Details of drawRawMap

  • From your git and using JS rather than TS.
  • Replacing two functions drawRawMap and get2DCanvas.
  • Creates working canvas wCanvas with tile size 1 to fill with B/W pixels
  • Gets imageData and uses view of Uint32Array d32 to have single write per pixel.
  • Creates a lookup table pxLu to convert raw 0 - 255 values to gray scale pixels
  • Draws pixels and puts the new pixels back on the working canvas.
  • Get on screen canvas and draws working canvas scaled by tile size onto it.
  • All done.

This will be an order of magnitude faster (at least) than the existing code.

You can do the same with other rendering calls. Draw to a working canvas pixels', use lookup table to get pixels colors. Use tile size 1 to avoid needing to set more than one pixel per tile, and scale by tilesize when drawing the result to display canvas.

    // Assumes tileSize &gt; 0 &amp;&amp; width &gt; 0 &amp;&amp; height &gt; 0
// Assumes rawMap rows and columns match height and width
// Assumes sizes rawMap.length === height &amp;&amp; rawMap[0 to height - 1].length === width
drawRawMap(name, rawMap, width, height, tilesize) {
// Next 4 lines best done only when needed (eg width or height change)
const wCanvas = Object.assign(document.createElement(&quot;canvas&quot;), {width, height});  // create working canvas
const wCtx = wCanvas.getContext(&quot;2d&quot;, {willReadFrequently: true});    
const imgData = wCtx.getImageData(0, 0, width, height);    
const d32 = new Uint32Array(imgData.data.buffer); // get 32 bit int view of pixels
// Next 2 lines best done once
const pxLu = new Uint32Array(256);   // Lookup gray scale pixels
for (let i = 0; i &lt; 255; i ++) { pxLu[i] = 0xFF000000 | (i &lt;&lt; 16) | (i &lt;&lt; 8) | i; } 
// draw rawMap into 32bit pixel view d32
var idx = 0;
for (const row of rawMap) {  // assumes rows
for (const val of row) {  // val for each column
d32[idx++] = pxLu[(val + 1) * 0.5 * 255 | 0]; // assumes val -1 to 1 convert to 0 -255, the | 0 forces integer
}
}
wCtx.putImageData(imgData, 0, 0);  // move pixels to work canvas
// draw working canvas onto display canvas.
const ctx = this.get2DCanvas(name, width, height, tilesize);
if (!ctx) { return; /* Fatal error */ }
ctx.imageSmoothingEnabled = false;
ctx.drawImage(wCanvas, 0, 0, width * tilesize, height * tileSize);
ctx.imageSmoothingEnabled = true;
}
get2DCanvas(name, width, height, tilesize, gap = 0) {
const canvas = document.getElementById(name);
canvas.width = width * (tilesize + gap);
canvas.height = height * (tilesize + gap);
return canvas.getContext(&quot;2d&quot;);
}

Avoid blocking code

Use Workers

You can get improvement via workers but it will be complicated as moving data between offscreen canvases has many caveats.

Use Generators

Using a generator function you can split the task into sections (eg per row). Using yield to stop the generator functions' execution (without dumping its context) and let UI have a go.

You can display the result using a timer (eg requestAnimationFrame) as the data is created. The timer just calls next() on the generator to process the next section (row).

If the generator is less than 16ms per section (yield token) the user will experience zero lag.

An example of a generator to show progress to a solution.

This will prevent the task from blocking the UI and let the user see the progress to the solution (or wait till done to show result).

Noise

I did not look at the source of your simplex noise, the following is a fork of open simplex noise that has a good performance increase on the original.

huangapple
  • 本文由 发表于 2023年6月5日 23:49:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/76408085.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定