playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
346 lines (345 loc) • 13.8 kB
TypeScript
/**
* 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';