Smooth Mouse Rotation in Three.js
This example demonstrates Smooth axis rotation via mouse in three.js. Click and drag around in the above.
It is based off of this post and this example on the three.js website.
Achieving a momentum effect during mouse drag has always been a nice feature that I’ve see many times in different examples of 3D for the web. After experimenting with three.js and seeing it’s potential I thought this is a must have feature. The problem I found though is that from the examples that use it, it was difficult to find proper source code that worked just how I wanted it.
The examples on the three.js website accomplish something close… The trackball example seems to accomplish the smooth mouse tracking, but the only problem is that the camera tilts in all directions when navigating. I wanted a stable view plane while navigating. Other examples like this one have the above, but would only rotate as far as the screen would allow the mouse to go hence you couldn’t spin an object around. The Orbit Controls example has the right kind of navigation, but lacks the mouse momentum. Finally I decided to take this example and tweak it a bit.
var container, stats; var camera, scene, renderer; var group, text, plane; var targetRotationX = 0; var targetRotationOnMouseDownX = 0; var targetRotationY = 0; var targetRotationOnMouseDownY = 0; var mouseX = 0; var mouseXOnMouseDown = 0; var mouseY = 0; var mouseYOnMouseDown = 0; var windowHalfX = window.innerWidth / 2; var windowHalfY = window.innerHeight / 2; var finalRotationY init(); animate(); function init() { camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 10000 ); camera.position.z = 5; scene = new THREE.Scene(); // lights light = new THREE.DirectionalLight( 0xffffff ); light.position.set( 1, 1, 1 ); scene.add( light ); light = new THREE.DirectionalLight( 0x002288 ); light.position.set( -1, -1, -1 ); scene.add( light ); light = new THREE.AmbientLight( 0x222222 ); scene.add( light ); // texture - texture must not be in same folder or there is an error. var texture = THREE.ImageUtils.loadTexture( 'images/texture.jpg', {}, function(){ // use to test when image gets loaded if it does render(); }, function(){ alert('error') }); //alert('WORKING'); material = new THREE.MeshPhongMaterial({map: texture}); group = new THREE.Object3D(); //load mesh var loader = new THREE.JSONLoader(); loader.load('models/cube.js', modelLoadedCallback); // renderer renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } ); renderer.setSize( window.innerWidth, window.innerHeight ); container = document.getElementById( 'container' ); container.appendChild( renderer.domElement ); stats = new Stats(); stats.domElement.style.position = 'absolute'; stats.domElement.style.top = '0px'; container.appendChild( stats.domElement ); document.addEventListener( 'mousedown', onDocumentMouseDown, false ); document.addEventListener( 'touchstart', onDocumentTouchStart, false ); document.addEventListener( 'touchmove', onDocumentTouchMove, false ); // window.addEventListener( 'resize', onWindowResize, false ); //for debuging stats interval = setInterval( debugInfo, 50 ); } function modelLoadedCallback(geometry) { mesh = new THREE.Mesh( geometry, material ); group.add(mesh); scene.add( group ); } function onWindowResize() { windowHalfX = window.innerWidth / 2; windowHalfY = window.innerHeight / 2; camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize( window.innerWidth, window.innerHeight ); } // function onDocumentMouseDown( event ) { event.preventDefault(); document.addEventListener( 'mousemove', onDocumentMouseMove, false ); document.addEventListener( 'mouseup', onDocumentMouseUp, false ); document.addEventListener( 'mouseout', onDocumentMouseOut, false ); mouseXOnMouseDown = event.clientX - windowHalfX; targetRotationOnMouseDownX = targetRotationX; mouseYOnMouseDown = event.clientY - windowHalfY; targetRotationOnMouseDownY = targetRotationY; } function onDocumentMouseMove( event ) { mouseX = event.clientX - windowHalfX; mouseY = event.clientY - windowHalfY; targetRotationY = targetRotationOnMouseDownY + (mouseY - mouseYOnMouseDown) * 0.02; targetRotationX = targetRotationOnMouseDownX + (mouseX - mouseXOnMouseDown) * 0.02; } function onDocumentMouseUp( event ) { document.removeEventListener( 'mousemove', onDocumentMouseMove, false ); document.removeEventListener( 'mouseup', onDocumentMouseUp, false ); document.removeEventListener( 'mouseout', onDocumentMouseOut, false ); } function onDocumentMouseOut( event ) { document.removeEventListener( 'mousemove', onDocumentMouseMove, false ); document.removeEventListener( 'mouseup', onDocumentMouseUp, false ); document.removeEventListener( 'mouseout', onDocumentMouseOut, false ); } function onDocumentTouchStart( event ) { if ( event.touches.length == 1 ) { event.preventDefault(); mouseXOnMouseDown = event.touches[ 0 ].pageX - windowHalfX; targetRotationOnMouseDownX = targetRotationX; mouseYOnMouseDown = event.touches[ 0 ].pageY - windowHalfY; targetRotationOnMouseDownY = targetRotationY; } } function onDocumentTouchMove( event ) { if ( event.touches.length == 1 ) { event.preventDefault(); mouseX = event.touches[ 0 ].pageX - windowHalfX; targetRotationX = targetRotationOnMouseDownX + ( mouseX - mouseXOnMouseDown ) * 0.05; mouseY = event.touches[ 0 ].pageY - windowHalfY; targetRotationY = targetRotationOnMouseDownY + (mouseY - mouseYOnMouseDown) * 0.05; } } // function animate() { requestAnimationFrame( animate ); render(); stats.update(); } function render() { //horizontal rotation group.rotation.y += ( targetRotationX - group.rotation.y ) * 0.1; //vertical rotation finalRotationY = (targetRotationY - group.rotation.x); // group.rotation.x += finalRotationY * 0.05; // finalRotationY = (targetRotationY - group.rotation.x); if (group.rotation.x <= 1 && group.rotation.x >= -1 ) { group.rotation.x += finalRotationY * 0.1; } if (group.rotation.x > 1 ) { group.rotation.x = 1 } if (group.rotation.x < -1 ) { group.rotation.x = -1 } renderer.render( scene, camera ); } function debugInfo() { $('#debug').html("mouseX : " + mouseX + " mouseY : " + mouseY + " ") }
From the three.js site example, I simply added Y Axis versions for everything regarding x axis rotation, then added limits on the Y axis rotation:
//vertical rotation finalRotationY = (targetRotationY - group.rotation.x); if (group.rotation.x <= 1 && group.rotation.x >= -1) { group.rotation.x += finalRotationY * 0.1; } if (group.rotation.x > 1) { group.rotation.x = 1 } else if (group.rotation.x < -1) { group.rotation.x = -1 }
Download the full example files