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; }; }(); };
client.current_state.quaternionintegrates deltaTime. \$\endgroup\$client.current_state.quaternion.w += rotationFactor * deltaTime;\$\endgroup\$