UNPKG

@osobh/reactar

Version:

AR Library for React Native and Expo

304 lines (259 loc) 8.39 kB
// 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'); } }