简体   繁体   English

如何使用3D对象创建大肠菌病检测?

[英]How to create colision detection with 3d objects?

I'm creating a re-make of atari 8bit game called spindizzy as my school project. 我正在将我的学校项目重新制作一个名为spindizzyatari 8bit游戏。

I figured out how to render it using casting 3d points to 2d perspective point . 我想出了如何使用将3d points投射到2d perspective point进行渲染的方法。

I also can spin shapes (with minor problem of order of rendering) . 我还可以旋转形状(存在较小的渲染顺序问题)

My goal was something like this: 我的目标是这样的:

Vector3D -> Vector2D - done Vector3D -> Vector2D完成

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

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

But I have no idea what should chandle colision detection, and how to chandle it. 但是我不知道应该如何遏制大肠埃希氏菌的检测,以及如何进行控制。

My target language is js but it does not matter if given answer is in other language or just description of how to solve this problem. 我的目标语言是js但是给出的答案是其他语言还是只是描述如何解决此问题都没有关系。
Frameworks are not allowed. 不允许使用框架。

I'm using 2d context of canvas. 我正在使用2d画布背景。

Here you can see my code. 在这里您可以看到我的代码。

I'll be aslo grateful for the links and suggestions. 对于这些链接和建议,我也将不胜感激。

Surfaces and normals. 表面和法线。

Ignoring the projection and working only in 3D space with some assumptions. 忽略一些假设,仅在3D空间中工作。

  • All blocks are aligned with the x,y axis. 所有块均与x,y轴对齐。
  • All blocks have the height of each corner. 所有块都有每个角的高度。
  • All blocks can have either one flat surface a quad, or be split into two triangles. 所有块都可以在一个平面上有一个四边形,也可以分成两个三角形。
  • The Split blocks are either split from top left to bottom right or from top right to bottom left. 拆分块从左上角到右下角或从右上角到左下角拆分。
  • Quad (unsplit block) 4 height points must be on the same plane. 四边形(未分割的块)的4个高度点必须在同一平面上。
  • Blocks are always 1 unit width (x axis) and 1 unit in depth (y axis). 块始终为1单位宽度(x轴)和1单位深度(y轴)。
  • It is possible to have an invalid block. 可能有一个无效的块。 To be a valid block at least 2 points for each face must have the same height. 要成为有效块,每个面至少要有2个点具有相同的高度。 A block with all height set to a different value is not a valid block. 所有高度都设置为不同值的块不是有效块。 (this matches the conditions of the game in the video) (这符合视频中的游戏条件)

Each block is defined by one object that holds the position (x,y,z), corner heights, the type (split1, split2, or quad) and the surface norm/s 每个块由一个对象定义,该对象保留位置(x,y,z),角高,类型(split1,split2或quad)和曲面范数/ s

A single function will return the height of a point at x,y on the block. 单个函数将返回块上x,y处的点的高度。 The blocks pNorm property will be set to the surface normal at that point. 此时,块pNorm属性将设置为表面法线。 (Do not modify the normal. If you need to modify it create a copy first) (请勿修改常规。如果需要修改它,请先创建一个副本)

The surface normal is a line perpendicular to the plane. 表面法线是垂直于平面的线。 When you do a height test the property block.pNorm is set to the appropriate normal. 进行高度测试时,将属性block.pNorm设置为适当的法线。 The normal is used to determine the direction the ball should roll. 法线用于确定球应该滚动的方向。 (I have not included any z movement, the ball is stuck to the surface). (我没有包含任何z运动,球被粘在了表面上)。 The normal is also used to determine shading, and what direction the ball would bounce. 法线还用于确定阴影以及球反弹的方向。

A Demo 演示

The best way to explain is via a demo. 最好的解释方法是通过演示。 There is a lot of code to get the demo happening so please do ask if you have any questions. 有很多代码可以使演示进行,因此请务必询问您是否有任何问题。

Note the code is written with a little ES6 so will need babel to run on legacy browsers. 请注意,该代码是用少量ES6编写的,因此需要babel才能在旧版浏览器上运行。

UPDATE First post I had a bug I did not spot (normals were incorrectly set). 更新第一篇文章我有一个我没有发现的错误(法线设置不正确)。 I have fixed it now. 我已经修复了。 I have also added an error that will throw a RangeError if the map contains a invalid block. 我还添加了一个错误,如果地图包含无效块,该错误将引发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 + bz; } return (b.p2 - b.p1) * (x % 1) + b.p1 + bz; }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 + bz; } return (b.p3 - b.p1) * (y % 1) + b.p1 + bz; } if(b.p2 === b.p3){ return (b.p2 - b.p1) * (x % 1) + b.p1 + bz; } return (b.p3 - b.p2) * (y % 1) + (b.p2 - b.p1) * (x % 1) + b.p1 + bz; } // on bottom left size b.pNorm = b.norm2; if(b.p3 === b.p4){ if(b.p1 === b.p3){ return b.p1 + bz; } return (b.p3 - b.p1) * (y % 1) + b.p1 + bz; } if(b.p1 === b.p4){ return (b.p3 - b.p1) * (x % 1) + b.p1 + bz; } 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) + bz; } 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 + bz; } return (b.p3 - b.p2) * (y % 1) + b.p4 + bz; } if(b.p2 === b.p3){ return (b.p4 - b.p2) * (x % 1) + b.p2 + bz; } 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 + bz; } // on top left size b.pNorm = b.norm2; if(b.p1 === b.p2){ if(b.p1 === b.p4){ return b.p1 + bz; } return (b.p4 - b.p1) * (y % 1) + b.p1 + bz; } if(b.p1 === b.p4){ return (b.p2 - b.p1) * (x % 1) + b.p1 + bz; } var h = (b.p4 - b.p1) * (y % 1); var h1 = b.p2 + h; return (h1 - (h + b.p1)) * (x % 1) + (h + b.p1) + bz; } 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(bx, by, bz + b.p1, workArray, 0); projection.toScreen(bx + 1, by, bz + b.p2, workArray, 2); projection.toScreen(bx + 1, by + 1, bz + b.p3, workArray, 4); projection.toScreen(bx, by + 1, bz + 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(bx + 1, by, bz, workArray, 8); projection.toScreen(bx + 1, by + 1, bz, workArray, 10); projection.toScreen(bx, by + 1, bz, 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); 

To handle collision detection of a point x with a block (which is like a cube but with slanted walls allowed), subtract the point x from the 3 points a,b,c that define the block, and then calculate the vector product of point vector x with the block point vector a',b',c' to give you 3 numbers A,B,C. 要使用块(类似于立方体,但允许倾斜的壁)处理点x的碰撞检测,请从定义块的3个点a,b,c中减去点x,然后计算点的矢量积向量x与块点向量a',b',c'一起给您3个数字A,B,C。 If these numbers are between zero and the length of the vectors a,b,c squared respectively, the vector x is within the block. 如果这些数字在零与向量a,b,c的长度分别成平方之间,则向量x在块内。 Or put another way that each A/(length a' squared), B/(length b' squared), C/(length c' squared) is less or equal 1 and greater than zero. 或换一种说法,每个A /(长度a'平方),B /(长度b'平方),C /(长度c'平方)小于或等于1且大于零。

Likewise do for a tetrahedron, but with the additional condition that A/(length a' squared)+B/(length b' squared)+C/(length c' squared) is less or equal 1. 同样对四面体也是如此,但附加条件是A /(长度a'平方)+ B /(长度b'平方)+ C /(长度c'平方)小于或等于1。

I hope theres no mistake in this, it is how I would approach the problem. 我希望这没有错,这就是我要解决的问题。 This also doesn't handle the collision of two tetrahedrons edge-on-edge. 这也不能处理两个四面体边对边的碰撞。 So there is room for improvement over what I said here. 因此,我在这里所说的还有改进的余地。 Especially, a collision check for edges with cubes or tetrahedrons would be nice. 特别是,对带有立方体或四面体的边缘进行碰撞检查会很好。

For other shapes, divide these shapes up into cubes and tetrahedrons. 对于其他形状,请将这些形状分为立方体和四面体。

General 一般

I think for this game it would be enough to calculate the surface of the playground/base. 我认为对于此游戏,计算游乐场/基地的表面就足够了。 Then for each point on the surface you can calculate the derivatives in all directions. 然后,您可以为曲面上的每个点计算所有方向的导数。 Since they are all planes, this can simply be represented as a matrix of constants. 由于它们都是平面,因此可以简单地表示为常数矩阵。

The player is always on f( x, y ) = height and changes ∇f( int(x), int(z) ) + v with v being the force's vector inputed by the player. 玩家始终处于f( x, y ) = height并更改∇f( int(x), int(z) ) + v其中v是玩家输入的力矢量。

The borders where the change is ∞ (solid walls) have to be handled separately by eg. 变化为∞(实心墙)的边界必须分别通过例如来处理。 calculating 'jumps' in f( x, y ). 计算f(x,y)中的“跳跃”。


UPDATE 1 更新1

Approach 途径

So Basically what you could do is create the following functions: 因此,基本上,您可以创建以下功能:

  • function height(x,y) -> returning an integer function height(x,y) ->返回整数
    • giving the height of the base at position (x,y) 给出位置(x,y)的底座高度
  • function slope(x,y) -> returning a tuple of two integers (including infinity) function slope(x,y) ->返回两个整数的元组(包括无穷大)
    • giving the slope of the base at position (x,y) in x direction and in y direction 给出在x方向和y方向上位置(x,y)的底坡度

The 'player' starts at position (x_0, y_0) and has therefore height z_0 = height(x_0, y_0) . “玩家”从位置(x_0, y_0)开始,因此身高z_0 = height(x_0, y_0)

The next position is then going to be (x_1, y_1) = (x_0, y_0) + s_g * slope(x_0, y_0) + s_u * v , with height z_1 = height(x_1, y_1) . 然后下一个位置将是(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 : vector representing the direction of movement the user wants to apply onto the 'player'. v :代表用户想要应用到“玩家”上的运动方向的矢量。
s_g : speed/~gravity, s_g :速度/〜重力,
s_u : speed the user controls the player with. s_u :用户控制播放器的速度。

To handle gravity properly, the direction of the slope function would have to be reversed for positive directions. 为了正确处理重力,必须将slope函数的方向反转为正方向。

To avoid the player going up 90deg. 为了避免玩家上升90度。 slopes: Simply set +infinity values of the slope function to 0. 斜率:只需将斜率函数的+infinity值设置为0。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM