Question

I want to achieve momentum after touches ended. I am using GLKit on iOS with Objective-C and the OpenGL ES 2.0 API. I am using Quaternions to rotate an object centered about the origin of my world using Arcball Rotation. when I rotate with touches, I want to continue the rotation with diminishing speed for some time until there is no rotation left to do.

My thoughts: 1.) Get current angle from Quaternion 2.) Convert Quaternion to 4x4 Matrix 3.) Use GLKit to rotate Matrix with decreasing angles from the current quaternion 4.) At some point invalidate the timer

I've tried to rotate the matrix after touches ended with X,Y, and Z axis independently, as well as all together, but the resulting rotation is never about the axis I would expect from my Quaternion. The arcball rotation works just fine when I move the object using my finger - it is only when the finger is released that the rotation continues about some seemingly arbitrary axis. How do I get the axis to be consistent with the last state of quaternion rotation when I lifted my finger? Also, I noticed from the docs that the GetAngle function for the Quaternion should return negative values of radians when the object is rotated counter-clockwise. I always get positive values from this function even when rotating counter-clockwise.

Thank you very much for any insight you could provide!

// Called when touches are ended
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{

    // The momentum var is initialized to a value
    self.momentumVar = 0.05;

    // Now since we stopped touching, decay the rotation to simulate momentum
    self.momentumTimer = [NSTimer scheduledTimerWithTimeInterval:0.025
                                     target:self
                                   selector:@selector(decayGLKQuaternion)
                                   userInfo:nil
                                    repeats:YES];

}

// Rotate about the current axis after touches ended but for a greater angle ( radians )
- (void)decayGLKQuaternion {

    //return;

    // What is the current angle for the quaternion?
    float currentQuatAngle = GLKQuaternionAngle(self.quat);

    // Decay the value each time
    self.momentumVar = currentQuatAngle * 0.0055;

    NSLog(@"QUAT Angle %f %f",GLKQuaternionAngle(self.quat),GLKMathRadiansToDegrees(GLKQuaternionAngle(self.quat)));

    GLKMatrix4 newMat = GLKMatrix4MakeWithQuaternion(self.quat);
    float rotate = self.momentumVar * 0.75;
    //newMat = GLKMatrix4RotateX(newMat, rotate);
    //newMat = GLKMatrix4RotateY(newMat, rotate);
    //newMat = GLKMatrix4RotateZ(newMat, rotate);
    newMat = GLKMatrix4Rotate(newMat, rotate, 1, 0, 0);
    newMat = GLKMatrix4Rotate(newMat, rotate, 0, 1, 0);
    newMat = GLKMatrix4Rotate(newMat, rotate, 0, 1, 1);
    self.quat = GLKQuaternionMakeWithMatrix4(newMat);


}
Was it helpful?

Solution

Here's a simpler approach. Don't try to decay a quaternion, or a rotational velocity. Instead, store the movement vector of the touch (in 2D view coordinates) and decay that movement vector.

I assume that self.quat is the current rotation quaternion, and that in touchesMoved:withEvent:, you call some method that updates self.quat based on the touch movement vector. Let's say the method is called rotateQuaternionWithVector:. So your touchesMoved:withEvent: probably looks something like this:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self.view];

    // get touch delta
    CGPoint delta = CGPointMake(location.x - iniLocation.x, -(location.y - iniLocation.y));
    iniLocation = location;

    // rotate
    [self rotateQuaternionWithVector:delta];
}

(That method is from The Strange Agency's arcball code, which I used to test this answer.)

I assume you also have a method that gets called 60 times per second to draw each frame of your simulation. I assume that method is named update.

Give yourself some new instance variables:

@implementation ViewController {
    ... existing instance variables ...

    CGPoint pendingMomentumVector; // movement vector as of most recent touchesMoved:withEvent:
    NSTimeInterval priorMomentumVectorTime; // time of most recent touchesMoved:withEvent:
    CGPoint momentumVector; // momentum vector to apply at each simulation step
}

When the touch starts, initialize the variables:

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    priorMomentumVectorTime = touch.timestamp;
    momentumVector = CGPointZero;
    pendingMomentumVector = CGPointZero;

    ... existing touchesBegan:withEvent: code ...
}

When the touch moves, update the variables:

- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self.view];
    CGPoint priorLocation = [touch previousLocationInView:self.view];
    pendingMomentumVector = CGPointMake(location.x - priorLocation.x, -(location.y - priorLocation.y));
    priorMomentumVectorTime = touch.timestamp;

    ... existing touchesMoved:withEvent: code ...
}

When the touch ends, set momentumVector. This requires some care, because the touchesEnded:withEvent: message sometimes includes additional movement, and sometimes comes almost instantly after a separate movement event. So you need to check the timestamp:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self.view];
    if (touch.timestamp - priorMomentumVectorTime >= 1/60.0) {
        CGPoint priorLocation = [touch previousLocationInView:self.view];
        momentumVector = CGPointMake(location.x - priorLocation.x, -(location.y - priorLocation.y));
    } else {
        momentumVector = pendingMomentumVector;
    }
}

Finally, modify your update method (which I assume gets called 60 times per second) to update self.quat based on momentumVector and to decay momentumVector:

- (void)update {
    [self applyMomentum];

    ... existing update code ...
}

- (void)applyMomentum {
    [self rotateQuaternionWithVector:momentumVector];
    momentumVector.x *= 0.9f;
    momentumVector.y *= 0.9f;
}

OTHER TIPS

I assume you calculate a quaternion each time during touch - a quaternion that rotates from the original pose to a new pose. In this case, we need the time derivative of the quaternion at the end of the touch. The result will be the direction in which the object will rotate further. However, calculating the derivative is not easy. We can approximate the derivative by finite differences:

deriv_q = last_q * GLKQuaternionInvert(previous_q)

where last_q is the quaternion when touch has ended and previous_q is the quaternion "awhile ago". This may be the previous state.

If we have the overall rotation expressed as a quaternion, we can continue the rotation by multiplying the quaternion:

overall := overall * deriv_q

The order of multiplication might be reversed based on the API (I don't know iOS).

If you do this each frame, you'll get a smooth rotation in the direction of the last stroke. But it does not attenuate. What you would want to do is to calculate the root of the quaternion:

deriv_q := mth root (deriv_q)

where m is the amount of attenuation. However, calculating the root is not easy, either. Instead we could transform the quaternion to another representation: axis and angle:

axis = GLKQuaternionAxis(deriv_q)
angle = GLKQuaternionAngle(deriv_q)

Now we can attenuate the angle:

angle := angle * factor

and rebuild the quaternion again:

deriv_q := GLKQuaternionMakeWithAngleAndVector3Axis(angle, axis)

Repeat this until the absolute angle falls under a certain threshold (where there is no rotation any more).

If you need to derive a matrix to display the object, you can use the formula from Wikipedia on the overall quaternion.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top