UNPKG

playcanvas

Version:

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

615 lines (614 loc) 18.2 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Quat } from "../../../core/math/quat.js"; import { Vec3 } from "../../../core/math/vec3.js"; import { Asset } from "../../asset/asset.js"; import { Component } from "../component.js"; const _vec3 = new Vec3(); const _quat = new Quat(); class CollisionComponent extends Component { /** * Create a new CollisionComponent. * * @param {CollisionComponentSystem} system - The ComponentSystem that created this Component. * @param {Entity} entity - The Entity that this Component is attached to. */ constructor(system, entity) { super(system, entity); /** @private */ __publicField(this, "_type", "box"); /** @private */ __publicField(this, "_halfExtents", new Vec3(0.5, 0.5, 0.5)); /** @private */ __publicField(this, "_linearOffset", new Vec3()); /** @private */ __publicField(this, "_angularOffset", new Quat()); /** @private */ __publicField(this, "_radius", 0.5); /** @private */ __publicField(this, "_axis", 1); /** @private */ __publicField(this, "_height", 2); /** * @type {number|null} * @private */ __publicField(this, "_asset", null); /** * @type {number|null} * @private */ __publicField(this, "_renderAsset", null); /** @private */ __publicField(this, "_convexHull", false); /** @private */ __publicField(this, "_shape", null); /** * @type {Model|null} * @private */ __publicField(this, "_model", null); /** @private */ __publicField(this, "_render", null); /** @private */ __publicField(this, "_checkVertexDuplicates", true); /** @private */ __publicField(this, "_initialized", false); /** @private */ __publicField(this, "_compoundParent", null); /** @private */ __publicField(this, "_hasOffset", false); /** * The entity world scale a mesh shape was last built with - a change triggers a rebuild. * * @type {Vec3|null} * @private */ __publicField(this, "_builtWorldScale", null); this.entity.on("insert", this._onInsert, this); } /** * Sets the type of the collision volume. Can be: * * - "box": A box-shaped collision volume. * - "capsule": A capsule-shaped collision volume. * - "compound": A compound shape. Any descendant entities with a collision component of type * box, capsule, cone, cylinder or sphere will be combined into a single, rigid shape. * - "cone": A cone-shaped collision volume. * - "cylinder": A cylinder-shaped collision volume. * - "mesh": A collision volume that uses a model asset as its shape. * - "sphere": A sphere-shaped collision volume. * * Defaults to "box". * * @type {string} */ set type(arg) { if (this._type === arg) { return; } const previous = this._type; this._type = arg; this.system.changeType(this, previous, arg); } /** * Gets the type of the collision volume. * * @type {string} */ get type() { return this._type; } /** * Sets the half-extents of the box-shaped collision volume in the x, y and z axes. Defaults to * `[0.5, 0.5, 0.5]`. * * @type {Vec3} */ set halfExtents(arg) { if (arg instanceof Vec3) { this._halfExtents.copy(arg); } else { this._halfExtents.set(arg[0], arg[1], arg[2]); } if (this._initialized && this._type === "box") { this.system.recreatePhysicalShapes(this); } } /** * Gets the half-extents of the box-shaped collision volume in the x, y and z axes. Use the * setter to update the collision shape. * * @type {Readonly<Vec3>} */ get halfExtents() { return this._halfExtents; } /** * Sets the positional offset of the collision shape from the Entity position along the local * axes. Defaults to `[0, 0, 0]`. * * @type {Vec3} */ set linearOffset(arg) { if (arg instanceof Vec3) { this._linearOffset.copy(arg); } else { this._linearOffset.set(arg[0], arg[1], arg[2]); } this._updateHasOffset(); if (this._initialized) { this.system.recreatePhysicalShapes(this); } } /** * Gets the positional offset of the collision shape from the Entity position along the local * axes. Use the setter to update the collision shape. * * @type {Readonly<Vec3>} */ get linearOffset() { return this._linearOffset; } /** * Sets the rotational offset of the collision shape from the Entity rotation in local space. * Defaults to identity. * * @type {Quat} */ set angularOffset(arg) { if (arg instanceof Quat) { this._angularOffset.copy(arg); } else if (arg.length === 3) { this._angularOffset.setFromEulerAngles(arg[0], arg[1], arg[2]); } else { this._angularOffset.set(arg[0], arg[1], arg[2], arg[3]); } this._updateHasOffset(); if (this._initialized) { this.system.recreatePhysicalShapes(this); } } /** * Gets the rotational offset of the collision shape from the Entity rotation in local space. Use * the setter to update the collision shape. * * @type {Readonly<Quat>} */ get angularOffset() { return this._angularOffset; } /** * Sets the radius of the sphere, capsule, cylinder or cone-shaped collision volumes. * Defaults to 0.5. * * @type {number} */ set radius(arg) { this._radius = arg; const t = this._type; if (this._initialized && (t === "sphere" || t === "capsule" || t === "cylinder" || t === "cone")) { this.system.recreatePhysicalShapes(this); } } /** * Gets the radius of the sphere, capsule, cylinder or cone-shaped collision volumes. * * @type {number} */ get radius() { return this._radius; } /** * Sets the local space axis with which the capsule, cylinder or cone-shaped collision volume's * length is aligned. 0 for X, 1 for Y and 2 for Z. Defaults to 1 (Y-axis). * * @type {number} */ set axis(arg) { this._axis = arg; const t = this._type; if (this._initialized && (t === "capsule" || t === "cylinder" || t === "cone")) { this.system.recreatePhysicalShapes(this); } } /** * Gets the local space axis with which the capsule, cylinder or cone-shaped collision volume's * length is aligned. * * @type {number} */ get axis() { return this._axis; } /** * Sets the total height of the capsule, cylinder or cone-shaped collision volume from tip to * tip. Defaults to 2. * * @type {number} */ set height(arg) { this._height = arg; const t = this._type; if (this._initialized && (t === "capsule" || t === "cylinder" || t === "cone")) { this.system.recreatePhysicalShapes(this); } } /** * Gets the total height of the capsule, cylinder or cone-shaped collision volume from tip to * tip. * * @type {number} */ get height() { return this._height; } /** * Sets the asset or asset id for the model of the mesh collision volume. Defaults to null. * * @type {Asset|number|null} */ set asset(arg) { const assets = this.system.app.assets; if (this._asset) { const asset = assets.get(this._asset); if (asset) { asset.off("remove", this.onAssetRemoved, this); } } this._asset = arg instanceof Asset ? arg.id : arg; if (arg) { const asset = assets.get(this._asset); if (asset) { asset.off("remove", this.onAssetRemoved, this); asset.on("remove", this.onAssetRemoved, this); } } if (this._initialized && this._type === "mesh") { if (!arg) { this._model = null; } this.system.recreatePhysicalShapes(this); } } /** * Gets the asset or asset id for the model of the mesh collision volume. * * @type {Asset|number|null} */ get asset() { return this._asset; } /** * Sets the render asset or asset id of the mesh collision volume. Defaults to null. * If not set then the asset property will be checked instead. * * @type {Asset|number|null} */ set renderAsset(arg) { const assets = this.system.app.assets; if (this._renderAsset) { const asset = assets.get(this._renderAsset); if (asset) { asset.off("remove", this.onRenderAssetRemoved, this); } } this._renderAsset = arg instanceof Asset ? arg.id : arg; if (arg) { const asset = assets.get(this._renderAsset); if (asset) { asset.off("remove", this.onRenderAssetRemoved, this); asset.on("remove", this.onRenderAssetRemoved, this); } } if (this._initialized && this._type === "mesh") { if (!arg) { this._render = null; } this.system.recreatePhysicalShapes(this); } } /** * Gets the render asset id of the mesh collision volume. * * @type {Asset|number|null} */ get renderAsset() { return this._renderAsset; } /** * Sets whether the collision mesh should be treated as a convex hull. When false, the mesh can * only be used with a static body. When true, the mesh can be used with a static, dynamic or * kinematic body. Defaults to `false`. * * @type {boolean} */ set convexHull(arg) { this._convexHull = arg; if (this._initialized && this._type === "mesh") { this.system.doRecreatePhysicalShape(this); } } /** * Gets whether the collision mesh should be treated as a convex hull. * * @type {boolean} */ get convexHull() { return this._convexHull; } set shape(arg) { this._shape = arg; } get shape() { return this._shape; } /** * Sets the model that is added to the scene graph for the mesh collision volume. * * @type {Model | null} */ set model(arg) { this._model = arg; if (this._initialized && this._type === "mesh") { this.system.doRecreatePhysicalShape(this); } } /** * Gets the model that is added to the scene graph for the mesh collision volume. * * @type {Model | null} */ get model() { return this._model; } set render(arg) { this._render = arg; if (this._initialized && this._type === "mesh") { this.system.doRecreatePhysicalShape(this); } } get render() { return this._render; } /** * Sets whether checking for duplicate vertices should be enabled when creating collision meshes. * * @type {boolean} */ set checkVertexDuplicates(arg) { this._checkVertexDuplicates = arg; } /** * Gets whether checking for duplicate vertices should be enabled when creating collision meshes. * * @type {boolean} */ get checkVertexDuplicates() { return this._checkVertexDuplicates; } /** @private */ _updateHasOffset() { this._hasOffset = !this._linearOffset.equals(Vec3.ZERO) || !this._angularOffset.equals(Quat.IDENTITY); } /** * @param {Asset} asset - Asset that was removed. * @private */ onAssetRemoved(asset) { asset.off("remove", this.onAssetRemoved, this); if (this._asset === asset.id) { this.asset = null; } } /** * @param {Asset} asset - Asset that was removed. * @private */ onRenderAssetRemoved(asset) { asset.off("remove", this.onRenderAssetRemoved, this); if (this._renderAsset === asset.id) { this.renderAsset = null; } } /** * @param {GraphNode} parent - The parent node. * @private */ _onInsert(parent) { const world = this.system.physicsWorld; if (!world) { return; } if (this._compoundParent) { this.system.recreatePhysicalShapes(this); } else if (!this.entity.rigidbody) { let ancestor = this.entity.parent; while (ancestor) { if (ancestor.collision && ancestor.collision.type === "compound") { if (world.getCompoundChildCount(ancestor.collision.shape) === 0) { this.system.recreatePhysicalShapes(ancestor.collision); } else { this.system.recreatePhysicalShapes(this); } break; } ancestor = ancestor.parent; } } } /** * An {@link Entity#forEach} callback that refreshes the compound child transform of each * descendant wired to the same compound root. Invoked with `this` set to the compound * root's entity. * * @param {Entity} entity - The visited descendant entity. * @private */ _updateEachDescendantTransform(entity) { if (!entity.collision || entity.collision._compoundParent !== this.collision._compoundParent) { return; } this.collision.system.updateCompoundChildTransform(entity, false); } /** @private */ _updateCompound() { const entity = this.entity; if (entity._dirtyWorld) { let dirty = entity._dirtyLocal; let parent = entity; while (parent && !dirty) { if (parent.collision && parent.collision === this._compoundParent) { break; } if (parent._dirtyLocal) { dirty = true; } parent = parent.parent; } if (dirty) { entity.forEach(this._updateEachDescendantTransform, entity); const bodyComponent = this._compoundParent.entity.rigidbody; if (bodyComponent) { bodyComponent.activate(); } } } } /** * Returns the world position for the collision shape, taking into account of any offsets. * * @returns {Vec3} The world position for the collision shape. */ getShapePosition() { const pos = this.entity.getPosition(); if (this._hasOffset) { const rot = this.entity.getRotation(); const lo = this._linearOffset; _quat.copy(rot).transformVector(lo, _vec3); return _vec3.add(pos); } return pos; } /** * Returns the world rotation for the collision shape, taking into account of any offsets. * * @returns {Quat} The world rotation for the collision. */ getShapeRotation() { const rot = this.entity.getRotation(); if (this._hasOffset) { return _quat.copy(rot).mul(this._angularOffset); } return rot; } onEnable() { if (this._type === "mesh" && (this._asset || this._renderAsset) && this._initialized) { const asset = this.system.app.assets.get(this._asset || this._renderAsset); if (asset && (!asset.resource || !this._shape)) { this.system.recreatePhysicalShapes(this); return; } } if (this.entity.rigidbody) { if (this.entity.rigidbody.enabled) { this.entity.rigidbody.enableSimulation(); } } else if (this._compoundParent && this !== this._compoundParent) { if (this.system.physicsWorld.getCompoundChildCount(this._compoundParent.shape) === 0) { this.system.recreatePhysicalShapes(this._compoundParent); } else { this.system.updateCompoundChildTransform(this.entity, true); if (this._compoundParent.entity.rigidbody) { this._compoundParent.entity.rigidbody.activate(); } } } else if (this.entity.trigger) { this.entity.trigger.enable(); } } onDisable() { if (this.entity.rigidbody) { this.entity.rigidbody.disableSimulation(); } else if (this._compoundParent && this !== this._compoundParent) { if (!this._compoundParent.entity._destroying) { this.system._removeCompoundChild(this._compoundParent, this._shape); if (this._compoundParent.entity.rigidbody) { this._compoundParent.entity.rigidbody.activate(); } } } else if (this.entity.trigger) { this.entity.trigger.disable(); } } onBeforeRemove() { if (this.asset) { this.asset = null; } if (this.renderAsset) { this.renderAsset = null; } this.entity.off("insert", this._onInsert, this); this.off(); } } /** * Fired when a contact occurs between two rigid bodies. The handler is passed a * {@link ContactResult} object which contains details of the contact between the two rigid * bodies. * * @event * @example * entity.collision.on('contact', (result) => { * console.log(`Contact between ${entity.name} and ${result.other.name}`); * }); */ __publicField(CollisionComponent, "EVENT_CONTACT", "contact"); /** * Fired when two rigid bodies start touching. The handler is passed the {@link ContactResult} * object which contains details of the contact between the two rigid bodies. * * @event * @example * entity.collision.on('collisionstart', (result) => { * console.log(`${entity.name} started touching ${result.other.name}`); * }); */ __publicField(CollisionComponent, "EVENT_COLLISIONSTART", "collisionstart"); /** * Fired when two rigid bodies stop touching. The handler is passed an {@link Entity} that * represents the other rigid body involved in the collision. * * @event * @example * entity.collision.on('collisionend', (other) => { * console.log(`${entity.name} stopped touching ${other.name}`); * }); */ __publicField(CollisionComponent, "EVENT_COLLISIONEND", "collisionend"); /** * Fired when a rigid body enters a trigger volume. The handler is passed an {@link Entity} * representing the rigid body that entered this collision volume. * * @event * @example * entity.collision.on('triggerenter', (other) => { * console.log(`${other.name} entered trigger volume ${entity.name}`); * }); */ __publicField(CollisionComponent, "EVENT_TRIGGERENTER", "triggerenter"); /** * Fired when a rigid body exits a trigger volume. The handler is passed an {@link Entity} * representing the rigid body that exited this collision volume. * * @event * @example * entity.collision.on('triggerleave', (other) => { * console.log(`${other.name} exited trigger volume ${entity.name}`); * }); */ __publicField(CollisionComponent, "EVENT_TRIGGERLEAVE", "triggerleave"); export { CollisionComponent };