如何使用3D对象创建COLINION检测



我正在创建 atari 8bit 的重新制作,称为Spindizzy作为我的学校项目。

我想出了如何使用3d points将其渲染到2d perspective point

i也可以旋转形状(呈现效率的较小问题)

我的目标是这样:

Vector3D -> Vector2D-完成

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

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

,但我不知道应该什么Chandle Colision检测以及如何将其chandle。

我的目标语言是 js,但是如果给出的答案是其他语言,或者只是描述了如何解决此问题。
不允许框架。

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

在这里您可以看到我的代码。

我会感谢链接和建议。

表面和正常。

忽略投影并仅在3D空间中使用一些假设工作。

  • 所有块都与x,y轴对齐。
  • 所有块都有每个角的高度。
  • 所有块都可以具有一个平坦的表面,也可以分为两个三角形。
  • 拆分块要么从左上到右下或从左下角向左下方分开。
  • Quad(Unsplit Block)4高点必须在同一平面上。
  • 块始终是1个单位宽度(x轴)和1个单位深度(y轴)。
  • 可以拥有一个无效的块。要成为有效的块至少2分,必须具有相同的高度。所有高度设置为不同值的块不是有效的块。(这与视频中游戏的条件匹配)

每个块由一个对象定义的一个对象(x,y,z),角度高度,类型(Split1,Split2或Quad)和表面norm/s

单个功能将返回块上x,y处的点的高度。块PNorm属性将在此点设置为正常表面。(请勿修改正常。如果您需要修改它首先创建副本)

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

演示

最好的解释方法是通过演示。有很多代码可以使演示发生,所以请询问您是否有任何疑问。

note 代码用一点ES6编写,因此需要Babel在Legacy浏览器上运行。

update 第一篇文章我有一个我没有发现的错误(错误设置了正态)。我现在已经修复了。我还添加了一个错误,如果地图包含一个无效的块,它将抛出连击。

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);

处理用块的点x碰撞检测(这就像一个立方体,但允许倾斜的墙壁),从3中减去点x定义块的点A,B,C点,然后计算点矢量X的向量乘积与块点向量A',B',C',以给您3个数字A,B,c。如果这些数字分别介于零和向量A,B,C平方的长度之间,则向量X在块内。或另一种方法(每种a/'平方),b/(长度b'平方),c/(长度c'平方)较小或等于1,大于零。

同样适用于四面体,但额外的条件是a/(长度a'squared) b/(长度b'平方) c/(长度C'平方)较小或等于1。

我希望在这方面没有任何错误,这就是我如何解决问题的方式。这也无法处理两个四面体边缘的碰撞。因此,我在这里所说的有改进的余地。特别是,与立方体或四面体的边缘检查边缘会很好。

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

常规

我认为对于这个游戏,足以计算游乐场/底座的表面。然后,对于表面上的每个点,您可以在各个方向上计算衍生物。由于它们都是平面,因此可以简单地表示为常数矩阵。

播放器始终在f( x, y ) = height上,并且更改∇f( int(x), int(z) ) + vv是播放器输入的部队向量。

更改为∞(实心壁)的边界必须通过EG分别处理。计算f(x,y)中的"跳跃"。


更新1

方法

基本上您可以做的就是创建以下功能:

  • function height(x,y)->返回整数
    • 给出位置(x,y)的基数高度
  • function slope(x,y)->返回两个整数的元组(包括无穷大)
    • 在x方向和y方向上放置基数的斜率(x,y)

"播放器"从位置(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:代表用户想要应用于"播放器"的运动方向的向量。
s_g:速度/〜重力,
s_u:加快用户控制玩家。

要正确处理重力,必须将slope功能的方向逆转为正方向。

避免播放器上升90度。连续下坡:只需将坡度函数的+infinity值设置为0。

最新更新