【问题标题】:HTML5 Canvas, better pixel control and better speedHTML5 Canvas,更好的像素控制和更快的速度
【发布时间】:2018-02-11 13:07:54
【问题描述】:

我正在尝试在 HTML5 和 Javascript 的帮助下使用画布创建一个小模拟。然而,我的问题是,如果不将每个像素都作为对象,我真的想不出一种方法来控制像素的行为,这会导致我的模拟速度非常慢。

这是目前的代码:

var pixels = [];
class Pixel{
    constructor(color){
        this.color=color;
    }
}

window.onload=function(){
    canv = document.getElementById("canv");
    ctx = canv.getContext("2d");
    createMap();
    setInterval(game,1000/60);
};

function createMap(){
    pixels=[];
    for(i = 0; i <= 800; i++){
        pixels.push(sub_pixels = []);
        for(j = 0; j <= 800; j++){
            pixels[i].push(new Pixel("green"));
        }
    }
    pixels[400][400].color="red";
}

function game(){
    ctx.fillStyle = "white";
    ctx.fillRect(0,0,canv.width,canv.height);
    for(i = 0; i <= 800; i++){
        for(j = 0; j <= 800; j++){
            ctx.fillStyle=pixels[i][j].color;
            ctx.fillRect(i,j,1,1);
        }
    }
    for(i = 0; i <= 800; i++){
        for(j = 0; j <= 800; j++){
            if(pixels[i][j].color == "red"){
                direction = Math.floor((Math.random() * 4) + 1);
                switch(direction){
                    case 1:
                        pixels[i][j-1].color= "red";
                        break;
                    case 2:
                        pixels[i+1][j].color= "red";
                        break;
                    case 3:
                        pixels[i][j+1].color= "red";
                        break;
                    case 4:
                        pixels[i-1][j].color= "red";
                        break;
                }
            }
        }
    }

}

function retPos(){
    return Math.floor((Math.random() * 800) + 1);
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script language="javascript" type="text/javascript" src="game.js"></script>
</head>
<body>
    <canvas width="800px" height="800px" id="canv"></canvas>
</body>

</html>
所以我的两个大问题是,有什么更好的方法来控制这些像素?我怎样才能加快像素生成?

希望你能帮助我。

【问题讨论】:

    标签: javascript html canvas simulation pixel


    【解决方案1】:

    优化像素操作

    有很多选项可以加快您的代码速度

    像素为 32 位整数

    以下内容会影响大多数工作量过多的机器。

        // I removed fixed 800 and replaced with const size 
        for(i = 0; i <= size; i++){
            for(j = 0; j <= size; j++){
                ctx.fillStyle=pixels[i][j].color;
                ctx.fillRect(i,j,1,1);
            }
        }
    

    不要通过矩形写入每个像素。使用您可以通过createImageData 和相关函数从画布 API 获得的像素数据。它使用比数组快一点的类型化数组,并且可以对同一内容有多个视图。

    您可以在一次调用中将所有像素写入画布。不是快得令人眼花缭乱,但比你正在做的要快无数倍。

     const size = 800;
     const imageData = ctx.createImageData(size,size);
     // get a 32 bit view
     const data32 = new Uint32Array(imageData.data.buffer);
    
     // To set a single pixel
     data32[x+y*size] = 0xFF0000FF;  // set pixel to red
    
     // to set all pixels 
     data32.fill(0xFF00FF00);  // set all to green
    

    以像素坐标获取像素

     const pixel = data32[x + y * imageData.width];
    

    有关使用图像数据的更多信息,请参阅Accessing pixel data

    像素数据在你放到画布上之前不会显示

     ctx.putImageData(imageData,0,0);
    

    这会给你带来很大的进步。

    更好的数据组织。

    当性能至关重要时,您会牺牲内存和简单性,以获得更多的 CPU 周期来做您想做的事情,而不是做很多无所事事。

    红色像素随机扩展到场景中,读取每个像素并检查(通过慢速字符串比较)是否为红色。当你找到一个时,你会在它旁边添加一个随机的红色像素。

    检查绿色像素是一种浪费,可以避免。扩展完全被其他红色包围的红色像素也是没有意义的。他们什么都不做。

    您感兴趣的唯一像素是绿色像素旁边的红色像素。

    因此,您可以创建一个缓冲区来保存所有活动红色像素的位置,一个活动红色至少有一个绿色。每一帧你检查所有活跃的红色,如果可以的话,生成新的,如果它们被红色包围,则杀死它们。

    我们不需要存储每个红色的 x,y 坐标,只需要存储内存地址,这样我们就可以使用平面数组。

    const reds = new Uint32Array(size * size); // max size way over kill but you may need it some time.
    

    您不想在 reds 数组中搜索红色,因此您需要跟踪有多少活跃的红色。您希望所有活动的红色都位于数组的底部。您只需每帧检查一次每个活动的红色。如果红色比上面所有的都死了,它必须向下移动一个数组索引。但是您只想每帧只移动每个红色一次。

    气泡数组

    我不知道这种类型的阵列叫什么,它就像一个分离罐,死的东西慢慢上移,活的东西下移。或者未使用的物品冒泡使用的物品沉到底部。

    我会将它显示为功能性的,因为它更容易理解。但更好地实现为一种蛮力功能

    // data32 is the pixel data
    const size = 800; // width and height
    const red = 0xFF0000FF; // value of a red pixel
    const green = 0xFF00FF00; // value of a green pixel
    const reds = new Uint32Array(size * size); // max size way over kill but you     var count = 0; // total active reds
    var head = 0; // index of current red we are processing
    var tail = 0; // after a red has been process it is move to the tail
    var arrayOfSpawnS = [] // for each neighbor that is green you want
                          // to select randomly to spawn to. You dont want
                          // to spend time processing so this is a lookup 
                          // that has all the possible neighbor combinations
    for(let i = 0; i < 16; i ++){
          let j = 0;
          const combo = [];
          i & 1 && (combo[j++] = 1);  // right
          i & 2 && (combo[j++] = -1);  // left
          i & 4 && (combo[j++] = -size); // top
          i & 5 && (combo[j++] = size);  // bottom
          arrayOfSpawnS.push(combo);
    }
    
    
    function addARed(x,y){   // add a new red
         const pixelIndex = x + y * size;
         if(data32[pixelIndex] === green) { // check if the red can go there
             reds[count++] = pixelIndex;  // add the red with the pixel index
             data32[pixelIndex] = red; // and set the pixel
         }
    }
    function safeAddRed(pixelIndex) { // you know that some reds are safe at the new pos so a little bit faster 
         reds[count++] = pixelIndex;  // add the red with the pixel index
         data32[pixelIndex] = red; // and set the pixel
    }
    
    // a frame in the life of a red. Returns false if red is dead
    function processARed(indexOfRed) {
         // get the pixel index 
         var pixelIndex = reds[indexOfRed];
         // check reds neighbors right left top and bottom
         // we fill a bit value with each bit on if there is a green
         var n = data32[pixelIndex + 1] === green ? 1 : 0;
         n += data32[pixelIndex - 1] === green ? 2 : 0;          
         n += data32[pixelIndex - size] === green ? 4 : 0;
         n += data32[pixelIndex + size] === green ? 8 : 0;
    
         if(n === 0){ // no room to spawn so die
              return false;
         }
         // has room to spawn so pick a random
         var nCount = arrayOfSpawnS[n].length;
         // if only one spawn point then rather than spawn we move
         // this red to the new pos.
         if(nCount === 1){
              reds[indexOfRed] += arrayOfSpawnS[n][0]; // move to next pos
         }else{  // there are several spawn points
              safeAddRed(pixelIndex + arrayOfSpawnS[n][(Math.random() * nCount)|0]);
          }
         // reds frame is done so return still alive to spawn another frame
         return true;
      }
    

    现在处理所有的红色。

    这是气泡阵列的核心。 head 用于索引每个活动的红色。 tail 是移动当前head 的位置的索引,如果没有遇到死亡tail 等于head。但是,如果遇到死项目,head 会向上移动一个,而tail 仍然指向死项目。这会将所有活动项目移至底部。

    head === count 已检查所有活动项目时。 tail 的值现在包含迭代后设置的新 count

    如果您使用的是对象而不是整数,则无需向下移动活动项目,而是交换 headtail 项目。这有效地创建了一个可用对象池,可在添加新项目时使用。这种类型的数组管理不会产生 GC 或分配开销,因此与堆栈和对象池相比非常快。

       function doAllReds(){
           head = tail = 0; // start at the bottom
           while(head < count){
                if(processARed(head)){ // is red not dead
                    reds[tail++] = reds[head++];  // move red down to the tail
                 }else{ // red is dead so this creates a gap in the array  
                       // Move the head up but dont move the tail, 
                        // The tail is only for alive reds
                    head++;
                }
           }
           // All reads done. The tail is now the new count
           count = tail;
        }
    

    演示。

    演示将向您展示速度改进。我使用的是功能版,可能还有其他一些调整。

    您也可以考虑webWorkers 以加快活动速度。 Web Worker 在单独的 javascript 上下文中运行并提供真正的并发处理。

    为了获得终极速度,请使用 WebGL。所有逻辑都可以通过 GPU 上的片段着色器完成。这种类型的任务非常适合 GPU 设计的并行处理。

    稍后会回来清理这个答案(有点太长了)

    当红色从像素阵列中产生时,我还为像素阵列添加了一个边界。

    const size = canvas.width;
    canvas.height = canvas.width;
    const ctx = canvas.getContext("2d");
    const red = 0xFF0000FF;
    const green = 0xFF00FF00;
    const reds = new Uint32Array(size * size);    
    const wall = 0xFF000000;
    var count = 0;
    var head = 0;
    var tail = 0;
    var arrayOfSpawnS = []
    for(let i = 0; i < 16; i ++){
          let j = 0;
          const combo = [];
          i & 1 && (combo[j++] = 1);  // right
          i & 2 && (combo[j++] = -1);  // left
          i & 4 && (combo[j++] = -size); // top
          i & 5 && (combo[j++] = size);  // bottom
          arrayOfSpawnS.push(combo);
    }
    const imageData = ctx.createImageData(size,size);
    const data32 = new Uint32Array(imageData.data.buffer);
    function createWall(){//need to keep the reds walled up so they dont run free
        for(let j = 0; j < size; j ++){
             data32[j] = wall;
             data32[j * size] = wall;
             data32[j * size + size - 1] = wall;
             data32[size * (size - 1) +j] = wall;
         }
    }
    function addARed(x,y){  
         const pixelIndex = x + y * size;
         if (data32[pixelIndex] === green) { 
             reds[count++] = pixelIndex;  
             data32[pixelIndex] = red; 
         }
    }
    function processARed(indexOfRed) {
         var pixelIndex = reds[indexOfRed];
         var n = data32[pixelIndex + 1] === green ? 1 : 0;
         n += data32[pixelIndex - 1] === green ? 2 : 0;          
         n += data32[pixelIndex - size] === green ? 4 : 0;
         n += data32[pixelIndex + size] === green ? 8 : 0;
         if(n === 0) {  return false }
         var nCount = arrayOfSpawnS[n].length;
         if (nCount === 1) { reds[indexOfRed] += arrayOfSpawnS[n][0] }
         else { 
             pixelIndex += arrayOfSpawnS[n][(Math.random() * nCount)|0]
             reds[count++] = pixelIndex; 
             data32[pixelIndex] = red; 
         }
         return true;
    }
    
    function doAllReds(){
       head = tail = 0; 
       while(head < count) {
            if(processARed(head)) { reds[tail++] = reds[head++] }
            else { head++ }
       }
       count = tail;
    }
    function start(){
    	data32.fill(green);  
      createWall();
        var startRedCount = (Math.random() * 5 + 1) | 0;
        for(let i = 0; i < startRedCount; i ++) { addARed((Math.random() * size-2+1) | 0, (Math.random() * size-2+1) | 0) }
        ctx.putImageData(imageData,0,0);
        setTimeout(doItTillAllDead,1000);
        countSameCount = 0;
    }
    var countSameCount;
    var lastCount;
    function doItTillAllDead(){
        doAllReds();	
        ctx.putImageData(imageData,0,0);
    	if(count === 0 || countSameCount === 100){ // all dead
    	    setTimeout(start,1000);
    	}else{
            countSameCount += count === lastCount ? 1 : 0;
            lastCount = count; // 
    
            requestAnimationFrame(doItTillAllDead);
    	}
    }
    start();
    &lt;canvas width="800" height="800" id="canvas"&gt;&lt;/canvas&gt;

    【讨论】:

    • 这是一个非常详细的解释,但也很有帮助。我知道检查每个像素太多了,但不知道更好的方法,所以感谢你给了我多种方法来解决我的性能问题。完美运行。
    【解决方案2】:

    速度变慢的主要原因是您假设每次操作都需要遍历每个像素。您不这样做,因为对于您需要执行的每个操作,这将是 640,000 次迭代。

    您也不应该在渲染循环中执行任何操作逻辑。唯一应该存在的是绘图代码。因此,最好将其移至单独的线程(Web Workers)。如果无法使用那些 setTimeout/Interval 调用。

    所以先做几个小改动:

    1. 使 Pixel 类包含像素的坐标和颜色:

      class Pixel{
        constructor(color,x,y){
          this.color=color;
          this.x = x;
          this.y = y;
        }
      }
      
    2. 保留最终会创建新红色像素的像素数组。另一个用于跟踪哪些像素已更新,以便我们知道需要绘制哪些像素。

      var pixels = [];
      var infectedPixesl = [];
      var updatedPixels = [];
      

    现在代码中最容易更改的部分是渲染循环。因为它唯一需要做的就是绘制像素,所以它只有几行。

    function render(){
      var numUpdatedPixels = updatedPixels.length;
      for(let i=0; i<numUpdatedPixels; i++){
        let pixel = updatedPixels[i];
        ctx.fillStyle = pixel.color;
        ctx.fillRect(pixel.x,pixel.y,1,1);
      }
      //clear out the updatedPixels as they should no longer be considered updated.
      updatedPixels = [];
      //better method than setTimeout/Interval for drawing
      requestAnimationFrame(render);
    }
    

    从那里我们可以继续讨论逻辑。我们将遍历infectedPixels 数组,并根据每个像素决定一个随机方向并获取该像素。如果这个选定的像素是红色的,我们什么也不做,继续。否则我们改变它的颜色并将它添加到一个临时数组affectedPixels。之后我们测试看看原始像素周围的所有像素是否都是红色的,如果是,我们可以将其从infectedPixels 中删除,因为不需要再次检查它。然后将affectedPixels 中的所有像素添加到infectedPixels,因为这些现在是需要检查的新像素。最后一步是将affectedPixels 添加到updatedPixels 上,以便渲染循环绘制更改。

    function update(){
      var affectedPixels = [];
      //needed as we shouldn't change an array while looping over it
      var stillInfectedPixels = [];
    
      var numInfected = infectedPixels.length;
      for(let i=0; i<numInfected; i++){
        let pixel = infectedPixels[i];
        let x = pixel.x;
        let y = pixel.y;
    
        //instead of using a switch statement, use the random number as the index
        //into a surroundingPixels array
        let surroundingPixels = [
          (pixels[x] ? pixels[x][y - 1] : null),
          (pixels[x + 1] ? pixels[x + 1][y] : null),
          (pixels[x] ? pixels[x][y + 1] : null),
          (pixels[x - 1] ? pixels[x - 1][y] : null)
        ].filter(p => p);
        //filter used above to remove nulls, in the cases of edge pixels
    
        var rand = Math.floor((Math.random() * surroundingPixels.length));
        let selectedPixel = surroundingPixels[rand];
    
        if(selectedPixel.color == "green"){
          selectedPixel.color = "red";
          affectedPixels.push(selectedPixel);
        }
    
        if(!surroundingPixels.every(p=>p.color=="red")){
          stillInfectedPixels.push(pixel);  
        }
      }
      infectedPixels = stillInfectedPixel.concat( affectedPixels );
      updatedPixels.push(...affectedPixels);
    }
    

    演示

    var pixels = [],
      infectedPixels = [],
      updatedPixels = [],
      canv, ctx;
    
    window.onload = function() {
      canv = document.getElementById("canv");
      ctx = canv.getContext("2d");
      createMap();
      render();
      setInterval(() => {
        update();
      }, 16);
    };
    
    function createMap() {
      for (let y = 0; y < 800; y++) {
        pixels.push([]);
        for (x = 0; x < 800; x++) {
          pixels[y].push(new Pixel("green",x,y));
        }
      }
      pixels[400][400].color = "red";
      updatedPixels = [].concat(...pixels);
      infectedPixels.push(pixels[400][400]);
    }
    
    class Pixel {
      constructor(color, x, y) {
        this.color = color;
        this.x = x;
        this.y = y;
      }
    }
    
    function update() {
      var affectedPixels = [];
      var stillInfectedPixels = [];
    
      var numInfected = infectedPixels.length;
      for (let i = 0; i < numInfected; i++) {
        let pixel = infectedPixels[i];
        let x = pixel.x;
        let y = pixel.y;
    
        let surroundingPixels = [
          (pixels[x] ? pixels[x][y - 1] : null),
          (pixels[x + 1] ? pixels[x + 1][y] : null),
          (pixels[x] ? pixels[x][y + 1] : null),
          (pixels[x - 1] ? pixels[x - 1][y] : null)
        ].filter(p => p);
        var rand = Math.floor((Math.random() * surroundingPixels.length));
        let selectedPixel = surroundingPixels[rand];
    
        if (selectedPixel.color == "green") {
          selectedPixel.color = "red";
          affectedPixels.push(selectedPixel);
        }
    
        if (!surroundingPixels.every(p => p.color == "red")) {
          stillInfectedPixels.push(pixel);
        }
      }
      infectedPixels = stillInfectedPixels.concat(affectedPixels);
      updatedPixels.push(...affectedPixels);
    }
    
    function render() {
      var numUpdatedPixels = updatedPixels.length;
      for (let i = 0; i < numUpdatedPixels; i++) {
        let pixel = updatedPixels[i];
        ctx.fillStyle = pixel.color;
        ctx.fillRect(pixel.x, pixel.y, 1, 1);
      }
      updatedPixels = [];
      requestAnimationFrame(render);
    }
    &lt;canvas id="canv" width="800" height="800"&gt;&lt;/canvas&gt;

    【讨论】:

    • 还要感谢您为我提供了一种更好的方法来控制哪些像素受到影响,并且不必在每个单帧循环所有 640.000 次迭代。
    猜你喜欢
    • 2016-08-03
    • 2012-08-01
    • 2018-04-17
    • 2015-11-23
    • 2011-10-15
    • 1970-01-01
    • 2019-07-27
    • 2015-05-23
    • 2014-05-14
    相关资源
    最近更新 更多