From 84a6e92e23d0b60fc94745c8769c2780a43806b2 Mon Sep 17 00:00:00 2001 From: Alexandr Shamarin Date: Wed, 11 Jun 2025 19:11:51 +0300 Subject: [PATCH 1/8] Add fixedUpdate method --- src/framework/app-base.js | 53 ++++- src/framework/app-options.js | 9 +- .../components/rigid-body/component.js | 185 ++++++++++++++---- .../components/rigid-body/constants.js | 16 +- src/framework/components/rigid-body/system.js | 107 +++++++++- src/framework/components/script/component.js | 44 ++++- src/framework/components/script/system.js | 10 +- src/framework/script/constants.js | 3 +- src/framework/script/script.js | 10 +- 9 files changed, 386 insertions(+), 51 deletions(-) diff --git a/src/framework/app-base.js b/src/framework/app-base.js index ae50e71d74f..5cad7bf743a 100644 --- a/src/framework/app-base.js +++ b/src/framework/app-base.js @@ -189,6 +189,9 @@ class AppBase extends EventHandler { /** @ignore */ _time = 0; + /** @ignore */ + _fixedTimeDebt = 0; + /** * Set this to false if you want to run without using bundles. We set it to true only if * TextDecoder is available because we currently rely on it for untarring. @@ -215,6 +218,24 @@ class AppBase extends EventHandler { */ timeScale = 1; + /** + * A frame rate independent interval that dictates when physics calculations and fixedUpdate events are performed. Defaults to 0.02 + * + * @type {number} + * @example + * this.app.fixedTimeStep = 0.02; // (1 / 50) fixedUpdate calls 50 times per second + */ + fixedTimeStep = 1 / 50; + + /** + * Use fixedUpdate calls with a fixed step for physics calculations + * + * @type {boolean} + * @example + * this.app.useFixedTimeForPhysics = true; + */ + useFixedTimeForPhysics = false; + /** * Clamps per-frame delta time to an upper bound. Useful since returning from a tab * deactivation can generate huge values for dt, which can adversely affect game state. @@ -481,9 +502,11 @@ class AppBase extends EventHandler { init(appOptions) { const { assetPrefix, batchManager, componentSystems, elementInput, gamepads, graphicsDevice, keyboard, - lightmapper, mouse, resourceHandlers, scriptsOrder, scriptPrefix, soundManager, touch, xr + lightmapper, mouse, resourceHandlers, scriptsOrder, scriptPrefix, soundManager, touch, xr, useFixedTimeForPhysics } = appOptions; + this.useFixedTimeForPhysics = !!useFixedTimeForPhysics; + Debug.assert(graphicsDevice, 'The application cannot be created without a valid GraphicsDevice'); this.graphicsDevice = graphicsDevice; @@ -1011,6 +1034,7 @@ class AppBase extends EventHandler { * @param {number} dt - The time delta in seconds since the last frame. */ update(dt) { + this.frame++; this.graphicsDevice.updateClientRect(); @@ -1019,6 +1043,31 @@ class AppBase extends EventHandler { this.stats.frame.updateStart = now(); // #endif + if (this.useFixedTimeForPhysics) { + + this._fixedTimeDebt += dt; + + const fixedTimeStep = this.fixedTimeStep; + const numSimulationSubSteps = Math.floor(this._fixedTimeDebt / fixedTimeStep); + + this._fixedTimeDebt -= numSimulationSubSteps * fixedTimeStep; + + for (let i = 0; i < numSimulationSubSteps; i++) { + + this.systems.fire('fixedUpdate', fixedTimeStep); + this.fire('fixedUpdate', fixedTimeStep); + + this.systems.fire('physics-fixed-update', fixedTimeStep); + this.fire('physics-fixed-update', fixedTimeStep); + } + + this.systems.fire('physics-update', dt); + this.fire('physics-update', dt); + } + else { + this._fixedTimeDebt = 0; + } + this.systems.fire(this._inTools ? 'toolsUpdate' : 'update', dt); this.systems.fire('animationUpdate', dt); this.systems.fire('postUpdate', dt); @@ -2095,4 +2144,4 @@ const makeTick = function (_app) { }; }; -export { app, AppBase }; +export { app, AppBase }; \ No newline at end of file diff --git a/src/framework/app-options.js b/src/framework/app-options.js index bf849b30df1..65e3cca7b3c 100644 --- a/src/framework/app-options.js +++ b/src/framework/app-options.js @@ -122,6 +122,13 @@ class AppOptions { * @type {typeof ResourceHandler[]} */ resourceHandlers = []; + + /** + * Use fixedUpdate calls with a fixed step for physics calculations + * + * @type {boolean} + */ + useFixedTimeForPhysics = false; } -export { AppOptions }; +export { AppOptions }; \ No newline at end of file diff --git a/src/framework/components/rigid-body/component.js b/src/framework/components/rigid-body/component.js index 41c8d44a8a3..764e9d6f586 100644 --- a/src/framework/components/rigid-body/component.js +++ b/src/framework/components/rigid-body/component.js @@ -13,12 +13,15 @@ import { * @import { Entity } from '../../entity.js' */ +const ANGULAR_MOTION_THRESHOLD = 0.25 * Math.PI; + // Shared math variable to avoid excessive allocation let _ammoTransform; let _ammoVec1, _ammoVec2, _ammoQuat; const _quat1 = new Quat(); const _quat2 = new Quat(); -const _vec3 = new Vec3(); +const _vec31 = new Vec3(); +const _vec32 = new Vec3(); /** * The RigidBodyComponent, when combined with a {@link CollisionComponent}, allows your entities @@ -1066,49 +1069,165 @@ class RigidBodyComponent extends Component { } } + _setEntityPosAndRotFormTransform(transform) { + + const p = transform.getOrigin(); + const q = transform.getRotation(); + + const entity = this.entity; + const component = entity.collision; + + if (component && component._hasOffset) { + const lo = component.data.linearOffset; + const ao = component.data.angularOffset; + + // Un-rotate the angular offset and then use the new rotation to + // un-translate the linear offset in local space + // Order of operations matter here + const invertedAo = _quat2.copy(ao).invert(); + const entityRot = _quat1.set(q.x(), q.y(), q.z(), q.w()).mul(invertedAo); + + entityRot.transformVector(lo, _vec31); + + entity.setPositionAndRotation( + _vec31.set(p.x() - _vec31.x, p.y() - _vec31.y, p.z() - _vec31.z), + entityRot + ); + + } else { + entity.setPositionAndRotation( + _vec31.set(p.x(), p.y(), p.z()), + _quat1.set(q.x(), q.y(), q.z(), q.w()), + ); + } + } + /** * Sets an entity's transform to match that of the world transformation matrix of a dynamic * rigid body's motion state. - * + * @param {boolean} fromMotionState set transform from body motionsState * @private */ - _updateDynamic() { + _updateDynamic(fromMotionState = true) { + const body = this._body; // If a dynamic body is frozen, we can assume its motion state transform is // the same is the entity world transform if (body.isActive()) { - // Update the motion state. Note that the test for the presence of the motion - // state is technically redundant since the engine creates one for all bodies. - const motionState = body.getMotionState(); - if (motionState) { - const entity = this.entity; - - motionState.getWorldTransform(_ammoTransform); - - const p = _ammoTransform.getOrigin(); - const q = _ammoTransform.getRotation(); - - const component = entity.collision; - if (component && component._hasOffset) { - const lo = component.data.linearOffset; - const ao = component.data.angularOffset; - - // Un-rotate the angular offset and then use the new rotation to - // un-translate the linear offset in local space - // Order of operations matter here - const invertedAo = _quat2.copy(ao).invert(); - const entityRot = _quat1.set(q.x(), q.y(), q.z(), q.w()).mul(invertedAo); - - entityRot.transformVector(lo, _vec3); - entity.setPosition(p.x() - _vec3.x, p.y() - _vec3.y, p.z() - _vec3.z); - entity.setRotation(entityRot); - - } else { - entity.setPosition(p.x(), p.y(), p.z()); - entity.setRotation(q.x(), q.y(), q.z(), q.w()); + + if (fromMotionState) { + + // Update the motion state. Note that the test for the presence of the motion + // state is technically redundant since the engine creates one for all bodies. + const motionState = body.getMotionState(); + if (motionState) { + motionState.getWorldTransform(_ammoTransform); + this._setEntityPosAndRotFormTransform(_ammoTransform); } } + else { + const currentTransform = body.getWorldTransform(); + this._setEntityPosAndRotFormTransform(currentTransform); + } + } + } + + /** + * @param {Quat} rotation + * @param {Vec3} angularVelocity + * @param {number} timeStep + * @param {Quat} out + * + * @private + */ + _interpolationRotationByAngularVelocity(rotation, angularVelocity, timeStep, out) { + + let fAngle = angularVelocity.length(); + + //limit the angular motion + if (fAngle * timeStep > ANGULAR_MOTION_THRESHOLD) { + fAngle = ANGULAR_MOTION_THRESHOLD / timeStep; + } + + const factor = fAngle < 0.001 + ? 0.5 * timeStep - (timeStep * timeStep * timeStep) * 0.020833333333 * fAngle * fAngle // use Taylor's expansions of sync function + : Math.sin(0.5 * fAngle * timeStep) / fAngle; // sync(fAngle) = sin(c*fAngle)/t + + // q1 = q(angularVelocity, Math.cos(fAngle * timeStep * 0.5)) + // out = q1 * q2 + + const q1x = angularVelocity.x * factor; + const q1y = angularVelocity.y * factor; + const q1z = angularVelocity.z * factor; + const q1w = Math.cos(fAngle * timeStep * 0.5); + + const q2x = rotation.x; + const q2y = rotation.y; + const q2z = rotation.z; + const q2w = rotation.w; + const cx = q1y * q2z - q1z * q2y; + const cy = q1z * q2x - q1x * q2z; + const cz = q1x * q2y - q1y * q2x; + + const dot = q1x * q2x + q1y * q2y + q1z * q2z; + + out.x = q1x * q2w + q2x * q1w + cx; + out.y = q1y * q2w + q2y * q1w + cy; + out.z = q1z * q2w + q2z * q1w + cz; + out.w = q1w * q2w - dot + } + + _applyInterpolation(extrapolationTime) { + + if (!this._body || this._type !== BODYTYPE_DYNAMIC) { + return; + } + + const body = this._body; + + // If a dynamic body is frozen, we can assume its motion state transform is + // the same is the entity world transform + if (body.isActive()) { + + const currentTransform = body.getWorldTransform(); + const linearVelocity = body.getLinearVelocity(); + const angularVelocity = body.getAngularVelocity(); + const currentPosition = currentTransform.getOrigin(); + const currentRotation = currentTransform.getRotation(); + + const interpolationPos = _vec31.set( + currentPosition.x() + linearVelocity.x() * extrapolationTime, + currentPosition.y() + linearVelocity.y() * extrapolationTime, + currentPosition.z() + linearVelocity.z() * extrapolationTime + ); + + const angularVelocityO = _vec32.set(angularVelocity.x(), angularVelocity.y(), angularVelocity.z()); + const interpolationRot = _quat1.set(currentRotation.x(), currentRotation.y(), currentRotation.z(), currentRotation.w()); + + this._interpolationRotationByAngularVelocity(interpolationRot, angularVelocityO, extrapolationTime, interpolationRot); + + const entity = this.entity; + const component = entity.collision; + + if (component && component._hasOffset) { + const lo = component.data.linearOffset; + const ao = component.data.angularOffset; + + // Un-rotate the angular offset and then use the new rotation to + // un-translate the linear offset in local space + // Order of operations matter here + const invertedAo = _quat2.copy(ao).invert(); + + interpolationRot.mul(invertedAo); + interpolationRot.transformVector(lo, _vec32); + interpolationPos.sub(_vec32); + } + + entity.setPositionAndRotation( + interpolationPos, + interpolationRot + ); } } @@ -1220,4 +1339,4 @@ class RigidBodyComponent extends Component { } } -export { RigidBodyComponent }; +export { RigidBodyComponent }; \ No newline at end of file diff --git a/src/framework/components/rigid-body/constants.js b/src/framework/components/rigid-body/constants.js index 29c72d12860..1e3656924ad 100644 --- a/src/framework/components/rigid-body/constants.js +++ b/src/framework/components/rigid-body/constants.js @@ -1,3 +1,17 @@ +/** + * Event triggered after fixedUpdate to perform the physics step. + * + * @category Physics + */ +export const EVENT_PHYSICS_FIXED_UPDATE = 'physics-fixed-update'; + +/** + * Event that occurs after the physics simulation. + * + * @category Physics + */ +export const EVENT_PHYSICS_UPDATE = 'physics-update'; + /** * Rigid body has infinite mass and cannot move. * @@ -56,4 +70,4 @@ export const BODYMASK_NONE = 0; export const BODYMASK_ALL = 65535; export const BODYMASK_STATIC = 2; export const BODYMASK_NOT_STATIC = 65535 ^ 2; -export const BODYMASK_NOT_STATIC_KINEMATIC = 65535 ^ (2 | 4); +export const BODYMASK_NOT_STATIC_KINEMATIC = 65535 ^ (2 | 4); \ No newline at end of file diff --git a/src/framework/components/rigid-body/system.js b/src/framework/components/rigid-body/system.js index a0b35abea5d..2b098deb83a 100644 --- a/src/framework/components/rigid-body/system.js +++ b/src/framework/components/rigid-body/system.js @@ -4,7 +4,7 @@ import { Debug } from '../../../core/debug.js'; import { Vec3 } from '../../../core/math/vec3.js'; import { Component } from '../component.js'; import { ComponentSystem } from '../system.js'; -import { BODYFLAG_NORESPONSE_OBJECT } from './constants.js'; +import { BODYFLAG_NORESPONSE_OBJECT, EVENT_PHYSICS_FIXED_UPDATE, EVENT_PHYSICS_UPDATE } from './constants.js'; import { RigidBodyComponent } from './component.js'; import { RigidBodyComponentData } from './data.js'; @@ -413,6 +413,18 @@ class RigidBodyComponentSystem extends ComponentSystem { */ _compounds = []; + /** + * @type {number} + * @private + */ + _internalTime = 0; + + /** + * @type {number} + * @private + */ + _lastFixedTimeStep = 0; + /** * Create a new RigidBodyComponentSystem. * @@ -471,9 +483,13 @@ class RigidBodyComponentSystem extends ComponentSystem { this.singleContactResultPool = new ObjectPool(SingleContactResult, 1); this.app.systems.on('update', this.onUpdate, this); + this.app.systems.on(EVENT_PHYSICS_FIXED_UPDATE, this.onPhysicsFixedUpdate, this); + this.app.systems.on(EVENT_PHYSICS_UPDATE, this.onPhysicsUpdate, this); } else { // Unbind the update function if we haven't loaded Ammo by now this.app.systems.off('update', this.onUpdate, this); + this.app.systems.off(EVENT_PHYSICS_FIXED_UPDATE, this.onPhysicsFixedUpdate, this); + this.app.systems.off(EVENT_PHYSICS_UPDATE, this.onPhysicsUpdate, this); } } @@ -1048,12 +1064,13 @@ class RigidBodyComponentSystem extends ComponentSystem { this.singleContactResultPool.freeAll(); } - onUpdate(dt) { - let i, len; + /** + * A list of tasks that the system needs to perform after the physics step. + * @param {number} dt The amount of simulation time processed in the last simulation tick. + */ + _beforeStepSimulation(dt) { - // #if _PROFILER - this._stats.physicsStart = now(); - // #endif + let i, len; // downcast gravity to float32 so we can accurately compare with existing // gravity set in ammo. @@ -1085,9 +1102,15 @@ class RigidBodyComponentSystem extends ComponentSystem { for (i = 0, len = kinematic.length; i < len; i++) { kinematic[i]._updateKinematic(); } + } - // Step the physics simulation - this.dynamicsWorld.stepSimulation(dt, this.maxSubSteps, this.fixedTimeStep); + /** + * A list of tasks that the system needs to perform after the physics step. + * @param {number} dt The amount of simulation time processed in the last simulation tick. + */ + _afterStepSimulation(dt) { + + let i, len; // Update the transforms of all entities referencing a dynamic body const dynamic = this._dynamic; @@ -1098,6 +1121,70 @@ class RigidBodyComponentSystem extends ComponentSystem { if (!this.dynamicsWorld.setInternalTickCallback) { this._checkForCollisions(Ammo.getPointer(this.dynamicsWorld), dt); } + } + + onPhysicsFixedUpdate(dt) { + + this._lastFixedTimeStep = dt; + + // #if _PROFILER + this._stats.physicsStart = now(); + // #endif + + this._beforeStepSimulation(dt); + + // Step the physics simulation + this.dynamicsWorld.stepSimulation(dt, 0); + + this._afterStepSimulation(dt); + + // ??? + // #if _PROFILER + this._stats.physicsTime = now() - this._stats.physicsStart; + // #endif + } + + onPhysicsUpdate(dt) { + + this._internalTime += dt; + + // TODO: add interpolation type for component + + if (this._internalTime >= this._lastFixedTimeStep && this._lastFixedTimeStep > 0) { + const numSimulationSubSteps = Math.floor(this._internalTime / this._lastFixedTimeStep); + this._internalTime -= numSimulationSubSteps * this._lastFixedTimeStep; + } + + let i, len; + + // Apply transform interpolation to all entities referencing the dynamic body. + const extrapolationTime = this._internalTime - this._lastFixedTimeStep; + const dynamic = this._dynamic; + for (i = 0, len = dynamic.length; i < len; i++) { + dynamic[i]._applyInterpolation(extrapolationTime); + } + + // #if _PROFILER + this._stats.physicsTime = now() - this._stats.physicsStart; + // #endif + } + + onUpdate(dt) { + + if (this.app.useFixedTimeForPhysics) { + return; + } + + // #if _PROFILER + this._stats.physicsStart = now(); + // #endif + + this._beforeStepSimulation(dt); + + // Step the physics simulation + this.dynamicsWorld.stepSimulation(dt, this.maxSubSteps, this.fixedTimeStep); + + this._afterStepSimulation(dt); // #if _PROFILER this._stats.physicsTime = now() - this._stats.physicsStart; @@ -1108,6 +1195,8 @@ class RigidBodyComponentSystem extends ComponentSystem { super.destroy(); this.app.systems.off('update', this.onUpdate, this); + this.app.systems.off(EVENT_PHYSICS_FIXED_UPDATE, this.onPhysicsFixedUpdate, this); + this.app.systems.off(EVENT_PHYSICS_UPDATE, this.onPhysicsUpdate, this); if (typeof Ammo !== 'undefined') { Ammo.destroy(this.dynamicsWorld); @@ -1131,4 +1220,4 @@ class RigidBodyComponentSystem extends ComponentSystem { Component._buildAccessors(RigidBodyComponent.prototype, _schema); -export { ContactPoint, ContactResult, RaycastResult, RigidBodyComponentSystem, SingleContactResult }; +export { ContactPoint, ContactResult, RaycastResult, RigidBodyComponentSystem, SingleContactResult }; \ No newline at end of file diff --git a/src/framework/components/script/component.js b/src/framework/components/script/component.js index c13e8c50455..33b78d33bf5 100644 --- a/src/framework/components/script/component.js +++ b/src/framework/components/script/component.js @@ -5,7 +5,7 @@ import { Component } from '../component.js'; import { Entity } from '../../entity.js'; import { SCRIPT_INITIALIZE, SCRIPT_POST_INITIALIZE, SCRIPT_UPDATE, - SCRIPT_POST_UPDATE, SCRIPT_SWAP + SCRIPT_FIXED_UPDATE, SCRIPT_POST_UPDATE, SCRIPT_SWAP } from '../../script/constants.js'; import { ScriptType } from '../../script/script-type.js'; import { getScriptName } from '../../script/script.js'; @@ -196,6 +196,8 @@ class ScriptComponent extends Component { * @private */ this._scripts = []; + // holds all script instances with an fixedUpdate method + this._fixedUpdateList = new SortedLoopArray({ sortBy: '__executionOrder' }); // holds all script instances with an update method this._updateList = new SortedLoopArray({ sortBy: '__executionOrder' }); // holds all script instances with a postUpdate method @@ -499,6 +501,22 @@ class ScriptComponent extends Component { this.onPostStateChange(); } + _onFixedUpdate(dt) { + const list = this._fixedUpdateList; + if (!list.length) return; + + const wasLooping = this._beginLooping(); + + for (list.loopIndex = 0; list.loopIndex < list.length; list.loopIndex++) { + const script = list.items[list.loopIndex]; + if (script.enabled) { + this._scriptMethod(script, SCRIPT_FIXED_UPDATE, dt); + } + } + + this._endLooping(wasLooping); + } + _onUpdate(dt) { const list = this._updateList; if (!list.length) return; @@ -547,6 +565,11 @@ class ScriptComponent extends Component { this._scripts.push(scriptInstance); scriptInstance.__executionOrder = scriptsLength; + // append script to the fixedUpdate list if it has an update method + if (scriptInstance.fixedUpdate) { + this._fixedUpdateList.append(scriptInstance); + } + // append script to the update list if it has an update method if (scriptInstance.update) { this._updateList.append(scriptInstance); @@ -565,6 +588,12 @@ class ScriptComponent extends Component { // the script instances that come after this script this._resetExecutionOrder(index + 1, scriptsLength + 1); + // insert script to the fixedUpdate list if it has an update method + // in the right order + if (scriptInstance.fixedUpdate) { + this._fixedUpdateList.insert(scriptInstance); + } + // insert script to the update list if it has an update method // in the right order if (scriptInstance.update) { @@ -585,6 +614,10 @@ class ScriptComponent extends Component { this._scripts.splice(idx, 1); + if (scriptInstance.fixedUpdate) { + this._fixedUpdateList.remove(scriptInstance); + } + if (scriptInstance.update) { this._updateList.remove(scriptInstance); } @@ -913,6 +946,9 @@ class ScriptComponent extends Component { // set execution order and make sure we update // our update and postUpdate lists scriptInstance.__executionOrder = ind; + if (scriptInstanceOld.fixedUpdate) { + this._fixedUpdateList.remove(scriptInstanceOld); + } if (scriptInstanceOld.update) { this._updateList.remove(scriptInstanceOld); } @@ -920,6 +956,9 @@ class ScriptComponent extends Component { this._postUpdateList.remove(scriptInstanceOld); } + if (scriptInstance.fixedUpdate) { + this._fixedUpdateList.insert(scriptInstance); + } if (scriptInstance.update) { this._updateList.insert(scriptInstance); } @@ -1084,6 +1123,7 @@ class ScriptComponent extends Component { // reset execution order for scripts and re-sort update and postUpdate lists this._resetExecutionOrder(0, len); + this._fixedUpdateList.sort(); this._updateList.sort(); this._postUpdateList.sort(); @@ -1094,4 +1134,4 @@ class ScriptComponent extends Component { } } -export { ScriptComponent }; +export { ScriptComponent }; \ No newline at end of file diff --git a/src/framework/components/script/system.js b/src/framework/components/script/system.js index e3d28709e31..c4240fe18c3 100644 --- a/src/framework/components/script/system.js +++ b/src/framework/components/script/system.js @@ -10,6 +10,7 @@ import { ScriptComponentData } from './data.js'; const METHOD_INITIALIZE_ATTRIBUTES = '_onInitializeAttributes'; const METHOD_INITIALIZE = '_onInitialize'; const METHOD_POST_INITIALIZE = '_onPostInitialize'; +const METHOD_FIXED_UPDATE = '_onFixedUpdate'; const METHOD_UPDATE = '_onUpdate'; const METHOD_POST_UPDATE = '_onPostUpdate'; @@ -62,6 +63,7 @@ class ScriptComponentSystem extends ComponentSystem { this.on('beforeremove', this._onBeforeRemove, this); this.app.systems.on('initialize', this._onInitialize, this); this.app.systems.on('postInitialize', this._onPostInitialize, this); + this.app.systems.on('fixedUpdate', this._onFixedUpdate, this); this.app.systems.on('update', this._onUpdate, this); this.app.systems.on('postUpdate', this._onPostUpdate, this); } @@ -163,6 +165,11 @@ class ScriptComponentSystem extends ComponentSystem { this._callComponentMethod(this._enabledComponents, METHOD_POST_INITIALIZE); } + _onFixedUpdate(dt) { + // call onFixedUpdate on enabled components + this._callComponentMethod(this._enabledComponents, METHOD_FIXED_UPDATE, dt); + } + _onUpdate(dt) { // call onUpdate on enabled components this._callComponentMethod(this._enabledComponents, METHOD_UPDATE, dt); @@ -201,9 +208,10 @@ class ScriptComponentSystem extends ComponentSystem { this.app.systems.off('initialize', this._onInitialize, this); this.app.systems.off('postInitialize', this._onPostInitialize, this); + this.app.systems.off('fixedUpdate', this._onFixedUpdate, this); this.app.systems.off('update', this._onUpdate, this); this.app.systems.off('postUpdate', this._onPostUpdate, this); } } -export { ScriptComponentSystem }; +export { ScriptComponentSystem }; \ No newline at end of file diff --git a/src/framework/script/constants.js b/src/framework/script/constants.js index 548b72c6005..ea6895518c0 100644 --- a/src/framework/script/constants.js +++ b/src/framework/script/constants.js @@ -1,5 +1,6 @@ export const SCRIPT_INITIALIZE = 'initialize'; export const SCRIPT_POST_INITIALIZE = 'postInitialize'; +export const SCRIPT_FIXED_UPDATE = 'fixedUpdate'; export const SCRIPT_UPDATE = 'update'; export const SCRIPT_POST_UPDATE = 'postUpdate'; -export const SCRIPT_SWAP = 'swap'; +export const SCRIPT_SWAP = 'swap'; \ No newline at end of file diff --git a/src/framework/script/script.js b/src/framework/script/script.js index 62bae92e36b..9d15a9b099b 100644 --- a/src/framework/script/script.js +++ b/src/framework/script/script.js @@ -17,6 +17,7 @@ import { SCRIPT_INITIALIZE, SCRIPT_POST_INITIALIZE } from './constants.js'; * * - `Script#initialize` - Called once when the script is initialized. * - `Script#postInitialize` - Called once after all scripts have been initialized. + * - `Script#fixedUpdate` - Called every fixed time, if the script is enabled. * - `Script#update` - Called every frame, if the script is enabled. * - `Script#postUpdate` - Called every frame, after all scripts have been updated. * - `Script#swap` - Called when a script is redefined. @@ -326,6 +327,13 @@ export class Script extends EventHandler { * @description Called after all initialize methods are executed in the same tick or enabling chain of actions. */ + /** + * @function + * @name Script#[fixedUpdate] + * @description Called for enabled (running state) scripts on each fixed tick. + * @param {number} dt - The fixed delta time in seconds. + */ + /** * @function * @name Script#[update] @@ -363,4 +371,4 @@ export function getScriptName(constructorFn) { if (constructorFn === Function || constructorFn === Function.prototype.constructor) return 'Function'; const match = (`${constructorFn}`).match(funcNameRegex); return match ? match[1] : undefined; -} +} \ No newline at end of file From d6daa8b211b818064e1374a158a064c721a7f1cb Mon Sep 17 00:00:00 2001 From: Alexandr Shamarin Date: Wed, 11 Jun 2025 19:42:33 +0300 Subject: [PATCH 2/8] Refactoring lint --- src/framework/app-base.js | 11 +++-- src/framework/app-options.js | 4 +- .../components/rigid-body/component.js | 45 ++++++++++--------- .../components/rigid-body/constants.js | 2 +- src/framework/components/rigid-body/system.js | 13 +++--- src/framework/components/script/component.js | 2 +- src/framework/components/script/system.js | 2 +- src/framework/script/constants.js | 2 +- src/framework/script/script.js | 2 +- 9 files changed, 40 insertions(+), 43 deletions(-) diff --git a/src/framework/app-base.js b/src/framework/app-base.js index 5cad7bf743a..e5d5f6868e8 100644 --- a/src/framework/app-base.js +++ b/src/framework/app-base.js @@ -219,8 +219,8 @@ class AppBase extends EventHandler { timeScale = 1; /** - * A frame rate independent interval that dictates when physics calculations and fixedUpdate events are performed. Defaults to 0.02 - * + * A frame rate independent interval that dictates when physics calculations and fixedUpdate events are performed. Defaults to 0.02. + * * @type {number} * @example * this.app.fixedTimeStep = 0.02; // (1 / 50) fixedUpdate calls 50 times per second @@ -229,7 +229,7 @@ class AppBase extends EventHandler { /** * Use fixedUpdate calls with a fixed step for physics calculations - * + * * @type {boolean} * @example * this.app.useFixedTimeForPhysics = true; @@ -1063,8 +1063,7 @@ class AppBase extends EventHandler { this.systems.fire('physics-update', dt); this.fire('physics-update', dt); - } - else { + } else { this._fixedTimeDebt = 0; } @@ -2144,4 +2143,4 @@ const makeTick = function (_app) { }; }; -export { app, AppBase }; \ No newline at end of file +export { app, AppBase }; diff --git a/src/framework/app-options.js b/src/framework/app-options.js index 65e3cca7b3c..83f1de3031a 100644 --- a/src/framework/app-options.js +++ b/src/framework/app-options.js @@ -125,10 +125,10 @@ class AppOptions { /** * Use fixedUpdate calls with a fixed step for physics calculations - * + * * @type {boolean} */ useFixedTimeForPhysics = false; } -export { AppOptions }; \ No newline at end of file +export { AppOptions }; diff --git a/src/framework/components/rigid-body/component.js b/src/framework/components/rigid-body/component.js index 764e9d6f586..29624069550 100644 --- a/src/framework/components/rigid-body/component.js +++ b/src/framework/components/rigid-body/component.js @@ -1097,7 +1097,7 @@ class RigidBodyComponent extends Component { } else { entity.setPositionAndRotation( _vec31.set(p.x(), p.y(), p.z()), - _quat1.set(q.x(), q.y(), q.z(), q.w()), + _quat1.set(q.x(), q.y(), q.z(), q.w()) ); } } @@ -1105,7 +1105,9 @@ class RigidBodyComponent extends Component { /** * Sets an entity's transform to match that of the world transformation matrix of a dynamic * rigid body's motion state. - * @param {boolean} fromMotionState set transform from body motionsState + * + * @param {boolean} fromMotionState - set transform from body motionsState + * * @private */ _updateDynamic(fromMotionState = true) { @@ -1125,8 +1127,7 @@ class RigidBodyComponent extends Component { motionState.getWorldTransform(_ammoTransform); this._setEntityPosAndRotFormTransform(_ammoTransform); } - } - else { + } else { const currentTransform = body.getWorldTransform(); this._setEntityPosAndRotFormTransform(currentTransform); } @@ -1134,26 +1135,26 @@ class RigidBodyComponent extends Component { } /** - * @param {Quat} rotation - * @param {Vec3} angularVelocity - * @param {number} timeStep - * @param {Quat} out - * + * Performs interpolation of the body's rotation based on current rotation and angular velocity. + * + * @param {Quat} rotation - The current rotation of the body represented as a quaternion. Defines the body's current orientation. + * @param {Vec3} angularVelocity - The angular velocity vector of the body, indicating how fast and in which direction the body is rotating around its axes. + * @param {number} timeStep - The interpolation time step, representing the duration over which to interpolate. Typically a small value such as the time between frames. + * @param {Quat} out - The output quaternion where the interpolated rotation will be stored. Used to return the result without creating a new object. * @private */ _interpolationRotationByAngularVelocity(rotation, angularVelocity, timeStep, out) { + let fAngle = angularVelocity.length(); - let fAngle = angularVelocity.length(); + // limit the angular motion + if (fAngle * timeStep > ANGULAR_MOTION_THRESHOLD) { + fAngle = ANGULAR_MOTION_THRESHOLD / timeStep; + } - //limit the angular motion - if (fAngle * timeStep > ANGULAR_MOTION_THRESHOLD) { - fAngle = ANGULAR_MOTION_THRESHOLD / timeStep; - } + const factor = fAngle < 0.001 ? + 0.5 * timeStep - (timeStep * timeStep * timeStep) * 0.020833333333 * fAngle * fAngle : // use Taylor's expansions of sync function + Math.sin(0.5 * fAngle * timeStep) / fAngle; // sync(fAngle) = sin(c*fAngle)/t - const factor = fAngle < 0.001 - ? 0.5 * timeStep - (timeStep * timeStep * timeStep) * 0.020833333333 * fAngle * fAngle // use Taylor's expansions of sync function - : Math.sin(0.5 * fAngle * timeStep) / fAngle; // sync(fAngle) = sin(c*fAngle)/t - // q1 = q(angularVelocity, Math.cos(fAngle * timeStep * 0.5)) // out = q1 * q2 @@ -1171,11 +1172,11 @@ class RigidBodyComponent extends Component { const cz = q1x * q2y - q1y * q2x; const dot = q1x * q2x + q1y * q2y + q1z * q2z; - + out.x = q1x * q2w + q2x * q1w + cx; out.y = q1y * q2w + q2y * q1w + cy; out.z = q1z * q2w + q2z * q1w + cz; - out.w = q1w * q2w - dot + out.w = q1w * q2w - dot; } _applyInterpolation(extrapolationTime) { @@ -1189,7 +1190,7 @@ class RigidBodyComponent extends Component { // If a dynamic body is frozen, we can assume its motion state transform is // the same is the entity world transform if (body.isActive()) { - + const currentTransform = body.getWorldTransform(); const linearVelocity = body.getLinearVelocity(); const angularVelocity = body.getAngularVelocity(); @@ -1339,4 +1340,4 @@ class RigidBodyComponent extends Component { } } -export { RigidBodyComponent }; \ No newline at end of file +export { RigidBodyComponent }; diff --git a/src/framework/components/rigid-body/constants.js b/src/framework/components/rigid-body/constants.js index 1e3656924ad..abe9daff27d 100644 --- a/src/framework/components/rigid-body/constants.js +++ b/src/framework/components/rigid-body/constants.js @@ -70,4 +70,4 @@ export const BODYMASK_NONE = 0; export const BODYMASK_ALL = 65535; export const BODYMASK_STATIC = 2; export const BODYMASK_NOT_STATIC = 65535 ^ 2; -export const BODYMASK_NOT_STATIC_KINEMATIC = 65535 ^ (2 | 4); \ No newline at end of file +export const BODYMASK_NOT_STATIC_KINEMATIC = 65535 ^ (2 | 4); diff --git a/src/framework/components/rigid-body/system.js b/src/framework/components/rigid-body/system.js index 2b098deb83a..5038632607b 100644 --- a/src/framework/components/rigid-body/system.js +++ b/src/framework/components/rigid-body/system.js @@ -1066,7 +1066,7 @@ class RigidBodyComponentSystem extends ComponentSystem { /** * A list of tasks that the system needs to perform after the physics step. - * @param {number} dt The amount of simulation time processed in the last simulation tick. + * @param {number} dt - The amount of simulation time processed in the last simulation tick. */ _beforeStepSimulation(dt) { @@ -1106,7 +1106,7 @@ class RigidBodyComponentSystem extends ComponentSystem { /** * A list of tasks that the system needs to perform after the physics step. - * @param {number} dt The amount of simulation time processed in the last simulation tick. + * @param {number} dt - The amount of simulation time processed in the last simulation tick. */ _afterStepSimulation(dt) { @@ -1145,17 +1145,14 @@ class RigidBodyComponentSystem extends ComponentSystem { } onPhysicsUpdate(dt) { - + let i, len; this._internalTime += dt; - - // TODO: add interpolation type for component + // TODO: add interpolation type for component if (this._internalTime >= this._lastFixedTimeStep && this._lastFixedTimeStep > 0) { const numSimulationSubSteps = Math.floor(this._internalTime / this._lastFixedTimeStep); this._internalTime -= numSimulationSubSteps * this._lastFixedTimeStep; } - - let i, len; // Apply transform interpolation to all entities referencing the dynamic body. const extrapolationTime = this._internalTime - this._lastFixedTimeStep; @@ -1220,4 +1217,4 @@ class RigidBodyComponentSystem extends ComponentSystem { Component._buildAccessors(RigidBodyComponent.prototype, _schema); -export { ContactPoint, ContactResult, RaycastResult, RigidBodyComponentSystem, SingleContactResult }; \ No newline at end of file +export { ContactPoint, ContactResult, RaycastResult, RigidBodyComponentSystem, SingleContactResult }; diff --git a/src/framework/components/script/component.js b/src/framework/components/script/component.js index 33b78d33bf5..e2cc75955c9 100644 --- a/src/framework/components/script/component.js +++ b/src/framework/components/script/component.js @@ -1134,4 +1134,4 @@ class ScriptComponent extends Component { } } -export { ScriptComponent }; \ No newline at end of file +export { ScriptComponent }; diff --git a/src/framework/components/script/system.js b/src/framework/components/script/system.js index c4240fe18c3..d349496c5a3 100644 --- a/src/framework/components/script/system.js +++ b/src/framework/components/script/system.js @@ -214,4 +214,4 @@ class ScriptComponentSystem extends ComponentSystem { } } -export { ScriptComponentSystem }; \ No newline at end of file +export { ScriptComponentSystem }; diff --git a/src/framework/script/constants.js b/src/framework/script/constants.js index ea6895518c0..75c8ca418e0 100644 --- a/src/framework/script/constants.js +++ b/src/framework/script/constants.js @@ -3,4 +3,4 @@ export const SCRIPT_POST_INITIALIZE = 'postInitialize'; export const SCRIPT_FIXED_UPDATE = 'fixedUpdate'; export const SCRIPT_UPDATE = 'update'; export const SCRIPT_POST_UPDATE = 'postUpdate'; -export const SCRIPT_SWAP = 'swap'; \ No newline at end of file +export const SCRIPT_SWAP = 'swap'; diff --git a/src/framework/script/script.js b/src/framework/script/script.js index 9d15a9b099b..0412359cc39 100644 --- a/src/framework/script/script.js +++ b/src/framework/script/script.js @@ -371,4 +371,4 @@ export function getScriptName(constructorFn) { if (constructorFn === Function || constructorFn === Function.prototype.constructor) return 'Function'; const match = (`${constructorFn}`).match(funcNameRegex); return match ? match[1] : undefined; -} \ No newline at end of file +} From 3283d5a687ead4726c832b798622c16e8058c16e Mon Sep 17 00:00:00 2001 From: Alexandr Shamarin Date: Wed, 11 Jun 2025 19:45:50 +0300 Subject: [PATCH 3/8] Rename method _setEntityPosAndRotFromTransform --- src/framework/components/rigid-body/component.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/framework/components/rigid-body/component.js b/src/framework/components/rigid-body/component.js index 29624069550..8d5f0a303f1 100644 --- a/src/framework/components/rigid-body/component.js +++ b/src/framework/components/rigid-body/component.js @@ -1069,7 +1069,7 @@ class RigidBodyComponent extends Component { } } - _setEntityPosAndRotFormTransform(transform) { + _setEntityPosAndRotFromTransform(transform) { const p = transform.getOrigin(); const q = transform.getRotation(); @@ -1125,11 +1125,11 @@ class RigidBodyComponent extends Component { const motionState = body.getMotionState(); if (motionState) { motionState.getWorldTransform(_ammoTransform); - this._setEntityPosAndRotFormTransform(_ammoTransform); + this._setEntityPosAndRotFromTransform(_ammoTransform); } } else { const currentTransform = body.getWorldTransform(); - this._setEntityPosAndRotFormTransform(currentTransform); + this._setEntityPosAndRotFromTransform(currentTransform); } } } From 3d302db9f03fc8e6a5c58a9e17671d23a6347940 Mon Sep 17 00:00:00 2001 From: Alexapp Date: Wed, 11 Jun 2025 23:29:04 +0300 Subject: [PATCH 4/8] Sync interpolation position with fixedUpdate lag --- src/framework/components/rigid-body/system.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/framework/components/rigid-body/system.js b/src/framework/components/rigid-body/system.js index 5038632607b..3e85da90a31 100644 --- a/src/framework/components/rigid-body/system.js +++ b/src/framework/components/rigid-body/system.js @@ -1155,7 +1155,9 @@ class RigidBodyComponentSystem extends ComponentSystem { } // Apply transform interpolation to all entities referencing the dynamic body. - const extrapolationTime = this._internalTime - this._lastFixedTimeStep; + // multiply 2 because the fixedUpdate step was before physicsFixedUpdate, + // this will allow the lag to be synchronized + const extrapolationTime = this._internalTime - (this._lastFixedTimeStep * 2); const dynamic = this._dynamic; for (i = 0, len = dynamic.length; i < len; i++) { dynamic[i]._applyInterpolation(extrapolationTime); From cf4900289e2c5da691b4277284af96802ef2fe01 Mon Sep 17 00:00:00 2001 From: Alexapp Date: Wed, 11 Jun 2025 23:40:09 +0300 Subject: [PATCH 5/8] Fix lint error --- src/framework/components/rigid-body/system.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/framework/components/rigid-body/system.js b/src/framework/components/rigid-body/system.js index 3e85da90a31..1a11cac3b9d 100644 --- a/src/framework/components/rigid-body/system.js +++ b/src/framework/components/rigid-body/system.js @@ -1155,7 +1155,7 @@ class RigidBodyComponentSystem extends ComponentSystem { } // Apply transform interpolation to all entities referencing the dynamic body. - // multiply 2 because the fixedUpdate step was before physicsFixedUpdate, + // multiply 2 because the fixedUpdate step was before physicsFixedUpdate, // this will allow the lag to be synchronized const extrapolationTime = this._internalTime - (this._lastFixedTimeStep * 2); const dynamic = this._dynamic; From 8655ed7886a5b33f0a4ca9221459796df0f41f40 Mon Sep 17 00:00:00 2001 From: Alexapp Date: Fri, 13 Jun 2025 04:36:05 +0300 Subject: [PATCH 6/8] Simplifying logic and refactoring --- src/framework/app-base.js | 43 +++--- src/framework/app-options.js | 4 +- .../components/rigid-body/component.js | 86 ++++++------ .../components/rigid-body/constants.js | 14 -- src/framework/components/rigid-body/system.js | 128 ++++++++++-------- src/framework/stats.js | 3 + 6 files changed, 140 insertions(+), 138 deletions(-) diff --git a/src/framework/app-base.js b/src/framework/app-base.js index e5d5f6868e8..578f5bce60b 100644 --- a/src/framework/app-base.js +++ b/src/framework/app-base.js @@ -219,7 +219,7 @@ class AppBase extends EventHandler { timeScale = 1; /** - * A frame rate independent interval that dictates when physics calculations and fixedUpdate events are performed. Defaults to 0.02. + * A frame rate independent interval that dictates when fixedUpdate, postFixedUpdate events are performed. Defaults to 0.02. * * @type {number} * @example @@ -228,13 +228,13 @@ class AppBase extends EventHandler { fixedTimeStep = 1 / 50; /** - * Use fixedUpdate calls with a fixed step for physics calculations + * Use event postFixedUpdate for physics simulation. Defaults to false. * * @type {boolean} * @example - * this.app.useFixedTimeForPhysics = true; + * this.app.usePostFixedUpdateForPhysicsSim = true; */ - useFixedTimeForPhysics = false; + usePostFixedUpdateForPhysicsSim = false; /** * Clamps per-frame delta time to an upper bound. Useful since returning from a tab @@ -502,10 +502,10 @@ class AppBase extends EventHandler { init(appOptions) { const { assetPrefix, batchManager, componentSystems, elementInput, gamepads, graphicsDevice, keyboard, - lightmapper, mouse, resourceHandlers, scriptsOrder, scriptPrefix, soundManager, touch, xr, useFixedTimeForPhysics + lightmapper, mouse, resourceHandlers, scriptsOrder, scriptPrefix, soundManager, touch, xr, usePostFixedUpdateForPhysicsSim } = appOptions; - this.useFixedTimeForPhysics = !!useFixedTimeForPhysics; + this.usePostFixedUpdateForPhysicsSim = !!usePostFixedUpdateForPhysicsSim; Debug.assert(graphicsDevice, 'The application cannot be created without a valid GraphicsDevice'); this.graphicsDevice = graphicsDevice; @@ -1043,30 +1043,27 @@ class AppBase extends EventHandler { this.stats.frame.updateStart = now(); // #endif - if (this.useFixedTimeForPhysics) { + this._fixedTimeDebt += dt; - this._fixedTimeDebt += dt; + let fixedStepsCounter = 0; - const fixedTimeStep = this.fixedTimeStep; - const numSimulationSubSteps = Math.floor(this._fixedTimeDebt / fixedTimeStep); - - this._fixedTimeDebt -= numSimulationSubSteps * fixedTimeStep; - - for (let i = 0; i < numSimulationSubSteps; i++) { + while (this._fixedTimeDebt >= this.fixedTimeStep) { - this.systems.fire('fixedUpdate', fixedTimeStep); - this.fire('fixedUpdate', fixedTimeStep); + // we will save the value, because at the time of processing, it can be changed from the outside + const fixedTimeStep = this.fixedTimeStep; - this.systems.fire('physics-fixed-update', fixedTimeStep); - this.fire('physics-fixed-update', fixedTimeStep); - } + this.systems.fire(this._inTools ? 'toolsFixedUpdate' : 'fixedUpdate', fixedTimeStep, fixedStepsCounter); + this.systems.fire(this._inTools ? 'toolsPostFixedUpdate' : 'postFixedUpdate', fixedTimeStep, fixedStepsCounter); + this.fire('fixedUpdate', fixedTimeStep); + this._fixedTimeDebt -= fixedTimeStep; - this.systems.fire('physics-update', dt); - this.fire('physics-update', dt); - } else { - this._fixedTimeDebt = 0; + fixedStepsCounter++; } + // #if _PROFILER + this.stats.frame.fixedUpdateCount = fixedStepsCounter; + // #endif + this.systems.fire(this._inTools ? 'toolsUpdate' : 'update', dt); this.systems.fire('animationUpdate', dt); this.systems.fire('postUpdate', dt); diff --git a/src/framework/app-options.js b/src/framework/app-options.js index 83f1de3031a..d61b9973e0a 100644 --- a/src/framework/app-options.js +++ b/src/framework/app-options.js @@ -124,11 +124,11 @@ class AppOptions { resourceHandlers = []; /** - * Use fixedUpdate calls with a fixed step for physics calculations + * Use event postFixedUpdate for physics simulation * * @type {boolean} */ - useFixedTimeForPhysics = false; + usePostFixedUpdateForPhysicsSim = false; } export { AppOptions }; diff --git a/src/framework/components/rigid-body/component.js b/src/framework/components/rigid-body/component.js index 8d5f0a303f1..451a5031ab4 100644 --- a/src/framework/components/rigid-body/component.js +++ b/src/framework/components/rigid-body/component.js @@ -1106,11 +1106,9 @@ class RigidBodyComponent extends Component { * Sets an entity's transform to match that of the world transformation matrix of a dynamic * rigid body's motion state. * - * @param {boolean} fromMotionState - set transform from body motionsState - * * @private */ - _updateDynamic(fromMotionState = true) { + _updateDynamic() { const body = this._body; @@ -1118,18 +1116,12 @@ class RigidBodyComponent extends Component { // the same is the entity world transform if (body.isActive()) { - if (fromMotionState) { - - // Update the motion state. Note that the test for the presence of the motion - // state is technically redundant since the engine creates one for all bodies. - const motionState = body.getMotionState(); - if (motionState) { - motionState.getWorldTransform(_ammoTransform); - this._setEntityPosAndRotFromTransform(_ammoTransform); - } - } else { - const currentTransform = body.getWorldTransform(); - this._setEntityPosAndRotFromTransform(currentTransform); + // Update the motion state. Note that the test for the presence of the motion + // state is technically redundant since the engine creates one for all bodies. + const motionState = body.getMotionState(); + if (motionState) { + motionState.getWorldTransform(_ammoTransform); + this._setEntityPosAndRotFromTransform(_ammoTransform); } } } @@ -1191,44 +1183,48 @@ class RigidBodyComponent extends Component { // the same is the entity world transform if (body.isActive()) { - const currentTransform = body.getWorldTransform(); - const linearVelocity = body.getLinearVelocity(); - const angularVelocity = body.getAngularVelocity(); - const currentPosition = currentTransform.getOrigin(); - const currentRotation = currentTransform.getRotation(); + const motionState = body.getMotionState(); + if (motionState) { + motionState.getWorldTransform(_ammoTransform); - const interpolationPos = _vec31.set( - currentPosition.x() + linearVelocity.x() * extrapolationTime, - currentPosition.y() + linearVelocity.y() * extrapolationTime, - currentPosition.z() + linearVelocity.z() * extrapolationTime - ); + const currentPosition = _ammoTransform.getOrigin(); + const currentRotation = _ammoTransform.getRotation(); + const linearVelocity = body.getLinearVelocity(); + const angularVelocity = body.getAngularVelocity(); - const angularVelocityO = _vec32.set(angularVelocity.x(), angularVelocity.y(), angularVelocity.z()); - const interpolationRot = _quat1.set(currentRotation.x(), currentRotation.y(), currentRotation.z(), currentRotation.w()); + const interpolationPos = _vec31.set( + currentPosition.x() + linearVelocity.x() * extrapolationTime, + currentPosition.y() + linearVelocity.y() * extrapolationTime, + currentPosition.z() + linearVelocity.z() * extrapolationTime + ); - this._interpolationRotationByAngularVelocity(interpolationRot, angularVelocityO, extrapolationTime, interpolationRot); + const angularVelocityO = _vec32.set(angularVelocity.x(), angularVelocity.y(), angularVelocity.z()); + const interpolationRot = _quat1.set(currentRotation.x(), currentRotation.y(), currentRotation.z(), currentRotation.w()); - const entity = this.entity; - const component = entity.collision; + this._interpolationRotationByAngularVelocity(interpolationRot, angularVelocityO, extrapolationTime, interpolationRot); - if (component && component._hasOffset) { - const lo = component.data.linearOffset; - const ao = component.data.angularOffset; + const entity = this.entity; + const component = entity.collision; - // Un-rotate the angular offset and then use the new rotation to - // un-translate the linear offset in local space - // Order of operations matter here - const invertedAo = _quat2.copy(ao).invert(); + if (component && component._hasOffset) { + const lo = component.data.linearOffset; + const ao = component.data.angularOffset; - interpolationRot.mul(invertedAo); - interpolationRot.transformVector(lo, _vec32); - interpolationPos.sub(_vec32); - } + // Un-rotate the angular offset and then use the new rotation to + // un-translate the linear offset in local space + // Order of operations matter here + const invertedAo = _quat2.copy(ao).invert(); - entity.setPositionAndRotation( - interpolationPos, - interpolationRot - ); + interpolationRot.mul(invertedAo); + interpolationRot.transformVector(lo, _vec32); + interpolationPos.sub(_vec32); + } + + entity.setPositionAndRotation( + interpolationPos, + interpolationRot + ); + } } } diff --git a/src/framework/components/rigid-body/constants.js b/src/framework/components/rigid-body/constants.js index abe9daff27d..29c72d12860 100644 --- a/src/framework/components/rigid-body/constants.js +++ b/src/framework/components/rigid-body/constants.js @@ -1,17 +1,3 @@ -/** - * Event triggered after fixedUpdate to perform the physics step. - * - * @category Physics - */ -export const EVENT_PHYSICS_FIXED_UPDATE = 'physics-fixed-update'; - -/** - * Event that occurs after the physics simulation. - * - * @category Physics - */ -export const EVENT_PHYSICS_UPDATE = 'physics-update'; - /** * Rigid body has infinite mass and cannot move. * diff --git a/src/framework/components/rigid-body/system.js b/src/framework/components/rigid-body/system.js index 1a11cac3b9d..d7b21d5beae 100644 --- a/src/framework/components/rigid-body/system.js +++ b/src/framework/components/rigid-body/system.js @@ -4,7 +4,7 @@ import { Debug } from '../../../core/debug.js'; import { Vec3 } from '../../../core/math/vec3.js'; import { Component } from '../component.js'; import { ComponentSystem } from '../system.js'; -import { BODYFLAG_NORESPONSE_OBJECT, EVENT_PHYSICS_FIXED_UPDATE, EVENT_PHYSICS_UPDATE } from './constants.js'; +import { BODYFLAG_NORESPONSE_OBJECT } from './constants.js'; import { RigidBodyComponent } from './component.js'; import { RigidBodyComponentData } from './data.js'; @@ -417,7 +417,13 @@ class RigidBodyComponentSystem extends ComponentSystem { * @type {number} * @private */ - _internalTime = 0; + _dynamicTime = 0; + + /** + * @type {number} + * @private + */ + _fixedTime = 0; /** * @type {number} @@ -425,6 +431,12 @@ class RigidBodyComponentSystem extends ComponentSystem { */ _lastFixedTimeStep = 0; + /** + * @type {boolean} + * @private + */ + _usePostFixedUpdate = false; + /** * Create a new RigidBodyComponentSystem. * @@ -483,13 +495,11 @@ class RigidBodyComponentSystem extends ComponentSystem { this.singleContactResultPool = new ObjectPool(SingleContactResult, 1); this.app.systems.on('update', this.onUpdate, this); - this.app.systems.on(EVENT_PHYSICS_FIXED_UPDATE, this.onPhysicsFixedUpdate, this); - this.app.systems.on(EVENT_PHYSICS_UPDATE, this.onPhysicsUpdate, this); + this.app.systems.on('postFixedUpdate', this.onPostFixedUpdate, this); } else { // Unbind the update function if we haven't loaded Ammo by now this.app.systems.off('update', this.onUpdate, this); - this.app.systems.off(EVENT_PHYSICS_FIXED_UPDATE, this.onPhysicsFixedUpdate, this); - this.app.systems.off(EVENT_PHYSICS_UPDATE, this.onPhysicsUpdate, this); + this.app.systems.off('postFixedUpdate', this.onPostFixedUpdate, this); } } @@ -1123,79 +1133,89 @@ class RigidBodyComponentSystem extends ComponentSystem { } } - onPhysicsFixedUpdate(dt) { + /** + * Resets the time counters and switch usePostFixedUpdate flag if a change in the physical mode is detected. + */ + _resetTimeCountersAndFlagOnChange() { + + if (this._usePostFixedUpdate !== this.app.usePostFixedUpdateForPhysicsSim) { + this._usePostFixedUpdate = this.app.usePostFixedUpdateForPhysicsSim; + this._fixedTime = 0; + this._dynamicTime = 0; + this._lastFixedTimeStep = 0; + this._fixedTimeDebt = 0; + } + } - this._lastFixedTimeStep = dt; + onPostFixedUpdate(dt) { - // #if _PROFILER - this._stats.physicsStart = now(); - // #endif + this._resetTimeCountersAndFlagOnChange(); - this._beforeStepSimulation(dt); + if (this._usePostFixedUpdate) { - // Step the physics simulation - this.dynamicsWorld.stepSimulation(dt, 0); + // #if _PROFILER + this._stats.physicsStart = now(); + // #endif - this._afterStepSimulation(dt); + this._fixedTime += dt; + this._lastFixedTimeStep = dt; - // ??? - // #if _PROFILER - this._stats.physicsTime = now() - this._stats.physicsStart; - // #endif - } + this._beforeStepSimulation(dt); - onPhysicsUpdate(dt) { - let i, len; - this._internalTime += dt; + // Performs one physics step without applying interpolation + this.dynamicsWorld.stepSimulation(dt, 0); - // TODO: add interpolation type for component - if (this._internalTime >= this._lastFixedTimeStep && this._lastFixedTimeStep > 0) { - const numSimulationSubSteps = Math.floor(this._internalTime / this._lastFixedTimeStep); - this._internalTime -= numSimulationSubSteps * this._lastFixedTimeStep; - } + this._afterStepSimulation(dt); - // Apply transform interpolation to all entities referencing the dynamic body. - // multiply 2 because the fixedUpdate step was before physicsFixedUpdate, - // this will allow the lag to be synchronized - const extrapolationTime = this._internalTime - (this._lastFixedTimeStep * 2); - const dynamic = this._dynamic; - for (i = 0, len = dynamic.length; i < len; i++) { - dynamic[i]._applyInterpolation(extrapolationTime); + // ??? + // #if _PROFILER + this._stats.physicsTime = now() - this._stats.physicsStart; + // #endif } - - // #if _PROFILER - this._stats.physicsTime = now() - this._stats.physicsStart; - // #endif } onUpdate(dt) { - if (this.app.useFixedTimeForPhysics) { - return; - } + this._resetTimeCountersAndFlagOnChange(); - // #if _PROFILER - this._stats.physicsStart = now(); - // #endif + if (this._usePostFixedUpdate) { - this._beforeStepSimulation(dt); + this._dynamicTime += dt; + + // Apply transform interpolation to all entities referencing the dynamic body. + // subtract lastFixedTimeStep to synchronize the transformation + // between the last fixedUpdate and postFixedUpdate + const extrapolationTime = this._dynamicTime - this._fixedTime - this._lastFixedTimeStep * 2; - // Step the physics simulation - this.dynamicsWorld.stepSimulation(dt, this.maxSubSteps, this.fixedTimeStep); + const dynamic = this._dynamic; + for (let i = 0, len = dynamic.length; i < len; i++) { + dynamic[i]._applyInterpolation(extrapolationTime); + } - this._afterStepSimulation(dt); + } else { + + // #if _PROFILER + this._stats.physicsStart = now(); + // #endif + + this._beforeStepSimulation(dt); - // #if _PROFILER - this._stats.physicsTime = now() - this._stats.physicsStart; - // #endif + // Step the physics simulation + this.dynamicsWorld.stepSimulation(dt, this.maxSubSteps, this.fixedTimeStep); + + this._afterStepSimulation(dt); + + // #if _PROFILER + this._stats.physicsTime = now() - this._stats.physicsStart; + // #endif + } } destroy() { super.destroy(); + this.app.systems.off('postFixedUpdate', this.onPostFixedUpdate, this); this.app.systems.off('update', this.onUpdate, this); - this.app.systems.off(EVENT_PHYSICS_FIXED_UPDATE, this.onPhysicsFixedUpdate, this); - this.app.systems.off(EVENT_PHYSICS_UPDATE, this.onPhysicsUpdate, this); if (typeof Ammo !== 'undefined') { Ammo.destroy(this.dynamicsWorld); diff --git a/src/framework/stats.js b/src/framework/stats.js index 078c20e94fe..04a2ee74917 100644 --- a/src/framework/stats.js +++ b/src/framework/stats.js @@ -19,6 +19,9 @@ class ApplicationStats { ms: 0, dt: 0, + fixedTimeStep: 0, + fixedUpdateCount: 0, + updateStart: 0, updateTime: 0, renderStart: 0, From 0f7f6250a6ccae6dc8324db9333862f276f6011c Mon Sep 17 00:00:00 2001 From: Alexapp Date: Fri, 13 Jun 2025 04:37:53 +0300 Subject: [PATCH 7/8] Remove lint error --- src/framework/components/rigid-body/system.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/framework/components/rigid-body/system.js b/src/framework/components/rigid-body/system.js index d7b21d5beae..a7bbad5e44e 100644 --- a/src/framework/components/rigid-body/system.js +++ b/src/framework/components/rigid-body/system.js @@ -1181,7 +1181,7 @@ class RigidBodyComponentSystem extends ComponentSystem { if (this._usePostFixedUpdate) { this._dynamicTime += dt; - + // Apply transform interpolation to all entities referencing the dynamic body. // subtract lastFixedTimeStep to synchronize the transformation // between the last fixedUpdate and postFixedUpdate From d802813b0a31c0d007292352b477202a6f601e19 Mon Sep 17 00:00:00 2001 From: Alexapp Date: Fri, 13 Jun 2025 04:52:46 +0300 Subject: [PATCH 8/8] Remove fixedTimeStep from stats --- src/framework/stats.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/framework/stats.js b/src/framework/stats.js index 04a2ee74917..b449528b6e2 100644 --- a/src/framework/stats.js +++ b/src/framework/stats.js @@ -19,7 +19,6 @@ class ApplicationStats { ms: 0, dt: 0, - fixedTimeStep: 0, fixedUpdateCount: 0, updateStart: 0,