最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How can I optimize my full-screen rain animation to balance GPUCPU load and maintain control? - Stack Overflow

programmeradmin4浏览0评论

I'm developing a full-screen rain animation using HTML, CSS, and JavaScript. I use a single <canvas> element with a requestAnimationFrame loop to animate 500 raindrops. Despite this, my GPU memory usage remains high and load times increase at high FPS. Below is a minimal code snippet that reproduces the issue:

const canvas = document.getElementById('rainCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const numDrops = 500;
const drops = [];
for (let i = 0; i < numDrops; i++) {
  drops.push({
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    speed: 2 + Math.random() * 4,
    length: 10 + Math.random() * 10
  });
}

function drawRain() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.strokeStyle = "rgba(255, 255, 255, 0.5)";
  ctx.lineWidth = 2;
  
  drops.forEach(drop => {
    ctx.beginPath();
    ctx.moveTo(drop.x, drop.y);
    ctx.lineTo(drop.x, drop.y + drop.length);
    ctx.stroke();
    drop.y += drop.speed;
    if (drop.y > canvas.height) {
      drop.y = -drop.length;
    }
  });
  
  requestAnimationFrame(drawRain);
}

drawRain();
body, 
html { 
  margin: 0; 
  padding: 0; 
  overflow: hidden; 
  background-color: black;
}

canvas { 
  display: block; 
}
<canvas id="rainCanvas"></canvas>

I'm developing a full-screen rain animation using HTML, CSS, and JavaScript. I use a single <canvas> element with a requestAnimationFrame loop to animate 500 raindrops. Despite this, my GPU memory usage remains high and load times increase at high FPS. Below is a minimal code snippet that reproduces the issue:

const canvas = document.getElementById('rainCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const numDrops = 500;
const drops = [];
for (let i = 0; i < numDrops; i++) {
  drops.push({
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    speed: 2 + Math.random() * 4,
    length: 10 + Math.random() * 10
  });
}

function drawRain() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.strokeStyle = "rgba(255, 255, 255, 0.5)";
  ctx.lineWidth = 2;
  
  drops.forEach(drop => {
    ctx.beginPath();
    ctx.moveTo(drop.x, drop.y);
    ctx.lineTo(drop.x, drop.y + drop.length);
    ctx.stroke();
    drop.y += drop.speed;
    if (drop.y > canvas.height) {
      drop.y = -drop.length;
    }
  });
  
  requestAnimationFrame(drawRain);
}

drawRain();
body, 
html { 
  margin: 0; 
  padding: 0; 
  overflow: hidden; 
  background-color: black;
}

canvas { 
  display: block; 
}
<canvas id="rainCanvas"></canvas>

How can I optimize this canvas-based rain animation to reduce GPU load while maintaining smooth performance?

Share Improve this question edited Feb 7 at 13:32 KIKO Software 16.7k3 gold badges29 silver badges47 bronze badges asked Feb 7 at 13:31 Alex MelikjanianAlex Melikjanian 431 silver badge3 bronze badges 10
  • 1 If you're patient, you could have a try at posting your code on Code review. That's the community that is specially there to help you improve existing and working code, which is what you have. It might take a bit of time before you receive an answer there. – KIKO Software Commented Feb 7 at 13:34
  • 1 I tested your code on my desktop, and it seems to take only a few % of the CPU (i7-12700) and 15% of the GPU (GTX 1660 Ti), with a 2560 x 816 canvas. The latter is a bit much, but I've seen worse. – KIKO Software Commented Feb 7 at 13:47
  • (Unrelated to your performance question, but:) Your rain falls the faster the higher the frame rate per second. Is that really intended? – Heiko Theißen Commented Feb 7 at 13:56
  • 1 Thanks for your feedback! The code I shared is just a simplified version - I have much more. I'm optimizing for any FPS and device. Since I have multiple animations, at 144 FPS, GPU usage can hit 30%. That may seem fine, but with many animations, it can cause lag. Looking for ways to reduce GPU load! – Alex Melikjanian Commented Feb 7 at 17:42
  • 2 @AlexMelikjanian then you're eventually just looking at rewriting your graphics in WebGL. – AKX Commented Feb 7 at 21:11
 |  Show 5 more comments

2 Answers 2

Reset to default 0

To ease the loop, we could separate the logic into two different requestAnimationFrame() and compare the time for triggers.

By doing so we get an interval loop without using setInterval, so if something is going to throttle down, the frame will be skipped anyway, and not be enqueued.

In this example, i have slow down the animation to 200 milliseconds by frame for the demo.

Maybe implementing a variable instead of that 200 could give a great rain effect ;)

const canvas = document.getElementById('rainCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const numDrops = 500;
const drops = [];
for (let i = 0; i < numDrops; i++) {
  drops.push({
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    speed: 2 + Math.random() * 4,
    length: 10 + Math.random() * 10
  });
}

function drawRain() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.strokeStyle = "rgba(255, 255, 255, 0.5)";
  ctx.lineWidth = 2;
  
  drops.forEach(drop => {
    ctx.beginPath();
    ctx.moveTo(drop.x, drop.y);
    ctx.lineTo(drop.x, drop.y + drop.length);
    ctx.stroke();
    drop.y += drop.speed;
    if (drop.y > canvas.height) {
      drop.y = -drop.length;
    }
  });
  
  //requestAnimationFrame(drawRain);
}

//drawRain();

/* Timer loop ----------------------------------------*/
var job, origin = new Date().getTime(), i = 0;

const timer = () => {
  if (new Date().getTime() - i > origin){
   requestAnimationFrame(drawRain)
   i = i + 200 // ms
   job = requestAnimationFrame(timer)
  } else if (job !== null){
      requestAnimationFrame(timer)    
  }
}


requestAnimationFrame(timer)

const stop = () => job = null
body, 
html { 
  margin: 0; 
  padding: 0; 
  overflow: hidden; 
  background-color: black
}

canvas { 
  display: block 
}

button {
  z-index: 1;
  position: fixed;
  top: 0
}
<canvas id="rainCanvas"></canvas>

<button onclick="stop()">STOP</button>

Source from my snippet here, and more infos:

How can I use setInterval and clearInterval?

Avoid GPU state changes

The only space for improvement is in the render loop.

The general rule of thumb is to avoid GPU state changes. Calls to ctx.stroke and ctx.fill force GPU state changes.

This means that data (style and path to stroke) is moved from the CPU to the GPU.

As all the strokes in your code are the same style you can render the whole scene in one call to ctx.stroke.

Code Changes

Thus the change around the inner loop would be as follows

  ctx.beginPath();  // ADDED. Do only once per loop
  drops.forEach(drop => {

    // REMOVED ctx.beginPath();
    ctx.moveTo(drop.x, drop.y);
    ctx.lineTo(drop.x, drop.y + drop.length);

    // REMOVED ctx.stroke();
    drop.y += drop.speed;
    if (drop.y > canvas.height) { drop.y = -drop.length }
  });
  ctx.stroke(); // ADDED. Do once for all rain drops

Depending on the device, setup, GPU and number of drops this can provide a significant performance boost.

Note that if each stroke needed a different color/style you can group strokes with the same style and render each group with only one draw call (stroke and or fill)

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论