I recommend to do store the mouse position at the point where you initially click in the view. Calculate the amount of the mouse movement in window coordinates. The distance of the movement has to be mapped to an angle. The rotation axis is perpendicular (normal) to the direction of the mouse movement. The result is a rotation of an object similar to this WebGL demo.
Store the current mouse position in startRotation
. Note store the coordinates of the position mouse position not normalized vector:
// xy normalized device coordinates:
float ndcX = 2.0f * xPos / context->getWidth() - 1.0f;
float ndcY = 1.0 - 2.0f * yPos / context->getHeight();
startVector = QVector3D(ndcX, ndcY, 0.0);
Get the current position in updateRotation
:
// xy normalized device coordinates:
float ndcX = 2.0f * xPos / context->getWidth() - 1.0f;
float ndcY = 1.0 - 2.0f * yPos / context->getHeight();
endVector = QVector3D(ndcX, ndcY, 0.0);
Calculate the vector from the start position to the end position:
QVector3D direction = endVector - startVector;
The rotation axis is normal to the direction of movement:
rotationAxis = QVector3D(-direction.y(), direction.x(), 0.0).normalized();
Note even if the type of direction
is QVector3D
, it is still a 2 dimensional vector. It is a vector in the XY plane of the viewport representing the mouse movement on the viewport. The z coordinate is 0. A 2 dimensional vector (x, y), can be 90 degrees counter clockwise rotated, by (-y, x).
The length of the direction vector represents tha angle of rotation. A mouse motion over the entire screen results in a vector with length 2.0. So if a dragging over the full screen should result in a full rotation, the length of the vector has to be multiplied by PI. If the a hlf rotation should be performed, then by PI/2:
angle = (float)qRadiansToDegrees(direction.length() * 3.141593);
Finally the new rotation has to be applied to the existing rotation and not to the model:
QMatrix4x4 addRotation;
addRotation.rotate(angle, rotationAxis.x(), rotationAxis.y(), rotationAxis.z());
rotation = addRotation * rotation;
Final code listing of the methods startRotation
and updateRotation
:
void ArcBall::startRotation(int xPos, int yPos) {
// xy normalized device coordinates:
float ndcX = 2.0f * xPos / context->getWidth() - 1.0f;
float ndcY = 1.0 - 2.0f * yPos / context->getHeight();
startVector = QVector3D(ndcX, ndcY, 0.0);
endVector = startVector;
rotating = true;
}
void ArcBall::updateRotation(int xPos, int yPos) {
// xy normalized device coordinates:
float ndcX = 2.0f * xPos / context->getWidth() - 1.0f;
float ndcY = 1.0 - 2.0f * yPos / context->getHeight();
endVector = QVector3D(ndcX, ndcY, 0.0);
QVector3D direction = endVector - startVector;
rotationAxis = QVector3D(-direction.y(), direction.x(), 0.0).normalized();
angle = (float)qRadiansToDegrees(direction.length() * 3.141593);
QMatrix4x4 addRotation;
addRotation.rotate(angle, rotationAxis.x(), rotationAxis.y(), rotationAxis.z());
rotation = addRotation * rotation;
startVector = endVector;
}
If you want a rotation around the upwards axis of the object a tilting the object along the view space x axis, then the calculation is different. First apply the rotation matrix around the y axis (up vector) then the current view matrix and finally the rotation on the x axis:
view-matrix = rotate-X * view-matrix * rotate-Y
The function update rotation has to look like this:
void ArcBall::updateRotation(int xPos, int yPos) {
// xy normalized device coordinates:
float ndcX = 2.0f * xPos / context->getWidth() - 1.0f;
float ndcY = 1.0 - 2.0f * yPos / context->getHeight();
endVector = QVector3D(ndcX, ndcY, 0.0);
QVector3D direction = endVector - startVector;
float angleY = (float)qRadiansToDegrees(-direction.x() * 3.141593);
float angleX = (float)qRadiansToDegrees(-direction.y() * 3.141593);
QMatrix4x4 rotationX;
rotationX.rotate(angleX, 1.0f 0.0f, 0.0f);
QMatrix4x4 rotationUp;
rotationX.rotate(angleY, 0.0f 1.0f, 0.0f);
rotation = rotationX * rotation * rotationUp;
startVector = endVector;
}