From 4fafab17465bcd290467f865e72e694e7b4e0838 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Wed, 12 Jun 2024 18:49:47 +0300 Subject: [PATCH 1/4] wip --- src/index.mjs | 4 + src/physics/init.mjs | 264 ++++++++++++++------- src/physics/jolt/front/shape/component.mjs | 42 ++-- src/physics/jolt/manager.mjs | 116 ++++++++- src/physics/manager.mjs | 34 +++ 5 files changed, 343 insertions(+), 117 deletions(-) diff --git a/src/index.mjs b/src/index.mjs index ff08334..f0a5547 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -7,6 +7,8 @@ import { init } from './physics/init.mjs'; +export { JoltManager, ShapeSettings } from './physics/jolt/manager.mjs'; + export { ShapeComponent } from './physics/jolt/front/shape/component.mjs'; export { ShapeComponentSystem } from './physics/jolt/front/shape/system.mjs'; @@ -46,4 +48,6 @@ export { IndexedCache } from './physics/indexed-cache.mjs'; export * from './physics/jolt/front/constraint/types/settings.mjs'; export * from './physics/jolt/constants.mjs'; +export { JoltBackend } from './physics/jolt/back/backend.mjs'; + export { init }; diff --git a/src/physics/init.mjs b/src/physics/init.mjs index 4e83cc6..4e9ed9c 100644 --- a/src/physics/init.mjs +++ b/src/physics/init.mjs @@ -1,101 +1,185 @@ import { Debug } from './jolt/debug.mjs'; import { JoltManager } from './jolt/manager.mjs'; +/** + * An options object to configure Jolt Physics backend. All options are optional. + * + * @interface + * @group Managers + */ +class JoltInitSettings { + /** + * Specifies, if {@link CommandsBuffer} can be resized, when it is full. On every frame the + * buffer starts writing from the beginning, so it has a full capacity available for it. If + * there is more data that needs to be written than the initial size specified with + * {@link commandsBufferSize}, then it will grow by `50%` of the current size and try to write + * it again. It will never shrink down, and will remain at the new size until it has to grow + * again. + * + * If this is set to `false`, the growth becomes forbidden and the commands that did not fit + * into the buffer will be ignored. Generally, 10kb is enough to move about 100-150 primitive + * objects every frame. + * + * @type {boolean} + * @defaultValue true + */ + allowCommandsBufferResize; + + /** + * Baumgarte stabilization factor (how much of the position error to "fix" in 1 update): + * - `0`: nothing + * - `1`: 100% + * + * @type {number} + * @defaultValue 0.2 (dimensionless) + */ + baumgarte; + + /** + * Maximum relative delta orientation for body pairs to be able to reuse collision results from + * last update, stored as `cos(max angle / 2)`. + * + * @type {number} + * @defaultValue 0.9998476951563912 (radians) + */ + bodyPairCacheCosMaxDeltaRotationDiv2; + + /** + * Initial size of the {@link CommandsBuffer} in bytes. + * + * @type {number} + * @defaultValue 10000 (10kb) + */ + commandsBufferSize; + + /** + * Line color of the dynamic objects during a debug draw. + * + * @type {import('playcanvas').Color} + * @defaultValue Color.YELLOW + */ + debugColorDynamic; + + /** + * Line color of the kinematic objects during a debug draw. + * + * @type {import('playcanvas').Color} + * @defaultValue Color.MAGENTA + */ + debugColorKinematic; + + /** + * Line color of the static objects during a debug draw. + * + * @type {import('playcanvas').Color} + * @defaultValue Color.GRAY + */ + debugColorStatic; + + /** + * The PlayCanvas Layer ID, where to debug draw lines. + * + * @type {number} + * @defaultValue LAYERID_IMMEDIATE + */ + debugDrawLayerId; + + /** + * If `true`, debug draw will consider scene depth, so lines that are behind a visual mesh will + * not be drawn ontop of it. + * + * @type {boolean} + * @defaultValue true + */ + debugDrawDepth; + + /** + * A fixed time intervals to update physics simulation at. Affects performance, so try to set + * it as large as possible (fewer updates per second), until you start getting collision and + * motion artifacts. Generally, `1/30` (30 updates per second) is a good option. + * + * @type {number} + * @defaultValue 1/30 + */ + fixedStep; + + /** + * Maximum number of physics updates we allow to fast-forward. For example, if we switch a + * browser tab, the main thread will pause. Once the app comes back to focus, the delta time + * from the last update will be large. The system will try to catch up the missed time by + * looping the physics update until the accumulated time is exhausted. + * + * This setting effectively clamps the missed time, so the resulting delta time is small on + * resume. For example, if we missed 1000 updates, and this setting is set to 5, then once + * application resumes, the system will run 5 updates before continuing running updates as + * usual. + * + * It is difficult to advice on a good number, as it depends on {@link fixedStep}, + * {@link subSteps}, and others. Probably 2-5 is a good range to start with. Keep it low and + * experiment with your app switching tabs. The lower this number is the better. + * + * @type {number} + * @defaultValue 3 + */ + maxSkippedSteps; + + /** + * A number of sub-steps per single {@link fixedStep}. Affects performance, so try to keep it + * low. Increasing the number of substeps will make constraints feel stronger and collisions + * more rigid. + * + * @type {number} + * @defaultValue 2 + */ + subSteps; + + /** + * If `true`, enables the use of motion states for all physical objects that support it. When + * disabled, the position of an object is updated once a physics update completes (which + * happens at {@link fixedStep} intervals). If the browser refresh rate is faster than + * {@link fixedStep}, the object will visibly "stutter" while moving. To make its motion + * smooth, you can make {@link fixedStep} smaller to match the browser refresh rate (expensive) + * or use this motion state option. + * + * When enabled, the system will interpolate the object's position and rotation, based on its + * current velocities. It effectively "guesses" the isometry of an object at the next frame, + * until the real physics update happens. + * + * @type {boolean} + * @defaultValue true + */ + useMotionStates; + + /** + * If `true`, tries to use a `Shared Array Buffer` (SAB) for the {@link CommandsBuffer}. If SAB + * is not supported, or `false` is set, then falls back to `Array Buffer`. + * + * @type {boolean} + * @defaultValue true + */ + useSharedArrayBuffer; + + /** + * If `true`, tries to run the physics backend in a Web Worker. If the Web Worker is not + * supported, or this option is set to `false`, will fall back to main thread. + * + * Note, that some features, like debug draw, are disabled, when using Web Worker. + * + * @type {boolean} + * @defaultValue false + */ + useWebWorker; +} + /** * Components initialization method. * * @param {import('playcanvas').Application} app - PlayCanvas Application instance - * @param {object} opts - An options object to configure the physics backend. All options are - * optional: - * @param {string} opts.backend - Name of the physics backend to initialize. - * - * Default: `jolt`. - * @param {boolean} opts.useSharedArrayBuffer - If `true`, tries to use a Shared Array Buffer (SAB) - * for the {@link CommandsBuffer | Commands Buffer}. If SAB is not supported, or `false` is set, - * then falls back to Array Buffer. + * @param {JoltInitSettings} opts - Jolt Physics initialization settings. + * * - * Default: `true`. - * @param {number} opts.commandsBufferSize - Initial size of the - * {@link CommandsBuffer | Commands Buffer} in bytes. - * - * Default: `10000` (10kb). - * @param {boolean} opts.allowCommandsBufferResize - Specifies, if - * {@link CommandsBuffer | Commands Buffer} can be resized, when full. On every frame the buffer - * starts writing from the beginning, so it has a full capacity available for it. If there is more - * data that needs to be written than the initial size specified with `commandsBufferSize`, then it - * will grow by 50% of the current size and try to write it again. It will never shrink down, and - * remain at the new size until it has to grow again. If this is set to `false`, the growth becomes - * forbidden and the commands that did not fit into the buffer will be ignored. Generally, 10kb is - * enough to move about 100-150 primitive objects every frame. - * - * Default: `true`. - * @param {boolean} opts.useWebWorker - If `true`, tries to run the physics backend in Web Worker. - * If Web Worker is not supported, or this option is set to `false`, will fall back to main thread. - * Note, that some features, like debug draw, are disabled, when using Web Worker. - * - * Default: `false`. - * @param {number} opts.fixedStep - A fixed time intervals to update physics simulation at. Affects - * performance, so try to set it as large as possible (fewer updates per second), until you start - * getting collision and motion artifacts. - * - * Default: `1/30` (30 updates per second). - * @param {number} opts.subSteps - A number of sub-steps in a single update. Affects - * performance, so try to keep at 1, unless you start getting collision and motion artifacts. - * Increasing the number of substeps will make constraints feel stronger and collisions "harder". - * - * Default: `1`. - * @param {boolean} opts.useMotionStates - If `true`, enables the use of motion states for all - * physical objects that support it. When disabled, the position of an object is updated once a - * physics update completes (which happens at `fixedStep` intervals). If the browser refresh rate - * is faster than `fixedStep`, the object will visibly "stutter" while moving. To make its motion - * smooth, you can make `fixedStep` smaller to match the browser refresh rate (expensive) or use - * this motion state option. When enabled, the system will interpolate the object's position and - * rotation, based on its current velocities. It effectively "guesses" the isometry of an object - * at the next frame, until the real physics update happens. * - * Default: `true`. - * @param {import('playcanvas').Color} opts.debugColorStatic - Line color of the static objects - * during a debug draw. - * - * Default: `Color.GRAY` - * @param {import('playcanvas').Color} opts.debugColorKinematic - Line color of the kinematic - * objects during a debug draw. - * - * Default: `Color.MAGENTA` - * @param {import('playcanvas').Color} opts.debugColorDynamic - Line color of the dynamic objects - * during a debug draw. - * - * Default: `Color.YELLOW` - * @param {number} opts.debugDrawLayerId - The PlayCanvas Layer ID, where to debug draw lines. - * - * Default: `LAYERID_IMMEDIATE` (alias integer) - * @param {boolean} opts.debugDrawDepth - If `true`, debug draw will consider scene depth, so lines - * that are behind a visual mesh will not be drawn ontop of it. - * - * Default: `true`. - * @param {number} opts.baumgarte - Baumgarte stabilization factor (how much of the position error - * to "fix" in 1 update) - * - 0 = nothing - * - 1 = 100% - * - * Default: `0.2` (dimensionless) - * @param {number} opts.maxSkippedSteps - Maximum number of physics updates we allow to - * fast-forward. For example, if we switch a browser tab, the main thread will pause. Once the app - * comes back to focus, the delta time from the last update will be large. The system will try to - * catch up the missed time by looping the physics update until the accumulated time is exhausted. - * This setting effectively clamps the missed time, so the resulting delta time is small on resume. - * For example, if we missed 1000 updates, and this setting is set to 5, then once application - * resumes, the system will run 5 updates before continuing running updates as usual. It is - * difficult to advice on a good number, as it depends on `fixedStep`, `subSteps`, and others. - * Probably 2-5 is a good range to start with. Keep it low and experiment your app switching tabs. - * The lower this number is the better. - * - * Default: `3`. - * @param {number} opts.bodyPairCacheCosMaxDeltaRotationDiv2 - Maximum relative delta orientation - * for body pairs to be able to reuse collision results from last update, stored as - * `cos(max angle / 2)`. - * - * Default: `0.9998476951563912` (radians). + * @param {number} opts.bodyPairCacheMaxDeltaPositionSq - Maximum relative delta position for body * pairs to be able to reuse collision results from last update. * @@ -265,7 +349,7 @@ import { JoltManager } from './jolt/manager.mjs'; function init(app, opts = {}) { const options = { backend: 'jolt', - propertyName: 'physics', // TODO: remove this, as we remove the pc namespace + propertyName: 'physics', ...opts }; diff --git a/src/physics/jolt/front/shape/component.mjs b/src/physics/jolt/front/shape/component.mjs index 7b43ada..edab014 100644 --- a/src/physics/jolt/front/shape/component.mjs +++ b/src/physics/jolt/front/shape/component.mjs @@ -18,49 +18,49 @@ const defaultHalfExtent = new Vec3(0.5, 0.5, 0.5); * @category Shape Component */ class ShapeComponent extends Component { - _shape = SHAPE_BOX; + _convexRadius = 0.05; - _index = -1; + _debugDraw = false; - _renderAsset = null; + _density = 1000; - _mesh = null; + _halfExtent = defaultHalfExtent; - _isCompoundChild = false; + _halfHeight = 0.5; - _useEntityScale = true; + _hfActiveEdgeCosThresholdAngle = 0.996195; - _debugDraw = false; + _hfBitsPerSample = 8; - _halfExtent = defaultHalfExtent; + _hfBlockSize = 2; - _radius = 0.5; + _hfOffset = Vec3.ZERO; - _convexRadius = 0.05; + _hfSampleCount = 0; - _halfHeight = 0.5; + _hfSamples = null; - _density = 1000; + _hfScale = Vec3.ONE; - _shapePosition = Vec3.ZERO; + _index = -1; - _shapeRotation = Quat.IDENTITY; + _isCompoundChild = false; _massOffset = Vec3.ZERO; - _hfSamples = null; + _mesh = null; - _hfSampleCount = 0; + _radius = 0.5; - _hfBlockSize = 2; + _renderAsset = null; - _hfBitsPerSample = 8; + _shape = SHAPE_BOX; - _hfActiveEdgeCosThresholdAngle = 0.996195; + _shapePosition = Vec3.ZERO; - _hfScale = Vec3.ONE; + _shapeRotation = Quat.IDENTITY; - _hfOffset = Vec3.ZERO; + _useEntityScale = true; /** * Internally the convex radius will be subtracted from the half extent, so the total size will diff --git a/src/physics/jolt/manager.mjs b/src/physics/jolt/manager.mjs index cf76d7a..f33dc53 100644 --- a/src/physics/jolt/manager.mjs +++ b/src/physics/jolt/manager.mjs @@ -19,6 +19,72 @@ import { OPERATOR_CREATOR, OPERATOR_MODIFIER, OPERATOR_QUERIER } from './constants.mjs'; +/** + * @interface + * @group Managers + */ +class ShapeSettings { + /** + * @see {@link ShapeComponent.density} + * @type {number} + * @defaultValue 1000 + */ + density; + + /** + * @see {@link ShapeComponent.shapePosition} + * @type {Vec3} + * @defaultValue Vec3(0, 0, 0) + */ + shapePosition; + + /** + * @see {@link ShapeComponent.shapeRotation} + * @type {Quat} + * @defaultValue Quat(0, 0, 0, 1) + */ + shapeRotation; + + /** + * Scales the shape. Uniform scale is always fine. Non-uniform scale is supported only by some + * shapes. For example: + * - you can use non-uniform scale on a box shape, but not on a sphere, etc. + * - you can use non-uniform scale on a cylinder/capsule, but `X` and `Z` must be uniform. + * + * @type {Vec3} + * @defaultValue Vec3(1, 1, 1) + */ + scale; + + /** + * @see {@link ShapeComponent.halfExtent} + * @type {Vec3} + * @defaultValue Vec3(0.5, 0.5, 0.5) + */ + halfExtent; + + /** + * @see {@link ShapeComponent.convexRadius} + * @type {number} + * @defaultValue 0.05 (m) + */ + convexRadius; + + /** + * @see {@link ShapeComponent.halfHeight} + * @type {number} + * @defaultValue 0.5 (m) + */ + halfHeight; + + /** + * @see {@link ShapeComponent.radius} + * @type {number} + * @defaultValue 0.5 (m) + */ + radius; +} + function getColor(type, config) { switch (type) { case MOTION_TYPE_STATIC: @@ -78,9 +144,14 @@ function debugDraw(app, data, config) { } } -const halfExtent = new Vec3(0.5, 0.5, 0.5); - +/** + * Jolt Manager is responsible to handle the Jolt Physics backend. + * + * @group Managers + */ class JoltManager extends PhysicsManager { + defaultHalfExtent = new Vec3(0.5, 0.5, 0.5); + constructor(app, opts, resolve) { const config = { useSharedArrayBuffer: true, @@ -155,6 +226,11 @@ class JoltManager extends PhysicsManager { this.sendUncompressed(msg); } + /** + * Sets the physics world gravity. + * + * @param {Vec3} gravity - Gravity vector. + */ set gravity(gravity) { if ($_DEBUG) { const ok = Debug.checkVec(gravity, `Invalid gravity vector`, gravity); @@ -173,10 +249,20 @@ class JoltManager extends PhysicsManager { } } + /** + * Gets the current gravity vector. + * + * @type {Vec3} + * @defaultValue Vec3(0, -9.81, 0) + */ get gravity() { return this._gravity; } + /** + * @type {IndexedCache} + * @private + */ get queryMap() { return this._queryMap; } @@ -238,18 +324,31 @@ class JoltManager extends PhysicsManager { this._backend.updateCallback = null; } + /** + * Creates a shape in the physics backend. Note, that the shape is not added to the physics + * world after it is created, so it won't affect the simulation. + * + * This is useful, when you want to use a shape for a shapecast, or want your kinematic + * character controller to change current shape (e.g. standing, sitting, laying, etc). + * + * Once you no longer need the shape, you must {@link destroyShape} to avoid memory leaks. + * + * @param {number} type - Shape type number. + * options. + * @param {ShapeSettings} [options] - Optional shape settings. + * @see {@link ShapeComponent.shape} for available shape type options. + * @returns {number} Shape index. + */ createShape(type, options = {}) { const cb = this._outBuffer; - // TODO - // expose to docs? const opts = { // defaults density: 1000, shapePosition: Vec3.ZERO, shapeRotation: Quat.IDENTITY, scale: Vec3.ONE, - halfExtent, + halfExtent: JoltManager.defaultHalfExtent, convexRadius: 0.05, halfHeight: 0.5, radius: 0.5, @@ -275,6 +374,11 @@ class JoltManager extends PhysicsManager { return index; } + /** + * Destroys a shape that was previously created with {@link createShape}. + * + * @param {number} index - Shape index number. + */ destroyShape(index) { if ($_DEBUG) { const ok = Debug.checkUint(index, `Invalid shape number: ${index}`); @@ -493,4 +597,4 @@ class JoltManager extends PhysicsManager { } } -export { JoltManager }; +export { JoltManager, ShapeSettings }; diff --git a/src/physics/manager.mjs b/src/physics/manager.mjs index 4cb280d..b768f29 100644 --- a/src/physics/manager.mjs +++ b/src/physics/manager.mjs @@ -32,30 +32,64 @@ class PhysicsManager { this._app = app; } + /** + * @param {import('./jolt/back/backend.mjs').JoltBackend} instance - Jolt backend instance. + */ set backend(instance) { this._backend = instance; } + /** + * @type {import('./jolt/back/backend.mjs').JoltBackend | null} + * @private + */ get backend() { return this._backend; } + /** + * @type {Map} + * @private + */ get systems() { return this._systems; } + /** + * Allows to pause/unpause physics update. Useful, when you have some UI popup and want to + * freeze the game world, but still be able to interact with the application. + * + * @param {boolean} bool - If `true`, will pause the physics world update. + */ set paused(bool) { this._paused = bool; } + /** + * Gets the current state of the physics world. Whether it is paused or not. + * + * @type {boolean} + * @defaultValue false + */ get paused() { return this._paused; } + /** + * @type {CommandsBuffer} + * @private + */ get commandsBuffer() { return this._outBuffer; } + /** + * @type {} + */ get config() { return this._config; } From df7788718b6da7d77ddef1c9e076e3258ba21d05 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Thu, 13 Jun 2024 18:59:42 +0300 Subject: [PATCH 2/4] add init settings, casts --- src/index.mjs | 8 +- src/physics/indexed-cache.mjs | 6 +- src/physics/init.mjs | 517 ++++++++++++------ src/physics/jolt/back/backend.mjs | 6 + src/physics/jolt/back/operators/querier.mjs | 11 +- .../front/constraint/types/constraint.mjs | 6 +- .../jolt/front/constraint/types/settings.mjs | 6 +- src/physics/jolt/front/response-handler.mjs | 2 + src/physics/jolt/manager.mjs | 228 +++++++- src/physics/manager.mjs | 34 +- 10 files changed, 637 insertions(+), 187 deletions(-) diff --git a/src/index.mjs b/src/index.mjs index f0a5547..f22089a 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -5,9 +5,11 @@ * @module PhysicsComponents */ -import { init } from './physics/init.mjs'; +import { init, JoltInitSettings } from './physics/init.mjs'; -export { JoltManager, ShapeSettings } from './physics/jolt/manager.mjs'; +export { + JoltManager, ShapeSettings, CastRaySettings, CastShapeSettings +} from './physics/jolt/manager.mjs'; export { ShapeComponent } from './physics/jolt/front/shape/component.mjs'; export { ShapeComponentSystem } from './physics/jolt/front/shape/system.mjs'; @@ -50,4 +52,4 @@ export * from './physics/jolt/constants.mjs'; export { JoltBackend } from './physics/jolt/back/backend.mjs'; -export { init }; +export { init, JoltInitSettings }; diff --git a/src/physics/indexed-cache.mjs b/src/physics/indexed-cache.mjs index e8f27e3..a2fcd18 100644 --- a/src/physics/indexed-cache.mjs +++ b/src/physics/indexed-cache.mjs @@ -15,8 +15,8 @@ class IndexedCache { /** * Store an element in the cache. Will return one of the freed indices, or a new one. * - * @param {*} element - An object or a function to store in the cache - * @returns {number} - TODO + * @param {object | function} element - An object or a function to store in the cache + * @returns {number} - Index number under which the element is stored. */ add(element) { const freed = this._freed; @@ -33,7 +33,7 @@ class IndexedCache { * Retrieves a stored element under the given index. * * @param {number} index - An index of the element to retrieve. - * @returns {*} - TODO + * @returns {object | function} - Element stored under given index. */ get(index) { return this._storage[index]; diff --git a/src/physics/init.mjs b/src/physics/init.mjs index 4e9ed9c..a8f628c 100644 --- a/src/physics/init.mjs +++ b/src/physics/init.mjs @@ -25,6 +25,17 @@ class JoltInitSettings { */ allowCommandsBufferResize; + /** + * Specifies if objects can go to sleep or not. + * + * Note, you can specify it on each object individually with + * {@link BodyComponent.allowSleeping}. + * + * @type {boolean} + * @defaultValue true + */ + allowSleeping; + /** * Baumgarte stabilization factor (how much of the position error to "fix" in 1 update): * - `0`: nothing @@ -37,13 +48,52 @@ class JoltInitSettings { /** * Maximum relative delta orientation for body pairs to be able to reuse collision results from - * last update, stored as `cos(max angle / 2)`. + * last update, stored as + * ``` + * cos(max angle / 2) + * ``` * * @type {number} * @defaultValue 0.9998476951563912 (radians) */ bodyPairCacheCosMaxDeltaRotationDiv2; + /** + * Maximum relative delta position for body pairs to be able to reuse collision results from + * last update. + * + * @type {number} + * @defaultValue Math.sqrt(0.001) (meter^2) + */ + bodyPairCacheMaxDeltaPositionSq; + + /** + * Array of unique integer numbers, representing the broadphase layers. For additional details, + * refer to Jolt's documentation: + * [Collision Detection](https://jrouwe.github.io/JoltPhysics/index.html#collision-detection). + * + * @type {Array} + * @defaultValue [BP_LAYER_NON_MOVING, BP_LAYER_MOVING] ([0, 1]) + */ + broadPhaseLayers; + + /** + * If `false`, characters will not emit contact events, which saves performance (no additional + * JS callback calls from Wasm). + * + * @type {boolean} + * @defaultValue true + */ + charContactEventsEnabled; + + /** + * When false, we prevent collision against non-active (shared) edges. + * + * @type {boolean} + * @defaultValue true + */ + checkActiveEdges; + /** * Initial size of the {@link CommandsBuffer} in bytes. * @@ -52,6 +102,58 @@ class JoltInitSettings { */ commandsBufferSize; + /** + * Whether or not to use warm starting for constraints (initially applying previous frames + * impulses). + * + * @type {boolean} + * @defaultValue true + */ + constraintWarmStart; + + /** + * If `true`, the collision result of two bodies will include the contact points. If you don't + * need the exact points of collision (e.g. if it is enough to simply know if and which bodies + * collided, but not at which point in world space), you should disable it to save some CPU work. + * + * @type {boolean} + * @defaultValue true + */ + contactPoints; + + /** + * Ignored if {@link contactPoints} is `false`. If this is `true`, then the collision result + * points will be averaged into a single point. Otherwise all points from collision manifold of + * a body pair will be provided. + * + * For example, a box resting on a floor: + * - `false`: will generate 4 contact points, one per each vertex of the box touching the + * floor. + * - `true`: will generate 1 contact point in the middle of 4 vertices of the box. + * + * @type {number} + * @defaultValue true + */ + contactPointsAveraged; + + /** + * If `false`, bodies will not emit contact events, which saves performance (no additional JS + * callback calls from Wasm). + * + * @type {boolean} + * @defaultValue true + */ + contactEventsEnabled; + + /** + * Maximum allowed distance between old and new contact point to preserve contact forces for + * warm start. + * + * @type {number} + * @defaultValue Math.sqrt(0.01) (meter^2) + */ + contactPointPreserveLambdaMaxDistSq; + /** * Line color of the dynamic objects during a debug draw. * @@ -93,6 +195,15 @@ class JoltInitSettings { */ debugDrawDepth; + /** + * Makes the simulation deterministic at the cost of performance. Simulation runs faster, if + * determinism is disabled. + * + * @type {boolean} + * @defaultValue true + */ + deterministicSimulation; + /** * A fixed time intervals to update physics simulation at. Affects performance, so try to set * it as large as possible (fewer updates per second), until you start getting collision and @@ -103,6 +214,71 @@ class JoltInitSettings { */ fixedStep; + /** + * Fraction of its inner radius a body may penetrate another body for the + * `MOTION_QUALITY_LINEAR_CAST` of a {@link BodyComponent.motionQuality}. + * + * @type {number} + * @defaultValue 0.25 + */ + linearCastMaxPenetration; + + /** + * Fraction of its inner radius a body must move per step to enable casting for the + * `MOTION_QUALITY_LINEAR_CAST` of a {@link BodyComponent.motionQuality }. + * + * @type {number} + * @defaultValue 0.75 + */ + linearCastThreshold; + + /** + * Max squared distance to use to determine if two points are on the same plane for determining + * the contact manifold between two shape faces. + * + * @type {number} + * @defaultValue 1.0e-6 (meter^2) + */ + manifoldToleranceSq; + + /** + * Array of non-unique integers, representing of one-to-one map of object layer to broadphase + * layer. Each object layer can only belong to one and only one broadphase layer. + * + * For example, an array `[0, 0, 1, 1]` means that an object layer `0` belongs to broadphase + * layer `0`, and object layer `1` belongs to broadphase layer `1`. + * + * For additional details, refer to Jolt's documentation: + * [Collision Detection](https://jrouwe.github.io/JoltPhysics/index.html#collision-detection). + * + * @type {Array} + * @defaultValue [OBJ_LAYER_NON_MOVING, BP_LAYER_NON_MOVING, OBJ_LAYER_MOVING, BP_LAYER_MOVING] + * (0, 0, 1, 1) + */ + mapObjectToBroadPhaseLayer; + + /** + * Size of body pairs array, corresponds to the maximum amount of potential body pairs that can + * be in flight at any time. Setting this to a low value will use less memory inside Wasm (no + * change on JS heap), but slow down simulation as threads may run out of narrow phase work. + * + * Note, that you would need a custom Wasm build to support multi-threading. Refer to Jolt's + * [build section](https://github.com/jrouwe/JoltPhysics.js?tab=readme-ov-file#building) for + * details. + * + * @type {number} + * @defaultValue 16384 + */ + maxInFlightBodyPairs; + + /** + * Maximum distance to correct in a single iteration when solving position constraints. + * + * @type {number} + * @defaultValue 0.2 (meters) + */ + maxPenetrationDistance; + /** * Maximum number of physics updates we allow to fast-forward. For example, if we switch a * browser tab, the main thread will pause. Once the app comes back to focus, the delta time @@ -123,6 +299,104 @@ class JoltInitSettings { */ maxSkippedSteps; + /** + * Minimal velocity needed before a collision can be elastic. + * + * @type {number} + * @defaultValue 1 (m/s) + */ + minVelocityForRestitution; + + /** + * Number of solver position iterations to run. + * + * @type {number} + * @defaultValue 2 + */ + numPositionSteps; + + /** + * Number of solver velocity iterations to run. Note that this needs to be `>= 2` in order for + * friction to work (friction is applied using the non-penetration impulse from the previous + * iteration). + * + * @type {number} + * @defaultValue 10 + */ + numVelocitySteps; + + /** + * Array of non-unique integers, representing pairs of object layers. Objects that belong to + * one of the layers in the pair are allowed to collide. For additional details, refer to + * Jolt's documentation: + * [Collision Detection](https://jrouwe.github.io/JoltPhysics/index.html#collision-detection). + * + * For example: `[0, 1, 2, 0, 2, 2]` - means three pairs of layers `[0, 1]`, `[2, 0]` and + * `[2, 2]`: + * - Objects that belong to layer `0` can collide with layer `1` and `2`. + * - Objects that belong to layer `1` can only collide with layer `0`. + * - Objects that belong to layer `2` can collide with layer `0` and with other objects in the + * same layer `2`. + * + * @type {Array} + * @defaultValue [OBJ_LAYER_NON_MOVING, OBJ_LAYER_MOVING, OBJ_LAYER_MOVING, OBJ_LAYER_MOVING] + * ([0, 1, 1, 1]) + */ + objectLayerPairs; + + /** + * How much bodies are allowed to sink into each other. + * + * @type {number} + * @defaultValue 0.02 (meters) + */ + penetrationSlop; + + /** + * Velocity of points on bounding box of object below which an object can be considered + * sleeping. + * + * @type {number} + * @defaultValue 0.03 (m/s) + */ + pointVelocitySleepThreshold; + + /** + * Radius around objects inside which speculative contact points will be detected. Note that if + * this is too big you will get ghost collisions as speculative contacts are based on the + * closest points during the collision detection step which may not be the actual closest + * points by the time the two objects hit. + * + * @type {number} + * @defaultValue 0.02 (meters) + */ + speculativeContactDistance; + + /** + * How many step listener batches are needed before spawning another job (set to + * `Number.MAX_SAFE_INTEGER`, if no parallelism is desired). + * + * Note, that you would need a custom Wasm build to support multi-threading. Refer to Jolt's + * [build section](https://github.com/jrouwe/JoltPhysics.js?tab=readme-ov-file#building) for + * details. + * + * @type {number} + * @defaultValue 1 + */ + stepListenerBatchesPerJob; + + /** + * How many PhysicsStepListeners to notify in `1` batch. + * + * Note, that you would need a custom Wasm build to support multi-threading. Refer to Jolt's + * [build section](https://github.com/jrouwe/JoltPhysics.js?tab=readme-ov-file#building) for + * details. + * + * @type {number} + * @defaultValue 8 + */ + stepListenersBatchSize; + /** * A number of sub-steps per single {@link fixedStep}. Affects performance, so try to keep it * low. Increasing the number of substeps will make constraints feel stronger and collisions @@ -133,6 +407,43 @@ class JoltInitSettings { */ subSteps; + /** + * Time before object is allowed to go to sleep. + * + * @type {number} + * @defaultValue 0.5 (seconds) + */ + timeBeforeSleep; + + /** + * Whether or not to use the body pair cache, which removes the need for narrow phase collision + * detection when orientation between two bodies didn't change. + * + * @type {boolean} + * @defaultValue true + */ + useBodyPairContactCache; + + /** + * If we split up large islands into smaller parallel batches of work (to improve performance). + * + * Note, that you would need a custom Wasm build to support multi-threading. Refer to Jolt's + * [build section](https://github.com/jrouwe/JoltPhysics.js?tab=readme-ov-file#building) for + * details. + * + * @type {boolean} + * @defaultValue true + */ + useLargeIslandSplitter; + + /** + * Whether or not to reduce manifolds with similar contact normals into one contact manifold. + * + * @type {boolean} + * @defaultValue true + */ + useManifoldReduction; + /** * If `true`, enables the use of motion states for all physical objects that support it. When * disabled, the position of an object is updated once a physics update completes (which @@ -169,182 +480,44 @@ class JoltInitSettings { * @defaultValue false */ useWebWorker; + + /** + * If `false`, vehicles will not emit contact events, which saves performance (no additional + * JS callback calls from Wasm). + * + * @type {boolean} + * @defaultValue true + */ + vehicleContactEventsEnabled; } /** - * Components initialization method. + * A helper function to initialize the physics components. * - * @param {import('playcanvas').Application} app - PlayCanvas Application instance - * @param {JoltInitSettings} opts - Jolt Physics initialization settings. + * @example + * ```js + * // load-jolt.mjs * + * import { ScriptType } from 'playcanvas'; + * import { init } from './physics.dbg.mjs'; + * + * export class LoadJolt extends ScriptType { + * async initialize() { + * await init(this.app, { + * useSharedArrayBuffer: true, + * fixedStep: 1 / 45, + * useWebWorker: true + * }); + * + * this.app.fire('physics:ready'); + * } + * } + * ``` * - * - - * @param {number} opts.bodyPairCacheMaxDeltaPositionSq - Maximum relative delta position for body - * pairs to be able to reuse collision results from last update. - * - * Default: `Math.sqrt(0.001)` (meter^2). - * @param {number} opts.contactPointPreserveLambdaMaxDistSq - Maximum allowed distance between - * old and new contact point to preserve contact forces for warm start. - * Default: `Math.sqrt(0.01)` (meter^2). - * @param {boolean} opts.deterministicSimulation - Makes the simulation deterministic at the cost - * of performance. Simulation runs faster, if determinism is disabled. - * - * Default: `true` (enabled). - * @param {number} opts.linearCastMaxPenetration - Fraction of its inner radius a body may - * penetrate another body for the `MOTION_QUALITY_LINEAR_CAST` of a - * {@link BodyComponent.motionQuality | Body Component Motion Quality}. - * - * Default: `0.25`. - * @param {number} opts.linearCastThreshold - Fraction of its inner radius a body must move per - * step to enable casting for the `MOTION_QUALITY_LINEAR_CAST` of a - * {@link BodyComponent.motionQuality | Body Component Motion Quality}. - * - * Default: `0.75`. - * @param {number} opts.manifoldToleranceSq - Max squared distance to use to determine if two - * points are on the same plane for determining the contact manifold between two shape faces. - * - * Default: `1.0e-6` (meter^2). - * @param {number} opts.maxInFlightBodyPairs - Size of body pairs array, corresponds to the maximum - * amount of potential body pairs that can be in flight at any time. Setting this to a low value - * will use less memory inside Wasm (no change on JS heap), but slow down simulation as threads may - * run out of narrow phase work. Note, that you would need a custom Wasm build to support - * multi-threading. Refer to Jolt's - * [build section](https://github.com/jrouwe/JoltPhysics.js?tab=readme-ov-file#building) for - * details. - * - * Default: `16384`. - * @param {number} opts.maxPenetrationDistance - Maximum distance to correct in a single iteration - * when solving position constraints. - * - * Default: `0.2` (meters). - * @param {number} opts.minVelocityForRestitution - Minimal velocity needed before a collision can - * be elastic. - * - * Default: `1` (m/s). - * @param {number} opts.numPositionSteps - Number of solver position iterations to run. Default: 2. - * @param {number} opts.numVelocitySteps - Number of solver velocity iterations to run. Note that - * this needs to be >= 2 in order for friction to work (friction is applied using the - * non-penetration impulse from the previous iteration). - * - * Default: `10`. - * @param {number} opts.penetrationSlop - How much bodies are allowed to sink into each other. - * - * Default: `0.02` (meters). - * @param {number} opts.pointVelocitySleepThreshold - Velocity of points on bounding box of object - * below which an object can be considered sleeping. - * - * Default: `0.03` (m/s). - * @param {number} opts.speculativeContactDistance - Radius around objects inside which speculative - * contact points will be detected. Note that if this is too big you will get ghost collisions as - * speculative contacts are based on the closest points during the collision detection step which - * may not be the actual closest points by the time the two objects hit. - * - * Default: `0.02` (meters). - * @param {number} opts.stepListenerBatchesPerJob - How many step listener batches are needed - * before spawning another job (set to Number.MAX_SAFE_INTEGER, if no parallelism is desired). - * Note, that you would need a custom Wasm build to support multi-threading. Refer to Jolt's - * [build section](https://github.com/jrouwe/JoltPhysics.js?tab=readme-ov-file#building) for - * details. - * - * Default: `1`. - * @param {number} opts.stepListenersBatchSize - How many PhysicsStepListeners to notify in 1 - * batch. Note, that you would need a custom Wasm build to support multi-threading. Refer to Jolt's - * [build section](https://github.com/jrouwe/JoltPhysics.js?tab=readme-ov-file#building) for - * details. - * - * Default: `8`. - * @param {number} opts.timeBeforeSleep - Time before object is allowed to go to sleep. - * - * Default: `0.5` (seconds). - * @param {boolean} opts.constraintWarmStart - Whether or not to use warm starting for constraints - * (initially applying previous frames impulses). - * - * Default: `true`. - * @param {boolean} opts.useBodyPairContactCache - Whether or not to use the body pair cache, which - * removes the need for narrow phase collision detection when orientation between two bodies didn't - * change. - * - * Default: `true`. - * @param {boolean} opts.useManifoldReduction - Whether or not to reduce manifolds with similar - * contact normals into one contact manifold. - * - * Default: `true`. - * @param {boolean} opts.useLargeIslandSplitter - If we split up large islands into smaller - * parallel batches of work (to improve performance). Note, that you would need a custom Wasm build - * to support multi-threading. Refer to Jolt's - * [build section](https://github.com/jrouwe/JoltPhysics.js?tab=readme-ov-file#building) for - * details. - * - * Default: `true`. - * @param {boolean} opts.allowSleeping - Specifies if objects can go to sleep or not. - * - * Default: `true`. - * @param {boolean} opts.checkActiveEdges - When false, we prevent collision against non-active - * (shared) edges. - * - * Default: `true`. - * @param {boolean} opts.charContactEventsEnabled - If `false`, characters will not emit contact - * events, which saves performance (no additional JS callback calls from Wasm). - * - * Default: `true`. - * @param {boolean} opts.vehicleContactEventsEnabled - If `false`, vehicles will not emit contact - * events, which saves performance (no additional JS callback calls from Wasm). - * - * Default: `true`. - * @param {boolean} opts.contactEventsEnabled - If `false`, bodies will not emit contact - * events, which saves performance (no additional JS callback calls from Wasm). - * - * Default: `true`. - * @param {boolean} opts.contactPoints - If `true`, the collision result of two bodies will include - * the contact points. If you don't need the exact points of collision (e.g. if it is enough to - * simply know if and which bodies collided, but not at which point in world space), you should disable it to - * save some CPU work. - * - * Default: `true`. - * @param {boolean} opts.contactPointsAveraged - Ignored if `contactPoints` is `false`. If this is - * `true`, then the collision result points will be averaged into a single point. Otherwise all - * points from collision manifold of a body pair will be provided. - * - * Default: `true`. - * @param {array} opts.broadPhaseLayers - Array of unique integer numbers, representing the - * broadphase layers. - * For additional details, refer to Jolt's documentation: - * [Collision Detection](https://jrouwe.github.io/JoltPhysics/index.html#collision-detection). - * - * Default: `[BP_LAYER_NON_MOVING, BP_LAYER_MOVING]` (alias integers), where: - * - BP_LAYER_NON_MOVING = 0; - * - BP_LAYER_MOVING = 1; - * @param {array} opts.objectLayerPairs - Array of non-unique integers, representing pairs - * of object layers. Objects that belong to one of the layers in the pair are allowed to collide. - * For additional details, refer to Jolt's documentation: - * [Collision Detection](https://jrouwe.github.io/JoltPhysics/index.html#collision-detection). - * For example: [0, 1, 2, 0, 2, 2] - means three pairs of layers [0, 1], [2, 0] and [2, 2]: - * - Objects that belong to layer 0 can collide with layer 1 and 2. - * - Objects that belong to layer 1 can only collide with layer 0. - * - Objects that belong to layer 2 can collide with layer 0 and with other objects in the same - * layer. - * - * Default: `[OBJ_LAYER_NON_MOVING, OBJ_LAYER_MOVING, OBJ_LAYER_MOVING, OBJ_LAYER_MOVING]` (alias - * integers), where: - * - OBJ_LAYER_NON_MOVING = 0; - * - OBJ_LAYER_MOVING = 1; - * @param {array} opts.mapObjectToBroadPhaseLayer - Array of non-unique integers, - * representing of one-to-one map of object layer to broadphase layer. Each object layer can only - * belong to one and only one broadphase layer. For example, an array [0, 0, 1, 1] means that an - * object layer 0 belongs to broadphase layer 0, and object layer 1 belongs to broadphase - * layer 1. - * For additional details, refer to Jolt's documentation: - * [Collision Detection](https://jrouwe.github.io/JoltPhysics/index.html#collision-detection). - * - * Default: `[OBJ_LAYER_NON_MOVING, BP_LAYER_NON_MOVING, OBJ_LAYER_MOVING, BP_LAYER_MOVING]`, - * where: - * - OBJ_LAYER_NON_MOVING = 0; - * - OBJ_LAYER_MOVING = 1; - * - BP_LAYER_NON_MOVING = 0; - * - BP_LAYER_MOVING = 1; - * @group Utilities - * @returns {Promise} - A Promise to return a Jolt Manager + * @param {import('playcanvas').Application} app - PlayCanvas Application instance + * @param {JoltInitSettings} opts - Jolt Physics initialization settings. + * @group Managers + * @returns {Promise} - A Promise to return a Jolt Manager */ function init(app, opts = {}) { const options = { @@ -389,4 +562,4 @@ function init(app, opts = {}) { }); } -export { init }; +export { init, JoltInitSettings }; diff --git a/src/physics/jolt/back/backend.mjs b/src/physics/jolt/back/backend.mjs index fbb7882..a6935d9 100644 --- a/src/physics/jolt/back/backend.mjs +++ b/src/physics/jolt/back/backend.mjs @@ -15,6 +15,12 @@ import { OPERATOR_CREATOR, OPERATOR_MODIFIER, OPERATOR_QUERIER } from '../constants.mjs'; +/** + * Jolt Backend. + * + * @group Private + * @private + */ class JoltBackend { constructor(messenger, data) { const config = { diff --git a/src/physics/jolt/back/operators/querier.mjs b/src/physics/jolt/back/operators/querier.mjs index 44c9a9d..a5fbf77 100644 --- a/src/physics/jolt/back/operators/querier.mjs +++ b/src/physics/jolt/back/operators/querier.mjs @@ -1,5 +1,6 @@ import { Debug } from '../../debug.mjs'; import { + BFM_IGNORE_BACK_FACES, BUFFER_READ_BOOL, BUFFER_READ_FLOAT32, BUFFER_READ_UINT32, BUFFER_READ_UINT8, BUFFER_WRITE_BOOL, BUFFER_WRITE_FLOAT32, BUFFER_WRITE_JOLTVEC32, BUFFER_WRITE_UINT16, BUFFER_WRITE_UINT32, CMD_CAST_RAY, CMD_CAST_SHAPE, CMD_COLLIDE_POINT, CMD_COLLIDE_SHAPE_IDX, COMPONENT_SYSTEM_MANAGER @@ -289,8 +290,14 @@ class Querier { } else { offset.Set(0, 0, 0); } - if (cb.flag) castSettings.mBackFaceModeTriangles = cb.read(BUFFER_READ_UINT8); - if (cb.flag) castSettings.mBackFaceModeConvex = cb.read(BUFFER_READ_UINT8); + if (cb.flag) { + castSettings.mBackFaceModeTriangles = cb.read(BUFFER_READ_UINT8) === BFM_IGNORE_BACK_FACES ? + Jolt.EBackFaceMode_IgnoreBackFaces : Jolt.EBackFaceMode_CollideWithBackFaces; + } + if (cb.flag) { + castSettings.mBackFaceModeConvex = cb.read(BUFFER_READ_UINT8) === BFM_IGNORE_BACK_FACES ? + Jolt.EBackFaceMode_IgnoreBackFaces : Jolt.EBackFaceMode_CollideWithBackFaces; + } if (cb.flag) castSettings.mUseShrunkenShapeAndConvexRadius = cb.read(BUFFER_READ_BOOL); if (cb.flag) castSettings.mReturnDeepestPoint = cb.read(BUFFER_READ_BOOL); diff --git a/src/physics/jolt/front/constraint/types/constraint.mjs b/src/physics/jolt/front/constraint/types/constraint.mjs index b4cb414..4b0fe58 100644 --- a/src/physics/jolt/front/constraint/types/constraint.mjs +++ b/src/physics/jolt/front/constraint/types/constraint.mjs @@ -144,7 +144,8 @@ class Constraint { /** * Override for the number of solver velocity iterations to run. If set to `0`, the constraint - * will use global default set by Physics initialization setting (TODO add link). + * will use global default set by Physics initialization setting + * {@link JoltInitSettings.numVelocitySteps}. * * @returns {number} - Velocity steps override. * @defaultValue 0 @@ -155,7 +156,8 @@ class Constraint { /** * Override for the number of solver position iterations to run. If set to `0`, the constraint - * will use global default set by Physics initialization setting (TODO add link). + * will use global default set by Physics initialization setting + * {@link JoltInitSettings.numPositionSteps}. * * @returns {number} - Positions steps override. * @defaultValue 0 diff --git a/src/physics/jolt/front/constraint/types/settings.mjs b/src/physics/jolt/front/constraint/types/settings.mjs index 60d80f7..a2c95ee 100644 --- a/src/physics/jolt/front/constraint/types/settings.mjs +++ b/src/physics/jolt/front/constraint/types/settings.mjs @@ -6,7 +6,8 @@ class ConstraintSettings { /** * Override for the number of solver position iterations to run. If set to `0`, the constraint - * will use global default set by Physics initialization setting (TODO add link). + * will use global default set by Physics initialization setting + * {@link JoltInitSettings.numPositionSteps}. * * @type {number} * @defaultValue 0 @@ -15,7 +16,8 @@ class ConstraintSettings { /** * Override for the number of solver velocity iterations to run. If set to `0`, the constraint - * will use global default set by Physics initialization setting (TODO add link). + * will use global default set by Physics initialization setting + * {@link JoltInitSettings.numVelocitySteps}. * * @type {number} * @defaultValue 0 diff --git a/src/physics/jolt/front/response-handler.mjs b/src/physics/jolt/front/response-handler.mjs index e74ce8a..6820aab 100644 --- a/src/physics/jolt/front/response-handler.mjs +++ b/src/physics/jolt/front/response-handler.mjs @@ -180,6 +180,8 @@ class ResponseHandler { const firstOnly = cb.read(BUFFER_READ_BOOL); const hitsCount = cb.read(BUFFER_READ_UINT16); + // TODO + // always use array let result = firstOnly ? null : []; for (let i = 0; i < hitsCount; i++) { diff --git a/src/physics/jolt/manager.mjs b/src/physics/jolt/manager.mjs index f33dc53..16a7412 100644 --- a/src/physics/jolt/manager.mjs +++ b/src/physics/jolt/manager.mjs @@ -19,6 +19,143 @@ import { OPERATOR_CREATOR, OPERATOR_MODIFIER, OPERATOR_QUERIER } from './constants.mjs'; +/** + * @interface + * @group Managers + */ +class CastSettings { + /** + * Whether to return only the first contact. + * + * @type {boolean} + * @defaultValue true + */ + firstOnly; + + /** + * If `true`, will calculate and add to results a contact normal at contact point. + * + * @type {boolean} + * @defaultValue false + */ + calculateNormal; + + /** + * If `true`, the ray will ignore sensors. + * + * @type {boolean} + * @defaultValue false + */ + ignoreSensors; + + /** + * Broadphase layer number for filtering. + * + * @type {number} + * @defaultValue BP_LAYER_MOVING (1) + */ + bpFilterLayer; + + /** + * Object layer number for filtering. + * + * @type {number} + * @defaultValue OBJ_LAYER_MOVING (1) + */ + objFilterLayer; +} + + +/** + * @interface + * @group Managers + */ +class CastRaySettings extends CastSettings { + /** + * If `true`, the ray will ignore shape backfaces. + * + * @type {boolean} + * @defaultValue true + */ + ignoreBackFaces; + + /** + * If `true`, the convex shapes will be treated as "solid". That is, if a ray starts from + * inside a convex shape, it will report a contact. + * + * @type {boolean} + * @defaultValue true + */ + treatConvexAsSolid; +} + +/** + * @interface + * @group Managers + */ +class CastShapeSettings extends CastSettings { + /** + * Scales the shape used during a cast. Allows to re-use existing shapes, if only scale is + * different. + * + * @type {Vec3} + * @defaultValue Vec3(1, 1, 1) + */ + scale; + + /** + * All hit results will be returned relative to this offset, can be zero to get results in + * world position, but when you're testing far from the origin you get better precision by + * picking a position that's closer since floats are most accurate near the origin. + * + * @type {Vec3} + * @defaultValue Vec3(0, 0, 0) + */ + offset; + + /** + * Sets whether to ignore triangle backfaces. Following options available: + * ``` + * BFM_IGNORE_BACK_FACES + * ``` + * ``` + * BFM_COLLIDE_BACK_FACES + * ``` + * + * @type {number} + * @defaultValue BFM_IGNORE_BACK_FACES + */ + backFaceModeTriangles; + + /** + * Sets whether to ignore backfaces of convex shapes. See {@link backFaceModeTriangles} for + * available options. + * + * @type {number} + * @defaultValue BFM_IGNORE_BACK_FACES + */ + backFaceModeConvex; + + /** + * Indicates if we want to shrink the shape by the convex radius and then expand it again. This + * speeds up collision detection and gives a more accurate normal at the cost of a more + * "rounded" shape. + * + * @type {boolean} + * @defaultValue false + */ + useShrunkenShapeAndConvexRadius; + + /** + * When true, and the shape is intersecting at the beginning of the cast (fraction = 0) then + * this will calculate the deepest penetration point (costing additional CPU time). + * + * @type {boolean} + * @defaultValue false + */ + returnDeepestPoint; +} + /** * @interface * @group Managers @@ -302,6 +439,17 @@ class JoltManager extends PhysicsManager { } } + /** + * Sometimes it is useful to have a callback right before the physics world steps. You can set + * such a callback function via this method. + * + * Your given callback will be called after all commands have been executed and right before + * we update virtual kinematic characters and step the physics world. + * + * Note, this feature is disabled, when the backend runs in a Web Worker. + * + * @param {function} func - Callback function to execute before stepping the physics world. + */ addUpdateCallback(func) { if (this._config.useWebWorker) { if ($_DEBUG) { @@ -313,6 +461,9 @@ class JoltManager extends PhysicsManager { this._backend.updateCallback = func; } + /** + * Removes a callback that was set via {@link addUpdateCallback}. + */ removeUpdateCallback() { if (this._config.useWebWorker) { if ($_DEBUG) { @@ -395,6 +546,20 @@ class JoltManager extends PhysicsManager { this._shapeMap.free(index); } + /** + * Allows to create collision groups. Note, that collision groups are more expensive than + * broadphase layers. + * + * The groups are created by giving an array of numbers, where each number represents the count + * of subgroups in it. For example: + * - `[3, 5]` would create `2` groups. The first group will have `3` subgroups, and the second + * one will have `5`. + * + * For additional information, refer to Jolt's official documentation on + * [Collision Filtering](https://jrouwe.github.io/JoltPhysics/index.html#collision-filtering). + * + * @param {Array} groups - Collision groups. + */ createCollisionGroups(groups) { const cb = this._outBuffer; const groupsCount = groups.length; @@ -409,6 +574,14 @@ class JoltManager extends PhysicsManager { } } + /** + * Toggles a collision between 2 subgroups inside a group. + * + * @param {number} group - Group index number. + * @param {number} subGroup1 - First subgroup number. + * @param {number} subGroup2 - Second subgroup number. + * @param {boolean} enable - `true` to enable, `false` to disable collision. + */ toggleGroupPair(group, subGroup1, subGroup2, enable) { if ($_DEBUG) { let ok = Debug.checkUint(group, `Invalid group 1: ${group}`); @@ -430,6 +603,29 @@ class JoltManager extends PhysicsManager { cb.write(subGroup2, BUFFER_WRITE_UINT16, false); } + /** + * Creates a raycast query to the physics world. + * + * @example + * ``` + * function onResults(results) { + * if (results.length === 0) { + * return; + * } + * // do something with results + * } + * + * // Cast a 10 meters ray from (0, 5, 0) straight down. + * const origin = new Vec3(0, 5, 0); + * const dir = new Vec3(0, -10, 0); + * app.physics.castRay(origin, dir, onResults, { firstOnly: false }); + * ``` + * + * @param {Vec3} origin - World point where the ray originates from. + * @param {Vec3} dir - Non-normalized ray direction. The magnitude is ray's distance. + * @param {function} callback - Your function that will accept the raycast result. + * @param {CastRaySettings} [opts] - Settings object to customize the query. + */ castRay(origin, dir, callback, opts) { if ($_DEBUG) { let ok = Debug.checkVec(origin, `Invalid origin vector`); @@ -481,6 +677,36 @@ class JoltManager extends PhysicsManager { cb.write(opts?.objFilterLayer, BUFFER_WRITE_UINT32); } + /** + * Creates a shapecast query to the physics world. + * + * @example + * ``` + * import { SHAPE_SPHERE } from './physics.dbg.mjs'; + * + * function onResults(results) { + * if (results.length === 0) { + * return; + * } + * // do something with results + * } + * + * // Do a 10 meters cast with a 0.3 radius sphere from (0, 5, 0) straight down. + * const shapeIndex = app.physics.createShape(SHAPE_SPHERE, { radius: 0.3 }); + * const pos = new Vec3(0, 5, 0); + * const dir = new Vec3(0, -10, 0); + * app.physics.castShape(shapeIndex, pos, Quat.IDENTITY, dir, onResults, { + * ignoreSensors: true + * }); + * ``` + * + * @param {number} shapeIndex - Shape index number. Create one using {@link createShape}. + * @param {Vec3} pos - World point where the cast is originated from. + * @param {Quat} rot - Shape rotation. + * @param {Vec3} dir - Non-normalized ray direction. The magnitude is ray's distance. + * @param {function} callback - Your function that will accept the shapecast result. + * @param {CastShapeSettings} [opts] - Settings object to customize the query. + */ castShape(shapeIndex, pos, rot, dir, callback, opts) { if ($_DEBUG) { let ok = Debug.checkInt(shapeIndex, `Invalid shape index`); @@ -597,4 +823,4 @@ class JoltManager extends PhysicsManager { } } -export { JoltManager, ShapeSettings }; +export { JoltManager, ShapeSettings, CastRaySettings, CastShapeSettings }; diff --git a/src/physics/manager.mjs b/src/physics/manager.mjs index b768f29..c5f5ddf 100644 --- a/src/physics/manager.mjs +++ b/src/physics/manager.mjs @@ -40,8 +40,24 @@ class PhysicsManager { } /** + * Gets the Jolt Backend instance. This is useful, when components are not sufficient and you + * wish to access Jolt's API directly. + * + * Note, this will be `null`, if the backend runs in a Web Worker. + * + * @example + * ```js + * const backend = app.physics.backend; + * const Jolt = backend.Jolt; + * const joltVec = new Jolt.Vec3(0, 0, 0); + * + * // common Jolt interfaces, which the backend has instantiated + * backend.physicsSystem; + * backend.bodyInterface; + * backend.joltInterface; + * ``` + * * @type {import('./jolt/back/backend.mjs').JoltBackend | null} - * @private */ get backend() { return this._backend; @@ -88,16 +104,30 @@ class PhysicsManager { } /** - * @type {} + * @type {import('./init.mjs').JoltInitSettings} + * @private */ get config() { return this._config; } + /** + * Gets the number of times the physics world has been updated (steps count). + * + * @type {number} + * @defaultValue 0 + */ get steps() { return this._steps; } + /** + * Gets the fixed timestep size that the physics world is stepping with. This was set via + * {@link JoltInitSettings.fixedStep}. + * + * @type {number} + * @defaultValue 1/30 + */ get fixedStep() { return this._fixedStep; } From a7f1fc8e3e0ec1e48a67840b79cd9adb97c871b2 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Fri, 14 Jun 2024 11:58:24 +0300 Subject: [PATCH 3/4] jolt manager --- src/index.mjs | 9 +- src/physics/init.mjs | 14 +- src/physics/jolt/back/operators/querier.mjs | 2 +- src/physics/jolt/manager.mjs | 424 +++++++------------- src/physics/jolt/settings.mjs | 261 ++++++++++++ src/physics/manager.mjs | 4 +- 6 files changed, 420 insertions(+), 294 deletions(-) create mode 100644 src/physics/jolt/settings.mjs diff --git a/src/index.mjs b/src/index.mjs index f22089a..4976005 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -5,11 +5,8 @@ * @module PhysicsComponents */ -import { init, JoltInitSettings } from './physics/init.mjs'; - -export { - JoltManager, ShapeSettings, CastRaySettings, CastShapeSettings -} from './physics/jolt/manager.mjs'; +export { JoltManager } from './physics/jolt/manager.mjs'; +export * from './physics/jolt/settings.mjs'; export { ShapeComponent } from './physics/jolt/front/shape/component.mjs'; export { ShapeComponentSystem } from './physics/jolt/front/shape/system.mjs'; @@ -52,4 +49,4 @@ export * from './physics/jolt/constants.mjs'; export { JoltBackend } from './physics/jolt/back/backend.mjs'; -export { init, JoltInitSettings }; +export { init, JoltInitSettings } from './physics/init.mjs'; diff --git a/src/physics/init.mjs b/src/physics/init.mjs index a8f628c..3ee9bf3 100644 --- a/src/physics/init.mjs +++ b/src/physics/init.mjs @@ -6,6 +6,7 @@ import { JoltManager } from './jolt/manager.mjs'; * * @interface * @group Managers + * @category Utilities */ class JoltInitSettings { /** @@ -203,7 +204,7 @@ class JoltInitSettings { * @defaultValue true */ deterministicSimulation; - + /** * A fixed time intervals to update physics simulation at. Affects performance, so try to set * it as large as possible (fewer updates per second), until you start getting collision and @@ -278,7 +279,7 @@ class JoltInitSettings { * @defaultValue 0.2 (meters) */ maxPenetrationDistance; - + /** * Maximum number of physics updates we allow to fast-forward. For example, if we switch a * browser tab, the main thread will pause. Once the app comes back to focus, the delta time @@ -396,7 +397,7 @@ class JoltInitSettings { * @defaultValue 8 */ stepListenersBatchSize; - + /** * A number of sub-steps per single {@link fixedStep}. Affects performance, so try to keep it * low. Increasing the number of substeps will make constraints feel stronger and collisions @@ -451,7 +452,7 @@ class JoltInitSettings { * {@link fixedStep}, the object will visibly "stutter" while moving. To make its motion * smooth, you can make {@link fixedStep} smaller to match the browser refresh rate (expensive) * or use this motion state option. - * + * * When enabled, the system will interpolate the object's position and rotation, based on its * current velocities. It effectively "guesses" the isometry of an object at the next frame, * until the real physics update happens. @@ -497,10 +498,10 @@ class JoltInitSettings { * @example * ```js * // load-jolt.mjs - * + * * import { ScriptType } from 'playcanvas'; * import { init } from './physics.dbg.mjs'; - * + * * export class LoadJolt extends ScriptType { * async initialize() { * await init(this.app, { @@ -517,6 +518,7 @@ class JoltInitSettings { * @param {import('playcanvas').Application} app - PlayCanvas Application instance * @param {JoltInitSettings} opts - Jolt Physics initialization settings. * @group Managers + * @category Utilities * @returns {Promise} - A Promise to return a Jolt Manager */ function init(app, opts = {}) { diff --git a/src/physics/jolt/back/operators/querier.mjs b/src/physics/jolt/back/operators/querier.mjs index a5fbf77..f8a2171 100644 --- a/src/physics/jolt/back/operators/querier.mjs +++ b/src/physics/jolt/back/operators/querier.mjs @@ -296,7 +296,7 @@ class Querier { } if (cb.flag) { castSettings.mBackFaceModeConvex = cb.read(BUFFER_READ_UINT8) === BFM_IGNORE_BACK_FACES ? - Jolt.EBackFaceMode_IgnoreBackFaces : Jolt.EBackFaceMode_CollideWithBackFaces; + Jolt.EBackFaceMode_IgnoreBackFaces : Jolt.EBackFaceMode_CollideWithBackFaces; } if (cb.flag) castSettings.mUseShrunkenShapeAndConvexRadius = cb.read(BUFFER_READ_BOOL); if (cb.flag) castSettings.mReturnDeepestPoint = cb.read(BUFFER_READ_BOOL); diff --git a/src/physics/jolt/manager.mjs b/src/physics/jolt/manager.mjs index 16a7412..57d2131 100644 --- a/src/physics/jolt/manager.mjs +++ b/src/physics/jolt/manager.mjs @@ -19,209 +19,6 @@ import { OPERATOR_CREATOR, OPERATOR_MODIFIER, OPERATOR_QUERIER } from './constants.mjs'; -/** - * @interface - * @group Managers - */ -class CastSettings { - /** - * Whether to return only the first contact. - * - * @type {boolean} - * @defaultValue true - */ - firstOnly; - - /** - * If `true`, will calculate and add to results a contact normal at contact point. - * - * @type {boolean} - * @defaultValue false - */ - calculateNormal; - - /** - * If `true`, the ray will ignore sensors. - * - * @type {boolean} - * @defaultValue false - */ - ignoreSensors; - - /** - * Broadphase layer number for filtering. - * - * @type {number} - * @defaultValue BP_LAYER_MOVING (1) - */ - bpFilterLayer; - - /** - * Object layer number for filtering. - * - * @type {number} - * @defaultValue OBJ_LAYER_MOVING (1) - */ - objFilterLayer; -} - - -/** - * @interface - * @group Managers - */ -class CastRaySettings extends CastSettings { - /** - * If `true`, the ray will ignore shape backfaces. - * - * @type {boolean} - * @defaultValue true - */ - ignoreBackFaces; - - /** - * If `true`, the convex shapes will be treated as "solid". That is, if a ray starts from - * inside a convex shape, it will report a contact. - * - * @type {boolean} - * @defaultValue true - */ - treatConvexAsSolid; -} - -/** - * @interface - * @group Managers - */ -class CastShapeSettings extends CastSettings { - /** - * Scales the shape used during a cast. Allows to re-use existing shapes, if only scale is - * different. - * - * @type {Vec3} - * @defaultValue Vec3(1, 1, 1) - */ - scale; - - /** - * All hit results will be returned relative to this offset, can be zero to get results in - * world position, but when you're testing far from the origin you get better precision by - * picking a position that's closer since floats are most accurate near the origin. - * - * @type {Vec3} - * @defaultValue Vec3(0, 0, 0) - */ - offset; - - /** - * Sets whether to ignore triangle backfaces. Following options available: - * ``` - * BFM_IGNORE_BACK_FACES - * ``` - * ``` - * BFM_COLLIDE_BACK_FACES - * ``` - * - * @type {number} - * @defaultValue BFM_IGNORE_BACK_FACES - */ - backFaceModeTriangles; - - /** - * Sets whether to ignore backfaces of convex shapes. See {@link backFaceModeTriangles} for - * available options. - * - * @type {number} - * @defaultValue BFM_IGNORE_BACK_FACES - */ - backFaceModeConvex; - - /** - * Indicates if we want to shrink the shape by the convex radius and then expand it again. This - * speeds up collision detection and gives a more accurate normal at the cost of a more - * "rounded" shape. - * - * @type {boolean} - * @defaultValue false - */ - useShrunkenShapeAndConvexRadius; - - /** - * When true, and the shape is intersecting at the beginning of the cast (fraction = 0) then - * this will calculate the deepest penetration point (costing additional CPU time). - * - * @type {boolean} - * @defaultValue false - */ - returnDeepestPoint; -} - -/** - * @interface - * @group Managers - */ -class ShapeSettings { - /** - * @see {@link ShapeComponent.density} - * @type {number} - * @defaultValue 1000 - */ - density; - - /** - * @see {@link ShapeComponent.shapePosition} - * @type {Vec3} - * @defaultValue Vec3(0, 0, 0) - */ - shapePosition; - - /** - * @see {@link ShapeComponent.shapeRotation} - * @type {Quat} - * @defaultValue Quat(0, 0, 0, 1) - */ - shapeRotation; - - /** - * Scales the shape. Uniform scale is always fine. Non-uniform scale is supported only by some - * shapes. For example: - * - you can use non-uniform scale on a box shape, but not on a sphere, etc. - * - you can use non-uniform scale on a cylinder/capsule, but `X` and `Z` must be uniform. - * - * @type {Vec3} - * @defaultValue Vec3(1, 1, 1) - */ - scale; - - /** - * @see {@link ShapeComponent.halfExtent} - * @type {Vec3} - * @defaultValue Vec3(0.5, 0.5, 0.5) - */ - halfExtent; - - /** - * @see {@link ShapeComponent.convexRadius} - * @type {number} - * @defaultValue 0.05 (m) - */ - convexRadius; - - /** - * @see {@link ShapeComponent.halfHeight} - * @type {number} - * @defaultValue 0.5 (m) - */ - halfHeight; - - /** - * @see {@link ShapeComponent.radius} - * @type {number} - * @defaultValue 0.5 (m) - */ - radius; -} - function getColor(type, config) { switch (type) { case MOTION_TYPE_STATIC: @@ -285,6 +82,7 @@ function debugDraw(app, data, config) { * Jolt Manager is responsible to handle the Jolt Physics backend. * * @group Managers + * @category Jolt */ class JoltManager extends PhysicsManager { defaultHalfExtent = new Vec3(0.5, 0.5, 0.5); @@ -447,7 +245,7 @@ class JoltManager extends PhysicsManager { * we update virtual kinematic characters and step the physics world. * * Note, this feature is disabled, when the backend runs in a Web Worker. - * + * * @param {function} func - Callback function to execute before stepping the physics world. */ addUpdateCallback(func) { @@ -484,6 +282,17 @@ class JoltManager extends PhysicsManager { * * Once you no longer need the shape, you must {@link destroyShape} to avoid memory leaks. * + * @example + * ```js + * import { SHAPE_CAPSULE } from './physics.dbg.mjs'; + * + * // create a 2m high and 0.6m wide capsule. + * const shapeIndex = app.physics.createShape(SHAPE_CAPSULE, { + * halfHeight: 1, + * radius: 0.3 + * }); + * ``` + * * @param {number} type - Shape type number. * options. * @param {ShapeSettings} [options] - Optional shape settings. @@ -546,85 +355,28 @@ class JoltManager extends PhysicsManager { this._shapeMap.free(index); } - /** - * Allows to create collision groups. Note, that collision groups are more expensive than - * broadphase layers. - * - * The groups are created by giving an array of numbers, where each number represents the count - * of subgroups in it. For example: - * - `[3, 5]` would create `2` groups. The first group will have `3` subgroups, and the second - * one will have `5`. - * - * For additional information, refer to Jolt's official documentation on - * [Collision Filtering](https://jrouwe.github.io/JoltPhysics/index.html#collision-filtering). - * - * @param {Array} groups - Collision groups. - */ - createCollisionGroups(groups) { - const cb = this._outBuffer; - const groupsCount = groups.length; - - cb.writeOperator(OPERATOR_CREATOR); - cb.writeCommand(CMD_CREATE_GROUPS); - cb.write(groupsCount, BUFFER_WRITE_UINT32, false); - - for (let i = 0; i < groupsCount; i++) { - // sub groups count - cb.write(groups[i], BUFFER_WRITE_UINT32, false); - } - } - - /** - * Toggles a collision between 2 subgroups inside a group. - * - * @param {number} group - Group index number. - * @param {number} subGroup1 - First subgroup number. - * @param {number} subGroup2 - Second subgroup number. - * @param {boolean} enable - `true` to enable, `false` to disable collision. - */ - toggleGroupPair(group, subGroup1, subGroup2, enable) { - if ($_DEBUG) { - let ok = Debug.checkUint(group, `Invalid group 1: ${group}`); - ok = ok && Debug.checkUint(subGroup1, `Invalid group 1: ${subGroup1}`); - ok = ok && Debug.checkUint(subGroup2, `Invalid group 2: ${subGroup2}`); - ok = ok && Debug.checkBool(enable, `Invalid toggle flag: ${enable}`); - if (!ok) { - return; - } - } - - const cb = this._outBuffer; - - cb.writeOperator(OPERATOR_MODIFIER); - cb.writeCommand(CMD_TOGGLE_GROUP_PAIR); - cb.write(enable, BUFFER_WRITE_BOOL, false); - cb.write(group, BUFFER_WRITE_UINT16, false); - cb.write(subGroup1, BUFFER_WRITE_UINT16, false); - cb.write(subGroup2, BUFFER_WRITE_UINT16, false); - } - /** * Creates a raycast query to the physics world. - * + * * @example * ``` + * // Cast a 10 meters ray from (0, 5, 0) straight down. + * const origin = new Vec3(0, 5, 0); + * const dir = new Vec3(0, -10, 0); + * app.physics.castRay(origin, dir, onResults, { firstOnly: false }); + * * function onResults(results) { * if (results.length === 0) { * return; * } * // do something with results * } - * - * // Cast a 10 meters ray from (0, 5, 0) straight down. - * const origin = new Vec3(0, 5, 0); - * const dir = new Vec3(0, -10, 0); - * app.physics.castRay(origin, dir, onResults, { firstOnly: false }); * ``` - * + * * @param {Vec3} origin - World point where the ray originates from. * @param {Vec3} dir - Non-normalized ray direction. The magnitude is ray's distance. * @param {function} callback - Your function that will accept the raycast result. - * @param {CastRaySettings} [opts] - Settings object to customize the query. + * @param {import('./settings.mjs').CastRaySettings} [opts] - Settings object to customize the query. */ castRay(origin, dir, callback, opts) { if ($_DEBUG) { @@ -683,13 +435,6 @@ class JoltManager extends PhysicsManager { * @example * ``` * import { SHAPE_SPHERE } from './physics.dbg.mjs'; - * - * function onResults(results) { - * if (results.length === 0) { - * return; - * } - * // do something with results - * } * * // Do a 10 meters cast with a 0.3 radius sphere from (0, 5, 0) straight down. * const shapeIndex = app.physics.createShape(SHAPE_SPHERE, { radius: 0.3 }); @@ -698,14 +443,21 @@ class JoltManager extends PhysicsManager { * app.physics.castShape(shapeIndex, pos, Quat.IDENTITY, dir, onResults, { * ignoreSensors: true * }); + * + * function onResults(results) { + * if (results.length === 0) { + * return; + * } + * // do something with results + * } * ``` * * @param {number} shapeIndex - Shape index number. Create one using {@link createShape}. * @param {Vec3} pos - World point where the cast is originated from. * @param {Quat} rot - Shape rotation. - * @param {Vec3} dir - Non-normalized ray direction. The magnitude is ray's distance. + * @param {Vec3} dir - Non-normalized ray direction. The magnitude is ray's distance. * @param {function} callback - Your function that will accept the shapecast result. - * @param {CastShapeSettings} [opts] - Settings object to customize the query. + * @param {import('./settings.mjs').CastShapeSettings} [opts] - Settings object to customize the query. */ castShape(shapeIndex, pos, rot, dir, callback, opts) { if ($_DEBUG) { @@ -748,6 +500,28 @@ class JoltManager extends PhysicsManager { cb.write(opts?.objFilterLayer, BUFFER_WRITE_UINT32); } + /** + * Check if a world point is inside any body shape. For this test all shapes are treated as if + * they were solid. For a mesh shape, this test will only provide sensible information if the + * mesh is a closed manifold. + * + * @example + * ```js + * // get all entities that overlap a world position (0, 5, 0) + * app.physics.collidePoint(new Vec3(0, 5, 0), onResults, { ignoreSensors: true }); + * + * function onResults(results) { + * if (results.length === 0) { + * return; + * } + * // do something with results + * } + * ``` + * + * @param {Vec3} point - World position to test. + * @param {function} callback - Function to take the query results. + * @param {import('./settings.mjs').QuerySettings} opts - Query customization settings. + */ collidePoint(point, callback, opts) { if ($_DEBUG) { let ok = Debug.checkVec(point, `Invalid point vector`); @@ -774,6 +548,37 @@ class JoltManager extends PhysicsManager { cb.write(point, BUFFER_WRITE_VEC32, false); } + /** + * Gets all entities that collide with a given shape. + * + * @example + * ```js + * import { SHAPE_BOX } from './physics.dbg.mjs'; + * + * // create a box with a half extent (1, 1, 1) meters + * const shapeIndex = app.physics.createShape(SHAPE_BOX, { halfExtent: Vec3.ONE }); + * + * // get all entities that intersect a box with half extent (0.2, 0.5, 0.2) at world position + * // (0, 10, 0) + * const scale = new Vec3(0.2, 0.5, 0.2); + * const pos = new Vec3(0, 10, 0); + * app.physics.collideShape(shapeIndex, pos, Quat.IDENTITY, onResults, { scale }); + * + * function onResults(results) { + * if (results.length === 0) { + * return; + * } + * // do something with the results + * } + * + * ``` + * + * @param {number} shapeIndex - Shape index created with {@link createShape}. + * @param {Vec3} position - World position of the shape. + * @param {Vec3} rotation - World rotation of the shape. + * @param {function} callback - Callback function that will take the query results. + * @param {import('./settings.mjs').CollideShapeSettings} opts - Query customization settings. + */ collideShape(shapeIndex, position, rotation, callback, opts) { if ($_DEBUG) { let ok = Debug.checkInt(shapeIndex, `Invalid shape index`); @@ -821,6 +626,67 @@ class JoltManager extends PhysicsManager { cb.write(opts?.bpFilterLayer, BUFFER_WRITE_UINT32); cb.write(opts?.objFilterLayer, BUFFER_WRITE_UINT32); } + + /** + * Allows to create collision groups. Note, that collision groups are more expensive than + * broadphase layers. + * + * The groups are created by giving an array of numbers, where each element adds a group and + * its number represents the count of subgroups in it. + * + * @example + * ```js + * // Create `2` groups. The first group has `3` subgroups, the second `5`. + * app.physics.createGroups([3, 5]); + * ``` + * + * For additional information, refer to Jolt's official documentation on + * [Collision Filtering](https://jrouwe.github.io/JoltPhysics/index.html#collision-filtering). + * + * @param {Array} groups - Collision groups. + */ + createCollisionGroups(groups) { + const cb = this._outBuffer; + const groupsCount = groups.length; + + cb.writeOperator(OPERATOR_CREATOR); + cb.writeCommand(CMD_CREATE_GROUPS); + cb.write(groupsCount, BUFFER_WRITE_UINT32, false); + + for (let i = 0; i < groupsCount; i++) { + // sub groups count + cb.write(groups[i], BUFFER_WRITE_UINT32, false); + } + } + + /** + * Toggles a collision between 2 subgroups inside a group. + * + * @param {number} group - Group index number. + * @param {number} subGroup1 - First subgroup number. + * @param {number} subGroup2 - Second subgroup number. + * @param {boolean} enable - `true` to enable, `false` to disable collision. + */ + toggleGroupPair(group, subGroup1, subGroup2, enable) { + if ($_DEBUG) { + let ok = Debug.checkUint(group, `Invalid group 1: ${group}`); + ok = ok && Debug.checkUint(subGroup1, `Invalid group 1: ${subGroup1}`); + ok = ok && Debug.checkUint(subGroup2, `Invalid group 2: ${subGroup2}`); + ok = ok && Debug.checkBool(enable, `Invalid toggle flag: ${enable}`); + if (!ok) { + return; + } + } + + const cb = this._outBuffer; + + cb.writeOperator(OPERATOR_MODIFIER); + cb.writeCommand(CMD_TOGGLE_GROUP_PAIR); + cb.write(enable, BUFFER_WRITE_BOOL, false); + cb.write(group, BUFFER_WRITE_UINT16, false); + cb.write(subGroup1, BUFFER_WRITE_UINT16, false); + cb.write(subGroup2, BUFFER_WRITE_UINT16, false); + } } -export { JoltManager, ShapeSettings, CastRaySettings, CastShapeSettings }; +export { JoltManager }; diff --git a/src/physics/jolt/settings.mjs b/src/physics/jolt/settings.mjs new file mode 100644 index 0000000..5a4b1e4 --- /dev/null +++ b/src/physics/jolt/settings.mjs @@ -0,0 +1,261 @@ +/** + * @interface + * @group Managers + * @category Utilities + */ +class QuerySettings { + /** + * If `true`, the ray will ignore sensors. + * + * @type {boolean} + * @defaultValue false + */ + ignoreSensors; + + /** + * Broadphase layer number for filtering. + * + * @type {number} + * @defaultValue BP_LAYER_MOVING (1) + */ + bpFilterLayer; + + /** + * Object layer number for filtering. + * + * @type {number} + * @defaultValue OBJ_LAYER_MOVING (1) + */ + objFilterLayer; +} + +/** + * @interface + * @group Managers + * @category Utilities + */ +class CastSettings extends QuerySettings { + /** + * Whether to return only the first contact. + * + * @type {boolean} + * @defaultValue true + */ + firstOnly; + + /** + * If `true`, will calculate and add to results a contact normal at contact point. + * + * @type {boolean} + * @defaultValue false + */ + calculateNormal; +} + + +/** + * @interface + * @group Managers + * @category Utilities + */ +class CastRaySettings extends CastSettings { + /** + * If `true`, the ray will ignore shape backfaces. + * + * @type {boolean} + * @defaultValue true + */ + ignoreBackFaces; + + /** + * If `true`, the convex shapes will be treated as "solid". That is, if a ray starts from + * inside a convex shape, it will report a contact. + * + * @type {boolean} + * @defaultValue true + */ + treatConvexAsSolid; +} + +/** + * @interface + * @group Managers + * @category Utilities + */ +class CastShapeSettings extends CastSettings { + /** + * Scales the shape used during a cast. Allows to re-use existing shapes, if only scale is + * different. + * + * @type {Vec3} + * @defaultValue Vec3(1, 1, 1) + */ + scale; + + /** + * All hit results will be returned relative to this offset, can be zero to get results in + * world position, but when you're testing far from the origin you get better precision by + * picking a position that's closer since floats are most accurate near the origin. + * + * @type {Vec3} + * @defaultValue Vec3(0, 0, 0) + */ + offset; + + /** + * Sets whether to ignore triangle backfaces. Following options available: + * ``` + * BFM_IGNORE_BACK_FACES + * ``` + * ``` + * BFM_COLLIDE_BACK_FACES + * ``` + * + * @type {number} + * @defaultValue BFM_IGNORE_BACK_FACES + */ + backFaceModeTriangles; + + /** + * Sets whether to ignore backfaces of convex shapes. See {@link backFaceModeTriangles} for + * available options. + * + * @type {number} + * @defaultValue BFM_IGNORE_BACK_FACES + */ + backFaceModeConvex; + + /** + * Indicates if we want to shrink the shape by the convex radius and then expand it again. This + * speeds up collision detection and gives a more accurate normal at the cost of a more + * "rounded" shape. + * + * @type {boolean} + * @defaultValue false + */ + useShrunkenShapeAndConvexRadius; + + /** + * When true, and the shape is intersecting at the beginning of the cast (fraction = 0) then + * this will calculate the deepest penetration point (costing additional CPU time). + * + * @type {boolean} + * @defaultValue false + */ + returnDeepestPoint; +} + +/** + * @interface + * @group Managers + * @category Utilities + */ +class CollideShapeSettings extends CastSettings { + /** + * Scales the shape used during collision test. Allows to re-use existing shapes, if only scale + * is different. + * + * @type {Vec3} + * @defaultValue Vec3(1, 1, 1) + */ + scale; + + /** + * When > 0 contacts in the vicinity of the query shape can be found. All nearest contacts that + * are not further away than this distance will be found. + * + * @type {number} + * @defaultValue 0 (m) + */ + maxSeparationDistance; + + /** + * If `true`, the shape will ignore other shapes backfaces. + * + * @type {boolean} + * @defaultValue true + */ + ignoreBackFaces; + + /** + * All hit results will be returned relative to this offset, can be zero to get results in + * world position, but when you're testing far from the origin you get better precision by + * picking a position that's closer since floats are most accurate near the origin. + * + * @type {Vec3} + * @defaultValue Vec3(0, 0, 0) + */ + offset; +} + +/** + * @interface + * @group Managers + * @category Utilities + */ +class ShapeSettings { + /** + * @see {@link ShapeComponent.density} + * @type {number} + * @defaultValue 1000 + */ + density; + + /** + * @see {@link ShapeComponent.shapePosition} + * @type {Vec3} + * @defaultValue Vec3(0, 0, 0) + */ + shapePosition; + + /** + * @see {@link ShapeComponent.shapeRotation} + * @type {Quat} + * @defaultValue Quat(0, 0, 0, 1) + */ + shapeRotation; + + /** + * Scales the shape. Uniform scale is always fine. Non-uniform scale is supported only by some + * shapes. For example: + * - you can use non-uniform scale on a box shape, but not on a sphere, etc. + * - you can use non-uniform scale on a cylinder/capsule, but `X` and `Z` must be uniform. + * + * @type {Vec3} + * @defaultValue Vec3(1, 1, 1) + */ + scale; + + /** + * @see {@link ShapeComponent.halfExtent} + * @type {Vec3} + * @defaultValue Vec3(0.5, 0.5, 0.5) + */ + halfExtent; + + /** + * @see {@link ShapeComponent.convexRadius} + * @type {number} + * @defaultValue 0.05 (m) + */ + convexRadius; + + /** + * @see {@link ShapeComponent.halfHeight} + * @type {number} + * @defaultValue 0.5 (m) + */ + halfHeight; + + /** + * @see {@link ShapeComponent.radius} + * @type {number} + * @defaultValue 0.5 (m) + */ + radius; +} + +export { + QuerySettings, CastSettings, CastRaySettings, CastShapeSettings, CollideShapeSettings, + ShapeSettings +}; diff --git a/src/physics/manager.mjs b/src/physics/manager.mjs index c5f5ddf..ed60a51 100644 --- a/src/physics/manager.mjs +++ b/src/physics/manager.mjs @@ -50,13 +50,13 @@ class PhysicsManager { * const backend = app.physics.backend; * const Jolt = backend.Jolt; * const joltVec = new Jolt.Vec3(0, 0, 0); - * + * * // common Jolt interfaces, which the backend has instantiated * backend.physicsSystem; * backend.bodyInterface; * backend.joltInterface; * ``` - * + * * @type {import('./jolt/back/backend.mjs').JoltBackend | null} */ get backend() { From 9b02e2e226418537d4183e49d8f35fb0aaa65a93 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Fri, 14 Jun 2024 12:04:06 +0300 Subject: [PATCH 4/4] add operators to private --- src/index.mjs | 4 ++++ src/physics/jolt/back/operators/cleaner.mjs | 4 ++++ src/physics/jolt/back/operators/creator.mjs | 4 ++++ src/physics/jolt/back/operators/listener.mjs | 4 ++++ src/physics/jolt/back/operators/querier.mjs | 4 ++++ src/physics/jolt/back/operators/tracker.mjs | 4 ++++ 6 files changed, 24 insertions(+) diff --git a/src/index.mjs b/src/index.mjs index 4976005..69bc1e9 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -48,5 +48,9 @@ export * from './physics/jolt/front/constraint/types/settings.mjs'; export * from './physics/jolt/constants.mjs'; export { JoltBackend } from './physics/jolt/back/backend.mjs'; +export { Querier } from './physics/jolt/back/operators/querier.mjs'; +export { Creator } from './physics/jolt/back/operators/creator.mjs'; +export { Listener } from './physics/jolt/back/operators/listener.mjs'; +export { Tracker } from './physics/jolt/back/operators/tracker.mjs'; export { init, JoltInitSettings } from './physics/init.mjs'; diff --git a/src/physics/jolt/back/operators/cleaner.mjs b/src/physics/jolt/back/operators/cleaner.mjs index 6ea0d96..aa6585c 100644 --- a/src/physics/jolt/back/operators/cleaner.mjs +++ b/src/physics/jolt/back/operators/cleaner.mjs @@ -3,6 +3,10 @@ import { BUFFER_READ_UINT32, CMD_DESTROY_BODY, CMD_DESTROY_CONSTRAINT, CMD_DESTROY_SHAPE } from '../../constants.mjs'; +/** + * @group Private + * @private + */ class Cleaner { static cleanDebugDrawData(body, Jolt) { if (body.debugDrawData) { diff --git a/src/physics/jolt/back/operators/creator.mjs b/src/physics/jolt/back/operators/creator.mjs index 0436f6f..7c53bd8 100644 --- a/src/physics/jolt/back/operators/creator.mjs +++ b/src/physics/jolt/back/operators/creator.mjs @@ -11,6 +11,10 @@ import { } from '../../constants.mjs'; import { ConstraintCreator } from './helpers/constraint-creator.mjs'; +/** + * @group Private + * @private + */ class Creator { static createShapeSettings(cb, meshBuffers, Jolt, jv, jq) { const shapeType = cb.read(BUFFER_READ_UINT8); diff --git a/src/physics/jolt/back/operators/listener.mjs b/src/physics/jolt/back/operators/listener.mjs index f5fe6d3..bdec222 100644 --- a/src/physics/jolt/back/operators/listener.mjs +++ b/src/physics/jolt/back/operators/listener.mjs @@ -6,6 +6,10 @@ import { const eval2 = eval; +/** + * @group Private + * @private + */ class Listener { constructor(backend) { this._listener = null; diff --git a/src/physics/jolt/back/operators/querier.mjs b/src/physics/jolt/back/operators/querier.mjs index f8a2171..68f7452 100644 --- a/src/physics/jolt/back/operators/querier.mjs +++ b/src/physics/jolt/back/operators/querier.mjs @@ -53,6 +53,10 @@ function writeCollideShapeHit(cb, system, tracker, calculateNormal, hit, Jolt) { let collidePointResult; let params = []; +/** + * @group Private + * @private + */ class Querier { constructor(backend) { this._backend = backend; diff --git a/src/physics/jolt/back/operators/tracker.mjs b/src/physics/jolt/back/operators/tracker.mjs index 0bebc28..f9d5fd6 100644 --- a/src/physics/jolt/back/operators/tracker.mjs +++ b/src/physics/jolt/back/operators/tracker.mjs @@ -1,3 +1,7 @@ +/** + * @group Private + * @private + */ class Tracker { constructor(Jolt) { this._Jolt = Jolt;