Three.js 四元数旋转应用不正确

Three.js Quaternion Rotation Not Applying Properly

我正在构建一个包含旋转工具的应用程序。我几乎让它工作但不完全是。为了提供视觉效果,这里有一个屏幕截图:

图中,红点为中心点,绿点构成角度的第一条线,蓝点跟随鼠标。用户放置中心点(红色),放置第一条线(红点),然后旋转的项目(图像中的三个蓝色球体)跟随蓝点,以相同的角度旋转。当用户最后一次点击时,旋转的对象被放置,工具引导消失。

问题是尽管对象以适当的中心点旋转,但它们根本没有与蓝点对齐。随着角度变大,旋转似乎在加速,当角度看起来约为 60 度时,速度似乎是无穷大(旋转的物体完全在同一个地方,卡在那里)。有时旋转会反转方向。

考虑到这种行为,我认为它可能与某处某些三角函数的误用有关,但我不确定那是什么或在哪里可以找到它。

我正在尝试用四元数旋转来做到这一点。我会注意到,在构建 UI 工具之前,我使用欧拉角从控制台旋转这些对象,一切正常,但我认为四元数可能是更好的解决方案。

相关代码如下:

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 ); 
}

和....

/* 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 */

如您所见,我还设置了 Euler 函数,尽管我目前 运行 一切都通过四元数。

如有任何帮助,我们将不胜感激。谢谢!


更新:

似乎有两个不同的问题,我相信我今天早上解决了第一个问题。应用于球体的四元数旋转被应用于它们的并发(已经旋转)位置而不是它们的原始位置。我通过将它们的原始位置向量复制到一个对象并将不断更新的四元数值应用于该对象中的值以获得新位置来解决这个问题。

然而,旋转仍然无法正常工作。这里有一个小视频来解释:

(这里直接 link 因为上面的 iframe 可能不工作: Video demo of isssue )

视频注释: 1. 我在遇到调试点时注意到的修复不是我应用的修复,而是碰巧是我忘记的调试点。与这个问题无关。 2. 我正在使用 setFromUnitVectors()。视频里猜的,不记得了

相关代码由上面修改:

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 ] );
        }
    }   
}

还有...

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....

并且修改了这个函数....

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 ) );    

}

我希望这次更新能简化并澄清仍然存在的问题。感谢您的洞察力。

好吧,有一个朋友看着我,我很简单地解决了这个问题。事实证明,我需要对传递给 .setFromUnitVectors 的向量进行归一化。我通过在我用来捕获旋转工具生成的角度点之间的四元数的函数中添加两行来解决这个问题:

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

}

此函数接收定义角度的两个点,在从每个点中减去与原点的距离之后。

就是这样。干得漂亮。我希望这可以帮助某人。