Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for shapeCast and merged raycastFirst and raycastAll into raycast #5039

Open
wants to merge 100 commits into
base: main
Choose a base branch
from
Open
Changes from 31 commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
ac625fc
Added shape casts
MushAsterion Feb 3, 2023
4fd6099
Fixed ESLint
MushAsterion Feb 3, 2023
c5f8fc8
Fixed halfExtends into halfExtents
MushAsterion Feb 3, 2023
36d500a
Added shapecast shape destroying
MushAsterion Feb 3, 2023
624c8da
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 3, 2023
0e51d63
Reduced debug assets into one
MushAsterion Feb 3, 2023
2ace6a3
Removed variable type change from shapeCasts
MushAsterion Feb 3, 2023
2b1e039
Fixed JSDoc for optional parameters
MushAsterion Feb 3, 2023
0cdc17a
Added default position and rotation for _shapecast
MushAsterion Feb 3, 2023
052e8f8
Removed shapecast body world addition
MushAsterion Feb 3, 2023
7eff8db
Shape casts now return Entity array
MushAsterion Feb 3, 2023
5de3fef
Removed unused variables
MushAsterion Feb 3, 2023
9133182
Added Entity import
MushAsterion Feb 3, 2023
1e4415e
Fixed Entity JSDoc
MushAsterion Feb 3, 2023
53cb43f
Updated shape.type eval to switch
MushAsterion Feb 3, 2023
b4f005b
Fixed docs
MushAsterion Feb 3, 2023
502994c
Fix shape on boxCast
MushAsterion Feb 4, 2023
c8adb46
Added HitResult to shapecast
MushAsterion Feb 4, 2023
fd907d5
Fixed inconsistent documentation
MushAsterion Feb 4, 2023
9566b60
Fixed ESLint
MushAsterion Feb 4, 2023
6c2d55b
Changed naming from cast to test
MushAsterion Feb 4, 2023
9027bf3
Added Quat rotation for shape tests
MushAsterion Feb 4, 2023
c294abb
Fixed ESLint
MushAsterion Feb 4, 2023
845fb0a
Added sphereCast and boxCast
MushAsterion Feb 4, 2023
34d83b3
Fixed typo
MushAsterion Feb 4, 2023
a448e62
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 4, 2023
fcb70ea
Fixed sphereCast length
MushAsterion Feb 4, 2023
6213685
Added vector for shapecast position
MushAsterion Feb 5, 2023
a444069
Fixed boxCast shape halfExtents
MushAsterion Feb 5, 2023
3ae5522
Fixed typo
MushAsterion Feb 5, 2023
dc69842
Renamed boxCastAll and sphereCastAll
MushAsterion Feb 5, 2023
2d8149a
Fixed sphereCast length
MushAsterion Feb 5, 2023
8134d3f
Removed boxCastAll
MushAsterion Feb 5, 2023
87451da
Removed shapetest body deactivation
MushAsterion Feb 6, 2023
d66eaaf
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 6, 2023
bf5c384
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 8, 2023
73b10a7
Rename shape casts into shapeCastAll
MushAsterion Feb 8, 2023
2fc5fa8
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 10, 2023
3df7a28
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 11, 2023
d6d4539
Changed RaycastResult into HitResult
MushAsterion Feb 11, 2023
a9ea632
Changed RaycastResult into HitResult
MushAsterion Feb 11, 2023
db8274b
Changed RaycastResult into HitResult
MushAsterion Feb 11, 2023
faf1f00
Deprecated RaycastResult
MushAsterion Feb 11, 2023
6474880
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 12, 2023
e084be1
Added shapeCastFirst functions
MushAsterion Feb 22, 2023
c344241
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 22, 2023
2759da4
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 22, 2023
cf21ed8
Changed T and C to lowercase from Test and Cast
MushAsterion Feb 22, 2023
f5d91b6
Reverted previous commit
MushAsterion Feb 22, 2023
8ca124e
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 22, 2023
00d2d67
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 24, 2023
1ccc26a
Reduced code length
MushAsterion Feb 24, 2023
a4a8305
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Mar 4, 2023
54518b1
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Mar 10, 2023
73b751c
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Mar 11, 2023
77b3ed0
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Mar 17, 2023
956fd3e
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Mar 18, 2023
377f5ae
Added support for `shapeTestFirst`
MushAsterion Mar 19, 2023
383e9ad
Resolve conflicts from source
MushAsterion Mar 21, 2023
390bac6
Merge remote-tracking branch 'upstream/main' into shapecasts
MushAsterion Mar 21, 2023
e5c7a09
Match latest features from raycasting
MushAsterion Mar 21, 2023
9802d4c
Added sorting documentation.
MushAsterion Mar 21, 2023
6bd474b
Merge remote-tracking branch 'upstream/main' into shapecasts
MushAsterion Mar 22, 2023
ea47f72
Merge remote-tracking branch 'upstream/main' into shapecasts
MushAsterion Mar 22, 2023
969ab5b
Added filtering to shape testing and casting
MushAsterion Mar 22, 2023
290a35d
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Mar 25, 2023
7c1b2c1
Merge remote-tracking branch 'upstream/main' into shapecasts
MushAsterion Apr 2, 2023
20f6377
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Apr 8, 2023
6fc799a
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Apr 13, 2023
4c70791
Fixed tags filtering issue
MushAsterion Apr 17, 2023
e6a47a3
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Apr 17, 2023
230e1fa
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Apr 18, 2023
f0ab87b
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Apr 26, 2023
79a67ee
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Apr 29, 2023
ecf1583
Merge branch 'playcanvas:main' into shapecasts
MushAsterion May 10, 2023
74f509a
Merge branch 'playcanvas:main' into shapecasts
MushAsterion May 12, 2023
f23587a
Merge branch 'playcanvas:main' into shapecasts
MushAsterion May 28, 2023
3c3fa50
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Jun 20, 2023
13a7c28
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Jun 28, 2023
fa155c4
Merge remote-tracking branch 'upstream/main' into shapecasts
MushAsterion Jul 24, 2023
f373ac7
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Jul 28, 2023
f955f64
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Jul 31, 2023
ff3906b
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Aug 10, 2023
b96694a
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Aug 17, 2023
ece932e
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Oct 16, 2023
d442376
Merge remote-tracking branch 'upstream/main' into shapecasts
MushAsterion Oct 31, 2023
a1a5473
Merge branch 'playcanvas:main' into shapecasts
MushAsterion Feb 14, 2024
8c7a842
Merge remote-tracking branch 'upstream/main' into shapecasts
MushAsterion Mar 29, 2024
2b2b643
Fixed trailing space
MushAsterion Mar 29, 2024
654794e
Merge branch 'playcanvas:main' into shapecasts
MushAsterion May 11, 2024
02e33bf
Merge branch 'playcanvas:main' into shapecasts
MushAsterion May 29, 2024
7bae7dc
Merge branch 'main' into shapecasts
MushAsterion May 30, 2024
c5e35d6
Reduced public API functions
MushAsterion May 31, 2024
ba29b58
Used Object assign instead of ...
MushAsterion May 31, 2024
8c67c9f
Fixed documentation disclaimers
MushAsterion May 31, 2024
b3f213d
Changed raycastFirst/raycastAll occurrences
MushAsterion May 31, 2024
7a642a8
Removed extra Ammo.destroy
MushAsterion May 31, 2024
972c1f7
Fixed documentation and line width
MushAsterion May 31, 2024
ec7aa86
Merge branch 'main' into shapecasts
MushAsterion May 31, 2024
bae27b7
Merge branch 'main' into shapecasts
MushAsterion Jun 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
322 changes: 320 additions & 2 deletions src/framework/components/rigid-body/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,24 @@ 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 { Mat4 } from '../../../core/math/mat4.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, BODYSTATE_DISABLE_DEACTIVATION } 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;

// RigidBody for shape tests. Permanent to save performance.
let shapeTestBody;
const shapecastPosition = new Vec3();
const shapecastRotation = new Quat();
const shapecastRotationMatrix = new Mat4();

/**
* Object holding the result of a successful raycast hit.
Expand Down Expand Up @@ -227,6 +236,42 @@ class ContactResult {
}
}

/**
* Object holding the result of a hit on an Entity.
*/
class HitResult {
MushAsterion marked this conversation as resolved.
Show resolved Hide resolved
/**
* Create a new HitResult instance.
*
* @param {import('../../entity.js').Entity} entity - The entity that was hit.
* @param {Vec3} point - The point on the entity where the hit occurred, in world space. When returned by a shapecast it is the first point found to collide, it does not have to be the closest.
* @param {Vec3} normal - The normal vector of the hit on the entity, in world space.
* @hideconstructor
*/
constructor(entity, point, normal) {
/**
* The entity that was hit.
*
* @type {import('../../entity.js').Entity}
*/
this.entity = entity;

/**
* The point on the entity where the hit occurred, in world space. When returned by a shapecast it is the first point found to collide, it does not have to be the closest.
*
* @type {Vec3}
*/
this.point = point;

/**
* The normal vector of the hit on the entity, in world space.
*
* @type {Vec3}
*/
this.normal = normal;
}
}

const _schema = ['enabled'];

/**
Expand Down Expand Up @@ -347,6 +392,10 @@ 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();

RigidBodyComponent.onLibraryLoaded();

this.contactPointPool = new ObjectPool(ContactPoint, 1);
Expand Down Expand Up @@ -552,6 +601,275 @@ class RigidBodyComponentSystem extends ComponentSystem {
return results;
}

/**
* Perform a collision check on the world and return all entities the box hits.
* It returns an array of {@link HitResult}. If no hits are
* detected, the returned array will be of length 0.
*
* @param {Vec3} halfExtents - The half-extents of the box in the x, y and z axes. Cast distance will be added to Z-Axis.
* @param {Vec3} start - The world space point where the box starts.
* @param {Vec3} end - The world space point where the box ends.
*
* @returns {HitResult[]} An array of boxCast hit results (0 length if there were no hits).
*/
boxCastAll(halfExtents, start, end) {
// Sweeping
ammoVec3.setValue(halfExtents.x, halfExtents.y, halfExtents.z + start.distance(end) / 2);

// Find rotation
shapecastRotationMatrix.setLookAt(start, end, Vec3.UP);
shapecastRotation.setFromMat4(shapecastRotationMatrix);

// Transform start vector to make it beween initial start and end.
shapecastPosition.lerp(start, end, 0.5);

return this._shapeTest(new Ammo.btBoxShape(ammoVec3), shapecastPosition, shapecastRotation);
}

/**
* Perform a collision check on the world and return all entities the sphere hits.
* It returns an array of {@link HitResult}. If no hits are
* detected, the returned array will be of length 0.
*
* @param {number} radius - The radius for the sphere.
* @param {Vec3} start - The world space point for the center of the sphere at the beginning of the cast.
* @param {Vec3} end - The world space point for the center of the sphere at the end of the cast.
*
* @returns {HitResult[]} An array of sphereCast hit results (0 length if there were no hits).
*/
sphereCastAll(radius, start, end) {
// Sweeping
const height = start.distance(end) + radius;

// Find rotation
shapecastRotationMatrix.setLookAt(start, end, Vec3.UP);
shapecastRotation.setFromMat4(shapecastRotationMatrix);

// Transform start vector to make it beween initial start and end.
shapecastPosition.lerp(start, end, 0.5);

return this._shapeTest(new Ammo.btCapsuleShapeZ(radius, height), shapecastPosition, shapecastRotation);
}

/**
* Perform a collision check on the world and return all 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.
* @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". Defaults to "box".
* @param {number} shape.radius - The radius of the sphere, capsule, cylinder or cone.
* @param {Vec3} [position] - The world space position for the shape to be.
* @param {Vec3|Quat} [rotation] - The world space rotation for the shape to have.
*
* @returns {HitResult[]} An array of shapeTest hit results (0 length if there were no hits).
*/
shapeTest(shape, position, rotation) {
switch (shape.type) {
case 'capsule':
return this.capsuleTest(shape.radius, shape.height, shape.axis, position, rotation);
case 'cone':
return this.coneTest(shape.radius, shape.height, shape.axis, position, rotation);
case 'cylinder':
return this.cylinderTest(shape.radius, shape.height, shape.axis, position, rotation);
case 'sphere':
return this.sphereTest(shape.radius, position, rotation);
default:
return this.boxTest(shape.halfExtents, position, rotation);
}
}

/**
* Perform a collision check on the world and return all entities the box hits.
* It returns an array of {@link HitResult}. If no hits are
* detected, the returned array will be of length 0.
*
* @param {Vec3} halfExtents - The half-extents of the box in the x, y and z axes.
* @param {Vec3} [position] - The world space position for the box to be.
* @param {Vec3|Quat} [rotation] - The world space rotation for the box to have.
*
* @returns {HitResult[]} An array of boxTest hit results (0 length if there were no hits).
*/
boxTest(halfExtents, position, rotation) {
ammoVec3.setValue(halfExtents.x, halfExtents.y, halfExtents.z);
return this._shapeTest(new Ammo.btBoxShape(ammoVec3), position, rotation);
}

/**
* Perform a collision check on the world and return all entities the capsule hits.
* It returns an array of {@link HitResult}. If no hits are
* detected, the returned array will be of length 0.
*
* @param {number} radius - The radius of the capsule.
* @param {number} height - The total height of the capsule from tip to tip.
* @param {number} axis - The local space axis with which the capsule's length is aligned. 0 for X, 1 for Y and 2 for Z. Defaults to 1 (Y-axis).
* @param {Vec3} [position] - The world space position for the capsule to be.
* @param {Vec3|Quat} [rotation] - The world space rotation for the capsule to have.
*
* @returns {HitResult[]} An array of capsuletest hit results (0 length if there were no hits).
*/
capsuleTest(radius, height, axis, position, rotation) {
let fn = 'btCapsuleShape';

if (axis === 0) {
MushAsterion marked this conversation as resolved.
Show resolved Hide resolved
fn = 'btCapsuleShapeX';
} else if (axis === 2) {
fn = 'btCapsuleShapeZ';
}

return this._shapeTest(new Ammo[fn](radius, height), position, rotation);
}

/**
* Perform a collision check on the world and return all entities the cone hits.
* It returns an array of {@link HitResult}. If no hits are
* detected, the returned array will be of length 0.
*
* @param {number} radius - The radius of the cone.
* @param {number} height - The total height of the cone from tip to tip.
* @param {number} axis - The local space axis with which the cone's length is aligned. 0 for X, 1 for Y and 2 for Z. Defaults to 1 (Y-axis).
* @param {Vec3} [position] - The world space position for the cone to be.
* @param {Vec3|Quat} [rotation] - The world space rotation for the cone to have.
*
* @returns {HitResult[]} An array of conetest hit results (0 length if there were no hits).
*/
coneTest(radius, height, axis, position, rotation) {
let fn = 'btConeShape';

if (axis === 0) {
fn = 'btConeShapeX';
} else if (axis === 2) {
fn = 'btConeShapeZ';
}

return this._shapeTest(new Ammo[fn](radius, height), position, rotation);
}

/**
* Perform a collision check on the world and return all entities the cylinder hits.
* It returns an array of {@link HitResult}. If no hits are
* detected, the returned array will be of length 0.
*
* @param {number} radius - The radius of the cylinder.
* @param {number} height - The total height of the cylinder from tip to tip.
* @param {number} axis - The local space axis with which the cylinder's length is aligned. 0 for X, 1 for Y and 2 for Z. Defaults to 1 (Y-axis).
* @param {Vec3} [position] - The world space position for the cylinder to be.
* @param {Vec3|Quat} [rotation] - The world space rotation for the cylinder to have.
*
* @returns {HitResult[]} An array of cylinderTest hit results (0 length if there were no hits).
*/
cylinderTest(radius, height, axis, position, rotation) {
let fn = 'btCylinderShape';

if (axis === 0) {
fn = 'btCylinderShapeX';
} else if (axis === 2) {
fn = 'btCylinderShapeZ';
}

return this._shapeTest(new Ammo[fn](radius, height), position, rotation);
}

/**
* Perform a collision check on the world and return all entities the sphere hits.
* It returns an array of {@link HitResult}. If no hits are
* detected, the returned array will be of length 0.
*
* @param {number} radius - The radius of the sphere.
* @param {Vec3} [position] - The world space position for the sphere to be.
* @param {Vec3|Quat} [rotation] - The world space rotation for the sphere to have.
*
* @returns {HitResult[]} An array of sphereTest hit results (0 length if there were no hits).
*/
sphereTest(radius, position, rotation) {
return this._shapeTest(new Ammo.btSphereShape(radius), position, rotation);
}

/**
* Perform a collision check on the world and return all 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 {Ammo.btCollisionShape} shape - The Ammo.btCollisionShape to use for collision check.
* @param {Vec3} [position] - The world space position for the shape to be.
* @param {Vec3|Quat} [rotation] - The world space rotation for the shape to have.
*
* @returns {HitResult[]} An array of shapeTest hit results (0 length if there were no hits).
* @private
*/
_shapeTest(shape, position = Vec3.ZERO, rotation = Vec3.ZERO) {
Debug.assert(Ammo.ConcreteContactResultCallback, 'pc.RigidBodyComponentSystem#_shapecast: Your version of ammo.js does not expose Ammo.ConcreteContactResultCallback. Update it to latest.');

const results = [];

// Set proper position
ammoVec3.setValue(position.x, position.y, position.z);

// Set proper rotation
if (rotation instanceof Quat) {
ammoQuat.setValue(rotation.x, rotation.y, rotation.z, rotation.w);
} else {
ammoQuat.setEulerZYX(rotation.z, rotation.y, rotation.x);
}

// Assign position and rotation to transform.
ammoTransform.setIdentity();
ammoTransform.setOrigin(ammoVec3);
ammoTransform.setRotation(ammoQuat);

// We only initialize the shapeTast body here so we don't have an extra body unless the user uses this function
if (!shapeTestBody) {
shapeTestBody = this.createBody(0, shape, ammoTransform);
}

// Make sure the body has proper shape, transform and is active.
shapeTestBody.setCollisionShape(shape);
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) {
// Retrieve manifold point.
const manifold = Ammo.wrapPointer(cp, Ammo.btManifoldPoint);

// Make sure there is a collision
if (manifold.getDistance() < 0) {
const point = manifold.get_m_positionWorldOnB();
const normal = manifold.get_m_normalWorldOnB();

// Push the result.
results.push(new HitResult(
body1.entity,
new Vec3(point.x(), point.y(), point.z()),
new Vec3(normal.x(), normal.y(), normal.z())
));
}
}
};

// Check for contacts.
this.dynamicsWorld.contactTest(shapeTestBody, resultCallback);

// Disable body and remove shape.
shapeTestBody.forceActivationState(BODYSTATE_DISABLE_DEACTIVATION);
MushAsterion marked this conversation as resolved.
Show resolved Hide resolved
shapeTestBody.setCollisionShape(null);

// Destroy unused variables for performance.
Ammo.destroy(resultCallback);
Ammo.destroy(shape);

return results;
}

/**
* Stores a collision between the entity and other in the contacts map and returns true if it
* is a new collision.
Expand Down