diff --git a/examples/src/examples/animation/locomotion.example.mjs b/examples/src/examples/animation/locomotion.example.mjs index 545910d256..768a269b5c 100644 --- a/examples/src/examples/animation/locomotion.example.mjs +++ b/examples/src/examples/animation/locomotion.example.mjs @@ -324,7 +324,7 @@ assetListLoader.load(() => { const cameraEntity = app.root.findByName('Camera'); const near = cameraEntity.camera.screenToWorld(event.x, event.y, cameraEntity.camera.nearClip); const far = cameraEntity.camera.screenToWorld(event.x, event.y, cameraEntity.camera.farClip); - const result = app.systems.rigidbody.raycastFirst(far, near); + const result = app.systems.rigidbody.raycast(far, near)[0]; if (result) { targetPosition = new pc.Vec3(result.point.x, 0, result.point.z); characterEntity.anim.setInteger('speed', data.get('jogToggle') ? 2 : 1); diff --git a/examples/src/examples/physics/raycast.example.mjs b/examples/src/examples/physics/raycast.example.mjs index 7ad4f593c3..1e0b629244 100644 --- a/examples/src/examples/physics/raycast.example.mjs +++ b/examples/src/examples/physics/raycast.example.mjs @@ -167,7 +167,7 @@ assetListLoader.load(() => { // Render the ray used in the raycast app.drawLine(start, end, white); - const result = app.systems.rigidbody.raycastFirst(start, end); + const result = app.systems.rigidbody.raycast(start, end)[0]; if (result) { result.entity.render.material = red; @@ -183,7 +183,7 @@ assetListLoader.load(() => { // Render the ray used in the raycast app.drawLine(start, end, white); - const results = app.systems.rigidbody.raycastAll(start, end); + const results = app.systems.rigidbody.raycast(start, end, { findAll: true }); results.forEach(function (result) { result.entity.render.material = red; @@ -217,8 +217,8 @@ assetListLoader.load(() => { app.root.addChild(text); }; - createText(assets.font, 'raycastFirst', 0.5, 3.75, 0, 0); - createText(assets.font, 'raycastAll', 0.5, -0.25, 0, 0); + createText(assets.font, 'raycast', 0.5, 3.75, 0, 0); + createText(assets.font, 'raycast (with findAll)', 0.5, -0.25, 0, 0); }); export { app }; diff --git a/scripts/camera/first-person-camera.js b/scripts/camera/first-person-camera.js index 7a472885b3..80c1aa92ed 100644 --- a/scripts/camera/first-person-camera.js +++ b/scripts/camera/first-person-camera.js @@ -715,7 +715,7 @@ const start = this.entity.getPosition(); const end = tmpV1.copy(start).add(Vec3.DOWN); end.y -= 0.1; - this._grounded = !!this._rigidbody.system.raycastFirst(start, end); + this._grounded = !!this._rigidbody.system.raycast(start, end)[0]; } /** diff --git a/src/deprecated/deprecated.js b/src/deprecated/deprecated.js index 5306189674..3cf983143f 100644 --- a/src/deprecated/deprecated.js +++ b/src/deprecated/deprecated.js @@ -58,7 +58,7 @@ import { BODYTYPE_DYNAMIC, BODYTYPE_KINEMATIC, BODYTYPE_STATIC } from '../framework/components/rigid-body/constants.js'; import { RigidBodyComponent } from '../framework/components/rigid-body/component.js'; -import { RigidBodyComponentSystem } from '../framework/components/rigid-body/system.js'; +import { RigidBodyComponentSystem, HitResult } from '../framework/components/rigid-body/system.js'; import { LitShader } from '../scene/shader-lib/programs/lit-shader.js'; import { Geometry } from '../scene/geometry/geometry.js'; @@ -771,6 +771,7 @@ Object.defineProperty(MouseEvent.prototype, 'wheel', { // FRAMEWORK +export const RaycastResult = HitResult; export const RIGIDBODY_TYPE_STATIC = BODYTYPE_STATIC; export const RIGIDBODY_TYPE_DYNAMIC = BODYTYPE_DYNAMIC; export const RIGIDBODY_TYPE_KINEMATIC = BODYTYPE_KINEMATIC; @@ -891,3 +892,13 @@ RigidBodyComponentSystem.prototype.setGravity = function () { this.gravity.set(arguments[0], arguments[1], arguments[2]); } }; + +RigidBodyComponentSystem.prototype.raycastFirst = function (start, end, options) { + Debug.deprecated('pc.RigidBodyComponentSystem#raycastFirst is deprecated. Use pc.RigidBodyComponentSystem#raycast instead.'); + return this.raycast(start, end, options)[0] ?? null; +}; + +RigidBodyComponentSystem.prototype.raycastAll = function (start, end, options = {}) { + Debug.deprecated('pc.RigidBodyComponentSystem#raycastAll is deprecated. Use pc.RigidBodyComponentSystem#raycast with findAll option instead.'); + return this.raycast(start, end, Object.assign(options, { findAll: true })); +}; diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index d2be8cd1bc..c461ef2540 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -946,9 +946,10 @@ class CameraComponent extends Component { * const end = entity.camera.screenToWorld(clickX, clickY, entity.camera.farClip); * * // Use the ray coordinates to perform a raycast - * app.systems.rigidbody.raycastFirst(start, end, function (result) { + * const result = app.systems.rigidbody.raycast(start, end)[0]; + * if (result) { * console.log("Entity " + result.entity.name + " was selected"); - * }); + * } * @returns {import('../../../core/math/vec3.js').Vec3} The world space coordinate. */ screenToWorld(screenx, screeny, cameraz, worldCoord) { diff --git a/src/framework/components/rigid-body/system.js b/src/framework/components/rigid-body/system.js index d95b1989b8..09a52c186f 100644 --- a/src/framework/components/rigid-body/system.js +++ b/src/framework/components/rigid-body/system.js @@ -3,22 +3,27 @@ import { ObjectPool } from '../../../core/object-pool.js'; import { Debug } from '../../../core/debug.js'; import { Vec3 } from '../../../core/math/vec3.js'; +import { Quat } from '../../../core/math/quat.js'; import { Component } from '../component.js'; import { ComponentSystem } from '../system.js'; -import { BODYFLAG_NORESPONSE_OBJECT } from './constants.js'; +import { BODYFLAG_NORESPONSE_OBJECT, BODYSTATE_ACTIVE_TAG } from './constants.js'; import { RigidBodyComponent } from './component.js'; import { RigidBodyComponentData } from './data.js'; -let ammoRayStart, ammoRayEnd; +// Ammo.js variable for performance saving. +let ammoRayStart, ammoRayEnd, ammoVec3, ammoQuat, ammoTransform, ammoTransform2; + +// RigidBody for shape tests. Permanent to save performance. +let shapeTestBody; /** - * Object holding the result of a successful raycast hit. + * Object holding the result of a successful hit. * * @category Physics */ -class RaycastResult { +class HitResult { /** * The entity that was hit. * @@ -49,20 +54,29 @@ class RaycastResult { hitFraction; /** - * Create a new RaycastResult instance. + * The distance at which the hit occurred from the starting point. + * + * @type {number} + */ + distance; + + /** + * Create a new HitResult instance. * * @param {import('../../entity.js').Entity} entity - The entity that was hit. * @param {Vec3} point - The point at which the ray hit the entity in world space. * @param {Vec3} normal - The normal vector of the surface where the ray hit in world space. - * @param {number} hitFraction - The normalized distance (between 0 and 1) at which the ray hit + * @param {number} hitFraction - The normalized distance (between 0 and 1) at which the hit * occurred from the starting point. + * @param {number} distance - The distance at which the hit occurred from the starting point. * @ignore */ - constructor(entity, point, normal, hitFraction) { + constructor(entity, point, normal, hitFraction, distance) { this.entity = entity; this.point = point; this.normal = normal; this.hitFraction = hitFraction; + this.distance = distance; } } @@ -271,6 +285,28 @@ class ContactResult { const _schema = ['enabled']; +/** + * Creates a new shape. + * + * @param {string} name - Name of the shape. Must start with capital letter. + * @param {number} [axis] - The local space axis with which the shape's length is aligned. + * 0 for X, 1 for Y and 2 for Z. Defaults to 1 (Y-axis). + * @param {...any} [args] - Arguments to pass to creation. + * @returns {object} Created Ammo.btCollisionShape. + * @ignore + */ +function createShape(name, axis, ...args) { + let fn = `bt${name}Shape`; + + if (axis === 0) { + fn += 'X'; + } else if (axis === 2) { + fn += 'Z'; + } + + return new Ammo[fn](...args); +} + /** * The RigidBodyComponentSystem maintains the dynamics world for simulating rigid bodies, it also * controls global values for the world such as gravity. Note: The RigidBodyComponentSystem is only @@ -393,6 +429,11 @@ class RigidBodyComponentSystem extends ComponentSystem { // Lazily create temp vars ammoRayStart = new Ammo.btVector3(); ammoRayEnd = new Ammo.btVector3(); + ammoVec3 = new Ammo.btVector3(); + ammoQuat = new Ammo.btQuaternion(); + ammoTransform = new Ammo.btTransform(); + ammoTransform2 = new Ammo.btTransform(); + RigidBodyComponent.onLibraryLoaded(); this.contactPointPool = new ObjectPool(ContactPoint, 1); @@ -504,120 +545,68 @@ class RigidBodyComponentSystem extends ComponentSystem { } /** - * 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, end, options = {}) { - // Tags and custom callback can only be performed by looking at all results. - if (options.filterTags || options.filterCallback) { - options.sort = true; - return this.raycastAll(start, end, options)[0] || null; - } - - let result = null; - - ammoRayStart.setValue(start.x, start.y, start.z); - ammoRayEnd.setValue(end.x, end.y, end.z); - const rayCallback = new Ammo.ClosestRayResultCallback(ammoRayStart, ammoRayEnd); - - if (typeof options.filterCollisionGroup === 'number') { - rayCallback.set_m_collisionFilterGroup(options.filterCollisionGroup); - } - - if (typeof options.filterCollisionMask === 'number') { - rayCallback.set_m_collisionFilterMask(options.filterCollisionMask); - } - - this.dynamicsWorld.rayTest(ammoRayStart, ammoRayEnd, rayCallback); - if (rayCallback.hasHit()) { - const collisionObj = rayCallback.get_m_collisionObject(); - const body = Ammo.castObject(collisionObj, Ammo.btRigidBody); - - if (body) { - const point = rayCallback.get_m_hitPointWorld(); - const normal = rayCallback.get_m_hitNormalWorld(); - - result = new RaycastResult( - body.entity, - new Vec3(point.x(), point.y(), point.z()), - new Vec3(normal.x(), normal.y(), normal.z()), - rayCallback.get_m_closestHitFraction() - ); - } - } - - Ammo.destroy(rayCallback); - - return result; - } - - /** - * 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. + * Raycast the world and return the entities the ray hits. It returns an array of + * {@link HitResult}, one for each hit. If no hits are detected, the returned array will be + * of length 0. * * @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 {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 {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. + * @param {boolean} [options.findAll] - Whether to return all results. When false will return + * only the closest result. Defaults to false. * - * @returns {RaycastResult[]} An array of raycast hit results (0 length if there were no hits). + * @returns {HitResult[]} 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)); + * // Return all results of a raycast between (0, 2, 2) and (0, -2, -2) + * const hits = this.app.systems.rigidbody.raycast( + * new Vec3(0, 2, 2), + * new Vec3(0, -2, -2), + * { findAll: true } + * ); * @example - * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2 + * // 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" ] + * const hits = this.app.systems.rigidbody.raycast(new Vec3(0, 2, 2), new Vec3(0, -2, -2), { + * filterTags: [ "bird", "mammal" ], + * findAll: true * }); * @example - * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2 + * // 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 + * const hits = this.app.systems.rigidbody.raycast(new Vec3(0, 2, 2), new Vec3(0, -2, -2), { + * filterCallback: (entity) => entity && entity.camera, + * findAll: true * }); * @example - * // Return all results of a raycast between 0, 2, 2 and 0, -2, -2 + * // Return the closest result 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), { + * const hit = this.app.systems.rigidbody.raycast(new Vec3(0, 2, 2), new Vec3(0, -2, -2), { * filterTags: [ * [ "carnivore", "mammal" ], * [ "carnivore", "reptile" ] * ], * filterCallback: (entity) => entity && entity.anim - * }); + * })[0]; */ - raycastAll(start, end, options = {}) { - Debug.assert(Ammo.AllHitsRayResultCallback, 'pc.RigidBodyComponentSystem#raycastAll: Your version of ammo.js does not expose Ammo.AllHitsRayResultCallback. Update it to latest.'); - + raycast(start, end, options = {}) { + Debug.assert(Ammo.AllHitsRayResultCallback, 'pc.RigidBodyComponentSystem#raycast: Your version of ammo.js does not expose Ammo.AllHitsRayResultCallback. Update it to latest.'); const results = []; ammoRayStart.setValue(start.x, start.y, start.z); ammoRayEnd.setValue(end.x, end.y, end.z); - const rayCallback = new Ammo.AllHitsRayResultCallback(ammoRayStart, ammoRayEnd); + + const findAll = options.findAll || options.filterTags || options.filterCallback; + const rayCallback = findAll ? new Ammo.AllHitsRayResultCallback(ammoRayStart, ammoRayEnd) : new Ammo.ClosestRayResultCallback(ammoRayStart, ammoRayEnd); if (typeof options.filterCollisionGroup === 'number') { rayCallback.set_m_collisionFilterGroup(options.filterCollisionGroup); @@ -628,40 +617,300 @@ class RigidBodyComponentSystem extends ComponentSystem { } this.dynamicsWorld.rayTest(ammoRayStart, ammoRayEnd, rayCallback); + if (rayCallback.hasHit()) { - const collisionObjs = rayCallback.get_m_collisionObjects(); - const points = rayCallback.get_m_hitPointWorld(); - const normals = rayCallback.get_m_hitNormalWorld(); - const hitFractions = rayCallback.get_m_hitFractions(); - - const numHits = collisionObjs.size(); - for (let i = 0; i < numHits; i++) { - const body = Ammo.castObject(collisionObjs.at(i), Ammo.btRigidBody); - - if (body && body.entity) { - if (options.filterTags && !body.entity.tags.has(...options.filterTags) || options.filterCallback && !options.filterCallback(body.entity)) { - continue; + const rayDistance = start.distance(end); + + if (findAll) { + const collisionObjs = rayCallback.get_m_collisionObjects(); + const points = rayCallback.get_m_hitPointWorld(); + const normals = rayCallback.get_m_hitNormalWorld(); + const hitFractions = rayCallback.get_m_hitFractions(); + + const numHits = collisionObjs.size(); + for (let i = 0; i < numHits; i++) { + const body = Ammo.castObject(collisionObjs.at(i), Ammo.btRigidBody); + + if (body && body.entity) { + if ((options.filterTags && !body.entity.tags.has(...options.filterTags)) || (options.filterCallback && !options.filterCallback(body.entity))) { + continue; + } + + const point = points.at(i); + const normal = normals.at(i); + const hitFraction = hitFractions.at(i); + + results.push(new HitResult(body.entity, new Vec3(point.x(), point.y(), point.z()), new Vec3(normal.x(), normal.y(), normal.z()), hitFraction, rayDistance * hitFraction)); } + } + + if (options.sort || !options.findAll) { + results.sort((a, b) => a.hitFraction - b.hitFraction); - const point = points.at(i); - const normal = normals.at(i); - const result = new RaycastResult( - body.entity, - new Vec3(point.x(), point.y(), point.z()), - new Vec3(normal.x(), normal.y(), normal.z()), - hitFractions.at(i) + if (!options.findAll && results.length > 1) { + results.length = 1; + } + } + } else { + const collisionObj = rayCallback.get_m_collisionObject(); + const body = Ammo.castObject(collisionObj, Ammo.btRigidBody); + + if (body) { + const point = rayCallback.get_m_hitPointWorld(); + const normal = rayCallback.get_m_hitNormalWorld(); + const hitFraction = rayCallback.get_m_closestHitFraction(); + + results.push( + new HitResult( + body.entity, + new Vec3(point.x(), point.y(), point.z()), + new Vec3(normal.x(), normal.y(), normal.z()), + hitFraction, + rayDistance * hitFraction + ) ); + } + } + } + + Ammo.destroy(rayCallback); + return results; + } + + /** + * Perform a collision check on the world and return the entities the shape hits. + * It returns an array of {@link HitResult}. If no hits are + * detected, the returned array will be of length 0. + * + * @param {object} shape - The shape to use for collision. Can be a btCollisionShape + * or shape config. + * @param {number} [shape.axis] - The local space axis with which the capsule, cylinder or + * cone shape's length is aligned. 0 for X, 1 for Y and 2 for Z. Defaults to 1 (Y-axis). + * @param {Vec3} [shape.halfExtents] - The half-extents of the box in the x, y and z axes. + * @param {number} [shape.height] - The total height of the capsule, cylinder or cone from + * tip to tip. + * @param {string} shape.type - The type of shape to use. Available options are "box", + * "capsule", "cone", "cylinder" or "sphere". + * @param {number} [shape.radius] - The radius of the sphere, capsule, cylinder or cone. + * @param {Vec3} startPosition - The world space position for the shape to be at start. + * @param {Vec3|Quat} [startRotation] - The world space rotation for the shape to have + * at start. + * @param {Vec3} [endPosition] - The world space position for the shape to be at end. + * @param {object} [options] - The additional options for the shape casting. + * @param {Vec3|Quat} [options.endRotation] - The world space rotation for the shape to have + * at end. + * @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 shape cast. + * @param {number} [options.filterCollisionMask] - Collision mask to apply to the shape cast. + * @param {any[]} [options.filterTags] - Tags filters. Defined the same way as a + * {@link Tags#has} query but within an array. Only available if shape is not having different + * ending transform from starting one. + * @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. Only + * available if shape is not having different ending transform from starting one. + * @param {boolean} [options.destroyShape] - Whether to destroy the shape after the cast. + * Defaults to false, forced true when shape is not a btCollisionShape. + * @param {boolean} [options.findAll] - Whether to return all results. When false will return + * only the closest result. Defaults to false. Only available if shape is not having different + * ending transform from starting one. + * + * @returns {HitResult[]} An array of shapeCast hit results (0 length if there were no hits). + */ + shapeCast(shape, startPosition, startRotation, endPosition, options = {}) { + const results = []; + + let btShape; + switch (shape.type) { + case 'box': + ammoVec3.setValue( + shape.halfExtents?.x ?? 0.5, + shape.halfExtents?.y ?? 0.5, + shape.halfExtents?.z ?? 0.5 + ); + + btShape = new Ammo.btBoxShape(ammoVec3); + options.destroyShape = true; + break; + case 'capsule': + btShape = createShape('Capsule', shape.axis, shape.radius ?? 0.5, shape.height ?? 1); + options.destroyShape = true; + break; + case 'cone': + btShape = createShape('Cone', shape.axis, shape.radius ?? 0.5, shape.height ?? 1); + options.destroyShape = true; + break; + case 'cylinder': + btShape = createShape('Cylinder', shape.axis, shape.radius ?? 0.5, shape.height ?? 1); + options.destroyShape = true; + break; + case 'sphere': + btShape = new Ammo.btSphereShape(shape.radius ?? 0.5); + options.destroyShape = true; + break; + default: + btShape = shape; + break; + } + + ammoVec3.setValue(startPosition.x, startPosition.y, startPosition.z); + if (startRotation instanceof Quat) { + ammoQuat.setValue(startRotation.x, startRotation.y, startRotation.z, startRotation.w); + } else if (startRotation instanceof Vec3) { + ammoQuat.setEulerZYX(startRotation.z, startRotation.y, startRotation.x); + } else { + ammoQuat.setEulerZYX(0, 0, 0); + } + + ammoTransform.setIdentity(); + ammoTransform.setOrigin(ammoVec3); + ammoTransform.setRotation(ammoQuat); + + const endRotation = options.endRotation; + if ((endPosition && !endPosition.equals(startPosition)) || endRotation) { + Debug.assert(Ammo.ClosestConvexResultCallback && Ammo.ClosestConvexResultCallback.get_m_hitCollisionObject, 'pc.RigidBodyComponentSystem#shapeCast: Your version of ammo.js does not expose Ammo.ClosestConvexResultCallback or Ammo.ClosestConvexResultCallback#get_m_hitCollisionObject. Update it to latest.'); + + ammoVec3.setValue(endPosition.x, endPosition.y, endPosition.z); + if (endRotation instanceof Quat) { + ammoQuat.setValue(endRotation.x, endRotation.y, endRotation.z, endRotation.w); + } else if (endRotation instanceof Vec3) { + ammoQuat.setEulerZYX(endRotation.z, endRotation.y, endRotation.x); + } else { + ammoQuat.setEulerZYX(0, 0, 0); + } + + ammoTransform2.setIdentity(); + ammoTransform2.setOrigin(ammoVec3); + ammoTransform2.setRotation(ammoQuat); + + const resultCallback = new Ammo.ClosestConvexResultCallback(); - results.push(result); + if (typeof options.filterCollisionGroup === 'number') { + resultCallback.set_m_collisionFilterGroup(options.filterCollisionGroup); + } + + if (typeof options.filterCollisionMask === 'number') { + resultCallback.set_m_collisionFilterMask(options.filterCollisionMask); + } + + this.app.systems.rigidbody.dynamicsWorld.convexSweepTest( + btShape, + ammoTransform, + ammoTransform2, + resultCallback + ); + + if (resultCallback.hasHit()) { + const body = Ammo.castObject(resultCallback.get_m_hitCollisionObject(), Ammo.btRigidBody); + + if (body) { + const point = resultCallback.get_m_hitPointWorld(); + const normal = resultCallback.get_m_hitNormalWorld(); + + const pointVec = new Vec3(point.x(), point.y(), point.z()); + results.push( + new HitResult( + body.entity, + pointVec, + new Vec3(normal.x(), normal.y(), normal.z()), + resultCallback.get_m_closestHitFraction(), + startPosition.distance(pointVec) + ) + ); } } - if (options.sort) { - results.sort((a, b) => a.hitFraction - b.hitFraction); + // Destroy Ammo variables. + Ammo.destroy(resultCallback); + if (options.destroyShape) { + Ammo.destroy(btShape); } + + return results; } - Ammo.destroy(rayCallback); + Debug.assert(Ammo.ConcreteContactResultCallback, 'pc.RigidBodyComponentSystem#shapeCest: Your version of ammo.js does not expose Ammo.ConcreteContactResultCallback. Update it to latest.'); + + // We only initialize the shapeTest body here so we don't have an extra body unless the user uses this function + if (!shapeTestBody) { + shapeTestBody = this.createBody(0, btShape, ammoTransform); + } + + // Make sure the body has proper shape, transform and is active. + shapeTestBody.setCollisionShape(btShape); + shapeTestBody.setWorldTransform(ammoTransform); + shapeTestBody.forceActivationState(BODYSTATE_ACTIVE_TAG); + + // Callback for the contactTest results. + const resultCallback = new Ammo.ConcreteContactResultCallback(); + resultCallback.addSingleResult = function (cp, colObj0Wrap, partId0, index0, colObj1Wrap, p1, index1) { + // Retrieve collided entity. + const body1 = Ammo.castObject( + Ammo.wrapPointer(colObj1Wrap, Ammo.btCollisionObjectWrapper).getCollisionObject(), + Ammo.btRigidBody + ); + + // Make sure there is an existing entity. + if (body1.entity) { + if ((options.filterTags && !body1.entity.tags.has(...options.filterTags)) || (options.filterCallback && !options.filterCallback(body1.entity))) { + return 0; + } + + // Retrieve manifold point. + const manifold = Ammo.wrapPointer(cp, Ammo.btManifoldPoint); + + // Make sure there is a collision + const distance = manifold.getDistance(); + if (distance < 0) { + const point = manifold.get_m_positionWorldOnB(); + const normal = manifold.get_m_normalWorldOnB(); + + const pointVec = new Vec3(point.x(), point.y(), point.z()); + const startDistance = startPosition.distance(pointVec); + + // Push the result. + results.push( + new HitResult( + body1.entity, + pointVec, + new Vec3(normal.x(), normal.y(), normal.z()), + // Minus distance as it's negative. + startDistance / (startDistance - distance), + startDistance + ) + ); + + return 1; + } + } + + return 0; + }; + + if (typeof options.filterCollisionGroup === 'number') { + resultCallback.set_m_collisionFilterGroup(options.filterCollisionGroup); + } + + if (typeof options.filterCollisionMask === 'number') { + resultCallback.set_m_collisionFilterMask(options.filterCollisionMask); + } + + this.dynamicsWorld.contactTest(shapeTestBody, resultCallback); + + // Destroy Ammo variables. + shapeTestBody.setCollisionShape(null); + Ammo.destroy(resultCallback); + if (options.destroyShape) { + Ammo.destroy(btShape); + } + + if (options.sort || !options.findAll) { + results.sort((a, b) => a.distance - b.distance); + + if (!options.findAll && results.length > 1) { + results.length = 1; + } + } return results; } @@ -1055,4 +1304,4 @@ class RigidBodyComponentSystem extends ComponentSystem { Component._buildAccessors(RigidBodyComponent.prototype, _schema); -export { ContactPoint, ContactResult, RaycastResult, RigidBodyComponentSystem, SingleContactResult }; +export { ContactPoint, ContactResult, HitResult, RigidBodyComponentSystem, SingleContactResult }; diff --git a/src/index.js b/src/index.js index 7e0878a8ad..a6cdb5772e 100644 --- a/src/index.js +++ b/src/index.js @@ -254,7 +254,7 @@ export { RenderComponent } from './framework/components/render/component.js'; export { RenderComponentSystem } from './framework/components/render/system.js'; export * from './framework/components/rigid-body/constants.js'; export { RigidBodyComponent } from './framework/components/rigid-body/component.js'; -export { RigidBodyComponentSystem, ContactPoint, ContactResult, RaycastResult, SingleContactResult } from './framework/components/rigid-body/system.js'; +export { RigidBodyComponentSystem, ContactPoint, ContactResult, HitResult, SingleContactResult } from './framework/components/rigid-body/system.js'; export { SceneRegistry } from './framework/scene-registry.js'; export { SceneRegistryItem } from './framework/scene-registry-item.js'; export * from './framework/components/screen/constants.js';