Last active
November 17, 2022 16:25
-
-
Save sketchpunk/b92822e1cdb0e2444706e4e575671083 to your computer and use it in GitHub Desktop.
Movement functions for use with ThreeJS Camera, Also inc Springs, Mouse/Keboard Event Handlers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Move camera hased on camera view's axis clamped to the XZ plane | |
function panStepXZ( camera, xSteps, zSteps, initPos=null ){ | |
const rot = camera.quaternion.toArray(); | |
const pos = initPos?.slice() || camera.position.toArray(); | |
const fwd = vec3.transformQuat( [0,0,0], [0,0,-1], rot ); // compute forward | |
const rit = vec3.cross( [0,0,0], fwd, [0,1,0] ); // get left | |
vec3.cross( fwd, [0,1,0], rit ); // clamp forward to XZ plane by using UP | |
vec3.scaleAndAdd( pos, pos, fwd, zSteps ); | |
vec3.scaleAndAdd( pos, pos, rit, xSteps ); | |
// camera.position.fromArray( pos ); | |
return pos; | |
} | |
// Move camera based on camera view's UP and RIGHT Axis | |
function screenPanStep( camera, xSteps, ySteps, initPos=null ){ | |
const rot = camera.quaternion.toArray(); | |
const pos = initPos?.slice() || camera.position.toArray(); | |
const rit = vec3.transformQuat( [0,0,0], [1,0,0], rot ); | |
const up = vec3.transformQuat( [0,0,0], [0,1,0], rot ); | |
vec3.scaleAndAdd( pos, pos, rit, xSteps ); | |
vec3.scaleAndAdd( pos, pos, up, ySteps ); | |
// camera.position.fromArray( pos ); | |
return pos; | |
} | |
// Move camera based on camera view's FORWARD and RIGHT Axis | |
function screenStep( camera, xSteps, zSteps, initPos=null ){ | |
const rot = camera.quaternion.toArray(); | |
const pos = initPos?.slice() || camera.position.toArray(); | |
const fwd = vec3.transformQuat( [0,0,0], [0,0,-1], rot ); | |
const rit = vec3.transformQuat( [0,0,0], [1,0,0], rot ); | |
vec3.scaleAndAdd( pos, pos, fwd, zSteps ); | |
vec3.scaleAndAdd( pos, pos, rit, xSteps ); | |
camera.position.fromArray( pos ); | |
} | |
// Rotate camera for look movement. X Rotation is clamped so | |
// user can not keep rotating around, plus the final rotation | |
// is clamped to world up to prevent any disorientation. | |
function lookStep( camera, xRad=0, yRad=0, initRot=null ){ | |
const rot = initRot?.slice() || camera.quaternion.toArray(); | |
const q = [0,0,0,1]; | |
const fwd = [0,0,0]; | |
// ----------------------- | |
// Compute Y Rotation | |
if( yRad ){ | |
quat.setAxisAngle( q, [0,1,0], yRad ); | |
quat.mul( rot, q, rot ); | |
} | |
// ----------------------- | |
// Compute X Rotation | |
if( xRad ){ | |
// Clamp Forward to XZ Plane | |
const xzDir = vec3.transformQuat( [0,0,0], [0,0,1], rot ); | |
xzDir[1] = 0; | |
vec3.normalize( xzDir, xzDir ); | |
// Do X Rotation | |
const rit = vec3.transformQuat( [0,0,0], [1,0,0], rot ); // Local X Axis | |
quat.setAxisAngle( q, rit, xRad ); // Create Localized X Rotation in worldspace | |
const rotX = quat.mul( [0,0,0,1], q, rot ); // Add X rotation to Y's | |
// Clamp Rotation between UP & DOWN | |
vec3.transformQuat( fwd, [0,0,1], rotX ); | |
//console.log( vec3.dot( fwd, xzDir ), vec3.angle( fwd, xzDir ) * 180 / Math.PI ); | |
if( vec3.dot( fwd, xzDir ) < 0.01 ){ // Dot Product of 0.01 is about 89.38 degrees | |
// Compute Clamped Forward Direction | |
quat.setAxisAngle( q, rit, 89.38 * Math.PI / 180 * Math.sign( -fwd[1] ) ); | |
const cFwd = vec3.transformQuat( [0,0,0], xzDir, q ); | |
// Rotate Fwd to Clamped Forward Direction | |
quat.rotationTo( q, fwd, cFwd ); | |
quat.mul( rotX, q, rotX ); | |
} | |
// Save clamped X Rotation as final rotation | |
quat.copy( rot, rotX ); | |
} | |
// ----------------------- | |
// Z rotation can creap in with a lot of mouse movements & build up over time. | |
// Clamping rotation to world up prevents any disorentation from z rotation. | |
vec3.transformQuat( fwd, [0,0,1], rot ); | |
// camera.quaternion.fromArray( lookDirection( rot, fwd, [0,1,0] ) ); | |
return lookDirection( rot, fwd, [0,1,0] ) | |
} | |
// Compute rotation from a look direction & up direction | |
function lookDirection( out, dir, up=[0,1,0] ){ | |
const z = dir.slice(); | |
const x = vec3.cross([0, 0, 0], up, z); | |
const y = vec3.cross([0, 0, 0], z, x); | |
vec3.normalize( x, x ); | |
vec3.normalize( y, y ); | |
vec3.normalize( z, z ); | |
// Format: column-major, when typed out it looks like row-major | |
quat.fromMat3( out, [ ...x, ...y, ...z ] ); | |
return quat.normalize( out, out ); | |
} | |
// Create a Ray projection from the mouse position, then move | |
// the camera in that direction. Great for moving toward what | |
// the mouse is pointing at. | |
function mouseRayStep( e, camera, steps, initPos=null ){ | |
const coord = mouseCoord( e ); | |
const pos = initPos?.slice() || camera.position.toArray(); | |
const rect = e.target.getBoundingClientRect(); // Need canvas's size for ray projection | |
const [ rayStart, rayEnd ] = mouseScreenProjectionRay( | |
coord[0], coord[1], | |
rect.width, rect.height, | |
camera.projectionMatrix.toArray(), camera.matrixWorld.toArray() | |
); | |
const dir = vec3.sub( [0,0,0], rayEnd, rayStart ); // Get Ray Direction | |
vec3.normalize( dir, dir ); | |
vec3.scaleAndAdd( pos, pos, dir, steps ); // Step movement on ray | |
// camera.position.fromArray( pos ); | |
return pos; | |
} | |
// Using spherical coordinates in DEGREEs to orbit around a target, then pointing | |
// the camera to look at the target | |
function sphericalOrbitLook( camera, lon, lat, radius, target=[0,0,0] ){ | |
const phi = ( 90 - lat ) * Math.PI / 180; | |
const theta = ( lon + 180 ) * Math.PI / 180; | |
const pos = [ | |
target[ 0 ] -(radius * Math.sin( phi ) * Math.sin(theta)), | |
target[ 1 ] + radius * Math.cos( phi ), | |
target[ 2 ] -(radius * Math.sin( phi ) * Math.cos(theta)), | |
]; | |
camera.position.fromArray( pos ); | |
camera.quaternion.fromArray( lookRotation( [0,0,0,1], pos, target )); | |
} | |
// Compute look rotation from the camera toward a target position | |
function lookRotation( out, eye, target=[0,0,0], up=[0,1,0] ){ | |
// Forward is inverted, will face correct direction when converted | |
// to a ViewMatrix as it'll invert the Forward direction anyway | |
const z = vec3.sub( [0,0,0], eye, target ); | |
const x = vec3.cross([0, 0, 0], up, z); | |
const y = vec3.cross([0, 0, 0], z, x); | |
vec3.normalize( x, x ); | |
vec3.normalize( y, y ); | |
vec3.normalize( z, z ); | |
// Format: column-major, when typed out it looks like row-major | |
quat.fromMat3( out, [ ...x, ...y, ...z ] ); | |
return quat.normalize( out, out ); | |
} | |
// Orbit movement around a target position & rotate to look at the target | |
function orbitStep( camera, xRad, yRad, initPos=null, target=[0,0,0] ){ | |
const pos = initPos?.slice() || camera.position.toArray(); | |
const dir = vec3.sub( [0,0,0], pos, target ); | |
const axis = [0,1,0]; | |
const q = [0,0,0,1]; | |
// World Y Rotation | |
if( yRad !== 0 ){ | |
quat.setAxisAngle( q, axis, yRad ); | |
vec3.transformQuat( dir, dir, q ); | |
} | |
// Local X Rotation | |
if( xRad !== 0 ){ | |
const xzDir = vec3.normalize( [0,0,0], [ dir[0], 0, dir[2] ] ); | |
vec3.cross( axis, [0,1,0], dir ); // Compute X Axis using direction as Z Axis with World UP | |
vec3.normalize( axis, axis ); | |
quat.setAxisAngle( q, axis, xRad ); | |
vec3.transformQuat( dir, dir, q ); | |
// Clamp between UP and DOWN without flipping Z Direction | |
const nDir = vec3.normalize( [0,0,0], dir ); | |
if( vec3.dot( nDir, xzDir ) < 0.01 ){ | |
const len = vec3.length( dir ); | |
quat.setAxisAngle( q, axis, 89.99 * Math.PI / 180 * Math.sign( -nDir[1] ) ); // Max angle rotation | |
vec3.transformQuat( dir, xzDir, q ); // Rotate XZ dir to max dir | |
vec3.scale( dir, dir, len ); // XZ is a unit vector, apply original distance | |
} | |
} | |
vec3.add( pos, dir, target ); | |
camera.position.fromArray( pos ); | |
camera.quaternion.fromArray( lookRotation( q, pos, target ) ); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function KeyboardEvents(){ | |
let self; | |
const keys = new Map(); // State of a key press, lets user hold down multiple buttons | |
// #region HANDLERS | |
const onKeyDown = e=>{ keys.set( e.key, true ); }; | |
const onKeyUp = e=>{ keys.set( e.key, false ); }; | |
// #endregion | |
// #region MAIN | |
let isEnabled = false; | |
self = { | |
enable : ()=>{ | |
if( isEnabled ) return self; | |
window.addEventListener( 'keydown', onKeyDown, true ); | |
window.addEventListener( 'keyup', onKeyUp, true ); | |
isEnabled = true; | |
return self; | |
}, | |
disable : ()=>{ | |
if( !isEnabled ) return self; | |
window.removeEventListener( 'keydown', onKeyDown, true ); | |
window.removeEventListener( 'keyup', onKeyUp, true ); | |
isEnabled = false; | |
return self; | |
}, | |
isDown( key ){ return ( keys.has( e.key ) )? keys.get( key ) : false; }, | |
isShift(){ return !!keys.get( 'Shift' ); }, | |
isControl(){ return !!keys.get( 'Control' ); }, | |
getWASDAxis(){ | |
return [ | |
( keys.get( 'a' ) )? -1 : ( keys.get( 'd' ) )? 1 : 0, | |
( keys.get( 's' ) )? -1 : ( keys.get( 'w' ) )? 1 : 0, | |
]; | |
}, | |
getArrowAxis(){ | |
return [ | |
( keys.get( 'ArrowLeft' ) )? -1 : ( keys.get( 'ArrowRight' ) )? 1 : 0, | |
( keys.get( 'ArrowDown' ) )? -1 : ( keys.get( 'ArrowUp' ) ) ? 1 : 0, | |
]; | |
}, | |
}; | |
return self.enable(); | |
// #endregion | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function MouseEvents( canvas ){ | |
let self; | |
// #region HELPERS | |
const mouseCoord = e=>{ | |
const rect = canvas.getBoundingClientRect(); // need canvas sceen location & size | |
const x = e.clientX - rect.x; // canvas x position | |
const y = e.clientY - rect.y; // canvas y position | |
return [x, y]; | |
} | |
// #endregion | |
// #region HANDLERS | |
let pointerId = null; | |
let initCoord = null; | |
const onWheel = e=>{ self?.wheel( e, mouseCoord( e ) ); }; | |
const onPointerDown = e=>{ | |
//e.stopPropagation(); | |
pointerId = e.pointerId; | |
initCoord = mouseCoord( e ); | |
if( self.pointerDown ) self.pointerDown( e, initCoord ); | |
canvas.addEventListener( 'pointermove', onPointerMove, true ); | |
canvas.addEventListener( 'pointerup', onPointerUp, true ); | |
e.target.focus(); | |
}; | |
const onPointerUp = e=>{ | |
canvas.releasePointerCapture( pointerId ); | |
canvas.removeEventListener( 'pointermove', onPointerMove, true ); | |
canvas.removeEventListener( 'pointerup', onPointerUp, true ); | |
const coord = mouseCoord( e ); | |
const dx = coord[ 0 ] - initCoord[ 0 ]; | |
const dy = coord[ 1 ] - initCoord[ 1 ]; | |
if( self.pointerUp ) self.pointerUp( e, initCoord, [dx,dy] ); | |
}; | |
const onPointerMove = e=>{ | |
canvas.setPointerCapture( pointerId ); // Keep receiving events | |
// e.preventDefault(); | |
// e.stopPropagation(); | |
const coord = mouseCoord( e ); | |
const dx = coord[ 0 ] - initCoord[ 0 ]; | |
const dy = coord[ 1 ] - initCoord[ 1 ]; | |
if( self.pointerMove ) self.pointerMove( e, initCoord, [dx,dy] ); | |
}; | |
// #endregion | |
// #region MAIN | |
let isEnabled = false; | |
self = { | |
pointerDown : null, // ( e, coord ) | |
pointerMove : null, // ( e, coord, delta ) | |
pointerUp : null, // ( e, coord, delta ) | |
wheel : null, // ( e ) | |
enable : ()=>{ | |
if( isEnabled ) return self; | |
canvas.addEventListener( 'wheel', onWheel, true ); | |
canvas.addEventListener( 'pointerdown', onPointerDown, true ); | |
isEnabled = true; | |
return self; | |
}, | |
disable : ()=>{ | |
if( !isEnabled ) return self; | |
canvas.releasePointerCapture( pointerId ); | |
canvas.removeEventListener( 'wheel', onWheel, true ); | |
canvas.removeEventListener( 'pointerdown', onPointerDown, true ); | |
canvas.removeEventListener( 'pointermove', onPointerMove, true ); | |
canvas.removeEventListener( 'pointerup', onPointerUp, true ); | |
isEnabled = false; | |
return self; | |
}, | |
}; | |
return self.enable(); | |
// #endregion | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function mouseCoord( e ){ | |
const rect = e.target.getBoundingClientRect(); // target should be canvas | |
const x = e.clientX - rect.x; // canvas x position | |
const y = e.clientY - rect.y; // canvas y position | |
return [x, y]; | |
} | |
function mouseScreenProjectionRay( x, y, w, h, projMatrix, camMatrix ){ | |
// http://antongerdelan.net/opengl/raycasting.html | |
// Normalize Device Coordinate | |
const nx = x / w * 2 - 1; | |
const ny = 1 - y / h * 2; | |
// inverseWorldMatrix = invert( ProjectionMatrix * ViewMatrix ) OR | |
// inverseWorldMatrix = localMatrix * invert( ProjectionMatrix ) | |
const invMatrix = [ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0 ]; | |
mat4.invert( invMatrix, projMatrix ) | |
mat4.mul( invMatrix, camMatrix, invMatrix ); | |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
// https://stackoverflow.com/questions/20140711/picking-in-3d-with-ray-tracing-using-ninevehgl-or-opengl-i-phone/20143963#20143963 | |
// Clip Cords would be [nx,ny,-1,1]; | |
const clipNear = [ nx, ny, -1, 1 ]; // Ray Start | |
const clipFar = [ nx, ny, 1, 1 ]; // Ray End | |
// using 4d Homogeneous Clip Coordinates | |
vec4.transformMat4( clipNear, clipNear, invMatrix ); | |
vec4.transformMat4( clipFar, clipFar, invMatrix ); | |
// Normalize by using W component | |
for( let i=0; i < 3; i++){ | |
clipNear[ i ] /= clipNear[ 3 ]; | |
clipFar [ i ] /= clipFar [ 3 ]; | |
} | |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
return [ clipNear, clipFar ]; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// http://allenchou.net/2014/04/game-math-interpolating-quaternions-with-circular-blending/ | |
// https://gafferongames.com/post/spring_physics/ | |
// http://allenchou.net/2015/04/game-math-more-on-numeric-springing/ | |
// http://allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/ | |
/** Implicit Euler Spring */ | |
function SpringIEVec( size=3 ){ | |
let self; | |
let osc_ps = Math.PI * 2; // Oscillation per Second : How many Cycles (Pi*2) per second. | |
let damping = 1; // How much to slow down : Value between 0 and 1, 1 creates critical damping. | |
let epsilon = 0.0001 | |
const val = new Array( size ).fill( 0 ); | |
const tar = val.slice(); | |
const vel = val.slice(); | |
// #region Oscillation & Damping | |
const setOscPerSec = ( sec )=>{ osc_ps = Math.PI * 2 * sec; return self; }; | |
const setDamping = ( d )=>{ damping = d; return self; }; | |
// Damp Time, in seconds to damp. So damp 0.5 for every 2 seconds. | |
// With the idea that for every 2 seconds, about 0.5 damping has been applied | |
// IMPORTANT : Need to set OSC Per Sec First | |
const dampRatio = ( d, sec )=>{ | |
damping = Math.log( d ) / ( -osc_ps * sec ); | |
return self; | |
}; | |
// Reduce oscillation by half in X amount of seconds | |
// IMPORTANT : Need to set OSC Per Sec First | |
const dampHalflife = ( sec )=>{ | |
damping = 0.6931472 / ( osc_ps * sec ); // float zeta = -ln(0.5f) / ( omega * lambda ); | |
return self; | |
}; | |
// Critical Damping with a speed control of how fast the cycle to run | |
const dampExpo = ( sec )=>{ | |
osc_ps = 0.6931472 / sec; // -Log(0.5) but in terms of OCS its 39.7 degrees over time | |
damping = 1; | |
return self | |
}; | |
// #endregion | |
// #region Resetting | |
const reset = ( v=null )=>{ | |
zero( vel ); | |
if( v != null ){ | |
copy( val, v ); | |
copy( tar, v ); | |
}else{ | |
zero( val ); | |
zero( tar ); | |
} | |
return self; | |
} | |
// #endregion | |
// #region Quaternion Usage | |
// Reset quaternions have a special starting value | |
const quatReset = ( v=null )=>{ | |
quat.identity( vel ); | |
if( v != null ){ | |
vec3.copy( val, v ); | |
vec3.copy( tar, v ); | |
}else{ | |
quat.identity( val ); | |
quat.identity( tar ); | |
} | |
return self; | |
} | |
// Special target setting, Need to check if the target is on the | |
// same hemisphere as the value, if not it needs to be negated. | |
const setQuatTarget = ( q )=>{ | |
quat.copy( tar, q ); | |
if( quat.dot( val, tar ) < 0 ) vec4.negate( tar, tar ); | |
} | |
// #endregion | |
// #region Utils | |
const hasVelocity = ()=>{ | |
for( let v of vel ) if( v !== 0 ) return true; | |
return false; | |
} | |
const sqrDist = ( a, b )=>{ | |
let rtn = 0; | |
for( let i=0; i < size; i++ ) rtn += ( a[i] - b[i] )**2; | |
return rtn; | |
} | |
const sqrLen = ( a )=>{ | |
let rtn = 0; | |
for( let i=0; i < size; i++ ) rtn += a[i]**2; | |
return rtn; | |
} | |
const copy = ( a, b )=>{ for( let i=0; i < size; i++ ) a[i] = b[i]; } | |
const zero = ( a )=>{ for( let i=0; i < size; i++ ) a[i] = 0; } | |
// #endregion | |
// #region MAIN | |
const update = ( dt )=>{ | |
if( !hasVelocity() && sqrDist( tar, val ) === 0 ) return false; | |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
if( sqrLen( vel ) < epsilon && sqrDist( tar, val ) < epsilon ){ | |
zero( vel ); | |
copy( val, tar ); | |
return true; | |
} | |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
const friction = 1.0 + 2.0 * dt * damping * osc_ps; | |
const dt_osc = dt * osc_ps**2; | |
const dt2_osc = dt * dt_osc; | |
const det_inv = 1.0 / ( friction + dt2_osc ); | |
for( let i=0; i < size; i++ ){ | |
vel[i] = ( vel[i] + dt_osc * ( tar[i] - val[i] ) ) * det_inv; | |
val[i] = ( friction * val[i] + dt * vel[i] + dt2_osc * tar[i] ) * det_inv; | |
} | |
return true; | |
} | |
// #endregion | |
self = { | |
getTarget : ()=>{ return tar.slice(); }, | |
setTarget : ( v )=>{ copy( tar, v ); return self; }, | |
getValue : ()=>{ return val.slice(); }, | |
getNormValue : ()=>{ | |
const rtn = val.slice(); | |
let len = sqrLen( rtn ); | |
if( len > 0 ) len = 1 / Math.sqrt( len ); | |
for( let i=0; i < size; i++ ) rtn[ i ] *= len; | |
return rtn; | |
}, | |
setQuatTarget, | |
setOscPerSec, | |
setDamping, | |
dampRatio, | |
dampHalflife, | |
dampExpo, | |
reset, | |
quatReset, | |
update, | |
}; | |
return self; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment