Math 将向量转换为与3J互补的向量
我在尝试用threeJS创建游戏引擎时遇到了一个麻烦的问题。 这是一个数学问题,也是一个编程问题 我已经为玩家的化身实现了一个基于速度的移动系统——我在这个例子中使用了一辆坦克 目前,当玩家撞到墙上时,无论角度如何,坦克都会死掉 然而,我希望它是这样的情况,坦克的速度变化,被强迫跟随墙壁的角度,并且也减少了与该角度相关的大小 例如,在图A中,当坦克撞到墙壁时,它会继续尝试向前移动,但它的速度会改变,所以它现在会以降低的速度向前移动,并向侧面移动 在图B中,坦克撞在墙上,其总速度达到0 在图C中,坦克掠过墙壁,其总速度仅降低了一小部分 我意识到我需要以某种方式将坦克的速度向量与墙壁的法向量结合起来,以生成调整后的向量,但我正在努力以数学/编程的方式表示这一点 我试过使用:Math 将向量转换为与3J互补的向量,math,three.js,game-engine,game-physics,Math,Three.js,Game Engine,Game Physics,我在尝试用threeJS创建游戏引擎时遇到了一个麻烦的问题。 这是一个数学问题,也是一个编程问题 我已经为玩家的化身实现了一个基于速度的移动系统——我在这个例子中使用了一辆坦克 目前,当玩家撞到墙上时,无论角度如何,坦克都会死掉 然而,我希望它是这样的情况,坦克的速度变化,被强迫跟随墙壁的角度,并且也减少了与该角度相关的大小 例如,在图A中,当坦克撞到墙壁时,它会继续尝试向前移动,但它的速度会改变,所以它现在会以降低的速度向前移动,并向侧面移动 在图B中,坦克撞在墙上,其总速度达到0 在图C中,
tank.velocity.multiply(wallFaceNormal)代码>(坦克速度和墙面法线都是向量3对象),但这似乎仅在墙面角度为0、90、180或270时才起作用
由于坦克不会跳跃或飞行,您只需要使用2D系统进行计算就可以了吗
我找到了一个链接,描述了汽车撞上实心砖墙的物理过程
希望这能对你有所帮助
编辑:
出于好奇,我在电话里问了一位理论物理学家关于你的问题
您需要解决两个不同的问题:
1.P1撞击墙壁时的速度v'是多少?
2.P2车辆的新角度是多少
P2应该相当容易,考虑到你的坦克正在调整墙壁的角度,你只需要计算墙壁“指向”的方向
P1在物理学中,我们讨论的是减小的力,而不是速度,但是给力F1一个恒定的限制(例如你的发动机),导致恒定的最大速度,
在给定的力作用下,墙对车辆F2
v = F1
v' = F1'
F1' = F1 - F2
我想
解释要做什么物理学家提供的一些代码,当我将其转换为Javascript并应用于程序时,部分代码起作用:
Vector3 wallNormal = new Vector3(-0.5, 0.0, 0.5);
Vector3 incomingVelocity = new Vector3(0.0, 0.0, -1.0);
double magnitudeProduct = wallNormal.Length() * incomingVelocity.Length();
double angleBetweenVelocityAndWall = ((-incomingVelocity).Dot(wallNormal)) / (magnitudeProduct);
double newVelocityMagnitude = incomingVelocity.Length() * Math.Sin(angleBetweenVelocityAndWall);
Vector3 upVector =incomingVelocity.Cross(wallNormal);
Vector3 newDirection = wallNormal.Cross(upVector);
Vector3 newVelocity = newDirection.Normalise() * newVelocityMagnitude;
我在这个问题上做了一些工作,并制作了一个小游戏“框架”,其中包括一个环境碰撞和运动衰减实用程序
我写了一篇文章解释了它是如何工作的,可以在这里找到
但为了保证线程的完整性,下面是对该部分的一个修改,该部分描述了可用于在ThreeJS模拟中衰减运动的方法之一:
关键的是,我的兴趣不是创建涉及大量物理的游戏,而是创建以下游戏:
- 玩家不能穿墙
- 球员不能从地板上摔下来
我曾尝试过几次实现一个系统来实现这种行为,但没有一次真正令人满意。直到现在
就ECS如何融入应用程序架构而言,它是一个实用类。这是它的API形状:
class Planeclamp {
constructor({ floors /*Mesh[]*/, walls /*Mesh[]*/ })
getSafePosition(startingPositionIn /*Vector3*/, intendedPositionIn /*Vector3*/) // Returns safePosition, which is a Vector3
}
如您所见,它是一个在其构造函数中接受两个网格数组的类:应视为楼板的网格和应视为墙的网格。当然,在现实中,陡峭的地板和浅角的墙壁之间没有明确的区别,但是为了模拟的目的,这种区别具有非常合理的完整性,并且将大大简化环境碰撞系统的逻辑
一旦构建了Planeclamp类的实例,就可以调用它的getSafePosition方法,将起始位置和预期位置转换为衰减位置。作为一个有辨别力的读者,您将推断出衰减位置是预期的位置,如果实用程序检测到任何碰撞,则该位置会发生一些变化
这就是如何在游戏循环中使用它,以确保玩家不会穿过墙壁或地板:
const planeclamp = new Planeclamp({
floors: [someFloorMesh, someOtherMesh],
walls: [houseMesh, perimeterMesh, truckMesh],
});
const player = new Player();
console.log(player.cage); // Object3D
let playerPreviousPosition = player.cage.position; // Vector3
function gameLoop(delta) {
const playerIntendedPosition = new Three.Vector3(
playerPreviousPosition.x,
playerPreviousPosition.y + (10 * delta), // i.e. Gravity
playerPreviousPosition.z + (1 * delta), // i.e. Walking forwards
);
let {
safePosition, // Vector3
grounded, // Boolean
groundMaterial, // String
} = planeclamp.getSafePosition(playerPreviousPosition, playerIntendedPosition);
player.cage.position.copy(safePosition);
playerPreviousPosition = player.cage.position; // Vector3
}
就这样!如果要使用此实用程序,可以在存储库中找到它。但如果你想知道更多关于其工作原理背后的逻辑,请继续阅读
Planeclamp.getSafePosition方法分两个阶段确定安全位置。首先,它使用一个垂直光线投射器来观察播放器下面的东西,然后看看它是否应该阻止播放器进一步向下移动。其次,它使用水平光线投射器来查看是否应该阻止玩家水平移动。让我们先看看垂直约束过程-这是两个步骤中比较简单的一个
// Before we do anything, create a variable called "gated".
// This will contain the safe new position that we will return at the end of
// the function. When creating it, we let it default to the
// intended position. If collisions are detected throughout the lifecycle
// of this function, these values will be overwritten.
let gated = {
x: intendedPosition.x,
y: intendedPosition.y,
z: intendedPosition.z,
};
// Define the point in 3D space where we will shoot a ray from.
// For those who haven't used raycasters before, a ray is just a line with a direction.
// We use the player's intended position as the origin of the ray, but we
// augment this by moving the origin up a little bit (backStepVert) to prevent tunneling.
const start = intendedPosition.clone().sub(new Three.Vector3(
0,
(backStepVert * -1) - (heightOffset / 2),
0)
);
// Now, define the direction of the ray, in the form of a vector.
// By giving the vector X and Z values of 0, and a Y value of -1,
// the ray shoots directly downwards.
const direction = new Three.Vector3(0, -1, 0).normalize();
// We now set the origin and direction of a raycaster that we instantiated
// in the class constructor method.
this.raycasters.vert.set(start, direction);
// Now, we use the `intersectObjects` method of the ray.
// This will return to us an array, filled with information about each
// thing that the ray collided with.
const dirCollisions = this.raycasters.vert.intersectObjects(this.floors, false);
// Initialise a distanceToGround, a grounded variable, and a groundMaterial variable.
let distanceToGround = null;
let grounded = false;
let groundMaterial = null;
// If the dirCollisions array has at least one item in it, the
// ray passed through one of our floor meshes.
if (dirCollisions.length) {
// ThreeJS returns the nearest intersection first in the collision
// results array. As we are only interested in the nearest collision,
// we pluck it out, and ignore the rest.
const collision = dirCollisions[0];
// Now, we work out the distance between where the players feet
// would be if the players intended position became the players
// actual position, and the collided object.
distanceToGround = collision.distance - backStepVert - heightOffset;
// If the distance is less than 0, then the player will pass through
// the groud if their intended position is allowed to become
// their actual position.
if (distanceToGround < 0) {
// We dont want that to hapen, so lets set the safe gated.y coordinate
// to the y coordinate of the point in space at which the collision
// happened. In other words, exactly where the ground is.
gated.y = intendedPosition.y - distanceToGround;
// Make a note that the player is now grounded.
// We return this at the end of the function, along with
// the safe position.
grounded = true;
// If the collided object also has a groundMaterial set inside
// its userData (the place that threeJS lets us attach arbitrary
// info to our objects), also set the groundMaterial. This is
// also returned at the end of the function alongside the grounded
// variable.
if (collision.object.userData.groundMaterial) {
groundMaterial = collision.object.userData.groundMaterial;
}
}
}
//在我们做任何事情之前,先创建一个名为“gated”的变量。
//这将包含安全的新位置,我们将在年底返回
//功能。在创建它时,我们让它默认为
//预定位置。如果在整个生命周期中检测到冲突
//对于此函数,这些值将被覆盖。
让选通={
x:intendedPosition.x,
y:预定位置。y,
z:预定位置,
};
//在3D空间中定义我们将从中拍摄光线的点。
//对于那些以前没有使用过光线投射器的人来说,光线只是一条有方向的线。
//我们使用玩家的预期位置作为光线的原点,但我们
//通过将原点向上移动一点(后退)来增强此功能,以防止隧道。
const start=intendeposition.clone().sub(新的3.Vector3(
0,
(后退*-1)-(高度偏移/2),
0)
);
//现在,以向量的形式定义光线的方向。
//
import * as Three from '../../../vendor/three/three.module.js';
class Planeclamp {
constructor({
scene,
floors = [],
walls = [],
drawRays = true,
} = {}) {
this.drawRays = drawRays;
this.floors = [];
this.walls = [];
this.scene = scene;
this.objects = [];
// Init collidable mesh lists
this.addFloors(floors);
this.addWalls(walls);
// Create rays
this.raycasters = {
vert: new Three.Raycaster(),
horzLeft: new Three.Raycaster(),
horzRight: new Three.Raycaster(),
correction: new Three.Raycaster(),
};
}
setDrawRays(draw) {
this.drawRays = draw;
}
addFloor(floor) {
this.floors.push(floor);
}
removeFloor(floor) {
this.floors = this.floors.filter(thisFloor => thisFloor !== floor);
}
addFloors(floors) {
floors.forEach(floor => this.addFloor(floor));
}
resetFloors() {
this.floors = [];
}
addWall(wall) {
this.walls.push(wall);
}
removeWall(wall) {
this.walls = this.walls.filter(thisWall => thisWall !== wall);
}
addWalls(walls) {
walls.forEach(wall => this.addWall(wall));
}
resetWalls() {
this.walls = [];
}
getSafePosition(startingPositionIn, intendedPositionIn, {
collisionPadding = .5,
heightOffset = 0,
} = {}) {
// ------------------ Setup -------------------
// Parse args
const startingPosition = startingPositionIn.clone();
const intendedPosition = intendedPositionIn.clone();
let grounded = false;
let groundMaterial = null;
// Augmenters
const backStepVert = 50;
const backStepHorz = 5;
const backStepCorrection = 5;
// Prepare output
let gated = {
x: intendedPosition.x,
y: intendedPosition.y,
z: intendedPosition.z,
};
// Clean up previous debug visuals
this.objects.map(object => this.scene.remove(object));
this.objects = [];
// ------------------ Vertical position gating -------------------
// Adjust vertical position in gated.y.
// Store grounded status in grounded.
const start = intendedPosition.clone().sub(new Three.Vector3(
0,
(backStepVert * -1) - (heightOffset / 2),
0)
);
const direction = new Three.Vector3(0, -1, 0).normalize();
this.raycasters.vert.set(start, direction);
const dirCollisions = this.raycasters.vert.intersectObjects(this.floors, false);
if (this.drawRays) {
const arrowColour = dirCollisions.length ? 0xff0000 : 0x0000ff;
const arrow = new Three.ArrowHelper(this.raycasters.vert.ray.direction, this.raycasters.vert.ray.origin, 300, arrowColour);
this.objects.push(arrow);
}
let distanceToGround = null;
if (dirCollisions.length) {
const collision = dirCollisions[0];
distanceToGround = collision.distance - backStepVert - heightOffset;
if (distanceToGround < 0) {
gated.y = intendedPosition.y - distanceToGround;
grounded = true;
if (collision.object.userData.groundMaterial) {
groundMaterial = collision.object.userData.groundMaterial;
}
}
}
// ------------------ Horizontal position gating -------------------
const horizontalOutputPosition = (() => {
// Init output position
const outputPosition = new Three.Vector3(intendedPosition.x, 0, intendedPosition.z);
// Store normalised input vector
const startingPos = startingPosition.clone();
const intendedPos = intendedPosition.clone();
startingPos.y = startingPositionIn.y + .5;
intendedPos.y = startingPositionIn.y + .5;
let inputVector = intendedPos.clone().sub(startingPos).normalize();
// Work out distances
const startingIntendedDist = startingPos.distanceTo(intendedPos);
const inputSpeed = startingIntendedDist;
// Define function for moving ray left and right
function adj(position, offset) {
const rayAdjuster = inputVector
.clone()
.applyAxisAngle(new Three.Vector3(0, 1, 0), Math.PI / 2)
.multiplyScalar(.5)
.multiplyScalar(offset);
return position.clone().add(rayAdjuster);
}
// Work out intersections and collision
let collisions = {
left: {
collision: null
},
right: {
collision: null
}
};
Object.keys(collisions).forEach(side => {
const rayOffset = side === 'left' ? -1 : 1;
const rayStart = adj(startingPos.clone().sub(inputVector.clone().multiplyScalar(2)), rayOffset);
const startingPosSide = adj(startingPos, rayOffset);
const intendedPosSide = adj(intendedPos, rayOffset);
const startingIntendedDistSide = startingPosSide.distanceTo(intendedPosSide);
const rayKey = 'horz' + _.startCase(side);
this.raycasters[rayKey].set(rayStart, inputVector);
const intersections = this.raycasters[rayKey].intersectObjects(this.walls, true);
for (let i = 0; i < intersections.length; i++) {
if (collisions[side].collision) break;
const thisIntersection = intersections[i];
const startingCollisionDist = startingPosSide.distanceTo(thisIntersection.point);
if (startingCollisionDist - collisionPadding <= startingIntendedDistSide) {
collisions[side].collision = thisIntersection;
collisions[side].offset = rayOffset;
}
}
if (inputSpeed && this.drawRays) {
this.objects.push(new Three.ArrowHelper(this.raycasters[rayKey].ray.direction, this.raycasters[rayKey].ray.origin, 300, 0x0000ff));
}
});
const [ leftCollision, rightCollision ] = [ collisions.left.collision, collisions.right.collision ];
const collisionData = (leftCollision?.distance || Infinity) < (rightCollision?.distance || Infinity) ? collisions.left : collisions.right;
if (collisionData.collision) {
// Var shorthands
const collision = collisionData.collision;
const normalVector = collision.face.normal.clone();
normalVector.transformDirection(collision.object.matrixWorld);
normalVector.normalize();
// Give output a baseline position that is the same as the collision position
let paddedCollision = collision.point.clone().sub(inputVector.clone().multiplyScalar(collisionPadding));
paddedCollision = adj(paddedCollision, collisionData.offset * -1);
outputPosition.x = paddedCollision.x;
outputPosition.z = paddedCollision.z;
if (leftCollision && rightCollision && leftCollision.face !== rightCollision.face) {
return startingPos;
}
// Work out difference between input vector and output / normal vector
const iCAngleCross = inputVector.clone().cross(normalVector).y; // -1 to 1
// Work out output vector
const outputVector = (() => {
const ivn = inputVector.clone().add(normalVector);
const xMultiplier = ivn.x > 0 ? 1 : -1;
const zMultiplier = ivn.z > 0 ? 1 : -1;
return new Three.Vector3(
Math.abs(normalVector.z) * xMultiplier,
0,
Math.abs(normalVector.x) * zMultiplier,
).normalize();
})();
if (inputSpeed && this.drawRays) {
this.objects.push(new Three.ArrowHelper(normalVector, startingPos, 300, 0xff0000));
}
// Work out output speed
const outputSpeed = inputSpeed * Math.abs(iCAngleCross) * 0.8;
// Increment output position with output vector X output speed
outputPosition.add(outputVector.clone().multiplyScalar(outputSpeed));
}
// ------------------ Done -------------------
return outputPosition;
})();
gated.x = horizontalOutputPosition.x;
gated.z = horizontalOutputPosition.z;
// ------------------ Culmination -------------------
// Add debug visuals
this.objects.map(object => this.scene.add(object));
// Return gated position
const safePosition = new Three.Vector3(gated.x, gated.y, gated.z);
return { safePosition, grounded, groundMaterial };
}
}
export default Planeclamp;