Skip to content

Commit

Permalink
Add simpler pointer to sphere mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul Elliott committed Oct 4, 2020
1 parent c2f7c23 commit 0cb9e69
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 176 deletions.
8 changes: 8 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

A control to rotate Object3Ds as if touching a trackball. So the point clicked on by the mouse stays under the moving mouse. Touch and momentum support.


## Example
https://paulhax.github.io/spin-controls/


## Use
```javascript
var radius = 50;
Expand All @@ -19,3 +21,9 @@ var spinControl = new SpinControls( mesh, radius, camera, renderer.domElement );
//Call every frame
spinControl.update();
```

## Links about trackball controls
https://www.mattkeeter.com/projects/rotation/
http://hjemmesider.diku.dk/~kash/papers/DSAGM2002_henriksen.pdf
https://www.khronos.org/opengl/wiki/Object_Mouse_Trackball
https://mimosa-pudica.net/3d-rotation/
235 changes: 65 additions & 170 deletions SpinControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,18 @@ var SpinControls = function ( object, trackBallRadius, camera, domElement ) {
_lastQuaternion = new THREE.Quaternion(),
_lastVelTime,

_mousePrev = new THREE.Vector2(),
_mouseCurr = new THREE.Vector2(),
_mousePrev = new THREE.Vector3(),
_mouseCurr = new THREE.Vector3(),
_lastMouseEventTime = 0,

// Separate touch variables as might be mouseing and touching at same time on laptop?
// Separate touch variables as might be mousing and touching at same time on laptop?
_touchPrev = new THREE.Vector2(),
_touchCurr = new THREE.Vector2(),
_lastTouchEventTime = 0,

_isPointerDown = false,

_ray = new THREE.Ray(),
_trackBallSphere = new THREE.Sphere(),

_EPS = 0.000001,
_OFF_TRACKBALL_VELOCITY_GAIN = 8.0; // ToDo: Base this on angle change around sphere edge?


var changeEvent = { type: 'change' };
var startEvent = { type: 'start' };
Expand Down Expand Up @@ -89,170 +84,28 @@ var SpinControls = function ( object, trackBallRadius, camera, domElement ) {

this.updateAngularVelocity = ( function () {

var objectPos = new THREE.Vector3(),
objectToCamera = new THREE.Vector3(),
objectPlane = new THREE.Plane(),

currentInputDirection = new THREE.Vector3(),
lastInputDirection = new THREE.Vector3(),
currentInputPos = new THREE.Vector3(),
lastInputPos = new THREE.Vector3(),

deltaMouse = new THREE.Vector2(),
polarVel = new THREE.Vector3(),

p0 = new THREE.Vector3(),
q0 = new THREE.Quaternion(),

angle;
var q0 = new THREE.Quaternion(),
q1 = new THREE.Quaternion(),
q0Conj = new THREE.Quaternion(),

return function updateAngularVelocity( currentNdc, lastNdc, deltaTime ) {

console.log(currentNdc);

const p1 = new THREE.Vector3();

const uv = currentNdc;
const v3 = p1;
const t = currentNdc.lengthSq();

// if (t < 1.0) {
// p1.set(currentNdc.x, currentNdc.y, Math.sqrt(1.0 - t));
// }
// else {
// currentNdc.normalize();
// p1.set(currentNdc.x, currentNdc.y, 0.0);
// }

if (t < 0.5) {
v3.set(uv.x, uv.y, Math.sqrt(1.0 - t));
}
else {
v3.set(uv.x, uv.y, 1.0 / (2.0 * Math.sqrt(t)));
v3.normalize();
}

const q = new THREE.Quaternion();
// q.setFromUnitVectors(p0, p1);
q.set(p1.x, p1.y, p1.z, 0.0).multiply(new THREE.Quaternion(p0.x, p0.y, p0.z, 0.0).conjugate());

// angle = lastInputPos.angleTo( currentInputPos ) / deltaTime;
angle = q.angleTo(q0) / deltaTime;
// Change in angular vel
_angularVelocity.crossVectors( p0, p1);
_angularVelocity.setLength( angle ); // Just set it because we are touching trackball without sliding
_this.applyVelocity(); // DO IT NOW!
p0.copy(p1)
q0.copy(q)
angleDelta;

return;


// Intersect mouse on plane at object with normal pointing to camera

objectPos.setFromMatrixPosition( _this.object.matrixWorld );
objectToCamera.copy( _this.camera.position ).sub( objectPos );
objectPlane.setFromNormalAndCoplanarPoint( objectToCamera, objectPos );

_ray.origin.copy( _this.camera.position );

currentInputDirection.set( currentNdc.x, currentNdc.y, .5 )
currentInputDirection.unproject( _this.camera ) // In world space
currentInputDirection.sub( _this.camera.position ).normalize() // Subtract to put around origin
_ray.direction.copy( currentInputDirection )

if( _ray.intersectPlane( objectPlane, currentInputPos ) == null ) {

return; // We could be facing 180 degrees away

}

// Put in object position space to find trackball radius
currentInputPos.sub( objectPos );

lastInputDirection.set( lastNdc.x, lastNdc.y, .5 );
lastInputDirection.unproject( _this.camera );
lastInputDirection.sub( _this.camera.position ).normalize();
_ray.direction.copy( lastInputDirection );

if( _ray.intersectPlane( objectPlane, lastInputPos ) == null ) {

return;

}

lastInputPos.sub( objectPos );

// Is pointer over trackball?
if( currentInputPos.length() < _this.trackballRadius && lastInputPos.length() < _this.trackballRadius ) {
return function updateAngularVelocity( p1, p0, timeDelta ) {

// Project mouse on trackball sphere

if( _this.trackballRadius <= objectToCamera.length() ) { // If trackball smaller than camera distance

_trackBallSphere.set( objectPos, _this.trackballRadius);
_ray.direction.copy( currentInputDirection );

if( _ray.intersectSphere( _trackBallSphere, currentInputPos ) == null ) {
// q0Conj.set(p0.x, p0.y, p0.z, 0.0)
// q0Conj.normalize();
// q0Conj.conjugate();
// q1.set(p1.x, p1.y, p1.z, 0.0).multiply(q0Conj); // path independent (Shoemake)
// timeDelta *= 2.0; // divide angleDelta by 2 to keep sphere under pointer. Might break algo properties. Todo: Investigate.

return;
q1.setFromUnitVectors(p0, p1); // path dependent

}

_ray.direction.copy( lastInputDirection );

if( _ray.intersectSphere( _trackBallSphere, lastInputPos ) == null ) {

return;

}

// Put in object position space
currentInputPos.sub( objectPos );
lastInputPos.sub( objectPos );

}

angle = lastInputPos.angleTo( currentInputPos ) / deltaTime;

// Change in angular vel
_angularVelocity.crossVectors( lastInputPos, currentInputPos );
_angularVelocity.setLength( angle ); // Just set it because we are touching trackball without sliding

} else {

// Keep rotating based on delta mouse in normalized device coordinates

//ToDo: Simplify by find delta mouse polar coordinates with THREE.Sphere?

objectPos.project( _this.camera );

deltaMouse.subVectors(currentNdc, lastNdc);

// Find change in mouse radius to trackball center
// var ballAngleSize = Math.atan( _this.trackballRadius / objectToCamera.length() );
// var ndcPerDegree = 1 / ( _this.camera.fov / 2 );
// var ndcPerBall = ndcPerDegree / ballAngleSize;
// Same as above in one line
var ndcPerBall = ( 1 / ( _this.camera.fov / 2 ) ) // NDC per degree
/ ( Math.atan( _this.trackballRadius / objectToCamera.length() ) ); // Ball angle size

lastNdc.sub(objectPos) // Move into object space
lastNdc.normalize();
var deltaRadius = deltaMouse.dot( lastNdc ) * ndcPerBall / deltaTime * _OFF_TRACKBALL_VELOCITY_GAIN;

_angularVelocity.crossVectors( objectToCamera, lastInputPos );
_angularVelocity.setLength( deltaRadius ); // Just set it because we are touching trackball without sliding

// Find polar angle change
angle = lastInputPos.angleTo( currentInputPos ) / deltaTime;
polarVel.crossVectors( lastInputPos, currentInputPos );
polarVel.setLength( angle );

_angularVelocity.add( polarVel );

}
q0.set(p0.x, p0.y, p0.z, 1.0);
angleDelta = q1.angleTo(q0) / timeDelta;

// Just set velocity because we are touching trackball without sliding
_angularVelocity.crossVectors( p0, p1);
_angularVelocity.setLength( angleDelta );
_this.applyVelocity(); // DO IT NOW!

};
Expand Down Expand Up @@ -350,22 +203,64 @@ var SpinControls = function ( object, trackBallRadius, camera, domElement ) {

}() );

var getPointerInSphere = ( function () {

var point = new THREE.Vector3();

return function getPointerInSphere( ndc ) {

var t = ndc.lengthSq();

// Todo Move sphere projection to spinning object space.

// Shoemake pointer mapping
if (t < 1.0) {
point.set(ndc.x, ndc.y, Math.sqrt(1.0 - t));
}
else {
ndc.normalize();
point.set(ndc.x, ndc.y, 0.0);
}

// Holroyd pointer mapping
// if (t < 0.5) {
// point.set(ndc.x, ndc.y, Math.sqrt(1.0 - t));
// }
// else {
// point.set(ndc.x, ndc.y, 1.0 / (2.0 * Math.sqrt(t)));
// point.normalize();
// }

// Azimuthal equidistant
// t = (Math.PI / 2.0) * ndc.length();
// var v3 = ndc.clone();
// var sined = t < Number.EPSILON ? 1.0 : Math.sin(t) / t;
// v3.multiplyScalar((Math.PI / 2.0) * sined);
// point.set(v3.x, v3.y, Math.cos(t));

return point;

};

}() );

// listeners

function onMouseDown( event ) {

if ( _this.enabled === false || event.button !== 0 ) return;

var pointerNDC = getPointerInNdc( event.pageX, event.pageY );

event.preventDefault(); // Prevent the browser from scrolling.
event.stopImmediatePropagation(); //Prevent other controls from working.
event.stopImmediatePropagation(); // Stop other controls working.

// Manually set the focus since calling preventDefault above
// prevents the browser from setting it automatically.
_this.domElement.focus ? _this.domElement.focus() : window.focus();

_mouseCurr.copy( pointerNDC );
// var pointerNDC = getPointerInNdc( event.pageX, event.pageY );

_mouseCurr.copy( getPointerInSphere( getPointerInNdc( event.pageX, event.pageY ) ) );
_lastMouseEventTime = performance.now();
_angularVelocity.set( 0, 0, 0 );
_isPointerDown = true;
Expand All @@ -384,7 +279,7 @@ var SpinControls = function ( object, trackBallRadius, camera, domElement ) {
event.preventDefault();

_mousePrev.copy( _mouseCurr );
_mouseCurr.copy( getPointerInNdc( event.pageX, event.pageY ) );
_mouseCurr.copy( getPointerInSphere( getPointerInNdc( event.pageX, event.pageY ) ) );

var currentTime = performance.now();
var deltaTime = ( currentTime - _lastMouseEventTime ) / 1000.0;
Expand Down
13 changes: 7 additions & 6 deletions example_simple.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<script>
(function(){var script=document.createElement('script');script.onload=function(){var stats=new Stats();document.body.appendChild(stats.dom);requestAnimationFrame(function loop(){stats.update();requestAnimationFrame(loop)});};script.src='stats.js';document.head.appendChild(script);})()
</script>
<head>
<title>Simple example - spin controls</title>
<meta charset="utf-8">
Expand Down Expand Up @@ -52,9 +49,13 @@
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 3000 );
// camera.position.set( 750, 300, 750 );
camera.position.set( 0, 0, 3 );
// camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 3000 );
width = 2;
height = 2;
camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );

// camera.position.set( 750, 300, 750 );
camera.position.set( 0, 0, 3 );
camera.lookAt( 0, 0, 0 );

scene = new THREE.Scene();
Expand Down

0 comments on commit 0cb9e69

Please sign in to comment.