【问题标题】:How to Improve Html5 Canvas Performance如何提高 Html5 画布性能
【发布时间】:2021-10-14 09:41:58
【问题描述】:

所以我有一个我一直在做的项目,它的目标是在 2D 平面上随机生成地形,并在背景中放置雨,我选择使用 html5 画布元素来完成这个目标。创建它后,我对结果感到满意,但我遇到了性能问题,可以就如何修复它提出一些建议。到目前为止,我试图只清除需要的画布位,它位于我在地形下绘制的矩形上方以填充它,但正因为如此,我必须重新绘制圆圈。 rn(rain number) 已经降低了大约 2 倍,仍然滞后,有什么建议吗?

注意 - sn-p 中的代码不会因为它的小尺寸而滞后,但如果我以实际降雨数(800)全屏运行它,它会滞后。我缩小了值以适应 sn-p。

var canvas = document.getElementById('gamecanvas');
var c = canvas.getContext('2d');

var ma = Math.random;
var mo = Math.round;

var wind = 5;

var rn = 100;
var rp = [];

var tp = [];
var tn;

function setup() {
    
    //fillstyle
    c.fillStyle = 'black';

    //canvas size
    canvas.height = window.innerHeight;
    canvas.width = window.innerWidth;

    //rain setup
    for (i = 0; i < rn; i++) {
        let x = mo(ma() * canvas.width);
        let y = mo(ma() * canvas.width);
        let w = mo(ma() * 1) + 1;
        let s = mo(ma() * 5) + 10;
        rp[i] = { x, y, w, s };
    }

    //terrain setup
    tn = (canvas.width) + 20;
    tp[0] = { x: -2, y: canvas.height - 50 };
    for (i = 1; i <= tn; i++) {
        let x = tp[i - 1].x + 2;
        let y = tp[i - 1].y + (ma() * 20) - 10;
        if (y > canvas.height - 50) {
            y = tp[i - 1].y -= 1;
        }
        if (y < canvas.height - 100) {
            y = tp[i - 1].y += 1;
        }
        tp[i] = { x, y };
        c.fillRect(x, y, 4, canvas.height - y);
    }
}

function gameloop() {

    //clearing canvas
    for (i = 0; i < tn; i++) {
        c.clearRect(tp[i].x - 2, 0, 2, tp[i].y);
    }

    for (i = 0; i < rn; i++) {

        //rain looping
        if (rp[i].y > canvas.height + 5) {
            rp[i].y = -5;
        }
        if (rp[i].x > canvas.width + 5) {
            rp[i].x = -5;
        }

        //rain movement
        rp[i].y += rp[i].s;
        rp[i].x += wind;

        //rain drawing
        c.fillRect(rp[i].x, rp[i].y, rp[i].w, 6);
    }

    for (i = 0; i < tn; i++) {

        //terrain drawing
        c.beginPath();
        c.arc(tp[i].x, tp[i].y, 6, 0, 7);
        c.fill();
    }
}

setup();
setInterval(gameloop, 1000 / 60);
body {
    background-color: white;
    overflow: hidden;
    margin: 0;
}
canvas {
    background-color: white;
}
<html>
<head>
    <link rel="stylesheet" href="index.css">
    <title>A Snowy Night</title>
</head>
<body id="body"> <canvas id="gamecanvas"></canvas>
    <script src="index.js"></script>
</body>
</html>

【问题讨论】:

  • @Calculuswhiz 使用 2 个画布叠加(1 个用于地形,1 个用于下雨),您只需绘制一次地形
  • @Wax 我该怎么做?
  • @FireRed 与 CSS,请随时在下面的答案中查看。

标签: javascript performance canvas drawing processing-efficiency


【解决方案1】:

叠加画布

就像我在评论中建议的那样,使用第二个画布点只需要绘制一次地形,因此它可以通过在每个新帧上保存重绘来提高动画的性能。这可以通过将一个放在另一个上(如图层)使用 CSS 来完成。

#canvasBase {
  position: relative;
}

#canvasLayer1 {
  position: absolute;
  top: 0;
  left: 0;
}

#canvasLayer2 {
  position: absolute;
  top: 0;
  left: 0;
}

// etc...

另外我建议你在 setinterval (see why) 上使用requestAnimationFrame

requestAnimationFrame

但是,通过使用requestAnimationFrame我们不控制刷新率,它与客户端硬件相关联。所以我们需要处理它,为此,我们将使用 DOMHighResTimeStamp 作为参数传递给我们的回调方法。

我们的想法是让它以本机速度运行并通过仅在所需时间更新逻辑(我们的计算)来管理 fps。例如,如果我们需要fps = 60;,这意味着我们需要在每个1000 / 60 = ~16,67 ms 更新我们的逻辑。所以我们检查最后一帧时间的 deltaTime 是否等于或大于~16,67ms。如果没有足够的时间过去,我们调用一个新的框架并我们返回(重要的是,否则我们刚刚做的控制是无用的,因为无论结果如何,代码都会继续运行)。

let fps = 60;

/* Check if we need to update the logic */
/* if not request a new frame & return */

if(deltaLastUpdate <= 1000 / fps){ // 1000 / 60 = ~16,67ms
  requestAnimationFrame(animate);
  return;
}

清除画布

因为您需要清除所有过去的雨滴,所以最简单且最便宜的资源可以一口气清除整个上下文。

ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);

路径2D

由于您的绘图使用相同的雨滴颜色,您也可以将所有这些组合到一个路径中:

rainPath = new Path2D();
...

所以你只需要一条指令来绘制它们(与 clearRect 相同的资源保存类型):

ctxRain.fill(rainPath);

结果

/* CANVAS "Terrain" */
const terrainCanvas = document.getElementById('gameTerrain');
const ctxTerrain = terrainCanvas.getContext('2d');
terrainCanvas.height = window.innerHeight;
terrainCanvas.width = window.innerWidth;

/*  CANVAS "Rain" */
const rainCanvas = document.getElementById('gameRain');
const ctxRain = rainCanvas.getContext('2d');
rainCanvas.height = window.innerHeight;
rainCanvas.width = window.innerWidth;

/* Game Constants */
const wind = 5;
const rainMaxParticules = 100;
const rain = [];
let rainPath;
const terrainMaxParticules = terrainCanvas.width + 20;
const terrain = [];
let terrainPath;

/* Maths help */
const ma = Math.random;
const mo = Math.round;

/* Clear */
function clearTerrain(){
    ctxTerrain.clearRect(0, 0, terrainCanvas.width, terrainCanvas.height);
}
function clearRain(){
    ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);
}

/* Logic */
function initTerrain(){
    terrain[0] = { x: -2, y: terrainCanvas.height - 50 };
    for (let i = 1; i <= terrainMaxParticules; i++) {
        let x = terrain[i - 1].x + 2;
        let y = terrain[i - 1].y + (ma() * 20) - 10;
        if (y > terrainCanvas.height - 50) {
            y = terrain[i - 1].y -= 1;
        }
        if (y < terrainCanvas.height - 100) {
            y = terrain[i - 1].y += 1;
        }
        terrain[i] = { x, y };
    }
}

function initRain(){
    for (let i = 0; i < rainMaxParticules; i++) {
        let x = mo(ma() * rainCanvas.width);
        let y = mo(ma() * rainCanvas.width);
        let w = mo(ma() * 1) + 1;
        let s = mo(ma() * 5) + 10;
        rain[i] = { x, y, w, s };
    }
}

function init(){
  initTerrain();
  initRain();
}

function updateTerrain(){
    terrainPath = new Path2D();
    for(let i = 0; i < terrain.length; i++){
      terrainPath.arc(terrain[i].x, terrain[i].y, 6, Math.PI/2, 5*Math.PI/2);
    }
    terrainPath.lineTo(terrainCanvas.width, terrainCanvas.height);
    terrainPath.lineTo(0, terrainCanvas.height);
}

function updateRain(){
    rainPath = new Path2D();
    for (let i = 0; i < rain.length; i++) {
      // Rain looping
      if (rain[i].y > rainCanvas.height + 5) {
        rain[i].y = -5;
      }
      if (rain[i].x > rainCanvas.width + 5) {
        rain[i].x = -5;
      }
      // Rain movement
      rain[i].y += rain[i].s;
      rain[i].x += wind;
      
      // Path containing all the drops
      rainPath.rect(rain[i].x, rain[i].y, rain[i].w, 6);
    }
}

/* Drawing */
function drawTerrain(){
    ctxTerrain.fillStyle = 'black';
    ctxTerrain.fill(terrainPath);
}

function drawRain(){
    ctxRain.fillStyle = 'black';    
    ctxRain.fill(rainPath);
}

/* Animation Constant */
const fps = 60;
let lastTimestampUpdate;
let terrainDrawn = false;

/*  Game loop */
function animate(timestamp){

  /* Initialize rain & terrain particules */
  if(rain.length === 0 || terrain.length === 0){
    init();
  }

  /* Define "lastTimestampUpdate" from the first call */
  if (lastTimestampUpdate === undefined){
    lastTimestampUpdate = timestamp;
  }

  /* Check if we need to update the logic & the drawing, if not, request a new frame & return */
  if(timestamp - lastTimestampUpdate <= 1000 / fps){
    requestAnimationFrame(animate);
    return;
  }

  if(!terrainDrawn){
  /* Terrain --------------------- */
  /* Clear */
  clearTerrain();
  /* Logic */  
  updateTerrain();
  /* Draw */
  drawTerrain();
  /* ----------------------------- */
    terrainDrawn = true;
  }
  
  /* --- Rain -------------------- */
  /* Clear */
  clearRain();
  /* Logic  */ 
  updateRain();
  /* Draw */
  drawRain();
  /* ----------------------------- */
    
  /*  Request another frame */
  lastTimestampUpdate = timestamp;
  requestAnimationFrame(animate);

}

/*  Start the animation */
requestAnimationFrame(animate);
body {
    background-color: white;
    overflow: hidden;
    margin: 0;
}

#gameTerrain {
  position: relative;
}

#gameRain {
  position: absolute;
  top: 0;
  left: 0;
}
<body>  
  <canvas id="gameTerrain"></canvas>
  <canvas id="gameRain"></canvas>
</body>

一边

这不会影响性能,但是我鼓励您使用 const & let over var (What's the difference between using “let” and “var”?)。


【讨论】:

    【解决方案2】:

    一般来说,拥有更多的绘制指令将是最昂贵的,这些绘制指令的复杂性只有在它真的复杂时才会发挥作用。

    在这里,您正在向 GPU 发送绘图指令:

    • (canvas.width) + 20 调用clearRect() clearRect() 绘制指令,而不是便宜的指令。偶尔使用它,但实际上,您应该只使用它来清除整个上下文。
    • 每次下雨一个fillRect()。它们都是相同的颜色,可以在单个子路径中合并并在单个绘制调用中绘制。
    • 构成地形的每个圆圈一个填充。

    因此,我们可以只用两次绘制调用来代替大量的绘制调用:

    一个clearRect,一个fill(),一个包含drops和 地形。

    但是,将地形和雨水分开肯定更实用,所以让我们让它进行三个绘制调用,将地形保持在自己的 Path2D 对象中,这对 CPU 更友好:

    var canvas = document.getElementById('gamecanvas');
    var c = canvas.getContext('2d');
    
    var ma = Math.random;
    var mo = Math.round;
    
    var wind = 5;
    
    var rn = 100;
    var rp = [];
    
    // this will hold our Path2D object
    // which will hold the full terrain drawing
    // set a 'let' because we will set it again on resize
    let terrain;
    var tp = [];
    var tn;
    
    function setup() {
        
        //fillstyle
        c.fillStyle = 'black';
    
        //canvas size
        canvas.height = window.innerHeight;
        canvas.width = window.innerWidth;
    
        //rain setup
        for (let i = 0; i < rn; i++) {
            let x = mo(ma() * canvas.width);
            let y = mo(ma() * canvas.width);
            let w = mo(ma() * 1) + 1;
            let s = mo(ma() * 5) + 10;
            rp[i] = { x, y, w, s };
        }
    
        //terrain setup
        tn = (canvas.width) + 20;
        tp[0] = { x: -2, y: canvas.height - 50 };
    
        terrain = new Path2D();
        for (let i = 1; i <= tn; i++) {
            let x = tp[i - 1].x + 2;
            let y = tp[i - 1].y + (ma() * 20) - 10;
            if (y > canvas.height - 50) {
                y = tp[i - 1].y -= 1;
            }
            if (y < canvas.height - 100) {
                y = tp[i - 1].y += 1;
            }
            tp[i] = { x, y };
            terrain.rect(x, y, 4, canvas.height - y);
            terrain.arc(x, y, 6, 0, Math.PI*2);
        }
    
    }
    
    function gameloop() {
    
        // clear the whole canvas
        c.clearRect(0, 0, canvas.width, canvas.height);
    
        // start a new sub-path for the rain
        c.beginPath();
        for (let i = 0; i < rn; i++) {
    
            //rain looping
            if (rp[i].y > canvas.height + 5) {
                rp[i].y = -5;
            }
            if (rp[i].x > canvas.width + 5) {
                rp[i].x = -5;
            }
    
            //rain movement
            rp[i].y += rp[i].s;
            rp[i].x += wind;
    
            //rain tracing
            c.rect(rp[i].x, rp[i].y, rp[i].w, 6);
        }
        // paint all the drops in a single op
        c.fill();
        // paint the whole terrain in a single op
        c.fill(terrain);
    
        // loop at screen refresh frequency
        requestAnimationFrame(gameloop);
    }
    
    setup();
    requestAnimationFrame(gameloop);
    
    onresize = () => setup();
    body {
      background-color: white;
      overflow: hidden;
      margin: 0;
    }
    canvas {
      background-color: white;
    }
    &lt;canvas id="gamecanvas"&gt;&lt;/canvas&gt;

    进一步可能的改进:

    • 与其使我们的地形路径成为一组矩形,不如仅使用lineTo 来追踪实际轮廓可能会有所帮助,在 init 时进行更多计算,但只是偶尔进行一次。

    • 如果地形变得更复杂,有更多细节,或者有各种颜色和阴影等,那么考虑只画一次,然后从画布上生成一个 ImageBitmap。然后在gameLoop 你只需要drawImage ImageBitmap(绘制位图非常快,但存储它会消耗内存,所以当你不再需要它时记得.close() ImageBitmap)。

    【讨论】:

      猜你喜欢
      • 2018-11-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-10
      • 1970-01-01
      • 2020-02-17
      • 2013-03-20
      • 2011-07-01
      相关资源
      最近更新 更多