0
\$\begingroup\$

I am in the process of implementing interpolation into a simple game loop I am playing with after reading the infamous "fix your timestep" article (which is amazing by the way). I have the position interpolation working correctly from what I can tell, however when attempting to implement interpolation for rotation using three.js's slerp method of the quaternion object I am experiencing strange behavior. What is happening is that whenever you attempt to rotate full 360 degrees you are stopped at the 180 degree range.

Looking at the output from the console logs it appears that it is trying to rotate past this point but then it is being re-set. It seems strange, but perhaps there is something that I am doing that is weird?


UPDATE: A working example of this scenario can be found here: https://stashcube.com/stackexamples/quaternion-interpolation/

// Run the main game loop as fast as we can
    setInterval(function(){

        var newTime = (+new Date()) / 1000.0;
        world.renderDeltaTime = (newTime - world.currentTime);
        world.totalRenderRunTime += world.renderDeltaTime;
        // Increment world.physicsAccumulator by world.renderDeltaTime. If it takes longer than
        // a quarter of a second for a frame to render then increment by max frame time of .25.
        // This prevents spiral of death issue.
        world.physicsAccumulator += (world.renderDeltaTime > 0.25) ? 0.25 : world.renderDeltaTime;
        world.currentTime = newTime;

        // Physics/Game logic 
        while(world.physicsAccumulator >= world.physicsDeltaTime){

            world.processServerUpdates(client);

            client.previous_state = client.current_state.clone();

            client.processInputs(socket, world.physicsDeltaTime);
            world.animatePeers(client);

            world.totalPhysicsRunTime += world.physicsDeltaTime;
            world.physicsAccumulator -= world.physicsDeltaTime;                

        }

        // Interpolate client object to remove the stuttering caused by unpredictable number 
        // of physics loops
        var alpha = world.physicsAccumulator / world.physicsDeltaTime;
        var client_physics_state = new THREE.Object3D();
        client_physics_state = client.current_state.clone();
        client.current_state.position.lerpVectors(client.previous_state.position, client.current_state.position, alpha);
        var testQuat = new THREE.Quaternion();
        THREE.Quaternion.slerp(client.previous_state.quaternion, client.current_state.quaternion, testQuat, alpha);
        client.current_state.quaternion.copy(testQuat);

        // Render world
        world.render();
        client.current_state.position.copy(client_physics_state.position);
        client.current_state.quaternion.copy(client_physics_state.quaternion);


    }, 0);


Client.prototype.processInputs = function(socket, delta){

    var input = new Object;
    input.input_sequence_number = this.input_sequence_number++;
    input.id = this.id;

    if(this.key_up){
        input.key_up = true;
    } else {
        input.key_up = false;
    }

    if(this.key_down){
        input.key_down = true;
    } else {
        input.key_down = false;
    }

    if(this.key_left){
        input.key_left = true;
    } else {
        input.key_left = false;
    }

    if(this.key_right){
        input.key_right = true;
    } else {
        input.key_right = false;
    }

    if(this.jump && this.velocity.y === 0){
        input.jump = true;
    } else {
        input.jump = false;
    }

    input.quat = {
        x:this.current_state.quaternion.x,
        y:this.current_state.quaternion.y,
        z:this.current_state.quaternion.z,
        w:this.current_state.quaternion.w
    };

    // Send this input to the server for processing
    socket.emit('client_input', input);

    // Do client-side prediction.
    this.applyInput(input, delta);

    // Save this input for later reconciliation.
    this.pending_inputs.push(input);

};

Client.prototype.applyInput = function(input, delta){

    if(input.jump){
        this.velocity.y += 350;
        this.jump = false;
    }

    this.velocity.x -= this.velocity.x * 10.0 * delta;
    this.velocity.z -= this.velocity.z * 10.0 * delta;
    this.velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass

    if ( input.key_up ) this.velocity.z -= 400.0 * delta;
    if ( input.key_down ) this.velocity.z += 400.0 * delta;
    if ( input.key_left ) this.velocity.x -= 400.0 * delta;
    if ( input.key_right ) this.velocity.x += 400.0 * delta;

    this.current_state.translateX( this.velocity.x * delta );
    this.current_state.translateY( this.velocity.y * delta );
    this.current_state.translateZ( this.velocity.z * delta );

    if ( this.current_state.position.y < 10 ) {
        this.velocity.y = 0;
        this.current_state.position.y = 10;
    }

};

*EDIT It might be worth mentioning that if I remove the lines:

var testQuat = new THREE.Quaternion();
THREE.Quaternion.slerp(client.previous_state.quaternion, client.current_state.quaternion, testQuat, alpha);
client.current_state.quaternion.copy(testQuat);

and

client.current_state.quaternion.copy(client_physics_state.quaternion);

Which are the lines that I am expecting to interpolate the rotation and then reset the rotation back to it's current position after render, that everything works fine. It's only when I am attempting to interpolate rotation with this method that I am experiencing this strange behavior.

Also, client.current_state is a reference to the yawObject of this THREE.PointerLockControls type (rotation is happening via mouse input):

THREE.PointerLockControls = function ( camera ) {

    var scope = this;

    camera.rotation.set( 0, 0, 0 );

    var pitchObject = new THREE.Object3D();
    pitchObject.add( camera );

    var yawObject = new THREE.Object3D();
    yawObject.position.y = 10;
    yawObject.add( pitchObject );

    var PI_2 = Math.PI / 2;

    var onMouseMove = function ( event ) {

        if ( scope.enabled === false ) return;

        var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
        var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;

        yawObject.rotation.y -= movementX * 0.002;
        pitchObject.rotation.x -= movementY * 0.002;

        pitchObject.rotation.x = Math.max( - PI_2, Math.min( PI_2, pitchObject.rotation.x ) );

    };

    this.dispose = function() {

        document.removeEventListener( 'mousemove', onMouseMove, false );

    };

    document.addEventListener( 'mousemove', onMouseMove, false );

    this.enabled = false;

    this.getObject = function () {

        return yawObject;

    };

    this.getDirection = function() {

        // assumes the camera itself is not rotated

        var direction = new THREE.Vector3( 0, 0, - 1 );
        var rotation = new THREE.Euler( 0, 0, 0, "YXZ" );


        return function(  ) {

                        var v = new THREE.Vector3();

            rotation.set( pitchObject.rotation.x, yawObject.rotation.y, 0 );

            v.copy( direction ).applyEuler( rotation );

            return v;

        };

    }();

};
\$\endgroup\$
  • \$\begingroup\$ Please confirm that the included code: saves the current state, edits the current state, renders using the current state, then overwrites the changes to current state with the unmodified saved state. \$\endgroup\$ – Jon Mar 4 '16 at 22:33
  • \$\begingroup\$ That is correct \$\endgroup\$ – nullReference Mar 4 '16 at 22:35
  • \$\begingroup\$ I don't see anything obvious here; show us where client.current_state.quaternion integrates deltaTime. \$\endgroup\$ – Jon Mar 4 '16 at 22:55
  • \$\begingroup\$ If I'm not mistaken you are asking for what happens within the processInputs() function, or rather the function applyInput which is called at the end of processInputs(). I have updated the question to include these two methods. \$\endgroup\$ – nullReference Mar 4 '16 at 23:01
  • \$\begingroup\$ Show where you: client.current_state.quaternion.w += rotationFactor * deltaTime; \$\endgroup\$ – Jon Mar 4 '16 at 23:04
1
+50
\$\begingroup\$

The problem lies here:

  yawObject.rotation.y -= movementX * 0.002;

There is no guarantee that this will rotate in the expected direction. You were safe while you were not modifying rotation elsewhere, but changing client.current_state.quaternion in the main loop forces rotation to be recomputed from the quaternion you just assigned. Unfortunately the Euler angles (or at least the ones with the convention used here) wrap around when y reaches 90 degrees, and all sorts of weird things happen.

Here is a quick hack to fix your problem:

  yawObject.rotation.y -= movementX *
               (Math.abs(yawObject.rotation.x) < PI_2 ? 0.002 : -0.002);

But a more robust solution would be to never modify yawObject or pitchObject outside of PointerLockControls.

\$\endgroup\$
  • \$\begingroup\$ Are there any drawbacks to using the quick hack? I might be able to refactor to have a physics object and a render object. The physics object would always be the object as it truly exists in the world and the render object saving the state that get's rendered to the user's screen. However I might hold off on doing so if there are no immediate negative effects of using your quick hack as it seems to be working correctly. \$\endgroup\$ – nullReference Mar 9 '16 at 14:22
  • \$\begingroup\$ There is no immediate negative effect that I can think of. \$\endgroup\$ – sam hocevar Mar 10 '16 at 8:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.