使用 JavaScript 和 canvas 做精確的像素碰撞檢測


原文:Pixel accurate collision detection with Javascript and Canvas

譯者:nzbin

我正在開發一個需要再次使用碰撞檢測的游戲。我通常會使用簡單高效的盒模型碰撞檢測。盒子模型的主要原則就是把所有的物體都抽象成正方形,如果兩個正方形有重疊,就認為是一次碰撞。這通常是一個簡單的游戲所需要的。但是因為這種模型我之前用過多次,我想嘗試一些更深刻更准確的方法。

我選擇從像素級層面來看是否發生了碰撞。首先我要了解“像素是什么”。我測試的元素透明度都不為 0,換句話說,所有的可見像素都被看做一個碰撞點。為了提高算法效率,我預先創建了一張圖片的像素映射圖。換句話說,就是一個數組內包含了屏幕上的所有可見像素。

    /* 描述像素圖的偽代碼 */
    var pixelMap = [];
    for( var y = 0; y < image.width; y++ ) {
        for( var x = 0; x < image.height; x++ ) {
            // 獲取當前位置的元素
            var pixel = ctx.getImageData( x, y, 1, 1 );
            // 判斷透明度不為0
            if( pixel.data[3] != 0 ) {
                pixelMap.push( { x:x, y:y } );
            }
        }
    }
    return pixelMap;

用這種方法,一張小圖片會變得很大。一張 40X40 的圖片會有 1600 像素,所以如果我在一個很大的 canvas 上做碰撞檢測將會非常緩慢。測試之前我先將盒子模型重疊起來,如果點擊測試返回 true,我會進一步測試是否有像素重疊。這意味着我們只需要測試一次。

    /* 盒模型測試, 碰撞返回 true */
    function hitBox( source, target ) {
        /* 源物體和目標物體都包含 x, y 以及 width, height */
        return !(
            ( ( source.y + source.height ) < ( target.y ) ) ||
            ( source.y > ( target.y + target.height ) ) ||
            ( ( source.x + source.width ) < target.x ) ||
            ( source.x > ( target.x + target.width ) )
        );
    }

如果 hitBox 函數返回 true,我們需要比較兩個物體的預渲染像素圖。然后我們需要測試源物體的每一個像素是否與目標物體的像素有重疊。這是一個非常耗時耗能的函數。其實源物體的每個像素與目標物體的每個像素的匹配需要檢測 n*x 次。假如我們匹配兩個 40*40 像素的正方形,最壞的情況就是,經過 2560000 次的計算而沒有得到一次匹配。

    /* 像素碰撞檢測的偽代碼 */
    function pixelHitTest( source, target ) {
        // 循環源圖像的所有像素
        for( var s = 0; s < source.pixelMap.length; s++ ) {
            var sourcePixel = source.pixelMap[s];
            // 添加位置偏移
            var sourceArea = {
                x: sourcePixel.x + source.x,
                y: sourcePixel.y + source.y,
                width: 1,
                height: 1
            };
     
            // 循環目標圖像的所有像素
            for( var t = 0; t < target.pixelMap.length; t++ ) {
                var targetPixel = target.pixelMap[t];
                // 添加位置偏移
                var targetArea = {
                    x: targetPixel.x + target.x,
                    y: targetPixel.y + target.y,
                    width: 1,
                    height: 1
                };
     
                /* 使用之前提到的 hitbox 函數 */
                if( hitBox( sourceArea, targetArea ) ) {
                    return true;
                }
            }
        }
    }

當我把物體描繪出來,我幾乎沒有時間測試物體是否發生了碰撞。如果我們想要一個平滑的 60 幀動畫(我相信大多數瀏覽器傾向於requestAnimationFrame函數),除了瀏覽器進程和幀渲染的時間,理論上我們測試兩幀的時間只有 16.6ms(實際的時間更少)。

為了解決這個問題,我們可以使用更大的分辨率。我們可以測試一組像素而不是單個像素。所以如果我們在像素圖渲染器和像素碰撞測試中使用更大的分辨率,我們必須把計算量降到一個合理的數字上。

   /* 描繪更大分辨率像素圖的偽代碼 */
    function generateRenderMap( image, resolution ) {
        var pixelMap = [];
        for( var y = 0; y < image.width; y=y+resolution ) {
            for( var x = 0; x < image.height; x=x+resolution ) {
                // 獲取當前位置的像素群
                var pixel = ctx.getImageData( x, y, resolution, resolution );
     
                // 判斷像素群的透明度不為0
                if( pixel.data[3] != 0 ) {
                    pixelMap.push( { x:x, y:y } );
                }
            }
        }
        return {
            data: pixelMap,
            resolution: resolution
        };
    }
     
    /* 像素碰撞測試偽代碼 */
    function pixelHitTest( source, target ) {
     
        // 源對象和目標對象包含兩張屬性
        // { data: a render-map, resolution: The precision of the render-map}
     
        // 循環源對象的所有像素
        for( var s = 0; s < source.pixelMap.data.length; s++ ) {
            var sourcePixel = source.data.pixelMap[s];
            // 添加位置偏移
            var sourceArea = {
                x: sourcePixel.x + source.x,
                y: sourcePixel.y + source.y,
                width: target.pixelMap.resolution,
                height: target.pixelMap.resolution
            };
     
            // 循環源對象的所有像素
            for( var t = 0; t < target.pixelMap.data.length; t++ ) {
                var targetPixel = target.pixelMap.data[t];
                // 添加位置偏移                
                var targetArea = {
                    x: targetPixel.x + target.x,
                    y: targetPixel.y + target.y,
                    width: target.pixelMap.resolution,
                    height: target.pixelMap.resolution
                };
     
                /*使用之前提到的 hitbox 函數 */
                if( hitBox( sourceArea, targetArea ) ) {
                    return true;
                }
            }
        }
    }

同樣的 40X40 的像素塊如今只有 100 組像素點,而之前是有1600像素的圖像。我們將 2650000 次的計算量降低到 10000 次的計算量,只有原始 計算量的 0.39%。如果你有更多不同分辨率的渲染圖,你會建立精度更高的系統,從分辨率大的像素群開始依次計算,當然系統的復雜度也會逐漸提高。在兩個 40X40 像素的圓形物體上使用3的分辨率(13.33X13.33),當前的方案在最差的碰撞測試中會耗時 1-2ms。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM