English 中文(简体)
How can I improve the performance of my HTML5 2D canvas?
原标题:

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?

问题回答

GPU State changes

Nice APP "biome gridmap playground" :)

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("2d", {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 > 0 && width > 0 && height > 0
    // Assumes rawMap rows and columns match height and width
    // Assumes sizes rawMap.length === height && 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("canvas"), {width, height});  // create working canvas
        const wCtx = wCanvas.getContext("2d", {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 < 255; i ++) { pxLu[i] = 0xFF000000 | (i << 16) | (i << 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("2d");
    }

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.





相关问题
selected text in iframe

How to get a selected text inside a iframe. I my page i m having a iframe which is editable true. So how can i get the selected text in that iframe.

How to fire event handlers on the link using javascript

I would like to click a link in my page using javascript. I would like to Fire event handlers on the link without navigating. How can this be done? This has to work both in firefox and Internet ...

How to Add script codes before the </body> tag ASP.NET

Heres the problem, In Masterpage, the google analytics code were pasted before the end of body tag. In ASPX page, I need to generate a script (google addItem tracker) using codebehind ClientScript ...

Clipboard access using Javascript - sans Flash?

Is there a reliable way to access the client machine s clipboard using Javascript? I continue to run into permissions issues when attempting to do this. How does Google Docs do this? Do they use ...

javascript debugging question

I have a large javascript which I didn t write but I need to use it and I m slowely going trough it trying to figure out what does it do and how, I m using alert to print out what it does but now I ...

Parsing date like twitter

I ve made a little forum and I want parse the date on newest posts like twitter, you know "posted 40 minutes ago ","posted 1 hour ago"... What s the best way ? Thanx.

热门标签