x3dom / x3dom

X3DOM. A framework for integrating and manipulating X3D scenes as HTML5/DOM elements.
http://x3dom.org
Other
815 stars 271 forks source link

Introduce Clock from three.js for step simulation of bullet world #1206

Open microaaron opened 2 years ago

microaaron commented 2 years ago

Introduce Clock from three.js for step simulation of bullet world. Clock.js requires es6.

andreasplesch commented 2 years ago

Yeah, the constant time step must be wrong.

Clock is a small object but it would be great if it would be possible to avoid introducing another object. Are there plans to use it for something else ? Ammo itself has a btClock but it does not seem to do much. Does Ammo have a timer or another clock ? I could not find much.. A new TimeSensor seems like overkill, hm..

The RAF callback actually gets the current time as an argument:

https://github.com/x3dom/x3dom/blob/22e9f81bfd3ace3e7259b6221a5cffde6913f65d/src/nodes/RigidBodyPhysics/RigidBodyPhysics.js#L1601

main() does not use it but it would be possible to keep track of deltaTime there: main( time ). If the other Clock methods are not used that would avoid having to introduce the Clock object. What about passing deltaTime to updateRigidBodies from main: updateRigidBodies( time - lastTime ) ? Could that work ? That could be leaner. Do you want to try that ?

[ In any case, src/util would be a better place for Clock.js

Would there be advantages in using performance.now instead of Date.now ? Perhaps there is a Three.js issue.

Is there a place to destroy/dereference the clock instance when the bulletworld/collidableShape is removed from the scene ? ]

microaaron commented 2 years ago

Using a btClock like this:

    initScene = function ()
    {
        var collisionConfiguration,
            dispatcher,
            overlappingPairCache,
            solver,
            WorldGravity = new x3dom.fields.SFVec3f();
        collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
        dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration );
        overlappingPairCache = new Ammo.btDbvtBroadphase();
        solver = new Ammo.btSequentialImpulseConstraintSolver();
        bulletWorld = new Ammo.btDiscreteDynamicsWorld( dispatcher, overlappingPairCache, solver, collisionConfiguration );
        bulletWorld.setGravity( new Ammo.btVector3( 0, -9.81, 0 ) );
        //Add a clock
        var bulletClock = new Ammo.btClock();
        var running = false;
        var lastTime = 0;
        bulletWorld.clock = {
            getDelta : function ()
            {
                var deltaTime;
                if ( running )
                {
                    deltaTime = ( bulletClock.getTimeMilliseconds() - lastTime ) / 1000;
                }
                else
                {
                    bulletClock.reset();
                    running = true;
                    deltaTime = 0;
                }
                lastTime = bulletClock.getTimeMilliseconds();
                return deltaTime;
            }
        };
    };
    updateRigidbodies = function ()
    {
        bulletWorld.stepSimulation( bulletWorld.clock.getDelta(), 100 );
        ……
    };

Using the current time of The RAF callback like this:

    var lastTime;
    main = function main ( time )
    {
        if ( lastTime === undefined )
        {
            lastTime = time;
        }
        updateRigidbodies( ( time - lastTime ) / 1000 );
        lastTime = time;
        ……
    };
    updateRigidbodies = function ( deltaTime )
    {
        bulletWorld.stepSimulation( deltaTime, 100 );
        ……
    };
microaaron commented 2 years ago

Would there be advantages in using performance.now instead of Date.now ? Perhaps there is a Three.js issue.

Well, performance.now may be better.

https://developer.mozilla.org/en-US/docs/Web/API/Performance/now Also unlike Date.now(), the values returned by performance.now() always increase at a constant rate, independent of the system clock (which might be adjusted manually or skewed by software like NTP). Otherwise, performance.timing.navigationStart + performance.now() will be approximately equal to Date.now().

microaaron commented 2 years ago

The RAF callback actually gets the current time as an argument.

https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame When multiple callbacks queued by requestAnimationFrame() begin to fire in a single frame, each receives the same timestamp even though time has passed during the computation of every previous callback's workload.

That means the timestamp may not be fresh.

andreasplesch commented 2 years ago

Thanks for looking into all these. Could we just use the time in the RAF callback ? This seems simplest unless the Clock class is needed elsewhere.

The lastTime declaration could be moved up into the first var statement. The check for undefined may be avoidable if lastTime is initialized in initScene or so but that does not seem to be worth the trouble.

microaaron commented 2 years ago
    var lastTime;
    updateRigidbodies = function ()
    {
        var timeStamp = ( typeof performance === "undefined" ? Date : performance ).now();
        if ( typeof lastTime === "undefined" )
        {
            lastTime = timeStamp;
        }
        bulletWorld.stepSimulation( ( timeStamp - lastTime ) / 1000, 100 );
        lastTime = timeStamp;
        ……
    };
andreasplesch commented 2 years ago

The RAF callback actually gets the current time as an argument.

https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame When multiple callbacks queued by requestAnimationFrame() begin to fire in a single frame, each receives the same timestamp even though time has passed during the computation of every previous callback's workload.

That means the timestamp may not be fresh.

I think that timestamp may be more correct since we are concerned about the delta time between displayed frames, not the exact time the timestepping occurs ?

RAF at time0: render: takes t_r time physics: updated to time time0; takes t_p time

RAF at time1 = time0 + 1/60 or possibly 2/60 or 3/60s: render physics: updated to time1

This sequence would mean that the rendered physics is one frame behind (but perhaps all the other X3D animation is also one step behind since it is using the same delta time). Ideally, the physics call probably should happen before the render callback but how the callbacks are queued is unclear.

If the physics callback happens first, I think the timestamp would be fresh anyways,

microaaron commented 2 years ago
    initScene = function ()
    {
        var collisionConfiguration,
            dispatcher,
            overlappingPairCache,
            solver,
            WorldGravity = new x3dom.fields.SFVec3f();
        collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
        dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration );
        overlappingPairCache = new Ammo.btDbvtBroadphase();
        solver = new Ammo.btSequentialImpulseConstraintSolver();
        bulletWorld = new Ammo.btDiscreteDynamicsWorld( dispatcher, overlappingPairCache, solver, collisionConfiguration );
        bulletWorld.setGravity( new Ammo.btVector3( 0, -9.81, 0 ) );
        //Initialize lastTime. updateRigidbodies should be executed immediately 
        lastTime = timeStamp = ( typeof performance === "undefined" ? Date : performance ).now();
    };
    updateRigidbodies = function ()
    {
        var timeStamp = ( typeof performance === "undefined" ? Date : performance ).now();
        var deltaTime = ( timeStamp - lastTime ) / 1000;
        lastTime = timeStamp;
        bulletWorld.stepSimulation( deltaTime, 100 );
        ……
    };
microaaron commented 2 years ago

Not only the callback of RAF, many other tasks may also be in the queue. That makes the timestamp not fresh. So I suggest getting a fresh timestamp when executing stepSimulation.

microaaron commented 2 years ago
timestamp0        timestamp1        timestamp2        timestamp3                          timestamp4        timestamp5
    |                 |                 |                 |                                   |                 |
 enqueue           enqueue           enqueue           enqueue                             enqueue           enqueue    
     \                 \                 \                 \                                   \                 \
      \                 \                 \                  -dropped a frame-                  \                 \
       \                 \                 \                                   \                 \                 \
     dequeue           dequeue           dequeue                             dequeue           dequeue           dequeue
        |                 |                 |                                   |                 |                 |   
   deltaTime=0s      deltaTime=1/60s   deltaTime=1/60s                     deltaTime=1/60s   deltaTime=1/30s   deltaTime=1/60s
                                                                            1/30s expected    1/60s expected
andreasplesch commented 2 years ago

In my mind, the timestamp should not be fresh for the Physics. It should be just close to the time a frame is rendered. This way the rendering can properly reflect the Physics. What am I missing ? Did you find a perceivable difference in practice ?

microaaron commented 2 years ago

Yes. https://video.twimg.com/ext_tw_video/1518956520926433280/pu/vid/840x652/kmZnQXbprK9kIN9I.mp4?tag=12

andreasplesch commented 2 years ago

Should all objects drop down at the same time ?

microaaron commented 2 years ago

Physics always earlier than graphics rendering. So the timestamp should be fresh.

microaaron commented 2 years ago

https://www.youtube.com/watch?v=dXZJdWgwOHk Delay 454ms. It's to much.

andreasplesch commented 2 years ago

I just thought such a delay may actually be better for a smooth experience since the rendered state does not need to reflect the actual current time but I take your word for it. So let's just use performance.now.

https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#browser_compatibility says performance.now is well supported. Do we still need the fallback to Date.now ?

microaaron commented 2 years ago

performance.now is good. This is for compatibility with older browsers https://github.com/mrdoob/three.js/blob/dev/src/core/Clock.js#L70 return ( typeof performance === 'undefined' ? Date : performance ).now(); // see #10732

There are two reasons to use Clock.js: 1.If in the future x3dom supports multiple bullet worlds, it will be necessary to assign a clock to each bullet world. 2.It's easy to pause physical motion, just call clock.stop().

andreasplesch commented 2 years ago

https://github.com/mrdoob/three.js/pull/10732 was for node.js support which x3dom does not have.

x3dom probably does not work well any more for browsers older than IE10 in any case.

We could attach a lastTime to each bulletWorld, same as a new Clock is attached but a little cheaper.

clock.stop() would be useful. On the other hand, the RigidBodyPhysics component ( https://www.web3d.org/specifications/X3Dv4Draft/ISO-IEC19775-1v4-CD1/Part01/components/rigidBodyPhysics.html#t-Topics ) does not seem to have a way to stop (or start) time.

So I do not think we really need Clock.js and can keep it simpler.

microaaron commented 2 years ago

Maybe x3dom can provide a pause feature earlier than x3d. Anyway, it is a suggestion for the future.

Now, the improvement should be like this:

( function ()
{
    var CollidableShapes = [],
        JointShapes = [],
        bulletWorld,
        x3dWorld = null,
        initScene,
        main,
        updateRigidbodies,
        MakeUpdateList,
        X3DRigidBodyComponents,
        CreateX3DCollidableShape,
        UpdateTransforms,
        CreateRigidbodies,
        rigidbodies = [],
        mousePickObject,
        mousePos = new x3dom.fields.SFVec3f(),
        drag = false,
        interactiveTransforms = [],
        UpdateRigidbody,
        intervalVar,
        building_constraints = true,
        ParseX3DElement,
        InlineObjectList,
        inline_x3dList = [],
        inlineLoad = false,
        completeJointSetup = false,
        lastTime;
        ……
} )();
    initScene = function ()
    {
        var collisionConfiguration,
            dispatcher,
            overlappingPairCache,
            solver,
            WorldGravity = new x3dom.fields.SFVec3f();
        collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
        dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration );
        overlappingPairCache = new Ammo.btDbvtBroadphase();
        solver = new Ammo.btSequentialImpulseConstraintSolver();
        bulletWorld = new Ammo.btDiscreteDynamicsWorld( dispatcher, overlappingPairCache, solver, collisionConfiguration );
        bulletWorld.setGravity( new Ammo.btVector3( 0, -9.81, 0 ) );
        //Initialize lastTime. updateRigidbodies should be executed immediately
        lastTime = performance.now();
    };
    updateRigidbodies = function ()
    {
        var timeStamp = performance.now();
        var deltaTime = ( timeStamp - lastTime ) / 1000;
        lastTime = timeStamp;
        bulletWorld.stepSimulation( deltaTime, 100 );
        ……
    };
andreasplesch commented 2 years ago

Looks good.

andreasplesch commented 2 years ago

I am not sure but it may be possible to have multiple bulletWorlds using Inlines or Protos since they set up their own scenes. So perhaps saving lastTime per bulletWorld is safer:

        bulletWorld.setGravity( new Ammo.btVector3( 0, -9.81, 0 ) );
        //Initialize lastTime. updateRigidbodies should be executed immediately
        bulletWorld.lastTime = performance.now();
        var deltaTime = ( timeStamp - bulletWorld.lastTime ) / 1000;
        bulletWorld.lastTime = timeStamp;

But it looks like substantial other changes would be also necessary. Getting and setting an object property like this is probably slightly slower than using a scoped variable.

microaaron commented 2 years ago

I prefer to put the lastTime into the bulletWorld. It works well.

andreasplesch commented 2 years ago

A bit less code and more future proof as well.

andreasplesch commented 2 years ago

Did you want to update the PR to just use performance.now and bulletWorld.lastTime ?