Animation current frame

Thanks!

With the following code I can get the current frame number which is playing.

scene.animationGroups[0].targetedAnimations[0].animation.runtimeAnimations[0].currentFrame

Indeed the approach about animating the animation is a bit confusing.
So let’s rephrase, what I’m after is the following:

I have a predesigned camera following a path in Blender/3dsMax exported in a glb file.
This camera (animated) comes along a few points of interest on the animated path (can be virtual, no stops, just a frame number).
I managed to import and make us of this animated file in BJS.
Depending on the scroll delta of your mouse I would like to initiate an animation of the camera.
Let’s say we have an animation of 400 frames in blender, and we’ve landed in BJS on frame 120.
When I scroll I’d like to initiate the camera animation forward 180 frames with an easing curve.

Here is a sketch about it:

Is that something that could be done?
Thanks for the help in advance.
Pieter

i think you could add a global variable that holds an ideal forward position or frame state that gets incremented when the user scrolls. also add an scene.registerBeforeRender callback that moves the camera or follow target some extra steps if the variable is greater than 0, and also decrement the variable by however much you moved. something like (pseudo code):

scrollBoost = 0
window.onscroll = () => scrollBoost + 100;

scene.registerBeforeRender(function () {
    if(scrollBoost > 0) {
         implmentationLogic()
        scrollBoost = scrollBoost - 20 (adjust speed and tweening here)
    }
  });
1 Like

Also I noticed that the “frame rate” from BJS is as follows:
400 frames in blender at 24fps, correspond with 16.6666 (frames?) in BJS.
So this scene.animationGroups[0].targetedAnimations[0].animation.runtimeAnimations[0].currentFrame is more like the number of seconds that have passed by?

The GLB format uses seconds instead of frames as its animation unit, so as to not assume anything about the original frame rate, when a GLB Animation is imported its converted to a 1fps animation. So frames ends up being the same as seconds. :slight_smile:

Ok, I think I follow along here, but if this would have become a 1fps animation wouldn’t it have taken 400seconds? I’ve exported a 400 frame animation @24fps from blender to glb.

In BJS the animation plays back for 16.6666 sec (at least that’s the duration (to) for the animation)

Another question about the animation. Could something like this work:

const ag = scene.animationGroups[0];
Animation.CreateAndStartAnimation('camAniMove', ag, 'goToFrame', 30, 120, 0, 8);

To animate the (animated) camera in order to tween it from frame 0 to 8. In this way I could play a subsection from the animation at a certain speed depending on some scroll parameters or so.
At the moment this isn’t working for me yet…

However the following did work for me, but I couldn’t set the from goToFrame

gsap.gsap.fromTo(
    scene.animationGroups[0],
    2,
    { goToFrame: scrllPosStart },
    {
        goToFrame: scrllPos,
        // onComplete: () => {
        //     console.log('done scrllPos', scrllPosStart, scrllPos);
        //     console.log(scene.animationGroups[0].targetedAnimations[0].animation.runtimeAnimations[0].currentFrame);
        // },
        // ease: 'power3.out',
    },
);

Using gsap would limit our commercial possibilities in the future so I’m looking for another way to get this done. Like with CreateAndStartAnimation or tweenjs or so.

@jeremy-coleman thanks for the suggestion, however I couldn’t get your sample code to work for me here in this case. I’ll try again later and let you know if I got it to work.

That wont work because goToFrame is a function not a property, but you can wrap it in a property called currentFrame and animate that property with CreateAndStartAnimation for example, like below where I animate the frame of an imported model’s AnimationGroup. :slight_smile:

400 frames @ 24 fps =~ 16s in the GLB, when BJS loads this 16s animation it transforms it into a ~16 frames @ 1 fps animation so the duration is the same :smiley:

Thank you very much @Blake
Looks like a very good solution to this problem.

I was testing with tweenjs for the moment, which I got working quite nicely by now, but looks way more complex…

let camPosTime = { pos: 0 };
let nextInc = 4;
let reset = false;
let toFrame = nextInc;
let scrollMovement = false;


if (scrollMovement) {
    return;
}
let camMax = scene.animationGroups[0].to;
if (toFrame > camMax) {
    console.log('reset');
    toFrame = camMax;
    reset = true;
}
console.log(reset);
scrollMovement = true;
const camTween = new TWEEN.Tween(camPosTime)
    .to({ pos: toFrame })
    .duration(1600)
    .easing(TWEEN.Easing.Sinusoidal.InOut)
    .delay(0)
    .onUpdate(() => {
        // console.log(scene.animationGroups[0].targetedAnimations[0].animation.runtimeAnimations[0].currentFrame.toFixed(2));
        console.log(camPosTime.pos.toFixed(2));
        scene.animationGroups[0].goToFrame(camPosTime.pos);
    })
    .onComplete(() => {
        console.log('done');
        if (reset === true) {
            camPosTime.pos = 0;
            toFrame = nextInc;
            reset = false;
        } else {
            toFrame += nextInc;
        }
        scrollMovement = false;
    })
    .start();

It does what it should, but I’ll definitely try your solution tomorrow.
Thanks

1 Like

ok :slight_smile: when you look at it like that it does make sense :stuck_out_tongue:
thanks

1 Like

LOL animationGroup.currentFrame is so much easier to remember and type out than animationGroup.targetedAnimations[0]?.animation.runtimeAnimations[0]?.currentFrame. Would make a good PR I guess to add a “currentFrame” property to AnimationGroup? :slight_smile:

EDIT: or are the group’s animations not necessarily always normalized to have the same number of frames and always be at the same frame? I thought they were but now I’m not sure. :thinking:

Yep they are not necessarily :frowning:

Aah well, I suppose a generalized solution would be more complicated then, like returning the max currentFrame of all the runtimeAnimations or something… :slightly_frowning_face: :thinking:

Good point, but if there is a possibility that there are different currentFrame values within, it’s better to write a custom function to shorten this.

What was mainly a bit frustrating is having to look for this which made it quite a task :stuck_out_tongue:

Yep, that’s what I meant by using the max currentFrame instead… And def agree it’s frustrating to have to deduce or find/lookup a long, complicated way to get the current frame… :slight_smile:

I’m not sure if the conversation has moved on from this point, but would using speedRatio be an option?

Load Animated Character | Babylon.js Playground (babylonjs.com)

After clicking on the canvas, press (or repeatedly press) the spacebar to see the animation get “wound up,” then slow to a stop again once you stop pressing. I didn’t do it with scrolling in this case (I think the Playground was eating my scroll, so I just switched to keydown to save time), but the idea is the same. If you look at lines 64 through 75, I essentially tether the animation’s speedRatio to a variable that decays to zero (line 71), then boost that value whenever I receive a certain event (in my case keydown, but you could use scroll). This is tantamount to the “winding it up” operation you mentioned, and by changing the increment and decay rate you can change the behavior by which the animation slows down or speeds up. You can even go backwards, I think, by setting your speedRatio negative. Not sure if this does quite what you’re looking for or if you’ve already solved it a way you’re happy with, but this might at least give another possibility. :slight_smile:

1 Like

Thank you @syntheticmagus

// let camPosTime = { pos: 0 };
let nextInc = 2;
let frmStart = 0;
let frmEnd = nextInc;
let scrollMovement = false;
let durAniFrms = 30;
let keyList = [0, 2, 4, 6, 8, 10, 12, 14, 16]; // n = 8
let pos = 0;
let forward = true;

let nextKey = function (forward: boolean) {
    let from: number = pos;
    let to: number = 0;
    if (forward) {
        if (pos == keyList.length - 1) {
            from = 0;
            to = 1;
            pos = to;
        } else {
            to = pos + 1;
            pos++;
        }
    } else {
        if (pos == 0) {
            from = keyList.length - 1;
            to = keyList.length - 2;
            pos = to;
        } else {
            to = pos - 1;
            pos--;
        }
    }
    return [from, to];
};

let camTrans = function (scene: Scene, options: { from?: number; to?: number }, forward: boolean = true, aniGroup?: undefined) {
    const animationGroup = scene.animationGroups[0];
    animationGroup.pause();
    let frmMax = animationGroup.to;

    let transFrom = typeof options.from != 'undefined' ? options.from : 0;
    let transTo = typeof options.to != 'undefined' ? options.to : frmMax;
    let bezierEase = new BezierCurveEase(0.5, 1.15, 0.18, 0.99); // https://cubic-bezier.com/#1,1,.5,1

    let [from, to] = nextKey(forward);

    Animation.CreateAndStartAnimation(
        'camAnimation',
        animationGroup,
        'currentFrame',
        30,
        durAniFrms,
        keyList[from],
        keyList[to],
        Animation.ANIMATIONLOOPMODE_CONSTANT,
        bezierEase,
        () => {
            scrollMovement = false;
        },
        scene,
    );
};

Initiated by:

camTrans(scene, { from: frmStart, to: frmEnd }, forward);

This runs part of the camera animation from position pos > pos + 1 in the array, which can be set from blender/max. This uses a fixed curve to control the animation speed.

The dimming out of your approach looks really interesting to me.
I’ll give it a try when I have the chance, and will let you know how it went.

Hi @Blake, can you give me some advice on how to implement this setup on multiple animation groups?

let agSetup: Array<AnimationGroup> = [];
const agAttachCF = function (scene: Scene, AG: AnimationGroup, cam?: Camera) {
    if (!agSetup.includes(AG)) {
        if (!AnimationGroup.prototype.hasOwnProperty('currentFrame')) {
            Object.defineProperty(AnimationGroup.prototype, 'currentFrame', {
                get() {
                    return this.targetedAnimations[0]?.animation.runtimeAnimations[0]?.currentFrame ?? 0;
                    // return AG.targetedAnimations[0]?.animation.runtimeAnimations[0]?.currentFrame ?? 0;
                },
                set(frame) {
                    this.goToFrame(frame);
                    // AG.goToFrame(frame);
                },
            });
        }
        agSetup.push(AG);
    } else {
        console.log('AG currentFrame already setup');
    }
};
agAttachCF(scene, activeAnimationGroup)

On which animation does this apply?
Does the this apply on the animationGroup itself?
Applying the action to the supplied AG didn’t do the trick.

Thanks!

Oh sorry, I forgot to explain that part. The if check was only for the playground in case it’s run more than once. For local dev thou, you can just put the code in the if block into a script and add it to your html file, after the babylon.js script. It extends the AnimationGroup class so that all AnimationGroup objects will have that property available. :slight_smile:

EDIT: well it will be a little different if you aren’t using simple index.html and js files setup, but still the code just needs to be run once, not for each animation group.

Hi @Pieter_van_Stee just checking in, do you have any further questions? :smiley: