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