如何使用3d对象创建colision检测?

时间:2017-02-25 10:39:13

标签: javascript canvas 3d collision-detection gravity

我正在创建一个名为spindizzy atari 8bit 游戏作为我的学校项目的重新制作。

我想出了如何使用3d points转换为2d perspective point来渲染它。

我也可以旋转形状(渲染顺序的小问题)

我的目标是这样的:

Vector3D -> Vector2D - 已完成

Block -> [[Vector3D]] -> [[Vector2D]] -> [Shape] - 已完成

Map -> [Location] -> [[[Block]]]

但是我不知道应该对什么进行检测,以及如何对其进行检测。

我的目标语言是js,但如果给出的答案是其他语言或只是描述如何解决此问题,则无关紧要。
不允许使用框架。

我正在使用画布的2d上下文。

Here您可以看到我的代码。

我会对这些链接和建议表示感谢。

3 个答案:

答案 0 :(得分:2)

表面和法线。

忽略投影并仅在具有某些假设的3D空间中工作。

  • 所有块都与x,y轴对齐。
  • 所有街区都有每个角落的高度。
  • 所有块可以有一个平面四边形,也可以分成两个三角形。
  • 拆分块从左上角到右下角或从右上角到左下角分开。
  • Quad(非分裂块)4个高度点必须在同一平面上。
  • 块总是1个单位宽度(x轴)和1个单位深度(y轴)。
  • 可能有一个无效的块。要成为一个有效的块,每个面至少2个点必须具有相同的高度。所有高度设置为不同值的块不是有效块。 (这符合视频中游戏的条件)

每个块由一个对象定义,该对象保持位置(x,y,z),角高,类型(split1,split2或quad)和表面norm / s

单个函数将返回块上x,y处的点的高度。块pNorm属性将在该点设置为曲面法线。 (不要修改法线。如果需要修改它,请先创建副本)

表面法线是垂直于平面的线。进行高度测试时,属性block.pNorm设置为适当的法线。法线用于确定球应该滚动的方向。 (我没有包括任何z运动,球被粘在表面上)。法线也用于确定阴影,以及球反弹的方向。

演示

解释的最佳方式是通过演示。有很多代码可以让演示发生,所以请问你是否有任何问题。

  

注意代码是用一点ES6编写的,因此需要babel才能在旧版浏览器上运行。

更新第一篇文章我有一个我没有发现的错误(法线错误设置)。我现在修好了。我还添加了一个错误,如果地图包含无效的块,将抛出RangeError。

var canvas = document.createElement("canvas");
canvas.width = 500;
canvas.height = 300;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);


// block types 
const types = {
    quad : 1,
    split1 : 2, // split from top left to bottom right
    split2 : 3, // split from top right to bottom left
}
/*
// A block object example to define meaning of properties
var blockObject = {
    x : 0, // top left base x pos
    y : 0, // top left base y pos
    z : 0, // top left base z pos
    norm1, // normal of quad or top right or bottom right triangles
    norm2, // normal of quad or top left or bottom left triangles 
    p1 : 0,  // top left
    p2 : 0,  // top right
    p3 : 0,  // bottom right
    p4 : 0,  // bottom left
    type : types.quad,
    pNorm : null, // this is set when a height test is done. It is the normal at the point of the height test
}*/

// compute the surface normal from two vectors on the surface. (cross product of v1,v2)
function getSurfaceNorm(x1,y1,z1,x2,y2,z2){
    // normalise vectors 
    var d1= Math.hypot(x1,y1,z1);
    x1 /= d1;
    y1 /= d1;
    z1 /= d1;
    var d2= Math.hypot(x2,y2,z2);
    x2 /= d2;
    y2 /= d2;
    z2 /= d2;
    var norm = {}
    norm.x = y1 * z2 - z1 * y2;
    norm.y = z1 * x2 - x1 * z2;
    norm.z = x1 * y2 - y1 * x2;
    return norm;
}
// This defines a block with p1-p2 the height of the corners 
// starting top left and clockwise around to p4 bottom left
// If the block is split with 2 slopes then it will be 
// of type.split1 or type.split2. If a single slope then it is a type.quad
// Also calculates the normals
function createBlock(x,y,z,h1,h2,h3,h4,type){
    var norm1,norm2;
    if(type === types.quad){
        norm1 = norm2 = getSurfaceNorm(1, 0, h2 - h1, 0, 1, h4 - h1);
    }else if(type === types.split1){
        norm1 = getSurfaceNorm(1, 0, h2 - h1, 1, 1, h3 - h1);
        norm2 = getSurfaceNorm(0, 1, h2 - h1, 1, 1, h3 - h1);
    }else{
        norm1 = getSurfaceNorm(0, 1, h2-h3, 1, 0, h4 - h3);
        norm2 = getSurfaceNorm(1, 0, h2 - h1, 0, 1, h4 - h1);
    } 
    return {
        p1 : h1,  // top left
        p2 : h2,  // top right
        p3 : h3,  // bottom right
        p4 : h4,  // bottom left
        x,y,z,type,
        norm1,norm2,
    }
}
        
        
// get the height on the block at x,y
// also sets the surface block.pNorm to match the correct normal
function getHeight(block,x,y){
    var b = block; // alias to make codes easier to read.
    if(b.type === types.quad){
        b.pNorm = b.norm1;
        if(b.p1 === b.p2){
            return (b.p3 - b.p1) * (y % 1) + b.p1 + b.z;
        }
        return (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
    }else if(b.type === types.split1){
        if(x % 1 > y % 1){ // on top right side
            b.pNorm = b.norm1;
            if(b.p1 === b.p2){
                if(b.p1 === b.p3){
                    return b.p1 + b.z;
                }
                return (b.p3 - b.p1) * (y % 1) + b.p1 + b.z;
            }
            if(b.p2 === b.p3){
                return (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
            }
            return (b.p3 - b.p2) * (y % 1) + (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
        }
        // on bottom left size
        b.pNorm = b.norm2;
        if(b.p3 === b.p4){
            if(b.p1 === b.p3){
                return b.p1 + b.z;
            }
            return (b.p3 - b.p1) * (y % 1) + b.p1 + b.z;
        }
        if(b.p1 === b.p4){
            return (b.p3 - b.p1) * (x % 1) + b.p1 + b.z;
        }
        var h = (b.p4 - b.p1) * (y % 1);
        var h1 = b.p3 - (b.p4 - b.p1) + h;
        return (h1 - (h + b.p1)) * (x % 1) + (h + b.p1) + b.z;
    }        
    if(1 - (x % 1) < y % 1){ // on bottom right side
        b.pNorm = b.norm1;
        if(b.p3 === b.p4){
            if(b.p3 === b.p2){
                return b.p2 + b.z;
            }
            return (b.p3 - b.p2) * (y % 1) + b.p4 + b.z;
        }
        if(b.p2 === b.p3){
            return (b.p4 - b.p2) * (x % 1) + b.p2 + b.z;
        }
        var h = (b.p3 - b.p2) * (y % 1);
        var h1 = b.p4 - (b.p3 - b.p2) + h;
        return (h + b.p2 - h1) * (x % 1) + h1 + b.z;
    }
    // on top left size
    b.pNorm = b.norm2;    
    if(b.p1 === b.p2){
        if(b.p1 === b.p4){
            return b.p1 + b.z;
        }
        return (b.p4 - b.p1) * (y % 1) + b.p1 + b.z;
    }
    if(b.p1 === b.p4){
        return (b.p2 - b.p1) * (x % 1) + b.p1 + b.z;
    }
    var h = (b.p4 - b.p1) * (y % 1);
    var h1 = b.p2 + h;
    return (h1 - (h + b.p1)) * (x % 1) + (h + b.p1) + b.z;

}
const projection = {
    width : 20,
    depth : 20, // y axis
    height : 8, // z axis
    xSlope : 0.5,
    ySlope : 0.5,
    originX : canvas.width / 2,
    originY : canvas.height / 4,
    toScreen(x,y,z,point = [],pos = 0){
        point[pos] = x * this.width - y * this.depth + this.originX;
        point[pos + 1] = x * this.width * this.xSlope + y * this.depth * this.ySlope -z * this.height + this.originY;
        return point;
    }
}
// working arrays to avoid excessive GC hits
var pointArray = [0,0]
var workArray = [0,0,0,0,0,0,0,0,0,0,0,0,0,0];
function drawBlock(block,col,lWidth,edge){
    var b = block;
    ctx.strokeStyle = col;
    ctx.lineWidth = lWidth;
    ctx.beginPath();
    projection.toScreen(b.x,     b.y,     b.z + b.p1, workArray, 0);
    projection.toScreen(b.x + 1, b.y,     b.z + b.p2, workArray, 2);
    projection.toScreen(b.x + 1, b.y + 1, b.z + b.p3, workArray, 4);
    projection.toScreen(b.x,     b.y + 1, b.z + b.p4, workArray, 6);
    if(b.type === types.quad){
        ctx.moveTo(workArray[0],workArray[1]);
        ctx.lineTo(workArray[2],workArray[3]);
        ctx.lineTo(workArray[4],workArray[5]);
        ctx.lineTo(workArray[6],workArray[7]);
        ctx.closePath();
    }else if(b.type === types.split1){
        ctx.moveTo(workArray[0],workArray[1]);
        ctx.lineTo(workArray[2],workArray[3]);
        ctx.lineTo(workArray[4],workArray[5]);
        ctx.closePath();
        ctx.moveTo(workArray[0],workArray[1]);
        ctx.lineTo(workArray[4],workArray[5]);
        ctx.lineTo(workArray[6],workArray[7]);
        ctx.closePath();
    }else if(b.type === types.split2){
        ctx.moveTo(workArray[0],workArray[1]);
        ctx.lineTo(workArray[2],workArray[3]);
        ctx.lineTo(workArray[6],workArray[7]);
        ctx.closePath();
        ctx.moveTo(workArray[2],workArray[3]);
        ctx.lineTo(workArray[4],workArray[5]);
        ctx.lineTo(workArray[6],workArray[7]);
        ctx.closePath();
    }
    if(edge){
        projection.toScreen(b.x + 1, b.y,     b.z, workArray, 8);
        projection.toScreen(b.x + 1, b.y + 1, b.z, workArray, 10);
        projection.toScreen(b.x,     b.y + 1, b.z, workArray, 12);
        if(edge === 1){ // right edge
            ctx.moveTo(workArray[2],workArray[3]);
            ctx.lineTo(workArray[8],workArray[9]);
            ctx.lineTo(workArray[10],workArray[11]);
            ctx.lineTo(workArray[4],workArray[5]);
        }
        if(edge === 2){ // right edge
            ctx.moveTo(workArray[4],workArray[5]);
            ctx.lineTo(workArray[10],workArray[11]);
            ctx.lineTo(workArray[12],workArray[13]);
            ctx.lineTo(workArray[6],workArray[7]);
        }
        if(edge === 3){ // right edge
            ctx.moveTo(workArray[2],workArray[3]);
            ctx.lineTo(workArray[8],workArray[9]);
            ctx.lineTo(workArray[10],workArray[11]);
            ctx.lineTo(workArray[12],workArray[13]);
            ctx.lineTo(workArray[6],workArray[7]);
            ctx.moveTo(workArray[10],workArray[11]);
            ctx.lineTo(workArray[4],workArray[5]);
        }
        
        
        
    }
    ctx.stroke();
}

function createMap(){
    var base = "0".charCodeAt(0);
    for(var y = 0; y < mapSize.depth; y ++){
        for(var x = 0; x < mapSize.width; x ++){
            var index = y * (mapSize.width + 1) + x;
            var b;
            var p1= map.charCodeAt(index)-base;
            var p2= map.charCodeAt(index+1)-base;
            var p3= map.charCodeAt(index+1+mapSize.width + 1)-base;
            var p4= map.charCodeAt(index+mapSize.width + 1)-base;
            var type;
            if((p1 === p2 && p3 === p4) || (p1 === p4 && p2 === p3)){
                type = types.quad;
            }else if(p1 === p3){
                type = types.split1;
            }else if(p4 === p2){
                type = types.split2;
            }else{
               // throw new RangeError("Map has badly formed block")
               type = types.split2;
            }
            blocks.push(
                b = createBlock(
                    x,y,0,p1,p2,p3,p4,type
                )
            );
        }
    }
}
function drawMap(){
   for(var i = 0; i < blocks.length; i ++){
       var edge = 0;
       if(i % mapSize.width === mapSize.width- 1){
           edge = 1;
       }
       if(Math.floor(i / mapSize.width) === mapSize.width- 1){
           edge |= 2;
       }
       drawBlock(blocks[i],"black",1,edge);
   }
}
function drawBallShadow(ball){
    var i;
    var x,y,ix,iy;
    ctx.globalAlpha = 0.5;
    ctx.fillStyle = "black";
    ctx.beginPath();
    var first = 0;
    for(var i = 0; i < 1; i += 1/8){
        var ang = i * Math.PI * 2;
        x = ball.x + (ball.rad / projection.width ) * Math.cos(ang) * 0.7;
        y = ball.y + (ball.rad / projection.depth ) * Math.sin(ang) * 0.7;
        if(x < mapSize.width && x >= 0 && y < mapSize.depth && y > 0){
            ix = Math.floor(x + mapSize.width) % mapSize.width;
            iy = Math.floor(y + mapSize.depth) % mapSize.depth;
            var block = blocks[ix + iy * mapSize.width];
            var z = getHeight(block,x,y);
            projection.toScreen(x,y,z, pointArray);
            if(first === 0){
                first = 1;
                ctx.moveTo(pointArray[0],pointArray[1]);
            }else{
                ctx.lineTo(pointArray[0],pointArray[1]);
            }
        }
    }
    ctx.fill();
    ctx.globalAlpha = 1;
    
}

function drawBall(ball){
    projection.toScreen(ball.x, ball.y, ball.z, pointArray);
    ctx.fillStyle = ball.col;
    ctx.strokeStyle = "black";
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(pointArray[0],pointArray[1],ball.rad,0,Math.PI * 2);
    ctx.stroke();
    ctx.fill();
    ctx.fillStyle = "white";
    ctx.beginPath();
    ctx.arc(pointArray[0]-ball.rad/2,pointArray[1]-ball.rad/2,ball.rad/4,0,Math.PI * 2);
    ctx.fill();
}
function updateBall(ball){
    // reset ball if out of bounds;
    if(ball.x > mapSize.width || ball.y > mapSize.depth || ball.x < 0 || ball.y < 0){
        ball.x += ball.dx;
        ball.y += ball.dy;
        ball.z += ball.dz;
        ball.dz -= 0.1;
        if(ball.z < -10){
        
            ball.x = Math.random() * 3;
            ball.y = Math.random() * 3;
            ball.dz  = 0;
            // give random speed
            ball.dx = Math.random() * 0.01;
            ball.dy = Math.random() * 0.01;
        }else{
            ball.dist = Math.hypot(ball.x - 20,ball.y - 20, ball.z - 20);        
            return;
        }
        
    }
    // get the block under the ball
    var block = blocks[Math.floor(ball.x) + Math.floor(ball.y) * mapSize.width];
    const lastZ = ball.z;
    // get the height of the black at the balls position
    ball.z = getHeight(block,ball.x,ball.y);
    // use the face normal to add velocity in the direction of the normal
    ball.dx += block.pNorm.x * 0.01;
    ball.dy += block.pNorm.y * 0.01;
    // move the ball up by the amount of its radius
    ball.z += ball.rad / projection.height;
    ball.dz =lastZ - ball.z;
    // draw the shadow and ball
    ball.x += ball.dx;
    ball.y += ball.dy;
    // get distance from camera;
    ball.dist = Math.hypot(ball.x - 20,ball.y - 20, ball.z - 20);
}
function renderBall(ball){
    drawBallShadow(ball);
    drawBall(ball);
}
function copyCanvas(canvas){
    var can = document.createElement("canvas");
    can.width = canvas.width;
    can.height = canvas.height;
    can.ctx = can.getContext("2d");
    can.ctx.drawImage(canvas,0,0);
    return can;    
}
var map = `
    9988888789999
    9887787678999
    9877676567899
    9876765678789
    9876655567789
    8766555456789
    7655554456678
    6654443456789
    6543334566678
    5432345677889
    4321234567789
    4321234567899
    5412345678999
`.replace(/\n| |\t/g,"");
var mapSize = {width : 12, depth : 12}; // one less than map width and depth
var blocks = [];
ctx.clearRect(0,0,canvas.width,canvas.height)
createMap();
drawMap();
var background = copyCanvas(canvas);
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;  // center 
var ch = h / 2;
var globalTime;  // global to this 
var balls = [{
        x : -10,
        y : 0,
        z : 100,
        dx : 0,
        dy : 0,
        dz : 0,
        col : "red",
        rad : 10,
    },{
        x : -10,
        y : 0,
        z : 100,
        dx : 0,
        dy : 0,
        dz : 0,
        col : "Green",
        rad : 10,
    },{
        x : -10,
        y : 0,
        z : 100,
        dx : 0,
        dy : 0,
        dz : 0,
        col : "Blue",
        rad : 10,
    },{
        x : -10,
        y : 0,
        z : 100,
        dx : 0,
        dy : 0,
        dz : 0,
        col : "yellow",
        rad : 10,
    },{
        x : -10,
        y : 0,
        z : 100,
        dx : 0,
        dy : 0,
        dz : 0,
        col : "cyan",
        rad : 10,
    },{
        x : -10,
        y : 0,
        z : 100,
        dx : 0,
        dy : 0,
        dz : 0,
        col : "black",
        rad : 10,
    },{
        x : -10,
        y : 0,
        z : 100,
        dx : 0,
        dy : 0,
        dz : 0,
        col : "white",
        rad : 10,
    },{
        x : -10,
        y : 0,
        z : 100,
        dx : 0,
        dy : 0,
        dz : 0,
        col : "orange",
        rad : 10,
    }
];

// main update function
function update(timer){
    globalTime = timer;
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
    ctx.clearRect(0,0,w,h);
    ctx.drawImage(background,0,0);
    // get the block under the ball
    for(var i = 0; i < balls.length; i ++){
        updateBall(balls[i]);
    }
    balls.sort((a,b)=>b.dist - a.dist);
    for(var i = 0; i < balls.length; i ++){
        renderBall(balls[i]);
    }
    requestAnimationFrame(update);
}
requestAnimationFrame(update);

答案 1 :(得分:1)

要处理带有块的点x的碰撞检测(类似于立方体但允许倾斜的墙壁),从定义块的3个点a,b,c中减去点x,然后计算向量点向量x的乘积与块点向量a&#39;,b&#39;,c&#39;给你3个数字A,B,C。如果这些数字分别在零和矢量a,b,c的长度之间,则矢量x在块内。或者换句话说,每个A /(长度a&#39;平方),B /(长度b&#39;平方),C /(长度c&#39;平方)小于或等于1且大于零。

同样适用于四面体,但附加条件是A /(长度a&#39;平方)+ B /(长度b&#39;平方)+ C /(长度c&#39;平方)较小或等于1。

我希望在这方面没有错,这就是我如何处理这个问题。这也没有处理边缘边缘的两个四面体的碰撞。所以我在这里说的还有改进的余地。特别是,对立方体或四面体的边缘进行碰撞检查会很好。

对于其他形状,将这些形状分成立方体和四面体。

答案 2 :(得分:1)

一般

我认为对于这个游戏来说,计算游乐场/基地的表面就足够了。然后,对于曲面上的每个点,您可以计算所有方向的导数。由于它们都是平面,因此可以简单地表示为常量矩阵。

玩家始终在f( x, y ) = height,并且∇f( int(x), int(z) ) + v更改为v是玩家输入的力量向量。

变化为∞的边界(实心墙)必须单独处理,例如。计算&#39;跳跃&#39;在f(x,y)。

更新1

方法

所以基本上你可以做的是创建以下功能:

  • function height(x,y) - &gt;返回一个整数
    • 给出位置(x,y)处的基座高度
  • function slope(x,y) - &gt;返回两个整数的元组(包括无穷大)
    • 给出x方向和y方向上位置(x,y)处的基部斜率

&#39>玩家&#39;从位置(x_0, y_0)开始,因此身高z_0 = height(x_0, y_0)

接下来的位置将是 (x_1, y_1) = (x_0, y_0) + s_g * slope(x_0, y_0) + s_u * v,身高z_1 = height(x_1, y_1) v:表示用户想要应用于“玩家”的移动方向的矢量&#39;。
s_g:速度/〜重力,
s_u:加快用户控制播放器的速度。

为了正确处理重力,slope函数的方向必须反转为正方向。

避免玩家上升90度。连续下坡: 只需将斜率函数的+infinity值设置为0。