Skip to content

Commit

Permalink
Animations: storing key frames in typed arrays
Browse files Browse the repository at this point in the history
Keyframes in Babylon.js are stored with array of objects. Some animations exported with millions of key frames, with the object mode, it would take more memory than stored with typed arrays, like Float32Array.
Without the object per frame, it would be much harder for features like per-frame interpolation and tangent to be implemented, so this could be an optional feature for those suffering too many keyframes without these advanced features.
For AnimationCurveEditor it is hard to fully adapt it to CompactAnimation, maybe the fast way it to convert CompactAnimation to Animation, and replace all possible usage to converted animation.

Not supported yet:
* CUBICSPLINE animation with tangent
* per-frame interpolation or tangent
* serializing GLTF animation with quantization and 4-bytes alignment

Forum link:
https://forum.babylonjs.com/t/animations-storing-key-frames-in-typed-arrays/36566
  • Loading branch information
myfreeer committed Jan 16, 2023
1 parent 8c04266 commit b3886b6
Show file tree
Hide file tree
Showing 9 changed files with 3,146 additions and 62 deletions.
10 changes: 9 additions & 1 deletion packages/dev/core/src/Animations/animation.ts
Expand Up @@ -7,6 +7,7 @@ import type { Nullable } from "../types";
import type { Scene } from "../scene";
import { SerializationHelper } from "../Misc/decorators";
import { RegisterClass } from "../Misc/typeStore";
import { InstantiationTools } from "../Misc/instantiationTools";
import type { IAnimationKey } from "./animationKey";
import { AnimationKeyInterpolation } from "./animationKey";
import { AnimationRange } from "./animationRange";
Expand Down Expand Up @@ -93,7 +94,7 @@ export class Animation {
/**
* Stores the animation ranges for the animation
*/
private _ranges: { [name: string]: Nullable<AnimationRange> } = {};
protected _ranges: { [name: string]: Nullable<AnimationRange> } = {};

/**
* @internal Internal use
Expand Down Expand Up @@ -1302,6 +1303,13 @@ export class Animation {
* @returns Animation object
*/
public static Parse(parsedAnimation: any): Animation {
if (parsedAnimation.customType) {
const customType = InstantiationTools.Instantiate(parsedAnimation.customType);
if (customType?.Parse) {
return customType.Parse(parsedAnimation);
}
}

const animation = new Animation(parsedAnimation.name, parsedAnimation.property, parsedAnimation.framePerSecond, parsedAnimation.dataType, parsedAnimation.loopBehavior);

const dataType = parsedAnimation.dataType;
Expand Down
306 changes: 306 additions & 0 deletions packages/dev/core/src/Animations/animationTools.ts
@@ -0,0 +1,306 @@
import type { Scene } from "../scene";
import type { Animation } from "./animation";
import type { IAnimatable } from "./animatable.interface";

/**
* The callback for animation iteration in scene
*/
export interface IterateAnimationCallback {
/**
* The callback for animation iteration in scene
* @param animation current animation
* @param key key of context
* @param context object holding this animation
* @returns false to stop the iteration
*/
(animation: Animation, key: number | string, context: any): void | boolean;
}

/**
* Iterate through all animations of scene, could iterate an animation many times
* @param scene The scene holding animations
* @param callBack function to be called on any animation
*/
export function iterateAnimations(scene: Scene, callBack: IterateAnimationCallback): void {
const {
animations,
animatables,
animationGroups,
transformNodes,
meshes,
materials,
textures,
cameras,
particleSystems,
morphTargetManagers,
skeletons,
spriteManagers,
postProcesses,
} = scene;

let len = animations?.length;
if (len) {
for (let i = 0; i < len; i++) {
if (callBack(animations[i], i, animations) === false) {
return;
}
}
}

len = animatables?.length;
if (len) {
for (let i = 0; i < len; i++) {
const animatable = animatables[i];
const runtimeAnimations = animatable.getAnimations();
const length = runtimeAnimations?.length;
if (length) {
for (let j = 0; j < length; j++) {
const runtimeAnimation = runtimeAnimations[i];
const animation = runtimeAnimation?.animation;
if (animation && callBack(animation, "_animation", runtimeAnimation) === false) {
return;
}
const target = runtimeAnimation?.target;
if (target?.animations) {
const iAnimatable = target as IAnimatable;
const animations = iAnimatable.animations;
const animationsLength = animations?.length;
if (animations && animationsLength) {
for (let k = 0; k < animationsLength; k++) {
const animation = animations[k];
if (animation && callBack(animation, k, animations) === false) {
return;
}
}
}
}
}
}
}
}

len = animationGroups?.length;
if (len) {
for (let i = 0; i < len; i++) {
const group = animationGroups[i];
const targetedAnimations = group?.targetedAnimations;
let length = targetedAnimations?.length;
if (length) {
for (let j = 0; j < length; j++) {
const targetedAnimation = targetedAnimations[j];
const animation = targetedAnimation?.animation;
if (animation && callBack(animation, "animation", targetedAnimation)) {
return;
}
const target = targetedAnimation.target;
if (target?.animations) {
const iAnimatable = target as IAnimatable;
const animations = iAnimatable.animations;
const animationsLength = animations?.length;
if (animations && animationsLength) {
for (let k = 0; k < animationsLength; k++) {
const animation = animations[k];
if (animation && callBack(animation, k, animations) === false) {
return;
}
}
}
}
}
}
const animatables = group?.animatables;
length = animatables?.length;
if (length) {
for (let j = 0; j < length; j++) {
const animatable = animatables[j];
const runtimeAnimations = animatable.getAnimations();
const length = runtimeAnimations?.length;
if (length) {
for (let k = 0; k < length; k++) {
const runtimeAnimation = runtimeAnimations[k];
if (runtimeAnimation?.animation && callBack(runtimeAnimation.animation, "_animation", runtimeAnimation) === false) {
return;
}
}
}
}
}
}
}
len = morphTargetManagers?.length;
if (len) {
for (let i = 0; i < len; i++) {
const morphTargetManager = morphTargetManagers[i];
const targetsLength = morphTargetManager.numTargets;
if (!targetsLength) {
continue;
}
for (let j = 0; j < targetsLength; j++) {
const target = morphTargetManager.getTarget(j);
const animations = target?.animations;
const length = animations?.length;
if (!length) {
continue;
}
for (let k = 0; k < length; k++) {
const animation = animations[k];
if (animation && callBack(animation, k, animations) === false) {
return;
}
}
}
}
}

len = skeletons?.length;
if (len) {
for (let i = 0; i < len; i++) {
const skeleton = skeletons[i];
const bones = skeleton?.bones;
const length = bones?.length;
if (length) {
for (let j = 0; j < length; j++) {
const target = bones[j];
const animations = target?.animations;
const animationsLength = animations?.length;
if (!animationsLength) {
continue;
}
for (let k = 0; k < animationsLength; k++) {
const animation = animations[k];
if (animation && callBack(animation, k, animations) === false) {
return;
}
}
}
}
const animations = skeleton?.animations;
const animationsLength = animations?.length;
if (animationsLength) {
for (let j = 0; j < animationsLength; j++) {
const animation = animations[j];
if (animation && callBack(animation, j, animations) === false) {
return;
}
}
}
}
}

len = spriteManagers?.length;
if (len) {
for (let i = 0; i < len; i++) {
const spriteManager = spriteManagers[i];
const sprites = spriteManager?.sprites;
const length = sprites?.length;
if (length) {
for (let j = 0; j < length; j++) {
const target = sprites[j];
const animations = target?.animations;
const animationsLength = animations?.length;
if (!animationsLength) {
continue;
}
for (let k = 0; k < animationsLength; k++) {
const animation = animations[k];
if (animation && callBack(animation, k, animations) === false) {
return;
}
}
}
}
const animations = spriteManager?.texture?.animations;
const animationsLength = animations?.length;
if (animationsLength) {
for (let j = 0; j < animationsLength; j++) {
const animation = animations[j];
if (animation && callBack(animation, j, animations) === false) {
return;
}
}
}
}
}

let iAnimatables: IAnimatable[] = [];

len = transformNodes?.length;
if (len) {
iAnimatables = iAnimatables.concat(transformNodes);
}
len = meshes?.length;
if (len) {
iAnimatables = iAnimatables.concat(meshes);
}
len = cameras?.length;
if (len) {
iAnimatables = iAnimatables.concat(cameras);
}
len = materials?.length;
if (len) {
iAnimatables = iAnimatables.concat(materials);
}
len = textures?.length;
if (len) {
iAnimatables = iAnimatables.concat(textures);
}
len = particleSystems?.length;
if (len) {
iAnimatables = iAnimatables.concat(particleSystems);
}
len = postProcesses?.length;
if (len) {
iAnimatables = iAnimatables.concat(postProcesses);
}

len = iAnimatables.length;
if (len) {
for (let i = 0; i < len; i++) {
const iAnimatable = iAnimatables[i];
const animations = iAnimatable?.animations;
const length = animations?.length;
if (length) {
for (let j = 0; j < length; j++) {
const animation = animations[j];
if (animation && callBack(animation, j, animations) === false) {
return;
}
}
}
}
}
}

/**
* Replace animation from scene
* @param scene The scene holding animation
* @param animation The animation to be replaced
* @param replacement The animation to be replaced to
*/
export function replaceAnimation(scene: Scene, animation: Animation, replacement: Animation): number {
let count = 0;
iterateAnimations(scene, (current, key, context) => {
if (animation === current) {
context[key] = replacement;
count++;
}
});
return count;
}

/**
* Replace multiple animations from scene
* @param scene The scene holding animation
* @param map Map of animations where key holding the animation to be replaced, value holding replacement
*/
export function replaceAnimations(scene: Scene, map: Map<Animation, Animation>): number {
let count = 0;
iterateAnimations(scene, (current, key, context) => {
const replacement = map.get(current);
if (replacement) {
context[key] = replacement;
count++;
}
});
return count;
}

0 comments on commit b3886b6

Please sign in to comment.