Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion src/framework/app-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -215,6 +218,24 @@ class AppBase extends EventHandler {
*/
timeScale = 1;

/**
* A frame rate independent interval that dictates when fixedUpdate, postFixedUpdate 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 event postFixedUpdate for physics simulation. Defaults to false.
*
* @type {boolean}
* @example
* this.app.usePostFixedUpdateForPhysicsSim = true;
*/
usePostFixedUpdateForPhysicsSim = 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.
Expand Down Expand Up @@ -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, usePostFixedUpdateForPhysicsSim
} = appOptions;

this.usePostFixedUpdateForPhysicsSim = !!usePostFixedUpdateForPhysicsSim;

Debug.assert(graphicsDevice, 'The application cannot be created without a valid GraphicsDevice');
this.graphicsDevice = graphicsDevice;

Expand Down Expand Up @@ -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();
Expand All @@ -1019,6 +1043,27 @@ class AppBase extends EventHandler {
this.stats.frame.updateStart = now();
// #endif

this._fixedTimeDebt += dt;

let fixedStepsCounter = 0;

while (this._fixedTimeDebt >= this.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(this._inTools ? 'toolsFixedUpdate' : 'fixedUpdate', fixedTimeStep, fixedStepsCounter);
this.systems.fire(this._inTools ? 'toolsPostFixedUpdate' : 'postFixedUpdate', fixedTimeStep, fixedStepsCounter);
this.fire('fixedUpdate', fixedTimeStep);
this._fixedTimeDebt -= fixedTimeStep;

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);
Expand Down
7 changes: 7 additions & 0 deletions src/framework/app-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ class AppOptions {
* @type {typeof ResourceHandler[]}
*/
resourceHandlers = [];

/**
* Use event postFixedUpdate for physics simulation
*
* @type {boolean}
*/
usePostFixedUpdateForPhysicsSim = false;
}

export { AppOptions };
140 changes: 128 additions & 12 deletions src/framework/components/rigid-body/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1066,30 +1069,143 @@ class RigidBodyComponent extends Component {
}
}

_setEntityPosAndRotFromTransform(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.
*
* @private
*/
_updateDynamic() {

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);
this._setEntityPosAndRotFromTransform(_ammoTransform);
}
}
}

/**
* 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();

// 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 motionState = body.getMotionState();
if (motionState) {
motionState.getWorldTransform(_ammoTransform);

const p = _ammoTransform.getOrigin();
const q = _ammoTransform.getRotation();
const currentPosition = _ammoTransform.getOrigin();
const currentRotation = _ammoTransform.getRotation();
const linearVelocity = body.getLinearVelocity();
const angularVelocity = body.getAngularVelocity();

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;
Expand All @@ -1098,16 +1214,16 @@ class RigidBodyComponent extends Component {
// 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());
interpolationRot.mul(invertedAo);
interpolationRot.transformVector(lo, _vec32);
interpolationPos.sub(_vec32);
}

entity.setPositionAndRotation(
interpolationPos,
interpolationRot
);
}
}
}
Expand Down
Loading