UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

346 lines (345 loc) 13.8 kB
/** * The RigidBodyComponentSystem manages the physics simulation for all rigid body components * in the application. It creates and maintains the underlying Ammo.js physics world, handles * physics object creation and destruction, performs physics raycasting, detects and reports * collisions, and updates the transforms of entities with rigid bodies after each physics step. * * The system controls global physics settings like gravity and provides methods for raycasting * and collision detection. * * This system is only functional if your application has loaded the Ammo.js {@link WasmModule}. * * @category Physics */ export class RigidBodyComponentSystem extends ComponentSystem { /** * Fired when a contact occurs between two rigid bodies. The handler is passed a * {@link SingleContactResult} object containing details of the contact between the two bodies. * * @event * @example * app.systems.rigidbody.on('contact', (result) => { * console.log(`Contact between ${result.a.name} and ${result.b.name}`); * }); */ static EVENT_CONTACT: string; /** @ignore */ maxSubSteps: number; /** * @type {number} * @ignore */ fixedTimeStep: number; /** * The world space vector representing global gravity in the physics simulation. Defaults to * [0, -9.81, 0] which is an approximation of the gravitational force on Earth. * * @example * // Set the gravity in the physics world to simulate a planet with low gravity * app.systems.rigidbody.gravity = new Vec3(0, -3.7, 0); */ gravity: Vec3; /** * @type {PhysicsWorld|null} * @private */ private _world; /** * @type {RigidBodyComponent[]} * @private */ private _dynamic; /** * @type {RigidBodyComponent[]} * @private */ private _kinematic; /** * @type {Trigger[]} * @private */ private _triggers; /** * @type {CollisionComponent[]} * @private */ private _compounds; id: string; _stats: { fps: number; ms: number; dt: number; updateStart: number; updateTime: number; renderStart: number; renderTime: number; physicsStart: number; physicsTime: number; scriptUpdateStart: number; scriptUpdate: number; scriptPostUpdateStart: number; scriptPostUpdate: number; animUpdateStart: number; animUpdate: number; cullTime: number; sortTime: number; skinTime: number; morphTime: number; instancingTime: number; triangles: number; gsplats: number; gsplatSort: number; gsplatBufferCopy: number; otherPrimitives: number; shaders: number; materials: number; cameras: number; shadowMapUpdates: number; shadowMapTime: number; depthMapTime: number; forwardTime: number; lightClustersTime: number; lightClusters: number; _timeToCountFrames: number; _fpsAccum: number; }; ComponentType: typeof RigidBodyComponent; contactPointPool: ObjectPool<typeof ContactPoint>; contactResultPool: ObjectPool<typeof ContactResult>; singleContactResultPool: ObjectPool<typeof SingleContactResult>; collisions: {}; frameCollisions: {}; /** * Called once Ammo has been loaded. Responsible for creating the physics world. * * @ignore */ onLibraryLoaded(): void; /** * Installs a physics backend. Used internally for Ammo auto-detection and by tests to * inject a NullPhysicsWorld. A backend can be installed at most once. * * @param {PhysicsWorld} world - The physics backend. * @ignore */ setPhysicsWorld(world: PhysicsWorld): void; /** * The installed physics backend, or null when no physics library has loaded. * * @type {PhysicsWorld|null} * @ignore */ get physicsWorld(): PhysicsWorld | null; /** * The native physics world - btDiscreteDynamicsWorld when the Ammo backend is active, * null otherwise. * * @type {*} * @ignore */ get dynamicsWorld(): any; /** @ignore */ get collisionConfiguration(): any; /** @ignore */ get dispatcher(): any; /** @ignore */ get overlappingPairCache(): any; /** @ignore */ get solver(): any; initializeComponentData(component: any, data: any): void; cloneComponent(entity: any, clone: any): import("../component.js").Component; onBeforeRemove(entity: any, component: any): void; addBody(body: any, group: any, mask: any): void; removeBody(body: any): void; /** * Adds a component's body to the simulation and registers the component with the update * lists for its body type. Fires 'simulationenabled' on the component. No-op unless the * component has a body, an enabled collision component and is not already simulating. * * @param {RigidBodyComponent} component - The component to add to the simulation. * @ignore */ enableSimulation(component: RigidBodyComponent): void; /** * Removes a component's body from the simulation and unregisters the component from the * update lists. Fires 'simulationdisabled' on the component. No-op unless the component * has a body and is currently simulating. * * @param {RigidBodyComponent} component - The component to remove from the simulation. * @ignore */ disableSimulation(component: RigidBodyComponent): void; /** * Adds a trigger's body to the simulation and registers the trigger for per-frame * transform updates. No-op if the trigger is already registered. * * @param {Trigger} trigger - The trigger to add to the simulation. * @ignore */ addTrigger(trigger: Trigger): void; /** * Removes a trigger's body from the simulation and unregisters the trigger. No-op if the * trigger is not registered. * * @param {Trigger} trigger - The trigger to remove from the simulation. * @ignore */ removeTrigger(trigger: Trigger): void; /** * Raycast the world and return the first entity the ray hits. Fire a ray into the world from * start to end, if the ray hits an entity with a collision component, it returns a * {@link RaycastResult}, otherwise returns null. * * @param {Vec3} start - The world space point where the ray starts. * @param {Vec3} end - The world space point where the ray ends. * @param {object} [options] - The additional options for the raycasting. * @param {number} [options.filterCollisionGroup] - Collision group to apply to the raycast. * @param {number} [options.filterCollisionMask] - Collision mask to apply to the raycast. * @param {any[]} [options.filterTags] - Tags filters. Defined the same way as a {@link Tags#has} * query but within an array. * @param {Function} [options.filterCallback] - Custom function to use to filter entities. * Must return true to proceed with result. Takes one argument: the entity to evaluate. * * @returns {RaycastResult|null} The result of the raycasting or null if there was no hit. */ raycastFirst(start: Vec3, end: Vec3, options?: { filterCollisionGroup?: number; filterCollisionMask?: number; filterTags?: any[]; filterCallback?: Function; }): RaycastResult | null; /** * Raycast the world and return all entities the ray hits. It returns an array of * {@link RaycastResult}, one for each hit. If no hits are detected, the returned array will be * of length 0. Results are sorted by distance with closest first. * * @param {Vec3} start - The world space point where the ray starts. * @param {Vec3} end - The world space point where the ray ends. * @param {object} [options] - The additional options for the raycasting. * @param {boolean} [options.sort] - Whether to sort raycast results based on distance with closest * first. Defaults to false. * @param {number} [options.filterCollisionGroup] - Collision group to apply to the raycast. * @param {number} [options.filterCollisionMask] - Collision mask to apply to the raycast. * @param {any[]} [options.filterTags] - Tags filters. Defined the same way as a {@link Tags#has} * query but within an array. * @param {Function} [options.filterCallback] - Custom function to use to filter entities. * Must return true to proceed with result. Takes the entity to evaluate as argument. * * @returns {RaycastResult[]} An array of raycast hit results (0 length if there were no hits). * * @example * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2 * const hits = this.app.systems.rigidbody.raycastAll(new Vec3(0, 2, 2), new Vec3(0, -2, -2)); * @example * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2 * // where hit entity is tagged with `bird` OR `mammal` * const hits = this.app.systems.rigidbody.raycastAll(new Vec3(0, 2, 2), new Vec3(0, -2, -2), { * filterTags: [ "bird", "mammal" ] * }); * @example * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2 * // where hit entity has a `camera` component * const hits = this.app.systems.rigidbody.raycastAll(new Vec3(0, 2, 2), new Vec3(0, -2, -2), { * filterCallback: (entity) => entity && entity.camera * }); * @example * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2 * // where hit entity is tagged with (`carnivore` AND `mammal`) OR (`carnivore` AND `reptile`) * // and the entity has an `anim` component * const hits = this.app.systems.rigidbody.raycastAll(new Vec3(0, 2, 2), new Vec3(0, -2, -2), { * filterTags: [ * [ "carnivore", "mammal" ], * [ "carnivore", "reptile" ] * ], * filterCallback: (entity) => entity && entity.anim * }); */ raycastAll(start: Vec3, end: Vec3, options?: { sort?: boolean; filterCollisionGroup?: number; filterCollisionMask?: number; filterTags?: any[]; filterCallback?: Function; }): RaycastResult[]; /** * Stores a collision between the entity and other in the contacts map and returns true if it * is a new collision. * * @param {Entity} entity - The entity. * @param {Entity} other - The entity that collides with the first entity. * @returns {boolean} True if this is a new collision, false otherwise. * @private */ private _storeCollision; /** * Allocates a pooled contact point that is the given one seen from the other body's * perspective. * * @param {ContactPoint} forward - The contact point from body A's perspective. * @returns {ContactPoint} The reversed contact point. * @private */ private _createReverseContactPoint; _createSingleContactResult(a: any, b: any, contactPoint: any): SingleContactResult; _createContactResult(other: any, contacts: any): ContactResult; /** * Removes collisions that no longer exist from the collisions list and fires collisionend * events to the related entities. * * @private */ private _cleanOldCollisions; /** * Removes any stored collision keyed to the given entity. Called when a collision component is * removed so the persistent collisions map does not retain a destroyed entity. A new entity * that later reuses the same GUID (for example after reloading the same scene) would otherwise * inherit the stale entry and never fire `triggerleave` / `collisionend`, because the cached * entity no longer has a trigger or body. * * @param {Entity} entity - The entity whose stored collision should be removed. * @ignore */ clearEntityCollisions(entity: Entity): void; /** * Returns true if the entity has a contact event attached and false otherwise. * * @param {Entity} entity - Entity to test. * @returns {boolean} True if the entity has a contact and false otherwise. * @private */ private _hasContactEvent; /** * Called by the physics backend when a contact pass begins. * * @ignore */ onContactsBegin(): void; /** * Called by the physics backend for each contacting pair. Fires the trigger and collision * events. * * @param {PhysicsContactPair} pair - The contacting pair. Only valid during the call. * @ignore */ onContactPair(pair: PhysicsContactPair): void; /** * Called by the physics backend when a contact pass ends. Fires collisionend/triggerleave * events for lost contacts and frees the pooled results. * * @ignore */ onContactsEnd(): void; onUpdate(dt: any): void; } import { ComponentSystem } from '../system.js'; import { Vec3 } from '../../../core/math/vec3.js'; import { RigidBodyComponent } from './component.js'; import { ContactPoint } from './contact-point.js'; import { ObjectPool } from '../../../core/object-pool.js'; import { ContactResult } from './contact-result.js'; import { SingleContactResult } from './single-contact-result.js'; import type { PhysicsWorld } from '../../physics/physics-world.js'; import type { Trigger } from '../collision/trigger.js'; import type { RaycastResult } from './raycast-result.js'; import type { Entity } from '../../entity.js'; import type { PhysicsContactPair } from '../../physics/physics-world.js';