Reputation: 185
I'm building an app that includes a rotation tool. I almost have it working but not quite. To give a visual, here's a screenshot:
In the image, the red dot is the center point, the green dot forms the first line of the angle, and the blue dot follows the mouse. The user places the center point (red), places the first line (red dot), and then the items being rotated (the three blue spheres in the image), follow the blue dot, rotating at the same angle. When the user clicks the last time, the rotated objects are placed, and the tool guides disappear.
The problem is that though the objects are rotating with the appropriate centerpoint, they're not rotating in alignment with the blue dot at all. The rotation seems to accelerate as the angle widens, to the point where, when the angle appears to be ~60, the speed seems to be infinite (rotating objects are in exactly the same place, stuck there). Sometimes the rotation reverses direction.
Given the behavior I'm thinking that it may have to do with misappropration of some trig function somewhere, but I'm not sure what that would be or where to find it.
I'm attempting to do this with quaternion rotation. I will note that before building the UI tool, I was rotating these objects from the console using Euler angles, and things worked well, however I'm thinking that quaternions are probably the better solution.
Here's the relevant code:
var clickCounter;
var angleLineMaterial = new THREE.LineBasicMaterial({ color: 0x888888 });
function initRotationTool(){
rotToolState = {
points: [],
angleLines: [],
quaternion: null,
eul: {}
}
clickCounter = 0;
}
initRotationTool();
function lineToPoint( line, endPosition ){
var end = new THREE.Vector3( endPosition.x, endPosition.y, endPosition.z );
line.geometry.vertices[1] = end;
line.geometry.verticesNeedUpdate = true;
}
var angleLine0ToMouse = function( e ){
lineToPoint ( rotToolState.angleLines[0], placeAtPlaneIntersectionPoint( activeGuidePlane ) );
}
var angleLine1ToMouse = function( e ){
lineToPoint ( rotToolState.angleLines[1], placeAtPlaneIntersectionPoint( activeGuidePlane ) );
}
function movePointTo( point, position ){
point.position = { x: position.x, y: position.y, z: position.z };
point.displayEntity.position.copy( point.position );
}
var toolPoint2FollowMouse = function( e ){
movePointTo( rotToolState.points[ 2 ], placeAtPlaneIntersectionPoint( activeGuidePlane ) );
}
var getRotToolQuaternion = function( e ){
rotToolState.quaternion = getQuaternionBetweenVec3sOriginatingAtPoint( rotToolState.points[1].position, rotToolState.points[2].position, rotToolState.points[0].position );
console.log( "getRotToolQuaternion", rotToolState.quaternion );
}
var getRotToolEuler = function( e ){
rotToolState.eul = getEulerBetweenVec3sOriginatingAtPoint( rotToolState.points[1].position, rotToolState.points[2].position, rotToolState.points[0].position );
console.log( "getRotToolEul", rotToolState.eul );
}
var rotNodesWithTool = function( e ){
if ( SELECTED.nodes && SELECTED.nodes.length > 0 ){
//rotateNodeArrayOnAxisAroundPoint( SELECTED.nodes, "y", _Math.degToRad ( rotToolState.quaternion._y ) , rotToolState.points[0].position, order = 'XYZ' ); //nodeArr, axis, angle, point, order = 'XYZ' );
quaternionRotateNodeArrayAroundPoint( SELECTED.nodes, rotToolState.quaternion, rotToolState.points[0].position );
}
}
function rotationTool( position ){
if ( clickCounter === 0 ){
//create the startPoint
rotToolState.points.push ( new Point( position, 1.0, 0xff0000 ) );
// initiate a line of zero length....
var lineStart = rotToolState.points[0].position;
var lineEnd = position;
var geometry = new THREE.Geometry();
geometry.vertices.push(
new THREE.Vector3( lineStart.x, lineStart.y, lineStart.z ),
new THREE.Vector3( lineStart.x, lineStart.y, lineStart.z )
);
rotToolState.angleLines.push( new THREE.Line( geometry, angleLineMaterial ) );
scene.add( rotToolState.angleLines[0] );
// And now add an event listener that moves the first line's second vertex with the mouse.
document.getElementById('visualizationContainer').addEventListener( 'mousemove', angleLine0ToMouse, false );
clickCounter++;
return;
}
else if ( clickCounter === 1 ){
// remove the eventlistener that moves the first line's second vertex with the mouse.
document.getElementById('visualizationContainer').removeEventListener( 'mousemove', angleLine0ToMouse, false );
// drop the line-end and the endpoint ( rotToolState.points[1] ).
lineToPoint( rotToolState.angleLines[0], position );
rotToolState.points.push ( new Point( position, 1.0, 0x00ff00 ) );
// initiate a line of zero length....
var lineStart = rotToolState.points[0].position;
var lineEnd = position;
var geometry = new THREE.Geometry();
geometry.vertices.push(
new THREE.Vector3( lineStart.x, lineStart.y, lineStart.z ),
new THREE.Vector3( lineStart.x, lineStart.y, lineStart.z )
);
rotToolState.angleLines.push( new THREE.Line( geometry, angleLineMaterial ) );
scene.add( rotToolState.angleLines[1] );
// add a third point ( rotToolState.points[2] ) and line that both moves with the mouse
rotToolState.points.push ( new Point( position, 1.0, 0x0000ff ) );
document.getElementById('visualizationContainer').addEventListener( 'mousemove', toolPoint2FollowMouse, false );
document.getElementById('visualizationContainer').addEventListener( 'mousemove', angleLine1ToMouse, false );
document.getElementById('visualizationContainer').addEventListener( 'mousemove', getRotToolQuaternion, false );
document.getElementById('visualizationContainer').addEventListener( 'mousemove', getRotToolEuler, false );
document.getElementById('visualizationContainer').addEventListener( 'mousemove', rotNodesWithTool, false );
clickCounter++;
return;
}
else if ( clickCounter === 2 ){
// draw a second line to wherever the mouse is now.
document.getElementById('visualizationContainer').removeEventListener( 'mousemove', toolPoint2FollowMouse, false );
document.getElementById('visualizationContainer').removeEventListener( 'mousemove', angleLine1ToMouse, false );
document.getElementById('visualizationContainer').removeEventListener( 'mousemove', getRotToolQuaternion, false );
document.getElementById('visualizationContainer').removeEventListener( 'mousemove', getRotToolEuler, false );
document.getElementById('visualizationContainer').removeEventListener( 'mousemove', rotNodesWithTool, false );
// drop the triangulating third point ( temporary )
// rotToolState.points.push ( new Point( position, 1.0, 0x0000ff ) );
clickCounter++;
return;
}
else if ( clickCounter === 3 ){
// remove the eventlistener that moves second line's second vertex with the mouse & rotates everything.
/* document.getElementById('visualizationContainer').removeEventListener( 'mousemove', function(e){
rotToolState.angleLines[1].vertex[1].position.set( ... );
rotateEverythingSelected....
} ); */
// Drop everything in the new position.
// rotateEverythingSelected...
// remove the lines and points
scene.remove( rotToolState.angleLines[0] );
scene.remove( rotToolState.angleLines[1] );
scene.remove( rotToolState.points[0].displayEntity );
scene.remove( rotToolState.points[1].displayEntity );
scene.remove( rotToolState.points[2].displayEntity );
initRotationTool();
return;
}
console.log( "I shouldn't execute. clickCounter = " , clickCounter );
}
function getQuaternionBetweenVec3s( v1, v2 ){
return new THREE.Quaternion().setFromUnitVectors( v1, v2 );
}
function getQuaternionBetweenVec3sOriginatingAtPoint( v1, v2, point ){
var vSub1 = new THREE.Vector3();
var vSub2 = new THREE.Vector3();
vSub1.subVectors( v1, point );
vSub2.subVectors( v2, point );
return getQuaternionBetweenVec3s( vSub1, vSub2 );
}
function getEulerBetweenVec3s( v1, v2 ){
var vec1 = { z: { a: v1.x, b: v1.y },
y: { a: v1.x, b: v1.z },
x: { a: v1.y, b: v1.z }
};
var vec2 = { z: { a: v2.x, b: v2.y },
y: { a: v2.x, b: v2.z },
x: { a: v2.y, b: v2.z }
};
var eul = {
x: getAngleBetween2DVectors( vec1.x, vec2.x ),
y: getAngleBetween2DVectors( vec1.y, vec2.y ),
z: getAngleBetween2DVectors( vec1.z, vec2.z )
};
return eul;
}
function getEulerBetweenVec3sOriginatingAtPoint( v1, v2, point ){
var vSub1 = new THREE.Vector3();
var vSub2 = new THREE.Vector3();
vSub1.subVectors( v1, point );
vSub2.subVectors( v2, point );
return getEulerBetweenVec3s( vSub1, vSub2 );
}
function getAngleBetween2DVectors( v1, v2 ){
return Math.atan2( v2.b - v1.b, v2.a - v1.a );
}
and....
/* 3D ROTATION OF NODES AND NODE ARRAYS USING EULERS */
function rotateNodeOnAxisAroundPoint( node, axis, angle, point, order = 'XYZ' ){
if ( !point ){ point = new THREE.Vector3( 0, 0, 0 ); }
moveNodeTo( node, rotateVec3AroundAxisOnPoint( new THREE.Vector3( node.position.x, node.position.y, node.position.z ), axis, angle, point, order ) ) ;
}
function rotateNodeArrayOnAxisAroundPoint( nodeArr, axis, angle, point, order = 'XYZ' ){
if ( !point ){ point = new THREE.Vector3( 0, 0, 0 ); }
for ( var n = 0; n < nodeArr.length; n++ ){
rotateNodeOnAxisAroundPoint( nodeArr[ n ], axis, angle, point, order );
}
}
/* 3D VECTOR3D ROTATION EULER HELPER FUNCTIONS */
function rotateVec3AroundAxisOnPoint( v, axis, angle, point, order = 'XYZ' ){
var angles = {};
if ( !point ){ point = new THREE.Vector3( 0, 0, 0 ); }
if ( axis === "x" ){
angles = { x: angle, y: 0, z: 0 };
}
if ( axis === "y" ){
angles = { x: 0, y: angle, z: 0 };
}
if ( axis === "z" ){
angles = { x: 0, y: 0, z: angle };
}
v = rotateVec3AroundPoint( v, point, angles, order );
return v;
}
function rotateVec3AroundPoint( v, point, angles, order = 'XYZ' ){
var vecSub = new THREE.Vector3();
var vecSubRotated = new THREE.Vector3();
var vecAdd = new THREE.Vector3();
vecSub.subVectors( v, point );
vecSubRotated = rotateVec3AroundOrigin( vecSub, angles, order );
vecAdd.addVectors( vecSubRotated, point );
return vecAdd;
}
function rotateVec3AroundOrigin( v, angles, order = 'XYZ' ){
var euler = new THREE.Euler( angles.x, angles.y, angles.z, order );
v.applyEuler( euler );
return v;
}
/* 3D ROTATION OF NODES AND NODE ARRAYS USING QUATERNIONS */
function quaternionRotateNodeAroundPoint( node, quaternion, point ){
if ( !point ){ point = new THREE.Vector3( 0, 0, 0 ); }
moveNodeTo( node, quaternionRotateVec3AroundPoint( new THREE.Vector3( node.position.x, node.position.y, node.position.z ), quaternion, point ) );
}
function quaternionRotateNodeArrayAroundPoint( nodeArr, quaternion, point ){
if ( !point ){ point = new THREE.Vector3( 0, 0, 0 ); }
for ( var n = 0; n < nodeArr.length; n++ ){
quaternionRotateNodeAroundPoint( nodeArr[ n ], quaternion, point );
}
}
function quaternionRotateNodeOnAxisAroundPoint( node, axis, angle, point ){
if ( !point ){ point = new THREE.Vector3( 0, 0, 0 ); }
moveNodeTo( node, quaternionRotateVec3AroundAxisOnPoint( new THREE.Vector3( node.position.x, node.position.y, node.position.z ), axis, angle, point ) ) ;
}
function quaternionRotateNodeArrayOnAxisAroundPoint( nodeArr, axis, angle, point ){
if ( !point ){ point = new THREE.Vector3( 0, 0, 0 ); }
for ( var n = 0; n < nodeArr.length; n++ ){
quaternionRotateNodeOnAxisAroundPoint( nodeArr[ n ], axis, angle, point );
}
}
/* 3D VECTOR3D ROTATION QUATERNION HELPER FUNCTIONS */
function quaternionRotateVec3AroundAxisOnPoint( v, axis, angle, point ){
var quaternion = new THREE.Quaternion();
var axisAngle = new THREE.Vector3();
if ( !point ){ point = new THREE.Vector3( 0, 0, 0 ); }
if ( axis === "x" ){
axisAngle = { x: 1, y: 0, z: 0 };
}
if ( axis === "y" ){
axisAngle = { x: 0, y: 1, z: 0 };
}
if ( axis === "z" ){
axisAngle = { x: 0, y: 0, z: 1 };
}
quaternion.setFromAxisAngle( axisAngle, angle );
v = quaternionRotateVec3AroundPoint( v, quaternion, point );
return v;
}
function quaternionRotateVec3AroundPoint( v, quaternion, point ){
var vecSub = new THREE.Vector3();
var vecSubRotated = new THREE.Vector3();
var vecAdd = new THREE.Vector3();
vecSub.subVectors( v, point );
vecSubRotated = applyQuaternionToVec3( vecSub, quaternion );
vecAdd.addVectors( vecSubRotated, point );
return vecAdd;
}
function applyQuaternionToVec3( v, quaternion ){
v.applyQuaternion( quaternion );
return v;
}
/* END 3D VECTOR3D ROTATION QUATERNION HELPER FUNCTIONS */
As you can see, I've also got Euler functions set up, though I'm running everything through the quaternions currently.
Any help would be greatly appreciated. Thanks!
UPDATE:
There seem to be two separate issues, and I believe I resolved the first this morning. The quaternion rotation that was being applied to the spheres was being applied to their concurrent (already rotated) positions rather than their original positions. I took care of this by copying their original position vectors into an object and applying the continuously updating quaternion value to the values in that object to get the new positions.
The rotation is still not working correctly, however. Here's a little video to explain:
(Direct link here since the iframe above may not be working: Video demo of isssue )
Notes on the video:
1. the fix I note when I hit a debug point is not the fix I applied but happened to be a debug point I'd forgotten about. Not relevant to this question.
2. I AM using setFromUnitVectors()
. I guessed this in the video, not remembering.
The relevant code changes from above:
var clickCounter;
var rotToolState;
var origNodePositions = []; // THIS LINE WAS ADDED
var angleLineMaterial = new THREE.LineBasicMaterial({ color: 0x888888 });
function initRotationTool(){
rotToolState = {
points: [],
angleLines: [],
quaternion: {
last: null,
current: null
},
eul: {}
}
origNodePositions = []; // THIS LINE WAS ADDED
clickCounter = 0;
}
initRotationTool();
// Node Operations: Get Original Positions when the tool is initialized. THESE FUNCTIONS WERE ADDED
function getOrigNodePosition( node ){
if ( node && node.isNode ){
var origPos = new THREE.Vector3();
origPos.copy( node.position );
origNodePositions.push( origPos );
}
}
function getOrigNodeArrayPositions( nodeArr ){
if ( nodeArr.length > 0 ){
for ( var n = 0; n < nodeArr.length; n++ ){
getOrigNodePosition( nodeArr[ n ] );
}
}
}
And...
else if ( clickCounter === 1 ){
// remove the eventlistener that moves the first line's second vertex with the mouse.
document.getElementById('visualizationContainer').removeEventListener( 'mousemove', angleLine0ToMouse, false );
// drop the line-end and the endpoint ( rotToolState.points[1] ).
lineToPoint( rotToolState.angleLines[0], position );
rotToolState.points.push ( new Point( position, 1.0, 0x00ff00 ) );
// initiate a line of zero length....
var lineStart = rotToolState.points[0].position;
var lineEnd = position;
var geometry = new THREE.Geometry();
geometry.vertices.push(
new THREE.Vector3( lineStart.x, lineStart.y, lineStart.z ),
new THREE.Vector3( lineStart.x, lineStart.y, lineStart.z )
);
rotToolState.angleLines.push( new THREE.Line( geometry, angleLineMaterial ) );
scene.add( rotToolState.angleLines[1] );
getOrigNodeArrayPositions( SELECTED.nodes ); // THIS LINE WAS ADDED....
And this function was modified....
function quaternionRotateNodeAroundPoint( node, quaternion, point ){
if ( !point ){ point = new THREE.Vector3( 0, 0, 0 ); }
//var startPos = node.position;
var nodeIndex = SELECTED.nodes.indexOf( node );
var startPos2 = origNodePositions[ nodeIndex ];
//moveNodeTo( node, quaternionRotateVec3AroundPoint( startPos, quaternion, point ) );
moveNodeTo( node, quaternionRotateVec3AroundPoint( startPos2, quaternion, point ) );
}
I hope this update simplifies and clarifies the problem still remaining. Thanks for your insight.
Upvotes: 0
Views: 2765
Reputation: 185
Well with a friend looking over my shoulder, I was able to solve the problem quite simply. It turns out that I need to normalize the vectors that I pass to .setFromUnitVectors
. I took care of this by adding two lines to the function that I was using to capture the quaternion between the angle points generated by my rotation tool:
function getQuaternionBetweenVec3s( v1, v2 ){
var v1n = v1.normalize(); // Line was added
var v2n = v2.normalize(); // Line was added
return new THREE.Quaternion().setFromUnitVectors( v1n, v2n ); // params formerly v1, v2
}
This function receives the two points that define the angle, after having the distance from the origin has been subtracted from each of them.
That's it. Working beautifully. I hope this helps someone down the line.
Upvotes: 2