iOS OpenGL ES 2.0 GLKit GLKMatrix4 Rotate after Touches Ended
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);
}
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;
}
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.
上一篇: 轮换后翻译