【问题标题】:How to do pixel-perfect collision detection of two partially transparent images如何对两个部分透明的图像进行像素完美的碰撞检测
【发布时间】:2016-03-15 13:22:29
【问题描述】:

所以基本上我要做的是,当屏幕上的两个角色触摸并且有人按下按钮时,它会带走他们的健康。我唯一不知道该怎么做的是如何检测它们何时触摸。

$(document).ready(function(){


var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
canvas.width = 1000;
canvas.height = 600;
document.body.appendChild(canvas);

var kGroundHeight = 500;

/*var upKey = 38;
var downKey = 40;
var leftKey = 37;
var rightKey = 39;
*/


var render = function() {
  gravity();
  gravity1();



  context.clearRect(0, 0, canvas.width, canvas.height);

    context.fillRect(0,kGroundHeight,canvas.width,10);

  context.drawImage(kirby, kirbyObject.x, kirbyObject.y);

    context.drawImage(link, linkObject.x, linkObject.y);

};




var main = function() {

  render();
  window.requestAnimationFrame(main);
};

main();
});




var linkReady = false;
var link = new Image();
link.onLoad = function() {

  linkReady = true;
};


linkObject = {};
link.src= "https://vignette1.wikia.nocookie.net/zelda/images/1/18/Link_(Sprite)_The_Legend_of_Zelda.png/revision/latest?cb=20130117162823";

linkObject.x = 200;
linkObject.y = 200;






var keys = {};




$(document).keydown(function(e) {
    console.log(e);

   move1(e.keyCode);

    if (keys[87] && keys[65]) {
       linkObject.y -=50;
       linkObject.x -=50;


    }else if(keys[87] && keys[68]){

      linkObject.y -=50;
      linkObject.x +=50;


    }else if(keys[83] && keys[68]){

      linkObject.y +=50;
      linkObject.x +=50;


    }else if(keys[83] && keys[65]){

      linkObject.y +=50;
      linkObject.x -=50;
    }

    keys[e.keyCode] = true;
}).keyup(function(e) {

   keys[e.keyCode] = false;



});

var upKey1 = 87;
var downKey1 = 83;
var leftKey1 = 65;
var rightKey1 = 68;
var attackKey1 = 75; 


var move1 = function(key) {

  if (key == upKey1) {
    linkObject.y -= 50;
  }else if(key == downKey1){

      var kGroundHeight = 500;

     if(linkObject.y + link.height ==  kGroundHeight){

      linkObject.y +=0;
    }else{

      linkObject.y +=50;

    }


    //console.log("down");
        console.log(linkObject.y);


  }else if(key == leftKey1){

    linkObject.x -=50;
  }else if(key == rightKey1 ){

    linkObject.x +=50;
  }
  // MORE DIRECTIONS!!!
};




var gravity1 = function() {
    var kGravityScale = 1;
  var kGroundHeight = 500;

  if (linkObject.y + link.height ==  kGroundHeight) {

    linkObject.y += 0;

  }else{
        linkObject.y += kGravityScale;
//console.log(link.width);
  }
};


var attack = function(a){



};



 var kGroundHeight = 500;


var keys = {};




$(document).keydown(function(e) {
    console.log(e);

   move(e.keyCode);

    if (keys[38] && keys[37]) {
       kirbyObject.y -=50;
       kirbyObject.x -=50;


    }else if(keys[38] && keys[39]){

      kirbyObject.y -=50;
      kirbyObject.x +=50;


    }else if(keys[40] && keys[39]){

      kirbyObject.y +=50;
      kirbyObject.x +=50;


    }else if(keys[40] && keys[37]){

      kirbyObject.y +=50;
      kirbyObject.x -=50;
    }

    keys[e.keyCode] = true;
}).keyup(function(e) {

   keys[e.keyCode] = false;



});


var kirbyReady = false;
var kirby = new Image();
kirby.onLoad = function() {
  kirbyReady = true;
};

kirbyObject = {};
kirby.src = "https://vignette3.wikia.nocookie.net/spritechronicles/images/5/5c/Kirby.png/revision/latest?cb=20101010225540";




kirbyObject.x = 300;
kirbyObject.y = 100;



var upKey = 38;
var downKey = 40;
var leftKey = 37;
var rightKey = 39;
var attackKey = 32; 



var move = function(key) {

  if (key == upKey) {
    kirbyObject.y -= 50;
  }else if(key == downKey){

      var kGroundHeight = 500;

     if(kirbyObject.y + kirby.height ==  kGroundHeight){

      kirbyObject.y +=0;
    }else{

      kirbyObject.y +=50;

    }


    //console.log("down");
        console.log(kirbyObject.y);


  }else if(key == leftKey){

    kirbyObject.x -=50;
  }else if(key == rightKey ){

    kirbyObject.x +=50;
  }
  // MORE DIRECTIONS!!!
};




var gravity = function() {
    var kGravityScale = 1;
  var kGroundHeight = 500;

  if (kirbyObject.y + kirby.height ==  kGroundHeight) {

    kirbyObject.y += 0;

  }else{
        kirbyObject.y += kGravityScale;

  }
};

【问题讨论】:

  • 它们是方形/矩形字符,还是需要检查不规则形状的重叠?
  • 其他人也问过类似的问题hereherehere。这应该让你开始。
  • 第二部分。它们是我用作字符的图像。

标签: javascript jquery canvas


【解决方案1】:

径向周长测试

通过使用一组极坐标定义每个精灵的形状,可以实现快速几乎像素完美的碰撞。每个坐标都描述了到中心的距离(中心是任意的,但必须在精灵内部)和从中心到沿该方向的最远像素的中心的方向。坐标数 (n) 由最外像素的周长决定。我不知道这是否已经描述过,因为我现在才想到它,所以它的实际稳健性需要测试。

概念

下图显示了基本概念。

精灵 (1.) 覆盖极坐标和外边界圆 (2.) 从 0-15 索引每个坐标 (3.) 提取检查碰撞所需的信息。绿色 (a) 是每个精灵的角原点,黄线 P 是 A 和 B 之间的向量,标记为与角原点 (4) 成角度。

要获取您需要访问像素信息的坐标,这可以在生产期间完成并作为代码添加,或者在游戏设置期间完成。它将导致类似的数据结构

var sprite = {
    ...
    collisionData : {
        maxRadius : 128,
        minRadius : 20,
        coords : [128,30,50, ... ],
    }
}

sn-p 1.

现在您已经将每个精灵描述为一组您需要进行测试的极坐标。

假设精灵将有一个位置(x,y 以像素为单位)、一个旋转(r 以弧度为单位)和一个比例(s 为正方形)。

positionData = {  // position data structure
   x : 100,           // x pos
   y : 100,           // y pos
   r : Math.PI * 1.2, // rotation
   s : 1,             // scale
}

sn-p 2.

碰撞测试

对于pApB引用精灵位置数据(sn-p 2)和cAcB引用每个精灵的碰撞数据(sn-p 1)的两个精灵,我们首先进行距离测试,检查最大半径和最小半径。如果发生冲突,代码将返回 true。

const TAU = Math.PI * 2; // use this alot so make it a constant
var xd = pA.x - pB.x;          // get x distance
var yd = pA.y - pB.y;          // get y distance
var dist = Math.hypot(xd,yd);  // get the distance between sprites
                               // Please note that legacy browsers will not 
                               // support hypot 
// now scale the max radius of each sprite and test if the distance is less 
// than the sum of both.
if (dist <= cA.maxRadius * pA.s + cB.maxRadius * pB.s){
     // passed first test sprites may be touching
     // now check the min radius scaled
     if (dist <= Math.min(cA.minRadius * pA.s, cB.minRadius * pB.s) * 2 ){
          // the sprites are closer than the smallest of the two's min
          // radius scaled so must be touching
          return true;  // all done return true
     }

sn-p 3.

现在您需要进行极坐标测试。您需要获取每个精灵到另一个精灵的方向,然后调整该方向以匹配精灵旋转,然后将方向标准化为存储在碰撞数据中的极坐标数。

    // on from snippet 3.
    var dir = Math.atan2(yd, xd); // get the direction from A to B in radians
                                  // please note that y comes first in atan2

    // now subtract the rotation of each sprite from the directions
    var dirA = dir - pA.r;
    var dirB = dir + Math.PI - pB.r; // B's direction is opposite

    // now normalise the directions so they are in the range 0 - 1;
    dirA = (((dirA % TAU) + TAU) % TAU) / TAU;                  
    dirB = (((dirB % TAU) + TAU) % TAU) / TAU;         

下一步将归一化的相对方向转换为极坐标数组中的正确索引需要考虑每个极坐标的角宽度。参见图 3。在图中,每个极坐标顶部的扁平位是角宽度。为此,我们在从 0-1 放大到坐标数时使用Math.round。由于接近 1 的值会四舍五入到错误的索引,因此您还必须使用模 % 来确保它不会超出范围。

    var indexA = Math.round(dirA * cA.coords.length) % cA.coords.length;
    var indexB = Math.round(dirB * cB.coords.length) % cB.coords.length;

    // now we can get the length of the coordinates.
    // also scale them at the same time
    var la = cA.coords[indexA] * pA.s;
    var lb = cB.coords[indexB] * pB.s;

    // now test if the distance between the sprites is less than the sum
    // of the two length
    if( dist <= la + lb ){
        // yes the two are touching
        return true;
    }
}

注意事项

原来如此。与其他方法相比,它相对较快,尽管它不是像素完美的,因为它只考虑精灵的周长,如果精灵周长不是凸的,您可能会遇到算法返回不正确的命中的情况。

如果精灵非常粗糙,可能会出现碰撞无法检测到碰撞的情况,这同样适用于对坐标数进行过采样。该图显示了一个精灵的 16 个坐标,它接近 128 x 128,这似乎是一个不错的数字。更大的精灵需要更多更小的,但不要低于 8。

改进

在图表中,图 4. 显示了一次未命中,但如果精灵的 B 坐标 15(从它们之间的方向顺时针方向一个)稍长一些,则会出现未被检测到的命中。为了改进算法,您可以根据距离测试任一侧的索引坐标,但您需要减少极坐标距离以说明它们没有指向另一个精灵的中心这一事实。通过将极距乘以偏移角的余弦来做到这一点。

 // get the angle step for A and B
 var angleStepA = TAU / cA.coord.length;
 var angleStepB = TAU / cB.coord.length;

 // the number of coordinates to offset 
 var offCount = 1;

 // get next coord clockwise from A and scale it
 var lengOffA = cA.coord[(index + offCount) % cA.coord.length] * pA.s;

 // get next coordinate counter clockwise from B and scale it
 var lengOffB = cB.coord[(index + cB.coord.length - offCount) % cB.coord.length] * pB.s;

 // Now correct for the offest angle
 lengOffA *= Math.cos(offCount * angleStepA);
 lengOffB *= Math.cos(offCount * angleStepB);

 // Note that as you move away the length will end up being negative because
 // the coord will point away.

 if( dist < lengOffA + lengOffB ){
     // yes a hit
     return true;
 }

这会为算法增加一点处理,但不会太多,与 B 相比,A 的角大小应该只有一半。

您可能还希望将极坐标之间的区域设为线性斜率,并为第一次测试插入极距。

更多

进行测试的方法有很多,您使用的方法将取决于需要。如果你只有几个精灵,那么可以使用更复杂的算法来获得更好的结果,如果你有很多精灵,那么使用更简单的测试。

不要煮过头。

请记住,程序员想要的是像素完美测试,但玩家几乎无法辨别像素(现代显示器的像素小于人眼的径向分辨率)。如果两个几乎不可见的像素接触 1/60 秒,谁会认为这是命中???

【讨论】:

  • 还没有检查过代码,但这也是我建议的方法。
  • Hmmm ... 计算采样角处的径向极值(距原点最远的填充像素)并仅使用这些极值来测试碰撞。您提出了一个非常有趣的解决方案!同样有趣的是,它允许转换以及非居中的原点。热情的点赞!您是否要进行概念验证并且要检查限制性边缘情况?如果是,请随时通知我们。再次......很好的脑力劳动!
  • @markE 谢谢,当我有机会时,我会进行更详细的查看,其中包含一个重要的 mod 以适应非统一比例和动画精灵。完成后肯定会做一个自我回答的帖子。
【解决方案2】:

您可以分两步完成。

  1. 检查重叠的边界圆。

  2. 如果圆圈重叠,请检查(更精确的)边界矩形是否重叠。

如果你有两张图片,那么每张图片的中心都等于(x + w/2), (y + h/2)。并且它们每个的半径都等于sqrt((w/2)^2 + (h/2)^2)

当两个圆圈之间的距离(sqrt((x1-x2)^2 + (y1-y2)^2))小于max(radius1, radius2)时,两个圆圈重叠。

边界矩形碰撞检测很直观,但计算起来可能更难(可能会影响大量对象)。

【讨论】:

    【解决方案3】:

    我知道这是一个很老的问题,但我遇到了同样的问题并找到了一个更简单的解决方案。

    首先,我们需要编写一个长 if 语句来检测这两个图像是否真正接触。对于半透明的图像,即使不透明的部分实际上没有接触也是如此。

     if (/*horizontal*/ (sprite.x + sprite.width >= sprite2.x && sprite.x <= sprite2.x + sprite2.width && sprite2.hidden == false) && /*vertical*/ (sprite.y + sprite.height >= sprite2.y && sprite.y <= sprite2.y + sprite2.height && sprite.hidden == false)) {
    }
    

    由于图像是半透明的,仅此一项是行不通的。我的下一步是创建两个新的透明画布,其宽度和高度与原始画布相同。

    // draws sprite onto canvas
    var spriteCanvas = document.createElement("CANVAS");
    var spriteCtx = spriteCanvas.getContext("2d");
    spriteCanvas.width = canvas.width;
    spriteCanvas.height = canvas.height;
    
    var spriteImage = new Image();
    spriteImage.src = sprite.base64Src;
    spriteCtx.drawImage(spriteImage, sprite.x, sprite.y, sprite.width, sprite.height);
    
    // draws sprite2 onto canvas
    var sprite2Canvas = document.createElement("CANVAS");
    var sprite2Ctx = sprite2Canvas.getContext("2d");
    sprite2Canvas.width = canvas.width;
    sprite2Canvas.height = canvas.height;
    
    var sprite2Image = new Image();
    sprite2Image.src = sprite2.base64Src;
    sprite2Ctx.drawImage(sprite2Image, sprite2.x, sprite2.y, sprite2.width, sprite2.height);
    

    现在我们有两个孤立的图像。我们可以遍历画布上的每个像素,检查像素是否不透明,然后检查另一个画布上的相同像素是否不透明,但这将非常低效。相反,我们发现了两个图像的重叠。 我们可以使用 context.getImageData() 来获取每个像素的 RGBA(红、绿、蓝、透明)颜色。我们可以通过检查每个 A(透明度)值是否大于 0 来确定像素是否透明。如果您觉得这个值太低,请随意增加它。

    // gets the overlap of the two
    var spriteOverlap;
    var sprite2Overlap;
            
    var cropX = (sprite.x > sprite2.x) ? [sprite.x, (sprite2.x + sprite2.width) - sprite.x + 1] : [sprite2.x, (sprite.x + sprite.width) - sprite2.x + 1];
            var cropY = (sprite.y + sprite.height > sprite2.y + sprite2.height) ? [sprite.y, (sprite2.y + sprite2.height) - sprite.y + 1] : [sprite2.y, (sprite.y + sprite.height) - sprite2.y + 1];
            
    spriteOverlap = spriteCtx.getImageData(cropX[0], cropY[0], cropX[1], cropY[1]).data;
    sprite2Overlap = sprite2Ctx.getImageData(cropX[0], cropY[0], cropX[1], cropY[1]).data;
                
    

    现在我们只有重叠的部分,我们可以检查第一张图像上的每个像素是否不透明,然后检查另一张图像上的相同像素。

    pixelOverlap = false;
    
    // loops through every overlaping pixel in sprite2
    for(var i = 0; i < (sprite2Overlap.length / 4); i++){
        // checks if the current pixel has an opacity greater than one on both sprite or sprite2
        if(sprite2Overlap[i * 3] > 0 && spriteOverlap[i * 3] > 0){
            pixelOverlap = true;
        }
    }
    
    // if a pixel overlap was already found, sprite2 makes the function run faster
    if(!pixelOverlap){
        // loops through every overlaping pixel in sprite
        for(var i = 0; i < (spriteOverlap.length / 4); i++){
            // checks if the current pixel has an opacity greater than one on both sprite or sprite2
            if(sprite2Overlap[i * 3] > 0 && spriteOverlap[i * 3] > 0){
                pixelOverlap = true;
            }
        }
    }
    

    最后,如果这是一个函数,别忘了返回 pixelOverlap

    return pixelOverlap;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-08-20
      • 1970-01-01
      • 2013-09-03
      • 2015-09-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-04-18
      相关资源
      最近更新 更多