new physics engine - rapier instead of cannon

This commit is contained in:
2025-08-29 16:03:48 +03:00
parent 7eb7eb068a
commit a6827f4ddb
7 changed files with 625 additions and 97 deletions
+171
View File
@@ -0,0 +1,171 @@
import * as THREE from 'three'
export const CONTROLLER_BODY_RADIUS = 0.28;
export class CharacterControls {
// temporary data
walkDirection = new THREE.Vector3()
rotateAngle = new THREE.Vector3(0, 1, 0)
rotateQuarternion = new THREE.Quaternion()
cameraTarget = new THREE.Vector3()
storedFall = 0
// constants
fadeDuration = 0.2
runVelocity = 7
walkVelocity = 3
lerp = (x, y, a) => x * (1 - a) + y * a;
constructor(model, mixer, animationsMap, orbitControl, camera, currentAction, ray, rigidBody, pointerControls) {
this.model = model
this.mixer = mixer
this.animationsMap = animationsMap
this.currentAction = currentAction
this.animationsMap[currentAction].play()
this.ray = ray
this.rigidBody = rigidBody
this.pointerControls = pointerControls
this.orbitControl = orbitControl
this.camera = camera
this.updateCameraTarget(new THREE.Vector3(0,1,5))
}
switchRunToggle() {
this.toggleRun = !this.toggleRun
}
update(world, delta, pointerControls) {
const directionPressed = pointerControls.moving()
var play = '';
if (directionPressed && this.toggleRun) {
play = 'run'
} else if (directionPressed) {
play = 'walk'
} else {
play = 'idle'
}
if (this.currentAction != play) {
const toPlay = this.animationsMap[play]
const current = this.animationsMap[this.currentAction]
current.fadeOut(this.fadeDuration)
toPlay.reset().fadeIn(this.fadeDuration).play();
this.currentAction = play
}
this.mixer.update(delta)
this.walkDirection.x = this.walkDirection.y = this.walkDirection.z = 0
let velocity = 0
if (this.currentAction == 'run' || this.currentAction == 'walk') {
// calculate towards camera direction
var angleYCameraDirection = Math.atan2(
(this.camera.position.x - this.model.position.x),
(this.camera.position.z - this.model.position.z))
// diagonal movement angle offset
var directionOffset = this.directionOffset(pointerControls)
// rotate model
this.rotateQuarternion.setFromAxisAngle(this.rotateAngle, Math.PI + angleYCameraDirection + directionOffset)
this.model.quaternion.rotateTowards(this.rotateQuarternion, 0.2)
// calculate direction
this.camera.getWorldDirection(this.walkDirection)
this.walkDirection.y = 0
this.walkDirection.normalize()
this.walkDirection.applyAxisAngle(this.rotateAngle, directionOffset)
// run/walk velocity
velocity = this.currentAction == 'run' ? this.runVelocity : this.walkVelocity
}
const translation = this.rigidBody.translation();
if (translation.y < -1) {
// don't fall below ground
this.rigidBody.setNextKinematicTranslation( {
x: 0,
y: 10,
z: 0
});
} else {
const cameraPositionOffset = this.camera.position.sub(this.model.position);
// update model and camera
this.model.position.x = translation.x
this.model.position.y = translation.y
this.model.position.z = translation.z
this.updateCameraTarget(cameraPositionOffset)
this.walkDirection.y += this.lerp(this.storedFall, -9.81 * delta, 0.10)
this.storedFall = this.walkDirection.y
this.ray.origin.x = translation.x
this.ray.origin.y = translation.y
this.ray.origin.z = translation.z
let hit = world.castRay(this.ray, 0.5, false, 0xfffffffff);
if (hit) {
const point = this.ray.pointAt(hit.timeOfImpact);
let diff = translation.y - ( point.y + CONTROLLER_BODY_RADIUS);
if (diff < 0.0) {
this.storedFall = 0
this.walkDirection.y = this.lerp(0, -diff, 0.5)
}
}
this.walkDirection.x = this.walkDirection.x * velocity * delta
this.walkDirection.z = this.walkDirection.z * velocity * delta
this.rigidBody.setNextKinematicTranslation( {
x: translation.x + this.walkDirection.x,
y: translation.y + this.walkDirection.y,
z: translation.z + this.walkDirection.z
});
}
}
updateCameraTarget(offset) {
// move camera
const rigidTranslation = this.rigidBody.translation();
this.camera.position.x = rigidTranslation.x + offset.x
this.camera.position.y = rigidTranslation.y + offset.y
this.camera.position.z = rigidTranslation.z + offset.z
// update camera target
this.cameraTarget.x = rigidTranslation.x
this.cameraTarget.y = rigidTranslation.y + 1
this.cameraTarget.z = rigidTranslation.z
this.orbitControl.target = this.cameraTarget
}
directionOffset(pointerControls) {
var directionOffset = 0 // w
if (pointerControls.moveForward) {
if (pointerControls.moveLeft) {
directionOffset = Math.PI / 4 // w+a
} else if (pointerControls.moveRight) {
directionOffset = - Math.PI / 4 // w+d
}
} else if (pointerControls.moveBackward) {
if (pointerControls.moveLeft) {
directionOffset = Math.PI / 4 + Math.PI / 2 // s+a
} else if (pointerControls.moveRight) {
directionOffset = -Math.PI / 4 - Math.PI / 2 // s+d
} else {
directionOffset = Math.PI // s
}
} else if (pointerControls.moveLeft) {
directionOffset = Math.PI / 2 // a
} else if (pointerControls.moveRight) {
directionOffset = - Math.PI / 2 // d
}
return directionOffset
}
}
+107 -65
View File
@@ -1,7 +1,8 @@
import { AnimationMixer } from 'three';
import { PointerControls } from './PointerControls';
import { CharacterControls } from './CharacterControls';
import * as THREE from 'three';
import * as CANNON from 'cannon-es';
import * as RAPIER from '@dimforge/rapier3d'
class Hero{
@@ -16,7 +17,8 @@ class Hero{
this.gameEngine = gameEngine;
//this.gameEngine.orbitControls.object = this.model;
//this.gameEngine.orbitControls.target = this.model.position;
this.gameEngine.orbitControls.enabled = false;
//this.gameEngine.orbitControls.enabled = false;
//this.model.add(gameEngine.camera)
gameEngine.camera.position.set(0,17,-30)
@@ -24,66 +26,91 @@ class Hero{
this.heroCamera = new THREE.Object3D()
this.model.add(this.heroCamera)
this.heroCamera.applyMatrix4(gameEngine.camera.matrix)
this.heroCamera.applyMatrix4(gameEngine.camera.matrix)
this.mixer = new AnimationMixer(this.model);
gameEngine.mixers.push( this.mixer );
this.actionWalk = this.mixer.clipAction( this.object.animations.find(a=>a.name=='walk') );
this.actionIdle = this.mixer.clipAction( this.object.animations.find(a=>a.name=='idle') );
this.actionIdle.play();
this.activeAction = this.actionIdle;
this.animationsMap = {};
this.object.animations.forEach(a=>{
this.animationsMap[a.name] = this.mixer.clipAction(a);
})
// this.actionWalk = this.mixer.clipAction( this.object.animations.find(a=>a.name=='walk') );
// this.actionIdle = this.mixer.clipAction( this.object.animations.find(a=>a.name=='idle') );
// this.actionIdle.play();
// this.activeAction = this.actionIdle;
this.pointerControls = new PointerControls(gameEngine.camera, this.model, gameEngine.renderer.domElement);
gameEngine.hero = this;
// Character Collider
const characterCollider = new THREE.Object3D()
characterCollider.position.y = 3
gameEngine.activeObjects.add(characterCollider)
const colliderShape = new CANNON.Sphere(0.5)
const colliderBody = new CANNON.Body({ mass: 1, material: gameEngine.phy.slipperyMaterial })
colliderBody.addShape(colliderShape, new CANNON.Vec3(0, 0.5, 0))
colliderBody.addShape(colliderShape, new CANNON.Vec3(0, -0.5, 0))
colliderBody.position.set(
characterCollider.position.x,
characterCollider.position.y,
characterCollider.position.z
)
colliderBody.linearDamping = 0.95
colliderBody.angularFactor.set(0, 1, 0) // prevents rotation X,Z axis
//colliderBody.sleepSpeedLimit = 1.0;
gameEngine.phy.world.addBody(colliderBody)
// const characterCollider = new THREE.Object3D()
// characterCollider.position.y = 3
// gameEngine.activeObjects.add(characterCollider)
this.characterCollider = characterCollider;
this.colliderBody = colliderBody;
// const colliderShape = new CANNON.Sphere(0.5)
// const colliderBody = new CANNON.Body({ mass: 1, material: gameEngine.phy.slipperyMaterial })
// colliderBody.addShape(colliderShape, new CANNON.Vec3(0, 0.5, 0))
// colliderBody.addShape(colliderShape, new CANNON.Vec3(0, -0.5, 0))
// colliderBody.position.set(
// characterCollider.position.x,
// characterCollider.position.y,
// characterCollider.position.z
// )
// colliderBody.linearDamping = 0.95
// colliderBody.angularFactor.set(0, 1, 0) // prevents rotation X,Z axis
// //colliderBody.sleepSpeedLimit = 1.0;
// gameEngine.phy.world.addBody(colliderBody)
let bodyDesc = RAPIER.RigidBodyDesc.kinematicPositionBased().setTranslation(
// characterCollider.position.x,
// characterCollider.position.y,
// characterCollider.position.z
-1, 3, 1
)
let rigidBody = gameEngine.phy.world.createRigidBody(bodyDesc);
let dynamicCollider = RAPIER.ColliderDesc.ball(0.28);
gameEngine.phy.world.createCollider(dynamicCollider, rigidBody.handle);
this.characterControls = new CharacterControls(this.model, this.mixer,
this.animationsMap, gameEngine.orbitControls, gameEngine.camera, 'idle',
new RAPIER.Ray(
{ x: 0, y: 0, z: 0 },
{ x: 0, y: -1, z: 0}
), rigidBody, this.pointerControls)
// this.characterCollider = characterCollider;
// this.colliderBody = colliderBody;
this.clock = new THREE.Clock()
this.delta = 0
this.inputVelocity = new THREE.Vector3()
this.velocity = new CANNON.Vec3()
this.quat = new THREE.Quaternion()
this.v = new THREE.Vector3()
this.rotation = new THREE.Euler()
this.targetQuaternion = new THREE.Quaternion()
this.distance = 0
this.canJump = true;
// this.inputVelocity = new THREE.Vector3()
// this.velocity = new CANNON.Vec3()
// this.quat = new THREE.Quaternion()
// this.v = new THREE.Vector3()
// this.rotation = new THREE.Euler()
// this.targetQuaternion = new THREE.Quaternion()
// this.distance = 0
// this.canJump = true;
this.contactNormal = new CANNON.Vec3()
this.upAxis = new CANNON.Vec3(0, 1, 0)
// this.contactNormal = new CANNON.Vec3()
// this.upAxis = new CANNON.Vec3(0, 1, 0)
colliderBody.addEventListener('collide', function (e) {
const contact = e.contact
if (contact.bi.id == this.colliderBody.id) {
contact.ni.negate(this.contactNormal)
} else {
this.contactNormal.copy(contact.ni)
}
if (this.contactNormal.dot(this.upAxis) > 0.5) {
if (!this.canJump) {
setAction(animationActions[1], true)
}
this.canJump = true
}
}.bind(this))
// colliderBody.addEventListener('collide', function (e) {
// const contact = e.contact
// if (contact.bi.id == this.colliderBody.id) {
// contact.ni.negate(this.contactNormal)
// } else {
// this.contactNormal.copy(contact.ni)
// }
// if (this.contactNormal.dot(this.upAxis) > 0.5) {
// if (!this.canJump) {
// setAction(animationActions[1], true)
// }
// this.canJump = true
// }
// }.bind(this))
this.ready = true;
@@ -93,27 +120,42 @@ class Hero{
this.pointerControls.controls.lock();
}
setAction(toAction, loop){
if (toAction != this.activeAction) {
let lastAction = this.activeAction;
this.activeAction = toAction
lastAction.fadeOut(0.1)
this.activeAction.reset()
this.activeAction.fadeIn(0.1)
this.activeAction.play()
if (!loop) {
this.activeAction.clampWhenFinished = true
this.activeAction.loop = THREE.LoopOnce
}
}
}
// setAction(toAction, loop){
// if (toAction != this.activeAction) {
// let lastAction = this.activeAction;
// this.activeAction = toAction
// lastAction.fadeOut(0.1)
// this.activeAction.reset()
// this.activeAction.fadeIn(0.1)
// this.activeAction.play()
// if (!loop) {
// this.activeAction.clampWhenFinished = true
// this.activeAction.loop = THREE.LoopOnce
// }
// }
// }
update(){
//return
if (this.gameEngine.renderer.xr.isPresenting) return;
if (this.ready) {
let pc = this.pointerControls;
pc.update();
let dlt = this.clock.getDelta();
this.delta += dlt;
if (this.delta > 0.016){
this.characterControls.update(this.gameEngine.phy.world, this.delta, pc)
this.gameEngine.phy.world.step()
this.delta = 0;
}
}
return;
let { inputVelocity, velocity, quat, v, targetQuaternion, clock, rotation } = this;
let pc = this.pointerControls;
pc.update();
// let pc = this.pointerControls;
// pc.update();
if (this.ready) {
if (this.canJump) {
//walking
+7
View File
@@ -11,10 +11,13 @@ class PointerControls {
this.moveBackward = false;
this.moveLeft = false;
this.moveRight = false;
this.moveUp = false;
this.moveDown = false;
this.rotateLeft = false;
this.rotateRight = false;
this.canJump = false;
this.velocity = new Vector3();
this.direction = new Vector3();
@@ -163,6 +166,10 @@ class PointerControls {
};
}
moving(){
return this.moveForward || this.moveBackward || this.moveLeft || this.moveRight;
}
}
export { PointerControls };
+32 -22
View File
@@ -13,7 +13,7 @@ import { TransformControls } from 'three/addons/controls/TransformControls.js';
import { ARButton } from 'three/addons/webxr/ARButton.js';
import { XRButton } from 'three/addons/webxr/XRButton.js';
import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js';
import * as CANNON from 'cannon-es';
import * as RAPIER from '@dimforge/rapier3d'
import { Clickable } from './Clickable.js';
class GameEngine {
@@ -247,28 +247,38 @@ class GameEngine {
}
initPhysics() {
const world = new CANNON.World()
world.gravity.set(0, -9.82, 0)
const groundMaterial = new CANNON.Material('groundMaterial')
const slipperyMaterial = new CANNON.Material('slipperyMaterial')
const slippery_ground_cm = new CANNON.ContactMaterial(
groundMaterial,
slipperyMaterial,
{
friction: 0,
restitution: 0.3,
contactEquationStiffness: 1e10,
contactEquationRelaxation: 30,
}
)
world.addContactMaterial(slippery_ground_cm)
const planeShape = new CANNON.Plane()
const planeBody = new CANNON.Body({ mass: 0, material: groundMaterial })
planeBody.addShape(planeShape)
planeBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2)
world.addBody(planeBody)
//const world = new CANNON.World()
//world.gravity.set(0, -9.82, 0)
let gravity = { x: 0.0, y: -9.81, z: 0.0 };
let world = new RAPIER.World(gravity);
// Create Ground.
let bodyDesc = RAPIER.RigidBodyDesc.fixed();
let body = world.createRigidBody(bodyDesc);
let colliderDesc = RAPIER.ColliderDesc.cuboid(100.0, 0.1, 100.0);
world.createCollider(colliderDesc, body.handle);
// const groundMaterial = new CANNON.Material('groundMaterial')
// const slipperyMaterial = new CANNON.Material('slipperyMaterial')
// const slippery_ground_cm = new CANNON.ContactMaterial(
// groundMaterial,
// slipperyMaterial,
// {
// friction: 0,
// restitution: 0.3,
// contactEquationStiffness: 1e10,
// contactEquationRelaxation: 30,
// }
// )
// world.addContactMaterial(slippery_ground_cm)
// const planeShape = new CANNON.Plane()
// const planeBody = new CANNON.Body({ mass: 0, material: groundMaterial })
// planeBody.addShape(planeShape)
// planeBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2)
// world.addBody(planeBody)
this.phy = {
world, slipperyMaterial
world,
//slipperyMaterial
}
}