Animation timing - difference between localhost and playground

I am trying to animate a series of objects like this:

And it works fine in the playground but on my local version it does not. Is there a way to get robust timing offsets between animations? I tried using the “begin” argument to scene.beginAnimation but this does not work for looping animations. Instead I am using a setTimeout but this obviously has some error associated with it that is different between the localhost copy and the playground.

As you can see from this screen recording on the playground the boxes have a consistent spacing between them but on my localhost they do not:

On my localhost version there is a gap every other box, when there should be a small gap between each box.

No gap:

Larger Gap:

Timers like setTimeout and setInterval are known to not be as precise as one could require.

You could hook into scene.onBeforeRenderObservable and check how long has been spent there relying on engine.deltaTime to start playing once you reached your threshold ?

2 Likes

Great thanks. Yes that makes sense. Actualy when I looked for it I noticed the playground script also had an error, albeit a smaller one. I have modified the script to use and then remove from scene.onBeforeRenderObservable: https://playground.babylonjs.com/#YJVTI6#396 This still has some error but it appears a lot smaller than using setTimeout. As the frame rate drops this error would increase so it’s still not a robust solution but it’ll do for now! :slight_smile:

2 Likes

I noticed that sometimes there was still a lot of clumping of animation start times so I ended up writing this function that helps trigger a function at approximately the right time. And I use it as follows:

function create_smoke_plume (scene: Scene, emit_position: Vector3, count: number)
{
    const total_frames = 24
    const lifetime_ms = 3000

    const smokes: { mesh: AbstractMesh, delay_ms: number }[] = create_smoke_balls_with_animations({ scene, emit_position, count })

    const start_ms = performance.now()
    smokes.forEach(({ mesh, delay_ms }) =>
    {
        const funktion = () => scene.beginAnimation(mesh, 0, total_frames, true)
        trigger_function_at({ funktion, delay_ms, loop_ms: lifetime_ms, start_ms })
    })
}

The trigger_function_at implementation that seems to work well so far:

import { test } from "./test"



interface TriggerFunctionAtArgs
{
    funktion: () => void
    delay_ms: number
    loop_ms: number
    start_ms: number
    tolerance_ms?: number
    attempts_remaining?: number
}
// We use this function as during start up / running, i.e. when the tab
// is not the focused tab there may be delays in the setTimeout
// that results in it firing significantly later than expected.  For 
// animating a series of components such as puffs of smoke in a
// smoke plume, this results in distracting / un-appealing / confusing
// gaps and clumps.  This function is an attempted work around
// for that problem.
export function trigger_function_at (args: TriggerFunctionAtArgs)
{
    const { tolerance_ms = 20 } = args
    let { attempts_remaining = 5 } = args

    if (attempts_remaining <= 0)
    {
        console.warn(`Failed to start funktion: "${args.funktion.toString()}" on time`)
        args.funktion()
        return
    }

    const { with_in_tolerance, diff_ms } = is_within_tolerance({ ...args, tolerance_ms })

    if (with_in_tolerance)
    {
        args.funktion()
        return
    }

    const { aim_for_next_cycle, wait_for } = calc_wait_for({ diff_ms, loop_ms: args.loop_ms })

    setTimeout(() =>
    {
        attempts_remaining = attempts_remaining + (aim_for_next_cycle ? -1 : 0)

        if (aim_for_next_cycle)
        {
            console .warn(`Aiming to start funktion next cycle in `, wait_for, " attempts remaining ", attempts_remaining)
        }
        else
        {
            console .log(`Aiming to start funktion this cycle in `, wait_for, " attempts remaining ", attempts_remaining)
        }

        trigger_function_at({ ...args, tolerance_ms, attempts_remaining })
    }, wait_for)
}



function is_within_tolerance (args: { delay_ms: number, loop_ms: number, start_ms: number, tolerance_ms: number })
{
    const offset_ms = (performance.now() - args.start_ms) % args.loop_ms
    const diff_ms = args.delay_ms - offset_ms
    const with_in_tolerance = Math.abs(diff_ms) <= args.tolerance_ms

    return { with_in_tolerance, diff_ms }
}



function calc_wait_for (args: { diff_ms: number, loop_ms: number })
{
    const aim_for_next_cycle = args.diff_ms < 0
    const wait_for = aim_for_next_cycle
        ? args.loop_ms + args.diff_ms
        : args.diff_ms

    return { aim_for_next_cycle, wait_for }
}



function run_tests ()
{
    let start_ms = performance.now()
    const delay_ms = 150
    const loop_ms = 3000
    const tolerance_ms = 20

    let result_tolerance = is_within_tolerance({ delay_ms, loop_ms, start_ms, tolerance_ms })
    test(result_tolerance.with_in_tolerance, false)
    test(result_tolerance.diff_ms, delay_ms)

    result_tolerance = is_within_tolerance({ delay_ms, loop_ms, start_ms: performance.now() - delay_ms, tolerance_ms })
    test(result_tolerance.with_in_tolerance, true)
    test(result_tolerance.diff_ms, 0)

    result_tolerance = is_within_tolerance({ delay_ms, loop_ms, start_ms: performance.now() - delay_ms + tolerance_ms, tolerance_ms })
    test(result_tolerance.with_in_tolerance, true)
    test(result_tolerance.diff_ms, 20)

    result_tolerance = is_within_tolerance({ delay_ms, loop_ms, start_ms: performance.now() - delay_ms - tolerance_ms, tolerance_ms })
    test(result_tolerance.with_in_tolerance, true)
    test(result_tolerance.diff_ms, -20)

    result_tolerance = is_within_tolerance({ delay_ms, loop_ms, start_ms: performance.now() - delay_ms - tolerance_ms - 1, tolerance_ms })
    test(result_tolerance.with_in_tolerance, false)
    test(result_tolerance.diff_ms, -21)

    let result_wait_for = calc_wait_for({ diff_ms: 0, loop_ms })
    test(result_wait_for.aim_for_next_cycle, false)
    test(result_wait_for.wait_for, 0)

    result_wait_for = calc_wait_for({ diff_ms: 30, loop_ms })
    test(result_wait_for.aim_for_next_cycle, false)
    test(result_wait_for.wait_for, 30)

    result_wait_for = calc_wait_for({ diff_ms: -30, loop_ms })
    test(result_wait_for.aim_for_next_cycle, true)
    test(result_wait_for.wait_for, loop_ms - 30)
}

// run_tests()