UNPKG

babylon-mmd

Version:
1,093 lines (1,092 loc) 46.6 kB
import { Logger } from "@babylonjs/core/Misc/logger"; import { Observable } from "@babylonjs/core/Misc/observable"; import { MmdMesh } from "../mmdMesh"; import { MmdRuntimeShared } from "../mmdRuntimeShared"; import { WasmSpinlock } from "./Misc/wasmSpinlock"; import { MmdMetadataEncoder } from "./mmdMetadataEncoder"; import { MmdWasmModel } from "./mmdWasmModel"; import { NullPhysicsClock } from "./Physics/nullPhysicsClock"; /** * MMD WASM runtime animation evaluation type */ export var MmdWasmRuntimeAnimationEvaluationType; (function (MmdWasmRuntimeAnimationEvaluationType) { /** * Immediate animation evaluation for the current frame */ MmdWasmRuntimeAnimationEvaluationType[MmdWasmRuntimeAnimationEvaluationType["Immediate"] = 0] = "Immediate"; /** * Buffered animation evaluation for the next frame * * Asynchronous Multi-thread optimization applies when possible * * If you are using havok or ammo.js physics, only beforePhysics process is asynchronous */ MmdWasmRuntimeAnimationEvaluationType[MmdWasmRuntimeAnimationEvaluationType["Buffered"] = 1] = "Buffered"; })(MmdWasmRuntimeAnimationEvaluationType || (MmdWasmRuntimeAnimationEvaluationType = {})); /** * MMD WASM runtime orchestrates several MMD components (models, camera, audio) * * MMD WASM runtime handles updates and synchronization of MMD components * * It can also create and remove runtime components */ export class MmdWasmRuntime { /** * @internal */ wasmInstance; /** * @internal */ wasmInternal; /** * Spinlock for MMD WASM runtime to synchronize animation evaluation * @internal */ lock; _usingWasmBackBuffer; _lastRequestAnimationFrameTime; _needToSyncEvaluate; _externalPhysics; _physicsRuntime; _physicsClock; _mmdMetadataEncoder; _models; _animatables; _audioPlayer; /** * Whether to automatically initialize rigid bodies transform and velocity (default: true) * * auto physics initialization is triggered when * - animation seek is far from current frame time (more than 2 seconds) * - browser tab is stop rendering and resumed * - animation is played from the frame 0 */ autoPhysicsInitialization; _loggingEnabled; /** @internal */ log; /** @internal */ warn; /** @internal */ error; _isRegistered; /** * This observable is notified when animation duration is changed */ onAnimationDurationChangedObservable; /** * This observable is notified when animation is played */ onPlayAnimationObservable; /** * This observable is notified when animation is paused */ onPauseAnimationObservable; /** * This observable is notified when animation is seeked */ onSeekAnimationObservable; /** * This observable is notified when animation is evaluated (usually every frame) */ onAnimationTickObservable; _evaluationType; _currentFrameTime; _animationTimeScale; _animationPaused; _animationFrameTimeDuration; _useManualAnimationDuration; _needToInitializePhysicsModels; _needToInitializePhysicsModelsBuffer; _beforePhysicsBinded; _afterPhysicsBinded; _bindedDispose; _disposeObservableObject; /** * Creates a new MMD web assembly runtime * * For use external physics engine like ammo.js or havok, you need to set `physics` to instance of `IMmdPhysics` * * If you want to use the wasm binary built-in physics engine, you can set `physics` to `MmdWasmPhysics` and you should use physics version of the wasm instance (e.g. `MmdWasmInstanceTypeMPR`) * @param wasmInstance MMD WASM instance * @param scene Objects that limit the lifetime of this instance * @param physics IMmdPhysics instance or MmdWasmPhysics instance */ constructor(wasmInstance, scene = null, physics = null) { this.wasmInstance = wasmInstance; this.wasmInternal = wasmInstance.createMmdRuntime(); this.lock = new WasmSpinlock(wasmInstance.createTypedArray(Uint8Array, this.wasmInternal.getLockStatePtr(), 1)); this._usingWasmBackBuffer = false; this._lastRequestAnimationFrameTime = null; this._needToSyncEvaluate = true; if (physics?.createRuntime !== undefined) { this._externalPhysics = null; this._physicsRuntime = physics.createRuntime(this); this._physicsClock = physics.createPhysicsClock(); this._mmdMetadataEncoder = physics.createMetadataEncoder(this._physicsRuntime, this); } else { this._externalPhysics = physics; this._physicsRuntime = null; this._physicsClock = new NullPhysicsClock(); this._mmdMetadataEncoder = new MmdMetadataEncoder(this); } this._models = []; this._animatables = []; this._audioPlayer = null; this.autoPhysicsInitialization = true; this._loggingEnabled = false; this.log = this._logDisabled; this.warn = this._warnDisabled; this.error = this._errorDisabled; this._isRegistered = false; this.onAnimationDurationChangedObservable = new Observable(); this.onPlayAnimationObservable = new Observable(); this.onPauseAnimationObservable = new Observable(); this.onSeekAnimationObservable = new Observable(); this.onAnimationTickObservable = new Observable(); this._evaluationType = MmdWasmRuntimeAnimationEvaluationType.Immediate; this._currentFrameTime = 0; this._animationTimeScale = 1; this._animationPaused = true; this._animationFrameTimeDuration = 0; this._useManualAnimationDuration = false; this._needToInitializePhysicsModels = new Set(); this._needToInitializePhysicsModelsBuffer = new Set(); this._beforePhysicsBinded = null; this._afterPhysicsBinded = this.afterPhysics.bind(this); if (scene !== null) { this._bindedDispose = () => this.dispose(scene); this._disposeObservableObject = scene; if (this._disposeObservableObject !== null) { this._disposeObservableObject.onDisposeObservable.add(this._bindedDispose); } } else { this._bindedDispose = null; this._disposeObservableObject = null; } } /** * Dispose MMD WASM runtime * * Destroy all MMD models and unregister this runtime from scene * @param scene Scene */ dispose(scene) { this.lock.wait(); // ensure that the runtime is not evaluating animations this._physicsRuntime?.dispose(); for (let i = 0; i < this._models.length; ++i) this._models[i]._dispose(); this._models.length = 0; this._animatables.length = 0; this.setAudioPlayer(null); this.onAnimationDurationChangedObservable.clear(); this.onPlayAnimationObservable.clear(); this.onPauseAnimationObservable.clear(); this.onSeekAnimationObservable.clear(); this.onAnimationTickObservable.clear(); this._needToInitializePhysicsModels.clear(); this._needToInitializePhysicsModelsBuffer.clear(); this.unregister(scene); this.wasmInternal.free(); if (this._disposeObservableObject !== null && this._bindedDispose !== null) { this._disposeObservableObject.onDisposeObservable.removeCallback(this._bindedDispose); } } static _TextDecoder = new TextDecoder(); _flushWasmDiagnosticFromResultPtr(resultPtr, logFunction) { const textDecoder = MmdWasmRuntime._TextDecoder; const [stringArrayPtr, stringArrayLength] = this.wasmInstance.createTypedArray(Uint32Array, resultPtr, 4).array; if (stringArrayLength <= 0) return; const stringArray = this.wasmInstance.createTypedArray(Uint32Array, stringArrayPtr, stringArrayLength * 2).array; for (let i = 0; i < stringArrayLength; ++i) { const stringPtr = stringArray[i * 2]; const stringLength = stringArray[i * 2 + 1]; const textBuffer = new Uint8Array(stringLength); textBuffer.set(this.wasmInstance.createTypedArray(Uint8Array, stringPtr, stringLength).array); const text = textDecoder.decode(textBuffer); logFunction(text); } } // flush wasm diagnostic log is always thread safe _flushWasmDiagnosticLog() { // we need acquire and release the result to flush the log even if logging is disabled const errorResultPtr = this.wasmInternal.acquireDiagnosticErrorResult(); if (this._loggingEnabled) { this._flushWasmDiagnosticFromResultPtr(errorResultPtr, this.error); } this.wasmInternal.releaseDiagnosticResult(); const warningResultPtr = this.wasmInternal.acquireDiagnosticWarningResult(); if (this._loggingEnabled) { this._flushWasmDiagnosticFromResultPtr(warningResultPtr, this.warn); } this.wasmInternal.releaseDiagnosticResult(); const infoResultPtr = this.wasmInternal.acquireDiagnosticInfoResult(); if (this._loggingEnabled) { this._flushWasmDiagnosticFromResultPtr(infoResultPtr, this.log); } this.wasmInternal.releaseDiagnosticResult(); } static _NullPhysicsInitializeSet = { add(_model) { } }; _getPhysicsInitializeSet() { if (this._externalPhysics === null) { return this._physicsRuntime?.initializer ?? MmdWasmRuntime._NullPhysicsInitializeSet; } else { return this._needToInitializePhysicsModels; } } /** * Create MMD model from mesh that has MMD metadata * * The skeletons in the mesh where the MmdModel was created no longer follow the usual matrix update policy * @param mmdSkinnedMesh MmdSkinnedMesh * @param options Creation options * @returns MMD model * @throws {Error} if mesh is not `MmdSkinnedMesh` */ createMmdModel(mmdSkinnedMesh, options = {}) { if (!MmdMesh.isMmdSkinnedMesh(mmdSkinnedMesh)) throw new Error("Mesh validation failed."); return this.createMmdModelFromSkeleton(mmdSkinnedMesh, mmdSkinnedMesh.metadata.skeleton, options); } /** * Create MMD model from humanoid mesh and virtual skeleton * * this method is useful for supporting humanoid models, usually used by `HumanoidMmd` * @param mmdMesh MmdSkinnedMesh * @param skeleton Skeleton or Virtualized skeleton * @param options Creation options */ createMmdModelFromSkeleton(mmdMesh, skeleton, options = {}) { if (options.materialProxyConstructor === undefined) { options.materialProxyConstructor = MmdRuntimeShared.MaterialProxyConstructor; if (options.materialProxyConstructor === null) { this.log("Material proxy constructor is null. Material morphing features will be disabled for this model. Please provide a valid materialProxyConstructor in IMmdModelCreationOptions if you require material morphing support."); } } if (options.buildPhysics === undefined) { options.buildPhysics = true; } if (options.trimMetadata === undefined) { options.trimMetadata = true; } this.lock.wait(); // ensure that the runtime is not evaluating animations const wasmRuntime = this.wasmInternal; // sync buffer temporarily const usingWasmBackBuffer = this._usingWasmBackBuffer; if (usingWasmBackBuffer) { wasmRuntime.swapWorldMatrixBuffer(); this._usingWasmBackBuffer = false; } const metadataEncoder = this._mmdMetadataEncoder; metadataEncoder.setEncodePhysicsOptions(options.buildPhysics); const metadataSize = metadataEncoder.computeSize(mmdMesh); const metadataBufferPtr = this.wasmInstance.allocateBuffer(metadataSize); const metadataBuffer = this.wasmInstance.createTypedArray(Uint8Array, metadataBufferPtr, metadataSize); const wasmMorphIndexMap = metadataEncoder.encode(mmdMesh, skeleton.bones, metadataBuffer.array); const mmdModelPtr = wasmRuntime.createMmdModel(metadataBufferPtr, metadataSize); const model = new MmdWasmModel(this, mmdModelPtr, mmdMesh, skeleton, options.materialProxyConstructor, wasmMorphIndexMap, options.buildPhysics ? this._externalPhysics !== null ? { physicsImpl: this._externalPhysics, physicsOptions: typeof options.buildPhysics === "boolean" ? null : options.buildPhysics } : null : null, options.trimMetadata); this._models.push(model); this._getPhysicsInitializeSet().add(model); this.wasmInstance.deallocateBuffer(metadataBufferPtr, metadataSize); // desync again if (usingWasmBackBuffer) { wasmRuntime.swapWorldMatrixBuffer(); this._usingWasmBackBuffer = true; } this._flushWasmDiagnosticLog(); // because the model is created, the animation must be evaluated synchronously at least once this._needToSyncEvaluate = true; model.onAnimationDurationChangedObservable.add(this._onAnimationDurationChanged); return model; } /** * Destroy MMD model * * Dispose all resources used at MMD runtime and restores the skeleton to the usual matrix update policy * * After calling the `destroyMmdModel` once, the mesh is no longer able to `createMmdModel` because the metadata is lost * @param mmdModel MMD model to destroy * @throws {Error} if model is not found */ destroyMmdModel(mmdModel) { mmdModel._dispose(); const models = this._models; const index = models.indexOf(mmdModel); if (index < 0) throw new Error("Model not found."); models.splice(index, 1); this.lock.wait(); // ensure that the runtime is not evaluating animations this.wasmInternal.destroyMmdModel(mmdModel.ptr); } /** * Queue MMD model to initialize physics * * Actual physics initialization is done by the before physics stage * @param mmdModel MMD model */ initializeMmdModelPhysics(mmdModel) { this._getPhysicsInitializeSet().add(mmdModel); } /** * Queue all MMD models to initialize physics * * Actual physics initialization is done by the before physics stage * * If you set onlyAnimated true, it only initializes physics for animated models */ initializeAllMmdModelsPhysics(onlyAnimated) { const models = this._models; if (onlyAnimated) { for (let i = 0; i < models.length; ++i) { const model = models[i]; if (model.currentAnimation !== null) { this._getPhysicsInitializeSet().add(model); } } } else { for (let i = 0; i < models.length; ++i) { this._getPhysicsInitializeSet().add(models[i]); } } } /** * Add Animatable object to the runtime * * Usually this is MMD camera, but you can add any object that implements `IMmdRuntimeAnimatable` * @param animatable Animatable object * @return true if the animatable is added, false if the animatable is already added */ addAnimatable(animatable) { if (this._animatables.includes(animatable)) { return false; } animatable.onAnimationDurationChangedObservable?.add(this._onAnimationDurationChanged); this._animatables.push(animatable); this._onAnimationDurationChanged(animatable.animationFrameTimeDuration); return true; } /** * Remove Animatable object from the runtime * @param animatable Animatable object * @return true if the animatable is removed, false if the animatable is not found */ removeAnimatable(animatable) { const index = this._animatables.indexOf(animatable); if (index === -1) { return false; } animatable.onAnimationDurationChangedObservable?.removeCallback(this._onAnimationDurationChanged); this._animatables.splice(index, 1); this._onAnimationDurationChanged(0); return true; } _setAudioPlayerLastValue = null; /** * Set audio player to sync with animation * * If you set up audio Player while playing an animation, it try to play the audio from the current animation time * And returns Promise because this operation is asynchronous. In most cases, you don't have to await this Promise * @param audioPlayer Audio player * @returns Promise */ // eslint-disable-next-line @typescript-eslint/naming-convention async setAudioPlayer(audioPlayer) { if (this._audioPlayer === audioPlayer) return; this._setAudioPlayerLastValue = audioPlayer; if (this._audioPlayer !== null) { this._audioPlayer.onDurationChangedObservable.removeCallback(this._onAudioDurationChanged); this._audioPlayer.onPlaybackRateChangedObservable.removeCallback(this._onAudioPlaybackRateChanged); this._audioPlayer.onPlayObservable.removeCallback(this._onAudioPlay); this._audioPlayer.onPauseObservable.removeCallback(this._onAudioPause); this._audioPlayer.onSeekObservable.removeCallback(this._onAudioSeek); this._audioPlayer.pause(); } this._audioPlayer = null; if (audioPlayer === null) { this._onAudioDurationChanged(); return; } // sync audio player time with current frame time const audioFrameTimeDuration = audioPlayer.duration * 30; if (this._currentFrameTime < audioFrameTimeDuration) { audioPlayer.currentTime = this._currentFrameTime / 30; } // sync audio player pause state with animation pause state if (this._animationPaused !== audioPlayer.paused) { if (this._animationPaused) { audioPlayer.pause(); } else { if (audioFrameTimeDuration <= this._currentFrameTime) { await audioPlayer.play(); // for handle race condition if (this._setAudioPlayerLastValue !== audioPlayer) { audioPlayer.pause(); return; } } } } this._audioPlayer = audioPlayer; this._onAudioDurationChanged(); audioPlayer.onDurationChangedObservable.add(this._onAudioDurationChanged); audioPlayer.onPlaybackRateChangedObservable.add(this._onAudioPlaybackRateChanged); audioPlayer.onPlayObservable.add(this._onAudioPlay); audioPlayer.onPauseObservable.add(this._onAudioPause); audioPlayer.onSeekObservable.add(this._onAudioSeek); audioPlayer._setPlaybackRateWithoutNotify(this._animationTimeScale); } /** * Register MMD runtime to scene * * register `beforePhysics` and `afterPhysics` to scene Observables * * If you need a more complex update method you can call `beforePhysics` and `afterPhysics` manually * @param scene Scene */ register(scene) { if (this._isRegistered) return; this._isRegistered = true; this._beforePhysicsBinded = () => this.beforePhysics(scene.getEngine().getDeltaTime()); scene.onBeforeAnimationsObservable.add(this._beforePhysicsBinded); scene.onBeforeRenderObservable.add(this._afterPhysicsBinded); } /** * Unregister MMD runtime from scene * @param scene Scene */ unregister(scene) { if (!this._isRegistered) return; this._isRegistered = false; scene.onBeforeAnimationsObservable.removeCallback(this._beforePhysicsBinded); scene.onBeforeRenderObservable.removeCallback(this._afterPhysicsBinded); this._beforePhysicsBinded = null; } /** * Before the physics stage, update animations and run MMD runtime solvers * * @param deltaTime Delta time in milliseconds */ beforePhysics(deltaTime) { let elapsedFrameTime = null; if (!this._animationPaused) { if (this._audioPlayer !== null && !this._audioPlayer.paused) { // sync animation time with audio time const audioPlayerCurrentTime = this._audioPlayer.currentTime; const timeDiff = audioPlayerCurrentTime - this._currentFrameTime / 30; const timeDiffAbs = Math.abs(timeDiff); if (timeDiffAbs < 0.05) { // synced this._currentFrameTime += deltaTime / 1000 * 30 * this._animationTimeScale; } else if (timeDiffAbs < 0.5) { if (timeDiff < 0) { // animation is faster than audio this._currentFrameTime += deltaTime / 1000 * 30 * this._animationTimeScale * 0.9; } else { // animation is slower than audio this._currentFrameTime += deltaTime / 1000 * 30 * this._animationTimeScale * 1.1; } } else { if (this.autoPhysicsInitialization) { if (2 * 30 < Math.abs(audioPlayerCurrentTime - this._currentFrameTime)) { const physicsInitializeSet = this._getPhysicsInitializeSet(); for (let i = 0; i < this._models.length; ++i) { const model = this._models[i]; if (model.currentAnimation !== null) { physicsInitializeSet.add(model); } } } } this._currentFrameTime = audioPlayerCurrentTime * 30; } } else { // only use delta time to calculate animation time this._currentFrameTime += deltaTime / 1000 * 30 * this._animationTimeScale; } elapsedFrameTime = this._currentFrameTime; if (this._animationFrameTimeDuration <= elapsedFrameTime) { this._animationPaused = true; this._currentFrameTime = this._animationFrameTimeDuration; if (this._audioPlayer !== null && !this._audioPlayer.paused) { this._audioPlayer.pause(); } else { this.onPauseAnimationObservable.notifyObservers(); } } } const physicsDeltaTime = this._physicsClock.getDeltaTime(); // update mmd models world matrix in scene hierarchy this._physicsRuntime?.setMmdModelsWorldMatrix(this._models); if (this._evaluationType === MmdWasmRuntimeAnimationEvaluationType.Buffered) { const models = this._models; // evaluate vertex / uv animations on javascript side // they can't be buffered { const lastRequestAnimationFrameTime = this._lastRequestAnimationFrameTime ?? elapsedFrameTime; for (let i = 0; i < models.length; ++i) { const model = models[i]; if (model.currentAnimation?.wasmAnimate !== undefined) { models[i].beforePhysicsAndWasm(lastRequestAnimationFrameTime); } } if (lastRequestAnimationFrameTime !== null) { const animatables = this._animatables; for (let i = 0; i < animatables.length; ++i) { animatables[i].animate(lastRequestAnimationFrameTime); } } } if (this.wasmInstance.MmdRuntime.bufferedUpdate === undefined) { // single thread environment fallback this.wasmInternal.beforePhysics(this._lastRequestAnimationFrameTime ?? undefined, physicsDeltaTime); if (this._externalPhysics === null) this.wasmInternal.afterPhysics(); } this.lock.wait(); // ensure that the runtime is not evaluating animations this._physicsRuntime?.impl?.beforeStep(); // for physics object buffer swap and commit changes // desync buffer if (this._usingWasmBackBuffer === false) { this._usingWasmBackBuffer = true; this.wasmInternal.swapWorldMatrixBuffer(); } // if there is no previous evaluated frame time, evaluate animation synchronously if (this._lastRequestAnimationFrameTime === null) { if (elapsedFrameTime !== null) { // evaluate animations on javascript side for (let i = 0; i < models.length; ++i) { const model = models[i]; if (model.currentAnimation?.wasmAnimate === undefined) { models[i].beforePhysicsAndWasm(elapsedFrameTime); } } // compute world matrix on wasm side synchronously this.wasmInternal.beforePhysics(elapsedFrameTime ?? undefined, physicsDeltaTime); if (this._externalPhysics === null) this.wasmInternal.afterPhysics(); } else { // if there is uninitialized new model, evaluate animation synchronously if (this._needToSyncEvaluate) { this._needToSyncEvaluate = false; // move initialization sets to back buffer const needToInitializePhysicsModelsBuffer = this._needToInitializePhysicsModelsBuffer; for (const model of this._needToInitializePhysicsModels) { needToInitializePhysicsModelsBuffer.add(model); } // compute world matrix on wasm side synchronously this.wasmInternal.beforePhysics(undefined, physicsDeltaTime); if (this._externalPhysics === null) this.wasmInternal.afterPhysics(); } } } else { // if there is uninitialized new model, evaluate animation synchronously if (this._needToSyncEvaluate) { this._needToSyncEvaluate = false; // move initialization sets to back buffer const needToInitializePhysicsModelsBuffer = this._needToInitializePhysicsModelsBuffer; for (const model of this._needToInitializePhysicsModels) { needToInitializePhysicsModelsBuffer.add(model); } // evaluate animations on javascript side for (let i = 0; i < models.length; ++i) { const model = models[i]; if (model.currentAnimation?.wasmAnimate === undefined) { models[i].beforePhysicsAndWasm(this._lastRequestAnimationFrameTime); } } // compute world matrix on wasm side synchronously this.wasmInternal.beforePhysics(this._lastRequestAnimationFrameTime, physicsDeltaTime); if (this._externalPhysics === null) this.wasmInternal.afterPhysics(); } } for (let i = 0; i < models.length; ++i) models[i].swapWorldTransformMatricesBuffer(); this.wasmInternal.swapWorldMatrixBuffer(); for (let i = 0; i < models.length; ++i) { const model = models[i]; model.beforePhysics(); // sync body to bone } // update bone animation state on javascript side for (let i = 0; i < models.length; ++i) { const model = models[i]; if (model.currentAnimation?.wasmAnimate === undefined) { models[i].beforePhysicsAndWasm(elapsedFrameTime); } } // compute world matrix on wasm side asynchronously if (this._externalPhysics === null) { this.wasmInstance.MmdRuntime.bufferedUpdate?.(this.wasmInternal, elapsedFrameTime ?? undefined, physicsDeltaTime); } this._lastRequestAnimationFrameTime = elapsedFrameTime; if (this._externalPhysics !== null) { // physics initialization for external physics // physics initialization must be buffered 1 frame const needToInitializePhysicsModelsBuffer = this._needToInitializePhysicsModelsBuffer; for (const model of needToInitializePhysicsModelsBuffer) { model.initializePhysics(); } needToInitializePhysicsModelsBuffer.clear(); const needToInitializePhysicsModels = this._needToInitializePhysicsModels; for (const model of needToInitializePhysicsModels) { needToInitializePhysicsModelsBuffer.add(model); } needToInitializePhysicsModels.clear(); } // physics initialization for wasm integrated physics is done in wasm side } else { this._physicsRuntime?.impl?.beforeStep(); // for physics object buffer swap and commit changes // sync buffer if (this._usingWasmBackBuffer === true) { if (this._externalPhysics !== null) { // for external physics // move buffered initialization sets to current initialization sets const needToInitializePhysicsModels = this._needToInitializePhysicsModels; for (const model of this._needToInitializePhysicsModelsBuffer) { needToInitializePhysicsModels.add(model); } } // for wasm integrated physics, move initialization sets to back buffer is performed in wasm side this.lock.wait(); // ensure that the runtime is not evaluating animations this._usingWasmBackBuffer = false; this._lastRequestAnimationFrameTime = null; this.wasmInternal.swapWorldMatrixBuffer(); } const models = this._models; for (let i = 0; i < models.length; ++i) models[i].beforePhysicsAndWasm(elapsedFrameTime); this.wasmInternal.beforePhysics(elapsedFrameTime ?? undefined, physicsDeltaTime); for (let i = 0; i < models.length; ++i) models[i].beforePhysics(); this._needToSyncEvaluate = false; if (this._externalPhysics !== null) { // physics initialization for external physics const needToInitializePhysicsModels = this._needToInitializePhysicsModels; for (const model of needToInitializePhysicsModels) { model.initializePhysics(); } needToInitializePhysicsModels.clear(); } if (elapsedFrameTime !== null) { const animatables = this._animatables; for (let i = 0; i < animatables.length; ++i) { const animatable = animatables[i]; animatable.animate(elapsedFrameTime); } } } this._physicsRuntime?.impl?.afterStep(); // for trigger onSyncObservable and onTickObservable of physics runtime if (elapsedFrameTime !== null) { this.onAnimationTickObservable.notifyObservers(); } } /** * After the physics stage, update physics and run MMD runtime solvers */ afterPhysics() { const models = this._models; if (this._usingWasmBackBuffer) { for (let i = 0; i < models.length; ++i) { const model = models[i]; model.afterPhysicsAndWasm(); model.afterPhysics(); // actually, afterPhysics can be called before "wasm side afterPhysics" } if (this._externalPhysics !== null) { { // afterPhysics is not thread safe, buffered evaluation must be done after afterPhysics this.wasmInternal.swapWorldMatrixBuffer(); this.wasmInternal.afterPhysics(); this.wasmInternal.swapWorldMatrixBuffer(); } const physicsDeltaTime = this._physicsClock.getDeltaTime(); this.wasmInstance.MmdRuntime.bufferedBeforePhysics?.(this.wasmInternal, this._lastRequestAnimationFrameTime ?? undefined, physicsDeltaTime); } } else { for (let i = 0; i < models.length; ++i) models[i].afterPhysicsAndWasm(); this.wasmInternal.afterPhysics(); for (let i = 0; i < models.length; ++i) models[i].afterPhysics(); } } _onAnimationDurationChanged = (newAnimationFrameTimeDuration) => { if (this._useManualAnimationDuration) return; if (this._animationFrameTimeDuration < newAnimationFrameTimeDuration) { this._animationFrameTimeDuration = newAnimationFrameTimeDuration; } else if (newAnimationFrameTimeDuration < this._animationFrameTimeDuration) { this._animationFrameTimeDuration = this._computeAnimationDuration(); } this.onAnimationDurationChangedObservable.notifyObservers(); }; _computeAnimationDuration() { let duration = 0; const models = this._models; for (let i = 0; i < models.length; ++i) { const model = models[i]; if (model.currentAnimation !== null) { duration = Math.max(duration, model.currentAnimation.animation.endFrame); } } const animatables = this._animatables; for (let i = 0; i < animatables.length; ++i) { const animatable = animatables[i]; duration = Math.max(duration, animatable.animationFrameTimeDuration); } if (this._audioPlayer !== null) { duration = Math.max(duration, this._audioPlayer.duration * 30); } return duration; } _onAudioDurationChanged = () => { if (!this._animationPaused && this._audioPlayer !== null) { const audioPlayer = this._audioPlayer; const currentTime = this._currentFrameTime / 30; if (currentTime < audioPlayer.duration) { audioPlayer._setCurrentTimeWithoutNotify(currentTime); audioPlayer.play().then(() => { if (this._setAudioPlayerLastValue !== audioPlayer) { audioPlayer.pause(); return; } }); } } if (this._useManualAnimationDuration) return; const audioFrameTimeDuration = this._audioPlayer !== null ? this._audioPlayer.duration * 30 : 0; if (this._animationFrameTimeDuration < audioFrameTimeDuration) { this._animationFrameTimeDuration = audioFrameTimeDuration; } else { this._animationFrameTimeDuration = this._computeAnimationDuration(); } this.onAnimationDurationChangedObservable.notifyObservers(); }; _onAudioPlaybackRateChanged = () => { this._animationTimeScale = this._audioPlayer.playbackRate; }; _onAudioPlay = () => { this._playAnimationInternal(); }; _onAudioPause = () => { if (this._audioPlayer.currentTime === this._audioPlayer.duration) return; this._animationPaused = true; this.onPauseAnimationObservable.notifyObservers(); }; _onAudioSeek = () => { this._seekAnimationInternal(this._audioPlayer.currentTime * 30, this._animationPaused); }; _playAnimationInternal() { if (!this._animationPaused) return; this._animationPaused = false; if (this.autoPhysicsInitialization) { if (this._currentFrameTime === 0) { const models = this._models; const physicsInitializeSet = this._getPhysicsInitializeSet(); for (let i = 0; i < models.length; ++i) { physicsInitializeSet.add(models[i]); } } } this.onPlayAnimationObservable.notifyObservers(); } /** * Play animation from the current animation time * * If audio player is set, it try to play the audio from the current animation time * * It returns Promise because playing audio is asynchronous * @returns Promise */ // eslint-disable-next-line @typescript-eslint/naming-convention async playAnimation() { if (this._audioPlayer !== null && this._currentFrameTime < this._audioPlayer.duration * 30) { try { const currentTime = this._currentFrameTime / 30; if (0.05 < Math.abs(this._audioPlayer.currentTime - currentTime)) { this._audioPlayer._setCurrentTimeWithoutNotify(currentTime); } await this._audioPlayer.play(); } catch (e) { if (e instanceof DOMException && e.name === "NotSupportedError") { this.error("Failed to play audio."); this._playAnimationInternal(); } else { throw e; } } } else { this._playAnimationInternal(); } } /** * Pause animation */ pauseAnimation() { if (this._audioPlayer !== null && !this._audioPlayer.paused) { this._audioPlayer.pause(); this._animationPaused = true; } else { this._animationPaused = true; this.onPauseAnimationObservable.notifyObservers(); } } _seekAnimationInternal(frameTime, forceEvaluate) { if (this.autoPhysicsInitialization) { if (2 * 30 < Math.abs(frameTime - this._currentFrameTime)) { const physicsInitializeSet = this._getPhysicsInitializeSet(); for (let i = 0; i < this._models.length; ++i) { const model = this._models[i]; if (model.currentAnimation !== null) { physicsInitializeSet.add(model); } } } } this._currentFrameTime = frameTime; if (forceEvaluate) { const models = this._models; if (this._evaluationType === MmdWasmRuntimeAnimationEvaluationType.Buffered) { this._lastRequestAnimationFrameTime = frameTime; this._needToSyncEvaluate = true; } else { this.lock.wait(); // ensure that the runtime is not evaluating animations for (let i = 0; i < models.length; ++i) { const currentAnimation = models[i].currentAnimation; if (currentAnimation !== null) { if (currentAnimation.wasmAnimate !== undefined) { currentAnimation.wasmAnimate(frameTime); currentAnimation.animate(frameTime); } else { currentAnimation.animate(frameTime); } } } } const animatables = this._animatables; for (let i = 0; i < animatables.length; ++i) { animatables[i].animate(frameTime); } this.onAnimationTickObservable.notifyObservers(); } this.onSeekAnimationObservable.notifyObservers(); } /** * Seek animation to the specified frame time * * If you set forceEvaluate true, the animation is evaluated even if the animation is not playing * * If audio player is set and not paused, it try to play the audio from the seek time so it returns Promise * @param frameTime Time in 30fps frame * @param forceEvaluate Whether to force evaluate animation * @returns Promise */ // eslint-disable-next-line @typescript-eslint/naming-convention async seekAnimation(frameTime, forceEvaluate = false) { frameTime = Math.max(0, Math.min(frameTime, this._animationFrameTimeDuration)); if (this._audioPlayer !== null) { if (!this._audioPlayer.paused) { this._audioPlayer.currentTime = frameTime / 30; } else if (!this._animationPaused && // animation playing but audio paused this._audioPlayer.currentTime * 30 < this._animationFrameTimeDuration && // is player exausted frameTime < this._audioPlayer.duration * 30 // is seek time in audio duration ) { try { this._audioPlayer._setCurrentTimeWithoutNotify(frameTime / 30); await this._audioPlayer.play(); } catch (e) { if (e instanceof DOMException && e.name === "NotSupportedError") { this.error("Failed to play audio."); this._seekAnimationInternal(frameTime, forceEvaluate); } else { throw e; } } } else { this._seekAnimationInternal(frameTime, forceEvaluate); this._audioPlayer?._setCurrentTimeWithoutNotify(frameTime / 30); } } else { this._seekAnimationInternal(frameTime, forceEvaluate); } } /** * Whether animation is playing */ get isAnimationPlaying() { return !this._animationPaused; } /** * MMD models created by this runtime */ get models() { return this._models; } /** * Animatable objects that added to this runtime */ get animatables() { return this._animatables; } /** * Audio player */ get audioPlayer() { return this._audioPlayer; } /** * Current animation time scale (default: 1) */ get timeScale() { return this._animationTimeScale; } set timeScale(value) { this._animationTimeScale = value; if (this._audioPlayer !== null) { this._audioPlayer._setPlaybackRateWithoutNotify(value); } } /** * Current animation time in 30fps frame */ get currentFrameTime() { return this._currentFrameTime; } /** * Current animation time in seconds */ get currentTime() { return this._currentFrameTime / 30; } /** * Current animation duration in 30fps frame */ get animationFrameTimeDuration() { return this._animationFrameTimeDuration; } /** * Current animation duration in seconds */ get animationDuration() { return this._animationFrameTimeDuration / 30; } /** * Set animation duration manually * * When the difference between the length of the song and the length of the animation is large, it can be helpful to adjust the animation duration manually * @param frameTimeDuration Time in 30fps frame */ setManualAnimationDuration(frameTimeDuration) { if (frameTimeDuration === null && !this._useManualAnimationDuration) return; if (frameTimeDuration === null) { this._useManualAnimationDuration = false; this._animationFrameTimeDuration = this._computeAnimationDuration(); } else { this._useManualAnimationDuration = true; this._animationFrameTimeDuration = frameTimeDuration; } this.onAnimationDurationChangedObservable.notifyObservers(); } /** * Animation evaluation type */ get evaluationType() { return this._evaluationType; } set evaluationType(value) { if (this._evaluationType === value) return; if (value === MmdWasmRuntimeAnimationEvaluationType.Buffered) { this._evaluationType = value; } else { this._evaluationType = value; } const models = this._models; for (let i = 0; i < models.length; ++i) { models[i].onEvaluationTypeChanged(value); } this._physicsRuntime?.impl?.onEvaluationTypeChanged(value); } /** * get physics runtime * * If you don't set physics as `MmdWasmPhysics`, it returns null */ get physics() { return this._physicsRuntime; } /** * Enable or disable debug logging (default: false) */ get loggingEnabled() { return this._loggingEnabled; } set loggingEnabled(value) { this._loggingEnabled = value; if (value) { this.log = this._logEnabled; this.warn = this._warnEnabled; this.error = this._errorEnabled; } else { this.log = this._logDisabled; this.warn = this._warnDisabled; this.error = this._errorDisabled; } } _logEnabled(message) { Logger.Log(message); } _logDisabled() { // do nothing } _warnEnabled(message) { Logger.Warn(message); } _warnDisabled() { // do nothing } _errorEnabled(message) { Logger.Error(message); } _errorDisabled() { // do nothing } }