@osobh/reactar
Version:
AR Library for React Native and Expo
304 lines (259 loc) • 8.39 kB
text/typescript
// src/core/PhysicsManager.ts
import * as AmmoModule from 'ammojs3';
export interface AmmoInstance {
btDefaultCollisionConfiguration: any;
btCollisionDispatcher: any;
btDbvtBroadphase: any;
btSequentialImpulseConstraintSolver: any;
btDiscreteDynamicsWorld: any;
btTransform: any;
btVector3: any;
btQuaternion: any;
btDefaultMotionState: any;
btBoxShape: any;
btRigidBodyConstructionInfo: any;
btRigidBody: any;
btTypedConstraint: any;
btHingeConstraint: any;
btConeTwistConstraint: any;
btPoint2PointConstraint: any;
destroy: (obj: any) => void;
}
declare global {
const Ammo: AmmoInstance;
}
let ammoInstance: AmmoInstance;
import { Vector3, Quaternion, Matrix4 } from 'three';
import { EventEmitter } from 'events';
export interface PhysicsObject {
id: string;
body: AmmoInstance['btRigidBody'];
mass: number;
position: Vector3;
rotation: Quaternion;
scale: Vector3;
}
export interface PhysicsProperties {
mass?: number;
friction?: number;
restitution?: number;
linearDamping?: number;
angularDamping?: number;
isKinematic?: boolean;
}
export class PhysicsManager extends EventEmitter {
private physicsWorld: AmmoInstance['btDiscreteDynamicsWorld'];
private collisionConfiguration: AmmoInstance['btDefaultCollisionConfiguration'];
private dispatcher: AmmoInstance['btCollisionDispatcher'];
private broadphase: AmmoInstance['btDbvtBroadphase'];
private solver: AmmoInstance['btSequentialImpulseConstraintSolver'];
private objects: Map<string, PhysicsObject> = new Map();
private isInitialized: boolean = false;
private gravity: Vector3 = new Vector3(0, -9.81, 0);
private timeStep: number = 1 / 60;
constructor() {
super();
}
/**
* Initializes the physics engine
*/
public async initialize(): Promise<boolean> {
try {
// Initialize Ammo.js
const ammo = await AmmoModule.default();
// Assign to global Ammo variable
(globalThis as any).Ammo = ammo;
this.collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
this.dispatcher = new Ammo.btCollisionDispatcher(this.collisionConfiguration);
this.broadphase = new Ammo.btDbvtBroadphase();
this.solver = new Ammo.btSequentialImpulseConstraintSolver();
this.physicsWorld = new Ammo.btDiscreteDynamicsWorld(
this.dispatcher,
this.broadphase,
this.solver,
this.collisionConfiguration
);
// Set default gravity
const gravity = new Ammo.btVector3(this.gravity.x, this.gravity.y, this.gravity.z);
this.physicsWorld.setGravity(gravity);
this.isInitialized = true;
this.emit('initialized');
return true;
} catch (error) {
this.emit('error', error);
throw error;
}
}
/**
* Creates a physics object and adds it to the physics world
*/
public createObject(
id: string,
position: Vector3,
rotation: Quaternion,
scale: Vector3,
properties: PhysicsProperties = {}
): void {
if (!this.isInitialized) {
throw new Error('PhysicsManager must be initialized first');
}
const transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin(new Ammo.btVector3(position.x, position.y, position.z));
transform.setRotation(new Ammo.btQuaternion(rotation.x, rotation.y, rotation.z, rotation.w));
const motionState = new Ammo.btDefaultMotionState(transform);
const shape = this.createBoxShape(scale);
const mass = properties.mass ?? 0;
const localInertia = new Ammo.btVector3(0, 0, 0);
if (mass > 0) {
shape.calculateLocalInertia(mass, localInertia);
}
const rbInfo = new Ammo.btRigidBodyConstructionInfo(
mass,
motionState,
shape,
localInertia
);
const body = new Ammo.btRigidBody(rbInfo);
// Set additional properties
body.setFriction(properties.friction ?? 0.5);
body.setRestitution(properties.restitution ?? 0.2);
body.setDamping(
properties.linearDamping ?? 0.1,
properties.angularDamping ?? 0.1
);
if (properties.isKinematic) {
body.setCollisionFlags(body.getCollisionFlags() | 2); // CF_KINEMATIC_OBJECT
}
this.physicsWorld.addRigidBody(body);
this.objects.set(id, {
id,
body,
mass,
position: position.clone(),
rotation: rotation.clone(),
scale: scale.clone()
});
this.emit('object-created', id);
}
/**
* Updates the physics simulation
*/
public update(deltaTime: number = this.timeStep): void {
if (!this.isInitialized) return;
this.physicsWorld.stepSimulation(deltaTime, 10);
// Update object positions and rotations
this.objects.forEach((object) => {
const transform = new Ammo.btTransform();
object.body.getMotionState().getWorldTransform(transform);
const pos = transform.getOrigin();
const rot = transform.getRotation();
object.position.set(pos.x(), pos.y(), pos.z());
object.rotation.set(rot.x(), rot.y(), rot.z(), rot.w());
this.emit('object-updated', {
id: object.id,
position: object.position,
rotation: object.rotation
});
});
}
/**
* Applies a force to an object
*/
public applyForce(id: string, force: Vector3, point?: Vector3): void {
const object = this.objects.get(id);
if (!object) {
throw new Error(`Physics object ${id} not found`);
}
const btForce = new Ammo.btVector3(force.x, force.y, force.z);
if (point) {
const btPoint = new Ammo.btVector3(point.x, point.y, point.z);
object.body.applyForce(btForce, btPoint);
} else {
object.body.applyCentralForce(btForce);
}
}
/**
* Sets the gravity of the physics world
*/
public setGravity(gravity: Vector3): void {
this.gravity = gravity.clone();
const btGravity = new Ammo.btVector3(gravity.x, gravity.y, gravity.z);
this.physicsWorld.setGravity(btGravity);
}
/**
* Creates a box collision shape
*/
private createBoxShape(scale: Vector3): any {
const shape = new Ammo.btBoxShape(new Ammo.btVector3(
scale.x * 0.5,
scale.y * 0.5,
scale.z * 0.5
));
return shape;
}
/**
* Removes an object from the physics world
*/
public removeObject(id: string): void {
const object = this.objects.get(id);
if (!object) return;
this.physicsWorld.removeRigidBody(object.body);
this.objects.delete(id);
this.emit('object-removed', id);
}
/**
* Cleans up physics resources
*/
/**
* Creates a rigid body with specified position, rotation and mass
*/
public createRigidBody(position: { x: number; y: number; z: number }, rotation: { x: number; y: number; z: number; w: number }, mass: number): any {
if (!this.isInitialized) {
throw new Error('PhysicsManager must be initialized first');
}
const transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin(new Ammo.btVector3(position.x, position.y, position.z));
transform.setRotation(new Ammo.btQuaternion(rotation.x, rotation.y, rotation.z, rotation.w));
const motionState = new Ammo.btDefaultMotionState(transform);
const shape = new Ammo.btBoxShape(new Ammo.btVector3(0.5, 0.5, 0.5));
const localInertia = new Ammo.btVector3(0, 0, 0);
if (mass > 0) {
shape.calculateLocalInertia(mass, localInertia);
}
const rbInfo = new Ammo.btRigidBodyConstructionInfo(
mass,
motionState,
shape,
localInertia
);
const body = new Ammo.btRigidBody(rbInfo);
body.setMass(mass);
this.physicsWorld.addRigidBody(body);
return body;
}
/**
* Gets the physics world instance
*/
public getWorld(): AmmoInstance['btDiscreteDynamicsWorld'] {
if (!this.isInitialized) {
throw new Error('PhysicsManager must be initialized first');
}
return this.physicsWorld;
}
/**
* Cleans up physics resources
*/
public cleanup(): void {
Array.from(this.objects.keys()).forEach(id => this.removeObject(id));
// Clean up Ammo.js resources
Ammo.destroy(this.physicsWorld);
Ammo.destroy(this.solver);
Ammo.destroy(this.broadphase);
Ammo.destroy(this.dispatcher);
Ammo.destroy(this.collisionConfiguration);
this.isInitialized = false;
this.emit('cleanup-complete');
}
}