user189804
user189804

Reputation:

Apply rotation around axis defined by touched point

I have an object displayed using OpenGL ES on an iPad. The model is defined by vertices, normals and indexes to vertices. The origin of the model is 0,0,0. Using UIGestureRecognizer I can detect various gestures - two-fingered swipe horizontally for rotation about y, vertically for rotation about x. Two-fingered rotate gesture for rotation about y. Pan to move the model around. Pinch/zoom gesture to scale. I want the viewer to be able to manipulate the model to see (for example) the reverse of the model or the whole thing at once.

The basic strategy comes from Ray Wenderlich's tutorial but I have rewritten this in Swift.

I understand quaternions to be a vector and an angle. The vectors up, right and front represent the three axes:

front = GLKVector3Make(0.0, 0.0, 1.0)
right = GLKVector3Make(1.0, 0.0, 0.0)
up = GLKVector3Make(0.0, 1.0, 0.0)

so the quaternion apples a rotation around each of the three axes (though only one of dx, dy, dz has a value, decided by the gesture recognizer.)

func rotate(rotation : GLKVector3, multiplier : Float) {

    let dx = rotation.x - rotationStart.x
    let dy = rotation.y - rotationStart.y
    let dz = rotation.z - rotationStart.z
    rotationStart = GLKVector3Make(rotation.x, rotation.y, rotation.z)
    rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithAngleAndVector3Axis(dx * multiplier, up), rotationEnd)
    rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithAngleAndVector3Axis(dy * multiplier, right), rotationEnd)
    rotationEnd = GLKQuaternionMultiply((GLKQuaternionMakeWithAngleAndVector3Axis(-dz, front)), rotationEnd)
    state = .Rotation

}

Drawing uses the modelViewMatrix, calculated by the following function:

func modelViewMatrix() -> GLKMatrix4 {

    var modelViewMatrix = GLKMatrix4Identity
    // translation and zoom
    modelViewMatrix = GLKMatrix4Translate(modelViewMatrix, translationEnd.x, translationEnd.y, -initialDepth);
    // rotation
    let quaternionMatrix = GLKMatrix4MakeWithQuaternion(rotationEnd)
    modelViewMatrix = GLKMatrix4Multiply(modelViewMatrix, quaternionMatrix)
    // scale
    modelViewMatrix = GLKMatrix4Scale(modelViewMatrix, scaleEnd, scaleEnd, scaleEnd);
    // rotation

    return modelViewMatrix
}

And mostly this works. However everything is relative to the origin. If the model is rotated then the pivot is always an axis passing through the origin - if zoomed in looking at the end of the model away from the origin and then rotating, the model can rapidly swing out of view. If the model is scaled then the origin is always the fixed point with the model growing larger or smaller - if the origin is off-screen and scale is reduced the model can disappear from view as it collapses toward the origin...

What should happen is that whatever the current view, the model rotates or scales relative to the current view. For a rotation around the y axis that would mean defining the y axis around which the rotation occurs as passing vertically through the middle of the current view. For a scale operation the fixed point of the model would be in the centre of the screen with the model shrinking toward or growing outward from that point.

I know that in 2D the solution is always to translate to the origin, apply rotation and then apply the inverse of the first translation. I don't see why this should be different in 3D, but I cannot find any example doing this with quaternions only matrices. I have tried to apply a translation and its inverse around the rotation but nothing has an effect.

So I tried to do this in the rotate function:

let xTranslation : Float = 300.0
let yTranslation : Float = 300.0
let translation = GLKMatrix4Translate(GLKMatrix4Identity, xTranslation, yTranslation, -initialDepth);
rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithMatrix4(translation) , rotationEnd)

rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithAngleAndVector3Axis(dx * multiplier, up), rotationEnd)
rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithAngleAndVector3Axis(dy * multiplier, right), rotationEnd)
rotationEnd = GLKQuaternionMultiply((GLKQuaternionMakeWithAngleAndVector3Axis(-dz, front)), rotationEnd)

// inverse translation
let inverseTranslation = GLKMatrix4Translate(GLKMatrix4Identity, -xTranslation, -yTranslation, -initialDepth);
rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithMatrix4(inverseTranslation) , rotationEnd)

The translation is 300,300 but there is no effect at all, it still pivots around where I know the origin to be. I've searched a long time for sample code and not found any.

The modelViewMatrix is applied in update() with:

effect?.transform.modelviewMatrix = modelViewMatrix

I could also cheat by adjusting all of the values in the model so that 0,0,0 falls at a central point - but that would still be a fixed origin and would be only marginally better.

Upvotes: 4

Views: 448

Answers (1)

ELKA
ELKA

Reputation: 735

The problem is in the last operation you made, you should swap the inverseTranslation with rotationEnd :

rotationEnd = GLKQuaternionMultiply(rotationEnd, GLKQuaternionMakeWithMatrix4(inverseTranslation))

And I think the partial rotation(dx, dy, dz) should follow the same rule.

In fact, if you want to change the pivot, this is how your matrix multiplication should be done:

modelMatrix = translationMatrix * rotationMatrix * inverse(translationMatrix)

and the result in homogeneous coordinates will be calculated as follows:

newPoint = translationMatrix * rotationMatrix * inverse(translationMatrix) * v4(x,y,z,1)

Example

This is a 2D test example that you can run in a playground.

enter image description here

let v4 = GLKVector4Make(1, 0, 0, 1) // Point A

let T = GLKMatrix4Translate(GLKMatrix4Identity, 1, 2, 0); 
let rot = GLKMatrix4MakeWithQuaternion(GLKQuaternionMakeWithAngleAndVector3Axis(Float(M_PI)*0.5, GLKVector3Make(0, 0, 1))) //rotate by PI/2 around the z axis.
let invT = GLKMatrix4Translate(GLKMatrix4Identity, -1, -2, 0);

let partModelMat = GLKMatrix4Multiply(T, rot)
let modelMat = GLKMatrix4Multiply(partModelMat, invT) //The parameters were swapped in your code 
//and the result would the rot matrix, since T*invT will be identity

var v4r = GLKMatrix4MultiplyVector4(modelMat, v4) //ModelMatrix multiplication with pointA 
print(v4r.v) //(3,2,0,1)

//Step by step multiplication using the relation described above
v4r = GLKMatrix4MultiplyVector4(invT, v4)
v4r = GLKMatrix4MultiplyVector4(rot, v4r)
v4r = GLKMatrix4MultiplyVector4(T, v4r)
print(v4r.v) //(3,2,0,1)

As for the scale, if I understand correctly what you want, I would recommend to do it like it's done here: https://gamedev.stackexchange.com/questions/61473/combining-rotation-scaling-around-a-pivot-with-translation-into-a-matrix

Upvotes: 1

Related Questions