physics cleanup

This commit is contained in:
Evert Prants 2023-06-22 15:09:31 +03:00
parent 837be262a4
commit f0b877f11b
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
25 changed files with 521 additions and 425 deletions

View File

@ -40,9 +40,6 @@ export class GameplayComponent extends EngineComponent {
override update(delta: number): void {
this.controls?.update(delta);
for (const character of this.characters) {
character.tick(delta);
}
this.move.set(0, 0, 0);
this.look.setFromMatrixColumn(this.renderer.camera.matrix, 0);
@ -77,7 +74,6 @@ export class GameplayComponent extends EngineComponent {
const ctrl = getCharacterController(char);
this.world.add(char);
this.characters.push(ctrl);
ctrl.initialize();
this.character = ctrl;
this.controls = new ThirdPersonCamera(
@ -86,6 +82,7 @@ export class GameplayComponent extends EngineComponent {
this.renderer.renderer.domElement
);
this.controls.initialize();
this.events.emit('sceneJoin', char);
}
private bindEvents() {

View File

@ -59,13 +59,13 @@ export class LevelComponent extends EngineComponent {
};
}
public createObject(object: string, setParent?: Object3D) {
public createObject(object: string, setParent?: Object3D, skipJoin = false) {
const parent = setParent || this.world;
const ObjectType = instancableGameObjects[object];
if (!ObjectType) return;
const newObject = new ObjectType();
parent.add(newObject);
this.events.emit('sceneJoin', newObject);
if (!skipJoin) this.events.emit('sceneJoin', newObject);
return newObject;
}
@ -110,7 +110,7 @@ export class LevelComponent extends EngineComponent {
private recursiveCreate(entry: SerializedObject, setParent?: Object3D) {
const parent = setParent || this.world;
const newObject = this.createObject(entry.objectType, parent);
const newObject = this.createObject(entry.objectType, parent, true);
newObject?.deserialize(entry);
entry.children.forEach((child) => this.recursiveCreate(child, newObject));
return newObject;

View File

@ -1,15 +1,11 @@
import { SkinnedMesh, Vector3 } from 'three';
import { Object3D, SkinnedMesh, Vector3 } from 'three';
import { EngineComponent } from '../types/engine-component';
import { World } from '../gameobjects/world.object';
import {
PhysicsObjectAssociation,
getRapier,
ColliderFactory,
HumanoidPhysicsProxy,
} from '../physics';
import { Brick } from '../gameobjects/brick.object';
import type Rapier from '@dimforge/rapier3d';
import { getRapier, PhysicsTicking } from '../physics';
import { Humanoid } from '../gameobjects/humanoid.object';
import { PhysicsObject } from '../gameobjects/physics.object';
import type Rapier from '@dimforge/rapier3d';
import { GameObject } from '..';
export class PhysicsWorldComponent extends EngineComponent {
public name = PhysicsWorldComponent.name;
@ -17,11 +13,10 @@ export class PhysicsWorldComponent extends EngineComponent {
private physicsWorld!: Rapier.World;
private physicsEngine!: typeof Rapier;
private characterPhysics!: Rapier.KinematicCharacterController;
private characterRay!: Rapier.Ray;
private initEventFiredBeforeEngineLoad = false;
private cleanUpEvents?: Function;
private sceneInitialized = false;
private trackedObjects: PhysicsObjectAssociation[] = [];
private trackedObjects: PhysicsTicking[] = [];
initialize(): void {
this.world = this.renderer.scene.getObjectByName('World') as World;
@ -38,18 +33,19 @@ export class PhysicsWorldComponent extends EngineComponent {
dispose(): void {
this.cleanUpEvents?.call(this);
this.physicsWorld.removeCharacterController(this.characterPhysics);
for (const object of this.trackedObjects) object.dispose();
}
private createRapier() {
getRapier().then((R) => {
getRapier().then((physicsEngine) => {
const gravity = new Vector3(0, this.world.gravity, 0);
const world = new R.World(gravity);
const world = new physicsEngine.World(gravity);
this.physicsWorld = world;
this.physicsEngine = R;
this.physicsEngine = physicsEngine;
if (this.initEventFiredBeforeEngineLoad) {
this.initEventFiredBeforeEngineLoad = false;
setTimeout(() => this.initializePhysicsScene(), 500); // FIXME
this.initializePhysicsScene();
}
});
}
@ -60,93 +56,87 @@ export class PhysicsWorldComponent extends EngineComponent {
this.initEventFiredBeforeEngineLoad = true;
return;
}
setTimeout(() => this.initializePhysicsScene(), 500); // FIXME
this.initializePhysicsScene();
};
const sceneJoinEvent = (object: Object3D) => {
this.applyPhysics(object);
};
const sceneLeaveEvent = (object: Object3D) => {
this.removePhysics(object);
};
this.events.addListener('initialized', initializeEvent);
this.events.addListener('sceneJoin', sceneJoinEvent);
this.events.addListener('sceneLeave', sceneLeaveEvent);
return () => {
this.events.removeEventListener('initialized', initializeEvent);
this.events.removeEventListener('sceneJoin', sceneJoinEvent);
this.events.removeEventListener('sceneLeave', sceneLeaveEvent);
};
}
private applyPhysics(object: Brick) {
// This object has effecively no physics:
// doesn't move, doesn't collide
if (object.anchored && !object.canCollide) return;
private applyPhysics(root: Object3D) {
root.traverse((object) => {
// Prevent double-init
if (this.trackedObjects.some((entry) => entry.uuid === object.uuid))
return;
let bodyDesc: Rapier.RigidBodyDesc;
if (object.anchored) bodyDesc = this.physicsEngine.RigidBodyDesc.fixed();
else bodyDesc = this.physicsEngine.RigidBodyDesc.dynamic();
// Initialize humanoids
if (object instanceof Humanoid) {
object.initialize(
this.physicsEngine,
this.physicsWorld,
this.characterPhysics
);
this.trackedObjects.push(object);
return;
}
bodyDesc
.setTranslation(...object.position.toArray())
.setRotation(object.quaternion);
// Only track physics object instances
if (!(object instanceof PhysicsObject)) return;
const body = this.physicsWorld.createRigidBody(bodyDesc);
// Do not add physics to virtual objects
if ((object as GameObject).virtual) return;
let collider: Rapier.Collider | undefined;
if (object.canCollide) {
collider = ColliderFactory.createCollider(
object,
this.physicsEngine,
this.physicsWorld,
body
);
}
// Do not apply physics to animated objects
if ((object.getMesh() as SkinnedMesh).skeleton) return;
const tracker = new PhysicsObjectAssociation(object, collider, body);
console.log(tracker);
this.trackedObjects.push(tracker);
return tracker;
// Initialize object physics
object.initialize(this.physicsEngine, this.physicsWorld);
this.trackedObjects.push(object);
});
}
private applyHumanoidPhysics(humanoid: Humanoid) {
const halfVec = new Vector3(0, humanoid.characterHalfHeight, 0);
const colliderDesc = this.physicsEngine.ColliderDesc.cuboid(
1,
humanoid.characterHalfHeight,
0.5
private removePhysics(root: Object3D) {
const trackedObjects = [...this.trackedObjects];
const physicsLeaveUUIDs: string[] = [];
root.traverse((object) => {
if (trackedObjects.some((item) => item.uuid === object.uuid)) {
physicsLeaveUUIDs.push(object.uuid);
}
});
this.trackedObjects = this.trackedObjects.filter((object) =>
physicsLeaveUUIDs.includes(object.uuid)
);
const rigidBodyDesc =
this.physicsEngine.RigidBodyDesc.kinematicPositionBased();
const rigidBody = this.physicsWorld.createRigidBody(rigidBodyDesc);
const collider = this.physicsWorld.createCollider(colliderDesc, rigidBody);
rigidBody.setTranslation(humanoid.parent!.position.clone(), true);
rigidBody.setRotation(humanoid.parent!.quaternion, true);
collider.setTranslationWrtParent(halfVec);
const proxy = new HumanoidPhysicsProxy(
humanoid,
this.characterPhysics,
collider,
rigidBody
);
this.trackedObjects.push(proxy);
humanoid.setPhysics(proxy);
console.log('set physics', proxy);
}
private async initializePhysicsScene() {
if (this.sceneInitialized) return;
console.log('init scene');
this.characterRay = new this.physicsEngine.Ray(
{ x: 0, y: 0, z: 0 },
{ x: 0, y: -0.1, z: 0 }
);
this.characterPhysics = this.physicsWorld.createCharacterController(0.01);
this.characterPhysics.setApplyImpulsesToDynamicBodies(true);
this.characterPhysics.setMaxSlopeClimbAngle((75 * Math.PI) / 180);
this.characterPhysics.enableAutostep(1, 0.5, true);
// Automatically slide down on slopes smaller than 30 degrees.
// this.characterPhysics.setMinSlopeSlideAngle((30 * Math.PI) / 180);
this.world.traverse((object) => {
if (object instanceof Humanoid) return this.applyHumanoidPhysics(object);
if (!(object instanceof Brick)) return;
// Do not apply physics to animated objects
if ((object.getMesh() as SkinnedMesh).skeleton) return;
this.applyPhysics(object);
});
this.applyPhysics(this.world);
this.sceneInitialized = true;
}
}

View File

@ -1,85 +1,31 @@
import {
BufferGeometry,
Color,
ColorRepresentation,
Mesh,
MeshPhongMaterial,
} from 'three';
import { GameObject3D } from '../types/game-object';
import { PhysicsObject } from './physics.object';
import { gameObjectGeometries } from './geometries';
import { assetManager } from '../assets/manager';
import { AssetInfo } from '../types/asset';
import { EditorProperty } from '../decorators/property';
import { BufferGeometry, Mesh, MeshPhongMaterial } from 'three';
import type Rapier from '@dimforge/rapier3d';
export class Brick extends GameObject3D {
public objectType = Brick.name;
private texturePath?: string;
@EditorProperty({ type: Color })
get color() {
return this.material.color;
}
set color(color: ColorRepresentation) {
this.material.color = new Color(color);
}
@EditorProperty({ type: Number })
get transparency() {
return 1 - this.material.opacity;
}
set transparency(value: number) {
this.material.transparent = value != 0;
this.material.opacity = 1 - value;
this.material.needsUpdate = true;
}
@EditorProperty({ type: AssetInfo })
get texture() {
return this.texturePath;
}
set texture(path: string | undefined) {
if (!path) {
this.material.map = null;
this.texturePath = undefined;
this.material.needsUpdate = true;
return;
}
const asset = assetManager.getAssetByPath(path);
if (!asset || !asset.texture) {
console.error(`Asset ${path} does not exist or is not loaded`);
return;
}
this.texturePath = path;
this.material.map = asset.texture;
this.material.needsUpdate = true;
}
@EditorProperty({ type: Boolean })
public canCollide = true;
@EditorProperty({ type: Boolean })
public anchored = true;
@EditorProperty({ type: Number })
public mass = 1;
export class Brick extends PhysicsObject {
public objectType = 'Brick';
public name = 'Brick';
constructor(
protected geometry: BufferGeometry = gameObjectGeometries.boxGeometry,
protected material = new MeshPhongMaterial(),
protected mesh: Mesh = new Mesh(geometry, material)
geometry: BufferGeometry = gameObjectGeometries.boxGeometry,
material?: MeshPhongMaterial,
mesh?: Mesh
) {
super();
this.name = this.objectType;
this.add(this.mesh);
super(geometry, material, mesh);
}
getGeometry() {
return this.geometry;
}
getMesh() {
return this.mesh;
protected override createCollider(
factory: typeof Rapier,
world: Rapier.World,
body?: Rapier.RigidBody
) {
const scale = this.scale.clone().divideScalar(2);
const collider = factory.ColliderDesc.cuboid(
Math.abs(scale.x),
Math.abs(scale.y),
Math.abs(scale.z)
);
return world.createCollider(collider, body);
}
}

View File

@ -1,13 +1,28 @@
import { Mesh } from 'three';
import { Brick } from './brick.object';
import { gameObjectGeometries } from './geometries';
import type Rapier from '@dimforge/rapier3d';
export class Capsule extends Brick {
public objectType = Capsule.name;
public objectType = 'Capsule';
protected mesh = new Mesh(this.geometry, this.material);
constructor() {
super(gameObjectGeometries.capsuleGeometry);
this.name = this.objectType;
}
protected override createCollider(
factory: typeof Rapier,
world: Rapier.World,
body?: Rapier.RigidBody
) {
const height = this.scale.y / 2;
const radius = (this.scale.x / 2 + this.scale.z / 2) / 2;
const collider = factory.ColliderDesc.capsule(
Math.abs(height),
Math.abs(radius)
);
return world.createCollider(collider, body);
}
}

View File

@ -1,13 +1,28 @@
import { Mesh } from 'three';
import { Brick } from './brick.object';
import { gameObjectGeometries } from './geometries';
import type Rapier from '@dimforge/rapier3d';
export class Cylinder extends Brick {
public objectType = Cylinder.name;
public objectType = 'Cylinder';
protected mesh = new Mesh(this.geometry, this.material);
constructor() {
super(gameObjectGeometries.cylinderGeometry);
this.name = this.objectType;
}
protected override createCollider(
factory: typeof Rapier,
world: Rapier.World,
body?: Rapier.RigidBody
) {
const height = this.scale.y / 2;
const radius = (this.scale.x / 2 + this.scale.z / 2) / 2;
const collider = factory.ColliderDesc.cylinder(
Math.abs(height),
Math.abs(radius)
);
return world.createCollider(collider, body);
}
}

View File

@ -4,7 +4,7 @@ import { environmentDefaults } from '../defaults/environment';
import { EditorProperty, EditorPropertyExclude } from '../decorators/property';
export class Environment extends GameObject {
public objectType = Environment.name;
public objectType = 'Environment';
@EditorPropertyExclude()
public name = 'Environment';
public virtual = true;

View File

@ -3,20 +3,19 @@ import {
AnimationClip,
AnimationMixer,
LoopOnce,
Object3D,
Skeleton,
SkinnedMesh,
Vector3,
} from 'three';
import { GameObject } from '../types/game-object';
import { Ticking } from '../types/ticking';
import { EditorProperty } from '../decorators/property';
import { MeshPart } from './mesh.object';
import { clamp, lerp } from 'three/src/math/MathUtils.js';
import { clamp } from 'three/src/math/MathUtils.js';
import { NameTag } from './nametag.object';
import { CanvasUtils } from '../canvas/utils';
import { Disposable } from '../types/disposable';
import { HumanoidPhysicsProxy } from '../physics';
import { PhysicsTicking } from '../physics';
import type Rapier from '@dimforge/rapier3d';
import { Quaternion } from '@dimforge/rapier3d';
export type HumanoidBodyPart =
| 'Head'
@ -26,7 +25,7 @@ export type HumanoidBodyPart =
| 'LegRight'
| 'LegLeft';
export class Humanoid extends GameObject implements Ticking, Disposable {
export class Humanoid extends GameObject implements PhysicsTicking {
public isTickingObject = true;
public objectType = 'Humanoid';
public name = 'Humanoid';
@ -71,7 +70,6 @@ export class Humanoid extends GameObject implements Ticking, Disposable {
public characterHeight = 5.5;
public characterHalfHeight = this.characterHeight / 2;
private gravity = -12;
private shouldJump = false;
private mixer!: AnimationMixer;
@ -80,10 +78,17 @@ export class Humanoid extends GameObject implements Ticking, Disposable {
private jumpAction!: AnimationAction;
private nameTag?: NameTag;
private physics?: HumanoidPhysicsProxy;
protected collider?: Rapier.Collider;
protected rigidBody?: Rapier.RigidBody;
protected physicsWorldRef?: Rapier.World;
protected characterControllerRef?: Rapier.KinematicCharacterController;
@EditorProperty({ type: Number })
public weight = -16;
@EditorProperty({ type: Boolean })
public jumpPower = 6;
public jumpPower = 8;
@EditorProperty({ type: Number })
get health() {
@ -132,7 +137,11 @@ export class Humanoid extends GameObject implements Ticking, Disposable {
);
}
initialize(): void {
initialize(
physicsEngine: typeof Rapier,
physicsWorld: Rapier.World,
characterController: Rapier.KinematicCharacterController
): void {
if (!this.parent)
throw new Error('Cannot initialize Humanoid to empty parent');
@ -141,6 +150,7 @@ export class Humanoid extends GameObject implements Ticking, Disposable {
if (!this.skeleton) throw new Error('Could not find Skeleton for Humanoid');
this.skeleton.pose();
// Clip actions
this.mixer = new AnimationMixer(this.parent);
this.ready = true;
@ -153,7 +163,29 @@ export class Humanoid extends GameObject implements Ticking, Disposable {
this.jumpAction.setLoop(LoopOnce, 0);
this.idleAction.play();
// Create name tag
this.createNameTag();
if (this.virtual) return;
this.physicsWorldRef = physicsWorld;
this.characterControllerRef = characterController;
// Character Physics
const halfVec = new Vector3(0, this.characterHalfHeight, 0);
const colliderDesc = physicsEngine.ColliderDesc.cuboid(
1,
this.characterHalfHeight,
0.5
);
const rigidBodyDesc = physicsEngine.RigidBodyDesc.kinematicPositionBased()
.setTranslation(...this.parent!.position.toArray())
.setRotation(this.parent!.quaternion);
const rigidBody = physicsWorld.createRigidBody(rigidBodyDesc);
const collider = physicsWorld.createCollider(colliderDesc, rigidBody);
collider.setTranslationWrtParent(halfVec);
this.rigidBody = rigidBody;
this.collider = collider;
}
setVelocity(velocity: Vector3) {
@ -173,31 +205,39 @@ export class Humanoid extends GameObject implements Ticking, Disposable {
tick(dt: number): void {
if (!this.ready) return;
// Apply rigidbody transforms to object from last process tick
if (this.rigidBody) {
this.parent!.position.copy(this.rigidBody.translation() as any);
this.parent!.quaternion.copy(this.rigidBody.rotation() as any);
}
// Run animation
this.mixer.update(dt);
// Apply jumping
if (this.shouldJump) {
this._appliedGravity.y = Math.sqrt(-2 * this.gravity * this.jumpPower);
this._appliedGravity.y = Math.sqrt(-2 * this.weight * this.jumpPower);
this.grounded = false;
this.shouldJump = false;
}
this._appliedGravity.y += this.gravity * dt;
// Apply gravity
this._appliedGravity.y += this.weight * dt;
if (this.physics)
this.physics.applyMovement(
this.parent!.position,
this._velocity.clone().add(this._appliedGravity).multiplyScalar(dt)
);
else this.parent?.position.add(this._velocity.clone().multiplyScalar(dt));
// Apply velocity
this.applyVelocity(
this._velocity.clone().add(this._appliedGravity).multiplyScalar(dt)
);
// Apply look vector
this._currentLookAt.copy(this.parent!.position);
this._currentLookAt.add(this._lookAt);
this.parent?.lookAt(this._currentLookAt);
if (this.physics) {
this.physics.applyRotation(this.parent!.quaternion);
}
this.applyRotation(this.parent!.quaternion);
// Stick to ground
if (this.grounded) {
this._appliedGravity.y = 0;
}
@ -251,11 +291,43 @@ export class Humanoid extends GameObject implements Ticking, Disposable {
this.add(this.nameTag);
}
setPhysics(physics?: HumanoidPhysicsProxy) {
this.physics = physics;
private applyVelocity(velocity: Vector3) {
if (!this.characterControllerRef || !this.parent || !this.rigidBody) return;
const vec3 = this.parent.position.clone();
this.characterControllerRef.computeColliderMovement(
this.collider!,
velocity
);
const computed = this.characterControllerRef.computedMovement();
const grounded = this.characterControllerRef.computedGrounded();
vec3.copy(computed as Vector3);
this.rigidBody?.setNextKinematicTranslation(vec3.add(this.parent.position));
// After the collider movement calculation is done, we can read the
// collision events.
// for (let i = 0; i < this.controller.numComputedCollisions(); i++) {
// let collision = this.controller.computedCollision(i);
// // Do something with that collision information.
// console.log(collision);
// }
this._grounded = grounded;
}
private applyRotation(quat: Quaternion) {
this.rigidBody?.setNextKinematicRotation(quat);
}
dispose(): void {
this.nameTag?.dispose();
if (this.collider && !this.rigidBody) {
this.physicsWorldRef?.removeCollider(this.collider, false);
}
if (this.rigidBody) {
this.physicsWorldRef?.removeRigidBody(this.rigidBody);
}
}
}

View File

@ -27,6 +27,8 @@ export const instancableGameObjects: Record<string, Instancable<GameObject>> = {
export * from './environment.object';
export * from './world.object';
export * from './nametag.object';
export * from './physical.object';
export * from './physics.object';
export {
Group,
Cylinder,

View File

@ -1,14 +1,9 @@
import {
BufferGeometry,
Material,
Mesh,
MeshPhongMaterial,
SkinnedMesh,
} from 'three';
import { BufferGeometry, Mesh, MeshPhongMaterial, SkinnedMesh } from 'three';
import { Brick } from '.';
import type Rapier from '@dimforge/rapier3d';
export class MeshPart extends Brick {
public objectType = MeshPart.name;
public objectType = 'MeshPart';
constructor(
geometry: BufferGeometry,
@ -25,4 +20,18 @@ export class MeshPart extends Brick {
newObject.add(loaded);
return newObject;
}
protected override createCollider(
factory: typeof Rapier,
world: Rapier.World,
body?: Rapier.RigidBody
) {
const scale = this.scale.clone().divideScalar(2);
const collider = factory.ColliderDesc.cuboid(
Math.abs(scale.x),
Math.abs(scale.y),
Math.abs(scale.z)
);
return world.createCollider(collider, body);
}
}

View File

@ -4,7 +4,7 @@ import { Disposable } from '../types/disposable';
import { GameObject } from '../types/game-object';
export class NameTag extends GameObject implements Disposable {
public objectType = NameTag.name;
public objectType = 'NameTag';
public virtual = true;
public archivable = false;

View File

@ -0,0 +1,74 @@
import { GameObject3D } from '../types/game-object';
import {
BufferGeometry,
Color,
ColorRepresentation,
Mesh,
MeshPhongMaterial,
} from 'three';
import { assetManager } from '../assets/manager';
import { AssetInfo } from '../types/asset';
import { EditorProperty } from '../decorators/property';
export class PhysicalObject extends GameObject3D {
private texturePath?: string;
@EditorProperty({ type: Color })
get color() {
return this.material.color;
}
set color(color: ColorRepresentation) {
this.material.color = new Color(color);
}
@EditorProperty({ type: Number })
get transparency() {
return 1 - this.material.opacity;
}
set transparency(value: number) {
this.material.transparent = value != 0;
this.material.opacity = 1 - value;
this.material.needsUpdate = true;
}
@EditorProperty({ type: AssetInfo })
get texture() {
return this.texturePath;
}
set texture(path: string | undefined) {
if (!path) {
this.material.map = null;
this.texturePath = undefined;
this.material.needsUpdate = true;
return;
}
const asset = assetManager.getAssetByPath(path);
if (!asset || !asset.texture) {
console.error(`Asset ${path} does not exist or is not loaded`);
return;
}
this.texturePath = path;
this.material.map = asset.texture;
this.material.needsUpdate = true;
}
constructor(
protected geometry: BufferGeometry,
protected material = new MeshPhongMaterial(),
protected mesh: Mesh = new Mesh(geometry, material)
) {
super();
this.name = this.objectType;
this.add(this.mesh);
}
getGeometry() {
return this.geometry;
}
getMesh() {
return this.mesh;
}
}

View File

@ -0,0 +1,86 @@
import { EditorProperty } from '..';
import { PhysicsTicking } from '../physics/ticking';
import type Rapier from '@dimforge/rapier3d';
import { PhysicalObject } from './physical.object';
export class PhysicsObject extends PhysicalObject implements PhysicsTicking {
public objectType = 'PhysicsObject';
isTickingObject = true;
protected collider?: Rapier.Collider;
protected rigidBody?: Rapier.RigidBody;
protected physicsWorldRef?: Rapier.World;
@EditorProperty({ type: Boolean })
public canCollide = true;
@EditorProperty({ type: Boolean })
public anchored = true;
@EditorProperty({ type: Number })
public mass = 1;
@EditorProperty({ type: Number })
public friction = 0.2;
initialize(physicsEngine: typeof Rapier, physicsWorld: Rapier.World): void {
if (this.virtual) return;
this.physicsWorldRef = physicsWorld;
// This object has effecively no physics:
// doesn't move, doesn't collide
if (this.anchored && !this.canCollide) return;
let bodyDesc: Rapier.RigidBodyDesc;
if (this.anchored) bodyDesc = physicsEngine.RigidBodyDesc.fixed();
else bodyDesc = physicsEngine.RigidBodyDesc.dynamic();
bodyDesc
.setTranslation(...this.position.toArray())
.setRotation(this.quaternion)
.setAdditionalMass(this.mass)
.setLinearDamping(this.friction)
.setAngularDamping(this.friction);
const body = physicsWorld.createRigidBody(bodyDesc);
let collider: Rapier.Collider | undefined;
if (this.canCollide) {
collider = this.createCollider(physicsEngine, physicsWorld, body);
}
this.collider = collider;
this.rigidBody = body;
}
tick(dt: number): void {
if (!this.rigidBody || this.virtual) return;
this.position.copy(this.rigidBody.translation() as any);
this.quaternion.copy(this.rigidBody.rotation() as any);
}
dispose(): void {
if (this.collider && !this.rigidBody) {
this.physicsWorldRef?.removeCollider(this.collider, false);
}
if (this.rigidBody) {
this.physicsWorldRef?.removeRigidBody(this.rigidBody);
}
}
/**
* Create collision shape for physics object.
* @internal
* @param factory Physics engine
* @param world Physics world
* @param body RigidBody
*/
protected createCollider(
factory: typeof Rapier,
world: Rapier.World,
body?: Rapier.RigidBody
): Rapier.Collider | undefined {
return undefined;
}
}

View File

@ -1,13 +1,24 @@
import { Mesh } from 'three';
import { Brick } from './brick.object';
import { gameObjectGeometries } from './geometries';
import type Rapier from '@dimforge/rapier3d';
export class Sphere extends Brick {
public objectType = Sphere.name;
public objectType = 'Sphere';
protected mesh = new Mesh(this.geometry, this.material);
constructor() {
super(gameObjectGeometries.sphereGeometry);
this.name = this.objectType;
}
protected override createCollider(
factory: typeof Rapier,
world: Rapier.World,
body?: Rapier.RigidBody
) {
const radius = Math.max(this.scale.x, this.scale.y, this.scale.z) / 2;
const collider = factory.ColliderDesc.ball(Math.abs(radius));
return world.createCollider(collider, body);
}
}

View File

@ -1,13 +1,30 @@
import { Mesh } from 'three';
import { Mesh, Quaternion, Vector3 } from 'three';
import { Brick } from './brick.object';
import { gameObjectGeometries } from './geometries';
import type Rapier from '@dimforge/rapier3d';
export class Torus extends Brick {
public objectType = Torus.name;
public objectType = 'Torus';
protected mesh = new Mesh(this.geometry, this.material);
constructor() {
super(gameObjectGeometries.torusGeometry);
this.name = this.objectType;
}
protected override createCollider(
factory: typeof Rapier,
world: Rapier.World,
body?: Rapier.RigidBody
) {
const height = this.scale.y * 0.4;
const radius = (this.scale.x + this.scale.z) / 2;
const collider = factory.ColliderDesc.cylinder(
Math.abs(height),
Math.abs(radius) * 1.4
).setRotation(
new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), Math.PI / 2)
);
return world.createCollider(collider, body);
}
}

View File

@ -1,13 +1,29 @@
import { Mesh } from 'three';
import { Matrix4, Mesh } from 'three';
import { Brick } from './brick.object';
import { gameObjectGeometries } from './geometries';
import type Rapier from '@dimforge/rapier3d';
export class WedgeCorner extends Brick {
public objectType = WedgeCorner.name;
public objectType = 'WedgeCorner';
protected mesh = new Mesh(this.geometry, this.material);
constructor() {
super(gameObjectGeometries.wedgeCornerGeometry);
this.name = this.objectType;
}
protected override createCollider(
factory: typeof Rapier,
world: Rapier.World,
body?: Rapier.RigidBody
) {
const mat = new Matrix4();
mat.makeScale(...this.scale.toArray());
const points = this.getGeometry()
.getAttribute('position')
.clone()
.applyMatrix4(mat)?.array as Float32Array;
const collider = factory.ColliderDesc.convexMesh(points)!;
return world.createCollider(collider, body);
}
}

View File

@ -1,13 +1,29 @@
import { Mesh } from 'three';
import { Matrix4, Mesh } from 'three';
import { Brick } from './brick.object';
import { gameObjectGeometries } from './geometries';
import type Rapier from '@dimforge/rapier3d';
export class WedgeInnerCorner extends Brick {
public objectType = WedgeInnerCorner.name;
public objectType = 'WedgeInnerCorner';
protected mesh = new Mesh(this.geometry, this.material);
constructor() {
super(gameObjectGeometries.wedgeInnerCornerGeometry);
this.name = this.objectType;
}
protected override createCollider(
factory: typeof Rapier,
world: Rapier.World,
body?: Rapier.RigidBody
) {
const mat = new Matrix4();
mat.makeScale(...this.scale.toArray());
const points = this.getGeometry()
.getAttribute('position')
.clone()
.applyMatrix4(mat)?.array as Float32Array;
const collider = factory.ColliderDesc.convexMesh(points)!;
return world.createCollider(collider, body);
}
}

View File

@ -1,13 +1,29 @@
import { Mesh } from 'three';
import { Matrix4, Mesh } from 'three';
import { Brick } from './brick.object';
import { gameObjectGeometries } from './geometries';
import type Rapier from '@dimforge/rapier3d';
export class Wedge extends Brick {
public objectType = Wedge.name;
public objectType = 'Wedge';
protected mesh = new Mesh(this.geometry, this.material);
constructor() {
super(gameObjectGeometries.wedgeGeometry);
this.name = this.objectType;
}
protected override createCollider(
factory: typeof Rapier,
world: Rapier.World,
body?: Rapier.RigidBody
) {
const mat = new Matrix4();
mat.makeScale(...this.scale.toArray());
const points = this.getGeometry()
.getAttribute('position')
.clone()
.applyMatrix4(mat)?.array as Float32Array;
const collider = factory.ColliderDesc.convexMesh(points)!;
return world.createCollider(collider, body);
}
}

View File

@ -2,7 +2,7 @@ import { EditorProperty } from '..';
import { GameObject } from '../types/game-object';
export class World extends GameObject {
public objectType = World.name;
public objectType = 'World';
public name = 'World';
public virtual = true;

View File

@ -1,160 +0,0 @@
import {
Brick,
Cylinder,
Sphere,
Torus,
Capsule,
Wedge,
WedgeCorner,
WedgeInnerCorner,
} from '../gameobjects';
import type RAPIER from '@dimforge/rapier3d';
import { Instancable } from '../types/instancable';
import { Matrix4 } from 'three';
export class ColliderFactory {
static gameObjectColliders = [
{
instance: Cylinder,
createCollisionShape: (
obj: Brick,
factory: typeof RAPIER,
world: RAPIER.World,
body?: RAPIER.RigidBody
) => {
const height = obj.scale.y / 2;
const radius = (obj.scale.x / 2 + obj.scale.z / 2) / 2;
const collider = factory.ColliderDesc.cylinder(height, radius);
return world.createCollider(collider, body);
},
},
{
instance: Sphere,
createCollisionShape: (
obj: Brick,
factory: typeof RAPIER,
world: RAPIER.World,
body?: RAPIER.RigidBody
) => {
const radius = Math.max(obj.scale.x, obj.scale.y, obj.scale.z) / 2;
const collider = factory.ColliderDesc.ball(radius);
return world.createCollider(collider, body);
},
},
{
instance: Torus,
createCollisionShape: (
obj: Brick,
factory: typeof RAPIER,
world: RAPIER.World,
body?: RAPIER.RigidBody
) => {
const height = obj.scale.y / 2;
const radius = (obj.scale.x / 2 + obj.scale.z / 2) / 2;
const collider = factory.ColliderDesc.roundCylinder(
height,
radius,
height
);
return world.createCollider(collider, body);
},
},
{
instance: Capsule,
createCollisionShape: (
obj: Brick,
factory: typeof RAPIER,
world: RAPIER.World,
body?: RAPIER.RigidBody
) => {
const height = obj.scale.y / 2;
const radius = (obj.scale.x / 2 + obj.scale.z / 2) / 2;
const collider = factory.ColliderDesc.capsule(height, radius);
return world.createCollider(collider, body);
},
},
{
instance: Wedge,
createCollisionShape: (
obj: Brick,
factory: typeof RAPIER,
world: RAPIER.World,
body?: RAPIER.RigidBody
) => {
const mat = new Matrix4();
mat.makeScale(...obj.scale.toArray());
const points = obj
.getGeometry()
.getAttribute('position')
.clone()
.applyMatrix4(mat)?.array as Float32Array;
const collider = factory.ColliderDesc.convexMesh(points)!;
return world.createCollider(collider, body);
},
},
{
instance: WedgeCorner,
createCollisionShape: (
obj: Brick,
factory: typeof RAPIER,
world: RAPIER.World,
body?: RAPIER.RigidBody
) => {
const mat = new Matrix4();
mat.makeScale(...obj.scale.toArray());
const points = obj
.getGeometry()
.getAttribute('position')
.clone()
.applyMatrix4(mat)?.array as Float32Array;
const collider = factory.ColliderDesc.convexMesh(points)!;
return world.createCollider(collider, body);
},
},
{
instance: WedgeInnerCorner,
createCollisionShape: (
obj: Brick,
factory: typeof RAPIER,
world: RAPIER.World,
body?: RAPIER.RigidBody
) => {
const mat = new Matrix4();
mat.makeScale(...obj.scale.toArray());
const points = obj
.getGeometry()
.getAttribute('position')
.clone()
.applyMatrix4(mat)?.array as Float32Array;
const collider = factory.ColliderDesc.convexMesh(points)!;
return world.createCollider(collider, body);
},
},
{
instance: Brick,
createCollisionShape: (
obj: Brick,
factory: typeof RAPIER,
world: RAPIER.World,
body?: RAPIER.RigidBody
) => {
const scale = obj.scale.clone().divideScalar(2);
const collider = factory.ColliderDesc.cuboid(scale.x, scale.y, scale.z);
return world.createCollider(collider, body);
},
},
];
static createCollider<T extends Brick>(
obj: T,
factory: typeof RAPIER,
world: RAPIER.World,
body?: RAPIER.RigidBody
) {
const collider = ColliderFactory.gameObjectColliders.find(
(entry) => obj instanceof entry.instance
);
if (!collider) return undefined;
return collider.createCollisionShape(obj, factory, world, body);
}
}

View File

@ -1,3 +1,2 @@
export * from './rapier';
export * from './physics-object';
export * from './colliders';
export * from './ticking';

View File

@ -1,60 +0,0 @@
import { Quaternion, Vector3 } from 'three';
import { GameObject3D } from '../types/game-object';
import { Ticking } from '../types/ticking';
import type { Humanoid } from '../gameobjects/humanoid.object';
import type Rapier from '@dimforge/rapier3d';
export class PhysicsObjectAssociation implements Ticking {
uuid: string;
isTickingObject = true;
constructor(
public object: GameObject3D,
public collider?: Rapier.Collider,
public body?: Rapier.RigidBody
) {
this.uuid = object.uuid;
}
initialize(): void {}
tick(dt: number): void {
if (!this.body) return;
this.object.position.copy(this.body.translation() as any);
this.object.quaternion.copy(this.body.rotation() as any);
}
}
export class HumanoidPhysicsProxy extends PhysicsObjectAssociation {
constructor(
private humanoid: Humanoid,
private controller: Rapier.KinematicCharacterController,
collider: Rapier.Collider,
body: Rapier.RigidBody
) {
super(humanoid.parent! as GameObject3D, collider, body);
}
applyMovement(position: Vector3, velocity: Vector3) {
const vec3 = position.clone();
this.controller.computeColliderMovement(this.collider!, velocity);
const computed = this.controller.computedMovement();
const grounded = this.controller.computedGrounded();
vec3.copy(computed as Vector3);
this.body?.setNextKinematicTranslation(vec3.add(position));
// After the collider movement calculation is done, we can read the
// collision events.
// for (let i = 0; i < this.controller.numComputedCollisions(); i++) {
// let collision = this.controller.computedCollision(i);
// // Do something with that collision information.
// console.log(collision);
// }
// this.collider?.setTranslation(colliderHalf);
// this.humanoid.parent!.position.copy(computed as Vector3).sub(halfVec);
this.humanoid.grounded = grounded;
}
applyRotation(quat: Quaternion) {
this.body?.setNextKinematicRotation(quat);
}
}

View File

@ -0,0 +1,24 @@
import type Rapier from '@dimforge/rapier3d';
import { Disposable } from '../types/disposable';
export interface PhysicsTicking extends Disposable {
/**
* @internal
*/
isTickingObject: boolean;
/**
* @internal
*/
uuid: string;
/**
* @internal
*/
initialize(physicsEngine: typeof Rapier, physicsWorld: Rapier.World, controller?: Rapier.KinematicCharacterController): void;
/**
* @internal
*/
tick(dt: number): void;
}

View File

@ -1,5 +1,16 @@
export interface Ticking {
/**
* @internal
*/
isTickingObject: boolean;
/**
* @internal
*/
initialize(): void;
/**
* @internal
*/
tick(dt: number): void;
}

View File

@ -80,7 +80,7 @@ export const instanceCharacterObject = async (name: string) => {
controller.position.set(0, 4.75, 0);
controller.archivable = false;
baseObject.add(controller);
baseObject.position.set(0, 0.5, 0);
baseObject.position.set(0, 1, 0);
return baseObject;
};