babylon-mmd
Version:
babylon.js mmd loader and runtime
231 lines (230 loc) • 7.2 kB
JavaScript
import { Observable } from "@babylonjs/core/Misc/observable";
/**
* Represents a slice of an MMD animation
*/
export class MmdAnimationSpan {
/**
* The animation that this span uses
*/
animation;
/**
* Start frame of the span (default: animation.startFrame)
*
* You can slice the desired range by the difference from the starting point of the original animation
*/
startFrame;
/**
* End frame of the span (default: animation.endFrame)
*
* You can slice the desired range by the difference from the starting point of the original animation
*/
endFrame;
/**
* Offset of the span (default: 0)
*
* Determines at what point in the `MmdCompositeAnimation` the span will be played
*/
offset;
/**
* Animation weight (default: 1)
*
* Internally, it is normalized and treated as a value between 0 and 1 on evaluation
*/
weight;
/**
* easing function of the span (default: null)
*/
easingFunction;
/**
* Ease in frame time of the span (default: 0)
*/
easeInFrameTime;
/**
* Ease out frame time of the span (default: 0)
*/
easeOutFrameTime;
/**
* Create a new span
* @param animation Bindable animation, which typically means `MmdAnimation`
* @param startFrame Start frame of the span (default: animation.startFrame)
* @param endFrame End frame of the span (default: animation.endFrame)
* @param offset Offset of the span (default: 0)
* @param weight Animation weight (default: 1)
*/
constructor(animation, startFrame, endFrame, offset, weight) {
this.animation = animation;
this.startFrame = startFrame ?? animation.startFrame;
this.endFrame = endFrame ?? animation.endFrame;
this.offset = offset ?? 0;
this.weight = weight ?? 1;
this.easingFunction = null;
this.easeInFrameTime = 0;
this.easeOutFrameTime = 0;
}
/**
* Name of the animation
*/
get name() {
return this.animation.name;
}
/**
* Whether the frame time is in the span
* @param frameTime frame time in composite animation
* @returns Returns true if the frame time is in the span
*/
isInSpan(frameTime) {
return this.startFrame + this.offset <= frameTime && frameTime <= this.endFrame + this.offset;
}
/**
* Get the frame time in this span
* @param frameTime frame time in composite animation
* @returns Returns the frame time in this span
*/
getFrameTime(frameTime) {
return frameTime - this.offset;
}
/**
* Get the eased weight of this span
* @param frameTime frame time in this span
* @returns Returns the eased weight of this span
*/
getEasedWeight(frameTime) {
const startFrame = this.startFrame;
const endFrame = this.endFrame;
const easeInFrameTime = this.easeInFrameTime;
const easeOutFrameTime = this.easeOutFrameTime;
if (Math.abs(frameTime - startFrame) < Math.abs(frameTime - endFrame)) {
if (startFrame + easeInFrameTime <= frameTime) {
return this.weight;
}
else if (frameTime <= startFrame) {
return 0;
}
else {
// ease in
const frameTimeInEaseIn = frameTime - startFrame;
const weight = this.weight * (this.easingFunction?.ease(frameTimeInEaseIn / easeInFrameTime) ?? frameTimeInEaseIn / easeInFrameTime);
return weight;
}
}
else {
if (frameTime <= endFrame - easeOutFrameTime) {
return this.weight;
}
else if (endFrame <= frameTime) {
return 0;
}
else {
// ease out
const frameTimeInEaseOut = frameTime - endFrame + easeOutFrameTime;
const weight = this.weight * (1 - (this.easingFunction?.ease(frameTimeInEaseOut / easeOutFrameTime) ?? frameTimeInEaseOut / easeOutFrameTime));
return weight;
}
}
}
}
/**
* Combine multiple animations into a single animation
*
* Good for uses like QTE sequences where animation blending is determined at runtime
*/
export class MmdCompositeAnimation {
/**
* Observable that is triggered when a span is added
*/
onSpanAddedObservable;
/**
* Observable that is triggered when a span is removed
*/
onSpanRemovedObservable;
/**
* Animation name for identification
*/
name;
_startFrame;
_endFrame;
_spans;
/**
* Create a new composite animation
* @param name Animation name
*/
constructor(name) {
this.onSpanAddedObservable = new Observable();
this.onSpanRemovedObservable = new Observable();
this.name = name;
this._startFrame = 0;
this._endFrame = 0;
this._spans = [];
}
/**
* Add a span to the animation
*
* animation will be pushed to the end of the `spans` array
* @param span Span to add
*/
addSpan(span) {
this._startFrame = this._spans.length === 0
? span.startFrame + span.offset
: Math.min(this.startFrame, span.startFrame + span.offset);
this._endFrame = Math.max(this.endFrame, span.endFrame + span.offset);
this._spans.push(span);
this.onSpanAddedObservable.notifyObservers(span);
}
/**
* Remove a span from the animation
*
* If the span does not exist, do nothing
* @param span Span to remove
*/
removeSpan(span) {
const spans = this._spans;
const index = spans.indexOf(span);
this.removeSpanFromIndex(index);
}
/**
* Remove a span from the animation
*
* If index is out of range, do nothing
* @param index Index of the span to remove
*/
removeSpanFromIndex(index) {
const spans = this._spans;
if (index < 0 || spans.length <= index)
return;
spans.splice(index, 1);
if (spans.length === 0) {
this._startFrame = 0;
this._endFrame = 0;
}
else {
let startFrame = spans[0].startFrame + spans[0].offset;
let endFrame = spans[0].endFrame + spans[0].offset;
for (let i = 1; i < spans.length; i++) {
const span = spans[i];
startFrame = Math.min(startFrame, span.startFrame + span.offset);
endFrame = Math.max(endFrame, span.endFrame + span.offset);
}
this._startFrame = startFrame;
this._endFrame = endFrame;
}
this.onSpanRemovedObservable.notifyObservers(index);
}
/**
* The start frame of this animation
*/
get startFrame() {
return this._startFrame;
}
/**
* The end frame of this animation
*/
get endFrame() {
return this._endFrame;
}
/**
* The spans of this animation
*/
get spans() {
return this._spans;
}
}