UNPKG

@babylonjs/viewer

Version:

The Babylon Viewer aims to simplify a specific but common Babylon.js use case: loading, viewing, and interacting with a 3D model.

1,181 lines (1,179 loc) 107 kB
import { aE as Animation, M as Matrix, bZ as _StaticOffsetValueColor4, b_ as _StaticOffsetValueColor3, b$ as _StaticOffsetValueSize, c0 as _StaticOffsetValueVector2, c1 as _StaticOffsetValueVector3, c2 as _StaticOffsetValueQuaternion, bX as PrecisionDate, O as Observable, aS as TmpVectors, Q as Quaternion, V as Vector3, F as Scene, H as EngineStore, bt as Tags } from './index-FzOfPXLV.esm.js'; import { B as Bone } from './bone--mL3H5cQ.esm.js'; /** * Defines a runtime animation */ class RuntimeAnimation { /** * Gets the current frame of the runtime animation */ get currentFrame() { return this._currentFrame; } /** * Gets the weight of the runtime animation */ get weight() { return this._weight; } /** * Gets the current value of the runtime animation */ get currentValue() { return this._currentValue; } /** * Gets or sets the target path of the runtime animation */ get targetPath() { return this._targetPath; } /** * Gets the actual target of the runtime animation */ get target() { return this._currentActiveTarget; } /** * Gets the additive state of the runtime animation */ get isAdditive() { return this._host && this._host.isAdditive; } /** * Create a new RuntimeAnimation object * @param target defines the target of the animation * @param animation defines the source animation object * @param scene defines the hosting scene * @param host defines the initiating Animatable */ constructor(target, animation, scene, host) { this._events = new Array(); /** * The current frame of the runtime animation */ this._currentFrame = 0; /** * The original value of the runtime animation */ this._originalValue = new Array(); /** * The original blend value of the runtime animation */ this._originalBlendValue = null; /** * The offsets cache of the runtime animation */ this._offsetsCache = {}; /** * The high limits cache of the runtime animation */ this._highLimitsCache = {}; /** * Specifies if the runtime animation has been stopped */ this._stopped = false; /** * The blending factor of the runtime animation */ this._blendingFactor = 0; /** * The current value of the runtime animation */ this._currentValue = null; this._currentActiveTarget = null; this._directTarget = null; /** * The target path of the runtime animation */ this._targetPath = ""; /** * The weight of the runtime animation */ this._weight = 1.0; /** * The absolute frame offset of the runtime animation */ this._absoluteFrameOffset = 0; /** * The previous elapsed time (since start of animation) of the runtime animation */ this._previousElapsedTime = 0; this._yoyoDirection = 1; /** * The previous absolute frame of the runtime animation (meaning, without taking into account the from/to values, only the elapsed time and the fps) */ this._previousAbsoluteFrame = 0; this._targetIsArray = false; /** @internal */ this._coreRuntimeAnimation = null; this._animation = animation; this._target = target; this._scene = scene; this._host = host; this._activeTargets = []; animation._runtimeAnimations.push(this); // State this._animationState = { key: 0, repeatCount: 0, loopMode: this._getCorrectLoopMode(), }; if (this._animation.dataType === Animation.ANIMATIONTYPE_MATRIX) { this._animationState.workValue = Matrix.Zero(); } // Limits this._keys = this._animation.getKeys(); this._minFrame = this._keys[0].frame; this._maxFrame = this._keys[this._keys.length - 1].frame; this._minValue = this._keys[0].value; this._maxValue = this._keys[this._keys.length - 1].value; // Add a start key at frame 0 if missing if (this._minFrame !== 0) { const newKey = { frame: 0, value: this._minValue }; this._keys.splice(0, 0, newKey); } // Check data if (this._target instanceof Array) { let index = 0; for (const target of this._target) { this._preparePath(target, index); this._getOriginalValues(index); index++; } this._targetIsArray = true; } else { this._preparePath(this._target); this._getOriginalValues(); this._targetIsArray = false; this._directTarget = this._activeTargets[0]; } // Cloning events locally const events = animation.getEvents(); if (events && events.length > 0) { for (const e of events) { this._events.push(e._clone()); } } this._enableBlending = target && target.animationPropertiesOverride ? target.animationPropertiesOverride.enableBlending : this._animation.enableBlending; } _preparePath(target, targetIndex = 0) { const targetPropertyPath = this._animation.targetPropertyPath; if (targetPropertyPath.length > 1) { let property = target; for (let index = 0; index < targetPropertyPath.length - 1; index++) { const name = targetPropertyPath[index]; property = property[name]; if (property === undefined) { throw new Error(`Invalid property (${name}) in property path (${targetPropertyPath.join(".")})`); } } this._targetPath = targetPropertyPath[targetPropertyPath.length - 1]; this._activeTargets[targetIndex] = property; } else { this._targetPath = targetPropertyPath[0]; this._activeTargets[targetIndex] = target; } if (this._activeTargets[targetIndex][this._targetPath] === undefined) { throw new Error(`Invalid property (${this._targetPath}) in property path (${targetPropertyPath.join(".")})`); } } /** * Gets the animation from the runtime animation */ get animation() { return this._animation; } /** * Resets the runtime animation to the beginning * @param restoreOriginal defines whether to restore the target property to the original value */ reset(restoreOriginal = false) { if (restoreOriginal) { if (this._target instanceof Array) { let index = 0; for (const target of this._target) { if (this._originalValue[index] !== undefined) { this._setValue(target, this._activeTargets[index], this._originalValue[index], -1, index); } index++; } } else { if (this._originalValue[0] !== undefined) { this._setValue(this._target, this._directTarget, this._originalValue[0], -1, 0); } } } this._offsetsCache = {}; this._highLimitsCache = {}; this._currentFrame = 0; this._blendingFactor = 0; // Events for (let index = 0; index < this._events.length; index++) { this._events[index].isDone = false; } } /** * Specifies if the runtime animation is stopped * @returns Boolean specifying if the runtime animation is stopped */ isStopped() { return this._stopped; } /** * Disposes of the runtime animation */ dispose() { const index = this._animation.runtimeAnimations.indexOf(this); if (index > -1) { this._animation.runtimeAnimations.splice(index, 1); } } /** * Apply the interpolated value to the target * @param currentValue defines the value computed by the animation * @param weight defines the weight to apply to this value (Defaults to 1.0) */ setValue(currentValue, weight) { if (this._targetIsArray) { for (let index = 0; index < this._target.length; index++) { const target = this._target[index]; this._setValue(target, this._activeTargets[index], currentValue, weight, index); } return; } this._setValue(this._target, this._directTarget, currentValue, weight, 0); } _getOriginalValues(targetIndex = 0) { let originalValue; const target = this._activeTargets[targetIndex]; if (target.getLocalMatrix && this._targetPath === "_matrix") { // For bones originalValue = target.getLocalMatrix(); } else { originalValue = target[this._targetPath]; } if (originalValue && originalValue.clone) { this._originalValue[targetIndex] = originalValue.clone(); } else { this._originalValue[targetIndex] = originalValue; } } _registerTargetForLateAnimationBinding(runtimeAnimation, originalValue) { const target = runtimeAnimation.target; this._scene._registeredForLateAnimationBindings.pushNoDuplicate(target); if (!target._lateAnimationHolders) { target._lateAnimationHolders = {}; } if (!target._lateAnimationHolders[runtimeAnimation.targetPath]) { target._lateAnimationHolders[runtimeAnimation.targetPath] = { totalWeight: 0, totalAdditiveWeight: 0, animations: [], additiveAnimations: [], originalValue: originalValue, }; } if (runtimeAnimation.isAdditive) { target._lateAnimationHolders[runtimeAnimation.targetPath].additiveAnimations.push(runtimeAnimation); target._lateAnimationHolders[runtimeAnimation.targetPath].totalAdditiveWeight += runtimeAnimation.weight; } else { target._lateAnimationHolders[runtimeAnimation.targetPath].animations.push(runtimeAnimation); target._lateAnimationHolders[runtimeAnimation.targetPath].totalWeight += runtimeAnimation.weight; } } _setValue(target, destination, currentValue, weight, targetIndex) { // Set value this._currentActiveTarget = destination; this._weight = weight; if (this._enableBlending && this._blendingFactor <= 1.0) { if (!this._originalBlendValue) { const originalValue = destination[this._targetPath]; if (originalValue.clone) { this._originalBlendValue = originalValue.clone(); } else { this._originalBlendValue = originalValue; } } if (this._originalBlendValue.m) { // Matrix if (Animation.AllowMatrixDecomposeForInterpolation) { if (this._currentValue) { Matrix.DecomposeLerpToRef(this._originalBlendValue, currentValue, this._blendingFactor, this._currentValue); } else { this._currentValue = Matrix.DecomposeLerp(this._originalBlendValue, currentValue, this._blendingFactor); } } else { if (this._currentValue) { Matrix.LerpToRef(this._originalBlendValue, currentValue, this._blendingFactor, this._currentValue); } else { this._currentValue = Matrix.Lerp(this._originalBlendValue, currentValue, this._blendingFactor); } } } else { this._currentValue = Animation._UniversalLerp(this._originalBlendValue, currentValue, this._blendingFactor); } const blendingSpeed = target && target.animationPropertiesOverride ? target.animationPropertiesOverride.blendingSpeed : this._animation.blendingSpeed; this._blendingFactor += blendingSpeed; } else { if (!this._currentValue) { if (currentValue?.clone) { this._currentValue = currentValue.clone(); } else { this._currentValue = currentValue; } } else if (this._currentValue.copyFrom) { this._currentValue.copyFrom(currentValue); } else { this._currentValue = currentValue; } } if (weight !== -1) { this._registerTargetForLateAnimationBinding(this, this._originalValue[targetIndex]); } else { if (this._animationState.loopMode === Animation.ANIMATIONLOOPMODE_RELATIVE_FROM_CURRENT) { if (this._currentValue.addToRef) { this._currentValue.addToRef(this._originalValue[targetIndex], destination[this._targetPath]); } else { destination[this._targetPath] = this._originalValue[targetIndex] + this._currentValue; } } else { destination[this._targetPath] = this._currentValue; } } if (target.markAsDirty) { target.markAsDirty(this._animation.targetProperty); } } /** * Gets the loop pmode of the runtime animation * @returns Loop Mode */ _getCorrectLoopMode() { if (this._target && this._target.animationPropertiesOverride) { return this._target.animationPropertiesOverride.loopMode; } return this._animation.loopMode; } /** * Move the current animation to a given frame * @param frame defines the frame to move to * @param weight defines the weight to apply to the animation (-1.0 by default) */ goToFrame(frame, weight = -1) { const keys = this._animation.getKeys(); if (frame < keys[0].frame) { frame = keys[0].frame; } else if (frame > keys[keys.length - 1].frame) { frame = keys[keys.length - 1].frame; } // Need to reset animation events const events = this._events; if (events.length) { for (let index = 0; index < events.length; index++) { if (!events[index].onlyOnce) { // reset events in the future events[index].isDone = events[index].frame < frame; } } } this._currentFrame = frame; const currentValue = this._animation._interpolate(frame, this._animationState); this.setValue(currentValue, weight); } /** * @internal Internal use only */ _prepareForSpeedRatioChange(newSpeedRatio) { const newAbsoluteFrame = (this._previousElapsedTime * (this._animation.framePerSecond * newSpeedRatio)) / 1000.0; this._absoluteFrameOffset = this._previousAbsoluteFrame - newAbsoluteFrame; } /** * Execute the current animation * @param elapsedTimeSinceAnimationStart defines the elapsed time (in milliseconds) since the animation was started * @param from defines the lower frame of the animation range * @param to defines the upper frame of the animation range * @param loop defines if the current animation must loop * @param speedRatio defines the current speed ratio * @param weight defines the weight of the animation (default is -1 so no weight) * @returns a boolean indicating if the animation is running */ animate(elapsedTimeSinceAnimationStart, from, to, loop, speedRatio, weight = -1) { const animation = this._animation; const targetPropertyPath = animation.targetPropertyPath; if (!targetPropertyPath || targetPropertyPath.length < 1) { this._stopped = true; return false; } let returnValue = true; let currentFrame; const events = this._events; let frameRange = 0; if (!this._coreRuntimeAnimation) { // Check limits if (from < this._minFrame || from > this._maxFrame) { from = this._minFrame; } if (to < this._minFrame || to > this._maxFrame) { to = this._maxFrame; } frameRange = to - from; let offsetValue; // Compute the frame according to the elapsed time and the fps of the animation ("from" and "to" are not factored in!) let absoluteFrame = (elapsedTimeSinceAnimationStart * (animation.framePerSecond * speedRatio)) / 1000.0 + this._absoluteFrameOffset; let highLimitValue = 0; // Apply the yoyo function if required let yoyoLoop = false; const yoyoMode = loop && this._animationState.loopMode === Animation.ANIMATIONLOOPMODE_YOYO; if (yoyoMode) { const position = (absoluteFrame - from) / frameRange; // Apply the yoyo curve const sin = Math.sin(position * Math.PI); const yoyoPosition = Math.abs(sin); // Map the yoyo position back to the range absoluteFrame = yoyoPosition * frameRange + from; const direction = sin >= 0 ? 1 : -1; if (this._yoyoDirection !== direction) { yoyoLoop = true; } this._yoyoDirection = direction; } this._previousElapsedTime = elapsedTimeSinceAnimationStart; this._previousAbsoluteFrame = absoluteFrame; if (!loop && to >= from && ((absoluteFrame >= frameRange && speedRatio > 0) || (absoluteFrame <= 0 && speedRatio < 0))) { // If we are out of range and not looping get back to caller returnValue = false; highLimitValue = animation._getKeyValue(this._maxValue); } else if (!loop && from >= to && ((absoluteFrame <= frameRange && speedRatio < 0) || (absoluteFrame >= 0 && speedRatio > 0))) { returnValue = false; highLimitValue = animation._getKeyValue(this._minValue); } else if (this._animationState.loopMode !== Animation.ANIMATIONLOOPMODE_CYCLE) { const keyOffset = to.toString() + from.toString(); if (!this._offsetsCache[keyOffset]) { this._animationState.repeatCount = 0; this._animationState.loopMode = Animation.ANIMATIONLOOPMODE_CYCLE; // force a specific codepath in animation._interpolate()! const fromValue = animation._interpolate(from, this._animationState); const toValue = animation._interpolate(to, this._animationState); this._animationState.loopMode = this._getCorrectLoopMode(); switch (animation.dataType) { // Float case Animation.ANIMATIONTYPE_FLOAT: this._offsetsCache[keyOffset] = toValue - fromValue; break; // Quaternion case Animation.ANIMATIONTYPE_QUATERNION: this._offsetsCache[keyOffset] = toValue.subtract(fromValue); break; // Vector3 case Animation.ANIMATIONTYPE_VECTOR3: this._offsetsCache[keyOffset] = toValue.subtract(fromValue); break; // Vector2 case Animation.ANIMATIONTYPE_VECTOR2: this._offsetsCache[keyOffset] = toValue.subtract(fromValue); break; // Size case Animation.ANIMATIONTYPE_SIZE: this._offsetsCache[keyOffset] = toValue.subtract(fromValue); break; // Color3 case Animation.ANIMATIONTYPE_COLOR3: this._offsetsCache[keyOffset] = toValue.subtract(fromValue); break; } this._highLimitsCache[keyOffset] = toValue; } highLimitValue = this._highLimitsCache[keyOffset]; offsetValue = this._offsetsCache[keyOffset]; } if (offsetValue === undefined) { switch (animation.dataType) { // Float case Animation.ANIMATIONTYPE_FLOAT: offsetValue = 0; break; // Quaternion case Animation.ANIMATIONTYPE_QUATERNION: offsetValue = _StaticOffsetValueQuaternion; break; // Vector3 case Animation.ANIMATIONTYPE_VECTOR3: offsetValue = _StaticOffsetValueVector3; break; // Vector2 case Animation.ANIMATIONTYPE_VECTOR2: offsetValue = _StaticOffsetValueVector2; break; // Size case Animation.ANIMATIONTYPE_SIZE: offsetValue = _StaticOffsetValueSize; break; // Color3 case Animation.ANIMATIONTYPE_COLOR3: offsetValue = _StaticOffsetValueColor3; break; case Animation.ANIMATIONTYPE_COLOR4: offsetValue = _StaticOffsetValueColor4; break; } } // Compute value if (this._host && this._host.syncRoot) { // If we must sync with an animatable, calculate the current frame based on the frame of the root animatable const syncRoot = this._host.syncRoot; const hostNormalizedFrame = (syncRoot.masterFrame - syncRoot.fromFrame) / (syncRoot.toFrame - syncRoot.fromFrame); currentFrame = from + frameRange * hostNormalizedFrame; } else { if ((absoluteFrame > 0 && from > to) || (absoluteFrame < 0 && from < to)) { currentFrame = returnValue && frameRange !== 0 ? to + (absoluteFrame % frameRange) : from; } else { currentFrame = returnValue && frameRange !== 0 ? from + (absoluteFrame % frameRange) : to; } } // Reset event/state if looping if ((!yoyoMode && ((speedRatio > 0 && this.currentFrame > currentFrame) || (speedRatio < 0 && this.currentFrame < currentFrame))) || (yoyoMode && yoyoLoop)) { this._onLoop(); // Need to reset animation events for (let index = 0; index < events.length; index++) { if (!events[index].onlyOnce) { // reset event, the animation is looping events[index].isDone = false; } } this._animationState.key = speedRatio > 0 ? 0 : animation.getKeys().length - 1; } this._currentFrame = currentFrame; this._animationState.repeatCount = frameRange === 0 ? 0 : (absoluteFrame / frameRange) >> 0; this._animationState.highLimitValue = highLimitValue; this._animationState.offsetValue = offsetValue; } else { frameRange = to - from; currentFrame = this._coreRuntimeAnimation.currentFrame; this._currentFrame = currentFrame; this._animationState.repeatCount = this._coreRuntimeAnimation._animationState.repeatCount; this._animationState.highLimitValue = this._coreRuntimeAnimation._animationState.highLimitValue; this._animationState.offsetValue = this._coreRuntimeAnimation._animationState.offsetValue; } const currentValue = animation._interpolate(currentFrame, this._animationState); // Set value this.setValue(currentValue, weight); // Check events if (events.length) { for (let index = 0; index < events.length; index++) { // Make sure current frame has passed event frame and that event frame is within the current range // Also, handle both forward and reverse animations if ((frameRange >= 0 && currentFrame >= events[index].frame && events[index].frame >= from) || (frameRange < 0 && currentFrame <= events[index].frame && events[index].frame <= from)) { const event = events[index]; if (!event.isDone) { // If event should be done only once, remove it. if (event.onlyOnce) { events.splice(index, 1); index--; } event.isDone = true; event.action(currentFrame); } // Don't do anything if the event has already been done. } } } if (!returnValue) { this._stopped = true; } return returnValue; } } /** * Class used to store an actual running animation */ class Animatable { /** * Gets the root Animatable used to synchronize and normalize animations */ get syncRoot() { return this._syncRoot; } /** * Gets the current frame of the first RuntimeAnimation * Used to synchronize Animatables */ get masterFrame() { if (this._runtimeAnimations.length === 0) { return 0; } return this._runtimeAnimations[0].currentFrame; } /** * Gets or sets the animatable weight (-1.0 by default meaning not weighted) */ get weight() { return this._weight; } set weight(value) { if (value === -1) { // -1 is ok and means no weight this._weight = -1; return; } // Else weight must be in [0, 1] range this._weight = Math.min(Math.max(value, 0), 1.0); } /** * Gets or sets the speed ratio to apply to the animatable (1.0 by default) */ get speedRatio() { return this._speedRatio; } set speedRatio(value) { for (let index = 0; index < this._runtimeAnimations.length; index++) { const animation = this._runtimeAnimations[index]; animation._prepareForSpeedRatioChange(value); } this._speedRatio = value; // Resync _manualJumpDelay in case goToFrame was called before speedRatio was set. if (this._goToFrame !== null) { this.goToFrame(this._goToFrame); } } /** * Gets the elapsed time since the animatable started in milliseconds */ get elapsedTime() { return this._localDelayOffset === null ? 0 : this._scene._animationTime - this._localDelayOffset; } /** * Creates a new Animatable * @param scene defines the hosting scene * @param target defines the target object * @param fromFrame defines the starting frame number (default is 0) * @param toFrame defines the ending frame number (default is 100) * @param loopAnimation defines if the animation must loop (default is false) * @param speedRatio defines the factor to apply to animation speed (default is 1) * @param onAnimationEnd defines a callback to call when animation ends if it is not looping * @param animations defines a group of animation to add to the new Animatable * @param onAnimationLoop defines a callback to call when animation loops * @param isAdditive defines whether the animation should be evaluated additively * @param playOrder defines the order in which this animatable should be processed in the list of active animatables (default: 0) */ constructor(scene, /** defines the target object */ target, /** [0] defines the starting frame number (default is 0) */ fromFrame = 0, /** [100] defines the ending frame number (default is 100) */ toFrame = 100, /** [false] defines if the animation must loop (default is false) */ loopAnimation = false, speedRatio = 1.0, /** defines a callback to call when animation ends if it is not looping */ onAnimationEnd, animations, /** defines a callback to call when animation loops */ onAnimationLoop, /** [false] defines whether the animation should be evaluated additively */ isAdditive = false, /** [0] defines the order in which this animatable should be processed in the list of active animatables (default: 0) */ playOrder = 0) { this.target = target; this.fromFrame = fromFrame; this.toFrame = toFrame; this.loopAnimation = loopAnimation; this.onAnimationEnd = onAnimationEnd; this.onAnimationLoop = onAnimationLoop; this.isAdditive = isAdditive; this.playOrder = playOrder; this._localDelayOffset = null; this._pausedDelay = null; this._manualJumpDelay = null; /** @hidden */ this._runtimeAnimations = new Array(); this._paused = false; this._speedRatio = 1; this._weight = -1; this._previousWeight = -1; this._syncRoot = null; this._frameToSyncFromJump = null; this._goToFrame = null; /** * Gets or sets a boolean indicating if the animatable must be disposed and removed at the end of the animation. * This will only apply for non looping animation (default is true) */ this.disposeOnEnd = true; /** * Gets a boolean indicating if the animation has started */ this.animationStarted = false; /** * Observer raised when the animation ends */ this.onAnimationEndObservable = new Observable(); /** * Observer raised when the animation loops */ this.onAnimationLoopObservable = new Observable(); this._scene = scene; if (animations) { this.appendAnimations(target, animations); } this._speedRatio = speedRatio; scene._activeAnimatables.push(this); } // Methods /** * Synchronize and normalize current Animatable with a source Animatable * This is useful when using animation weights and when animations are not of the same length * @param root defines the root Animatable to synchronize with (null to stop synchronizing) * @returns the current Animatable */ syncWith(root) { this._syncRoot = root; if (root) { // Make sure this animatable will animate after the root const index = this._scene._activeAnimatables.indexOf(this); if (index > -1) { this._scene._activeAnimatables.splice(index, 1); this._scene._activeAnimatables.push(this); } } return this; } /** * Gets the list of runtime animations * @returns an array of RuntimeAnimation */ getAnimations() { return this._runtimeAnimations; } /** * Adds more animations to the current animatable * @param target defines the target of the animations * @param animations defines the new animations to add */ appendAnimations(target, animations) { for (let index = 0; index < animations.length; index++) { const animation = animations[index]; const newRuntimeAnimation = new RuntimeAnimation(target, animation, this._scene, this); newRuntimeAnimation._onLoop = () => { this.onAnimationLoopObservable.notifyObservers(this); if (this.onAnimationLoop) { this.onAnimationLoop(); } }; this._runtimeAnimations.push(newRuntimeAnimation); } } /** * Gets the source animation for a specific property * @param property defines the property to look for * @returns null or the source animation for the given property */ getAnimationByTargetProperty(property) { const runtimeAnimations = this._runtimeAnimations; for (let index = 0; index < runtimeAnimations.length; index++) { if (runtimeAnimations[index].animation.targetProperty === property) { return runtimeAnimations[index].animation; } } return null; } /** * Gets the runtime animation for a specific property * @param property defines the property to look for * @returns null or the runtime animation for the given property */ getRuntimeAnimationByTargetProperty(property) { const runtimeAnimations = this._runtimeAnimations; for (let index = 0; index < runtimeAnimations.length; index++) { if (runtimeAnimations[index].animation.targetProperty === property) { return runtimeAnimations[index]; } } return null; } /** * Resets the animatable to its original state */ reset() { const runtimeAnimations = this._runtimeAnimations; for (let index = 0; index < runtimeAnimations.length; index++) { runtimeAnimations[index].reset(true); } this._localDelayOffset = null; this._pausedDelay = null; } /** * Allows the animatable to blend with current running animations * @see https://doc.babylonjs.com/features/featuresDeepDive/animation/advanced_animations#animation-blending * @param blendingSpeed defines the blending speed to use */ enableBlending(blendingSpeed) { const runtimeAnimations = this._runtimeAnimations; for (let index = 0; index < runtimeAnimations.length; index++) { runtimeAnimations[index].animation.enableBlending = true; runtimeAnimations[index].animation.blendingSpeed = blendingSpeed; } } /** * Disable animation blending * @see https://doc.babylonjs.com/features/featuresDeepDive/animation/advanced_animations#animation-blending */ disableBlending() { const runtimeAnimations = this._runtimeAnimations; for (let index = 0; index < runtimeAnimations.length; index++) { runtimeAnimations[index].animation.enableBlending = false; } } /** * Jump directly to a given frame * @param frame defines the frame to jump to * @param useWeight defines whether the animation weight should be applied to the image to be jumped to (false by default) */ goToFrame(frame, useWeight = false) { const runtimeAnimations = this._runtimeAnimations; if (runtimeAnimations[0]) { const fps = runtimeAnimations[0].animation.framePerSecond; this._frameToSyncFromJump = this._frameToSyncFromJump ?? runtimeAnimations[0].currentFrame; const delay = this.speedRatio === 0 ? 0 : (((frame - this._frameToSyncFromJump) / fps) * 1000) / this.speedRatio; this._manualJumpDelay = -delay; } for (let index = 0; index < runtimeAnimations.length; index++) { runtimeAnimations[index].goToFrame(frame, useWeight ? this._weight : -1); } this._goToFrame = frame; } /** * Returns true if the animations for this animatable are paused */ get paused() { return this._paused; } /** * Pause the animation */ pause() { if (this._paused) { return; } this._paused = true; } /** * Restart the animation */ restart() { this._paused = false; } _raiseOnAnimationEnd() { if (this.onAnimationEnd) { this.onAnimationEnd(); } this.onAnimationEndObservable.notifyObservers(this); } /** * Stop and delete the current animation * @param animationName defines a string used to only stop some of the runtime animations instead of all * @param targetMask a function that determines if the animation should be stopped based on its target (all animations will be stopped if both this and animationName are empty) * @param useGlobalSplice if true, the animatables will be removed by the caller of this function (false by default) * @param skipOnAnimationEnd defines if the system should not raise onAnimationEnd. Default is false */ stop(animationName, targetMask, useGlobalSplice = false, skipOnAnimationEnd = false) { if (animationName || targetMask) { const idx = this._scene._activeAnimatables.indexOf(this); if (idx > -1) { const runtimeAnimations = this._runtimeAnimations; for (let index = runtimeAnimations.length - 1; index >= 0; index--) { const runtimeAnimation = runtimeAnimations[index]; if (animationName && runtimeAnimation.animation.name != animationName) { continue; } if (targetMask && !targetMask(runtimeAnimation.target)) { continue; } runtimeAnimation.dispose(); runtimeAnimations.splice(index, 1); } if (runtimeAnimations.length == 0) { if (!useGlobalSplice) { this._scene._activeAnimatables.splice(idx, 1); } if (!skipOnAnimationEnd) { this._raiseOnAnimationEnd(); } } } } else { const index = this._scene._activeAnimatables.indexOf(this); if (index > -1) { if (!useGlobalSplice) { this._scene._activeAnimatables.splice(index, 1); } const runtimeAnimations = this._runtimeAnimations; for (let index = 0; index < runtimeAnimations.length; index++) { runtimeAnimations[index].dispose(); } this._runtimeAnimations.length = 0; if (!skipOnAnimationEnd) { this._raiseOnAnimationEnd(); } } } } /** * Wait asynchronously for the animation to end * @returns a promise which will be fulfilled when the animation ends */ async waitAsync() { return await new Promise((resolve) => { this.onAnimationEndObservable.add(() => { resolve(this); }, undefined, undefined, this, true); }); } /** * @internal */ _animate(delay) { if (this._paused) { this.animationStarted = false; if (this._pausedDelay === null) { this._pausedDelay = delay; } return true; } if (this._localDelayOffset === null) { this._localDelayOffset = delay; this._pausedDelay = null; } else if (this._pausedDelay !== null) { this._localDelayOffset += delay - this._pausedDelay; this._pausedDelay = null; } if (this._manualJumpDelay !== null) { this._localDelayOffset += this.speedRatio < 0 ? -this._manualJumpDelay : this._manualJumpDelay; this._manualJumpDelay = null; this._frameToSyncFromJump = null; } this._goToFrame = null; if (this._weight === 0 && this._previousWeight === 0) { // We consider that an animatable with a weight === 0 is "actively" paused return true; } this._previousWeight = this._weight; // Animating let running = false; const runtimeAnimations = this._runtimeAnimations; let index; for (index = 0; index < runtimeAnimations.length; index++) { const animation = runtimeAnimations[index]; const isRunning = animation.animate(delay - this._localDelayOffset, this.fromFrame, this.toFrame, this.loopAnimation, this._speedRatio, this._weight); running = running || isRunning; } this.animationStarted = running; if (!running) { if (this.disposeOnEnd) { // Remove from active animatables index = this._scene._activeAnimatables.indexOf(this); this._scene._activeAnimatables.splice(index, 1); // Dispose all runtime animations for (index = 0; index < runtimeAnimations.length; index++) { runtimeAnimations[index].dispose(); } } this._raiseOnAnimationEnd(); if (this.disposeOnEnd) { this.onAnimationEnd = null; this.onAnimationLoop = null; this.onAnimationLoopObservable.clear(); this.onAnimationEndObservable.clear(); } } return running; } } /** @internal */ function ProcessLateAnimationBindingsForMatrices(holder) { if (holder.totalWeight === 0 && holder.totalAdditiveWeight === 0) { return holder.originalValue; } let normalizer = 1.0; const finalPosition = TmpVectors.Vector3[0]; const finalScaling = TmpVectors.Vector3[1]; const finalQuaternion = TmpVectors.Quaternion[0]; let startIndex = 0; const originalAnimation = holder.animations[0]; const originalValue = holder.originalValue; let scale = 1; let skipOverride = false; if (holder.totalWeight < 1.0) { // We need to mix the original value in scale = 1.0 - holder.totalWeight; originalValue.decompose(finalScaling, finalQuaternion, finalPosition); } else { startIndex = 1; // We need to normalize the weights normalizer = holder.totalWeight; scale = originalAnimation.weight / normalizer; if (scale == 1) { if (holder.totalAdditiveWeight) { skipOverride = true; } else { return originalAnimation.currentValue; } } originalAnimation.currentValue.decompose(finalScaling, finalQuaternion, finalPosition); } // Add up the override animations if (!skipOverride) { finalScaling.scaleInPlace(scale); finalPosition.scaleInPlace(scale); finalQuaternion.scaleInPlace(scale); for (let animIndex = startIndex; animIndex < holder.animations.length; animIndex++) { const runtimeAnimation = holder.animations[animIndex]; if (runtimeAnimation.weight === 0) { continue; } scale = runtimeAnimation.weight / normalizer; const currentPosition = TmpVectors.Vector3[2]; const currentScaling = TmpVectors.Vector3[3]; const currentQuaternion = TmpVectors.Quaternion[1]; runtimeAnimation.currentValue.decompose(currentScaling, currentQuaternion, currentPosition); currentScaling.scaleAndAddToRef(scale, finalScaling); currentQuaternion.scaleAndAddToRef(Quaternion.Dot(finalQuaternion, currentQuaternion) > 0 ? scale : -scale, finalQuaternion); currentPosition.scaleAndAddToRef(scale, finalPosition); } finalQuaternion.normalize(); } // Add up the additive animations for (let animIndex = 0; animIndex < holder.additiveAnimations.length; animIndex++) { const runtimeAnimation = holder.additiveAnimations[animIndex]; if (runtimeAnimation.weight === 0) { continue; } const currentPosition = TmpVectors.Vector3[2]; const currentScaling = TmpVectors.Vector3[3]; const currentQuaternion = TmpVectors.Quaternion[1]; runtimeAnimation.currentValue.decompose(currentScaling, currentQuaternion, currentPosition); currentScaling.multiplyToRef(finalScaling, currentScaling); Vector3.LerpToRef(finalScaling, currentScaling, runtimeAnimation.weight, finalScaling); finalQuaternion.multiplyToRef(currentQuaternion, currentQuaternion); Quaternion.SlerpToRef(finalQuaternion, currentQuaternion, runtimeAnimation.weight, finalQuaternion); currentPosition.scaleAndAddToRef(runtimeAnimation.weight, finalPosition); } const workValue = originalAnimation ? originalAnimation._animationState.workValue : TmpVectors.Matrix[0].clone(); Matrix.ComposeToRef(finalScaling, finalQuaternion, finalPosition, workValue); return workValue; } /** @internal */ function ProcessLateAnimationBindingsForQuaternions(holder, refQuaternion) { if (holder.totalWeight === 0 && holder.totalAdditiveWeight === 0) { return refQuaternion; } const originalAnimation = holder.animations[0]; const originalValue = holder.originalValue; let cumulativeQuaternion = refQuaternion; if (holder.totalWeight === 0 && holder.totalAdditiveWeight > 0) { cumulativeQuaternion.copyFrom(originalValue); } else if (holder.animations.length === 1) { Quaternion.SlerpToRef(originalValue, originalAnimation.currentValue, Math.min(1.0, holder.totalWeight), cumulativeQuaternion); if (holder.totalAdditiveWeight === 0) { return cumulativeQuaternion; } } else if (holder.animations.length > 1) { // Add up the override animations let normalizer = 1.0; let quaternions; let weights; if (holder.totalWeight < 1.0) { const scale = 1.0 - holder.totalWeight; quaternions = []; weights = []; quaternions.push(originalValue); weights.push(scale); } else { if (holder.animations.length === 2) { // Slerp as soon as we can Quaternion.SlerpToRef(holder.animations[0].currentValue, holder.animations[1].currentValue, holder.animations[1].weight / holder.totalWeight, refQuaternion); if (holder.totalAdditiveWeight === 0) { return refQuaternion; } } quaternions = []; weights = []; normalizer = holder.totalWeight; } for (let animIndex = 0; animIndex < holder.animations.length; animIndex++) { const runtimeAnimation = holder.animations[animIndex]; quaternions.push(runtimeAnimation.currentValue); weights.push(runtimeAnimation.weight / normalizer); } // https://gamedev.stackexchange.com/questions/62354/method-for-interpolation-between-3-quaternions let cumulativeAmount = 0; for (let index = 0; index < quaternions.length;) { if (!index) { Quaternion.SlerpToRef(quaternions[index], quaternions[index + 1], weights[index + 1] / (weights[index] + weights[index + 1]), refQuaternion); cumulativeQuaternion = refQuaternion; cumulativeAmount = weights[index] + weights[index + 1]; index += 2; continue; } cumulativeAmount += weights[index]; Quaternion.SlerpToRef(cumulativeQuaternion, quaternions[index], weights[index] / cumulativeAmount, cumulativeQuaternion); index++; } } // Add up the additive animations for (let animIndex = 0; animIndex < holder.additiveAnimations.length; animIndex++) { const runtimeAnimation = holder.additiveAnimations[animIndex]; if (runtimeAnimation.weight === 0) { continue; } cumulativeQuaternion.multiplyToRef(runtimeAnimation.currentValue, TmpVectors.Quaternion[0]); Quaternion.SlerpToRef(cumulativeQuaternion, TmpVectors.Quaternion[0], runtimeAnimation.weight, cumulativeQuaternion); } return cumulativeQuaternion; } /** @internal */ function ProcessLateAnimationBindings(scene) { if (!scene._registeredForLateAnimationBindings.length) { return; } for (let index = 0; index < scene._registeredForLateAnimationBindings.length; index++) { const target = scene._registeredForLateAnimationBindings.data[index]; for (const path in target._lateAnimationHolders) { const holder = target._lateAnimationHolders[path]; const originalAnimation = holder.animations[0]; const originalValue = holder.originalValue; if (originalValue === undefined || originalValue === null) { continue; } const matrixDecomposeMode = Animation.AllowMatrixDecomposeForInterpolation && originalValue.m; // ie. data is matrix let finalValue = target[path]; if (matrixDecomposeMode) { finalValue = ProcessLateAnimationBindingsForMatri