Creating a chase camera with GLKit
For the game I am working on I needed to create a camera that will follow an object from a specific distance and angle. This kind of camera - also known as a chase camera - is very popular in racing games and other third person games.
Before moving to iOS and Apple devices I did most of my games programming in XNA Game Studio. I was therefore lucky enough to own a copy of the excellent book "3D Graphics with XNA Game Studio 4.0" by Sean James and this book describes how to create a chase camera. Obviously, the book uses C# and XNA for it's code so it is not usable on the iOS platform directly - but the concept is the same.
The concept of the chase camera is visualized in the next figure: (source: 3D Graphics with XNA Game Studio 4.0 by Sean James (PACKT Publishing))
An offset from the camera position is used to determine the view direction and chase distance. The target position is calculated by using an offset from the object's position. Based on these values the view matrix can then be calculated.
The chase camera created by Sean James has the added bonus of having a relative rotation value that allows the camera to rotate independently of the object as well as a "springiness" value that allows the camera to "bend" from side-to-side instead of rigidly following the object. This makes the camera behave more like real-life and gives a much better feeling of movement.
As the code created by Jean James is targeted for XNA I have re-created to code in Objective-C using GLKit math helpers. As some of the math methods used in XNA does not exist in GLKit I have had to re-create these methods in Objective-C using code created for the MonoXNA project.
// // ChaseCamera.h // #import "AbstractCamera.h" @interface ChaseCamera : AbstractCamera @property (nonatomic, readwrite) GLKVector3 position; @property (nonatomic, readwrite) GLKVector3 target; @property (nonatomic, readwrite) GLKVector3 followTargetPosition; @property (nonatomic, readwrite) GLKVector3 followTargetRotation; @property (nonatomic, readwrite) GLKVector3 positionOffset; @property (nonatomic, readwrite) GLKVector3 targetOffset; @property (nonatomic, readwrite) GLKVector3 relativeCameraRotation; @property (nonatomic, readwrite) float springiness; - (id) initChaseCameraWithPositionOffset:(GLKVector3)positionOffset targetOffset:(GLKVector3)targetOffset relativeCameraRotation:(GLKVector3)relativeCameraRotation; - (void) moveToTargetFollowPosition:(GLKVector3)targetFollowPosition targetFollowRotation:(GLKVector3)targetFollowRotation; - (void) rotate:(GLKVector3)rotationChange; @end
// // ChaseCamera.m // #import "ChaseCamera.h" #import "MathHelper.h" @implementation ChaseCamera - (id) initChaseCameraWithPositionOffset:(GLKVector3)positionOffset targetOffset:(GLKVector3)targetOffset relativeCameraRotation:(GLKVector3)relativeCameraRotation { if (( self = [super init] )) { _positionOffset = positionOffset; _targetOffset = targetOffset; _relativeCameraRotation = relativeCameraRotation; } return self; } - (void) setSpringiness:(float)springiness { // Clamp the value between 0 and 1 _springiness = clamp(springiness, 0, 1); } - (void) moveToTargetFollowPosition:(GLKVector3)targetFollowPosition targetFollowRotation:(GLKVector3)targetFollowRotation { _followTargetPosition = targetFollowPosition; _followTargetRotation = targetFollowRotation; } - (void) rotate:(GLKVector3)rotationChange { _relativeCameraRotation = GLKVector3Add(_relativeCameraRotation, rotationChange); } - (void) update { // Sum the rotations of the model and the camera to ensure it is // rotated to the corret position relative to the model's rotation GLKVector3 combinedRotation = GLKVector3Add(_followTargetRotation, _relativeCameraRotation); // Calculate the rotation matrix for the camera GLKMatrix4 rotation = Matrix4MakeFromYawPitchRoll(combinedRotation.y, combinedRotation.x, combinedRotation.z); // Calculate the position the camera would be without the spring // value, using the rotation matrix and the target position GLKVector3 desiredPosition = GLKVector3Add(_followTargetPosition, Vector3Transform(_positionOffset, rotation)); // Interpolate between the current position and the desired position _position = GLKVector3Lerp(_position, desiredPosition, _springiness); // Calculate the new target using the rotation matrix _target = GLKVector3Add(_followTargetPosition, Vector3Transform(_targetOffset, rotation)); // Obtain the up vector from the matrix GLKVector3 up = Vector3Transform(GLKVector3Make(0.0f, 1.0f, 0.0f), rotation); // Recalculate the view matrix self.view = GLKMatrix4MakeLookAt(_position.x, _position.y, _position.z, _target.x, _target.y, _target.z, up.x, up.y, up.z); } @end
As you might have noticed the ChaseCamera is inheriting from the AbstractCamera class. The AbstractCamera class is very basic and I created it because I will have a few different camera's in my game. Using the abstract camera class allows be to switch cameras seamlessly as for any camera all you ever care about is the projection and view matrices . What else the camera is doing to calculate these are mostly irrelevant to the rest of the game. I have posted the code for the AbstractCamera class below.
// // AbstractCamera.h // @interface AbstractCamera : NSObject // The view matrix for the camera @property (nonatomic, readwrite) GLKMatrix4 view; // The projection matrix for the camera @property (nonatomic, readwrite) GLKMatrix4 projection; // Update the camera - (void) update; @end
// // AbstractCamera.m // #import "AbstractCamera.h" @implementation AbstractCamera - (id) init { if (( self = [super init] )) { [self generatePerspectiveProjectionMatrixWithFieldOfView:45.0f]; } return self; } - (void) generatePerspectiveProjectionMatrixWithFieldOfView:(float)fieldOfView { // Get the size of the screen CGRect winRect = [[UIScreen mainScreen] bounds]; // Calculate aspect ratio float aspectRatio = winRect.size.width / winRect.size.height; // Create perspective projection matrix self.projection = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(fieldOfView), aspectRatio, 0.1f, 2000.0f); } - (void) update {} @end
As already mentioned above some of the math methods available in XNA do not exist in GLKit. Therefore, I created a MathHelper class that has methods for these missing XNA methods. I have cut-out those methods and made a focused MathHelper class just for the ChaseCamera class
// // MathHelpers.h // #pragma mark - #pragma mark Prototypes #pragma mark - static __inline__ GLKVector3 Vector3DDirectionToAngle(GLKVector3 direction); // Transforms a GLKVector3 by the given matrix static __inline__ GLKVector3 Vector3Transform(GLKVector3 vector, GLKMatrix4 transform); // Creates a new rotation matrix from a specified yaw, pitch and roll angles static __inline__ GLKMatrix4 Matrix4MakeFromYawPitchRoll(float yaw, float pitch, float roll); // Creates a new quaternion from specified yaw, pitch and roll angles static __inline__ GLKQuaternion QuaternionMakeFromYawPitchRoll(float yaw, float pitch, float roll); #pragma mark - #pragma mark Implementations #pragma mark - static __inline__ GLKVector3 Vector3DDirectionToAngle(GLKVector3 direction) { // Convert direction vector to angle float rotation = (float) acos(direction.y > 0 ? -direction.x : direction.x); if (direction.y > 0) rotation = M_PI; rotation = M_PI_2; return GLKVector3Make(0, rotation, 0); } static __inline__ GLKVector3 Vector3Transform(GLKVector3 vector, GLKMatrix4 transform) { return GLKVector3Make(((vector.x * transform.m00) (vector.y * transform.m10) (vector.z * transform.m20) transform.m30), ((vector.x * transform.m01) (vector.y * transform.m11) (vector.z * transform.m21) transform.m31), ((vector.x * transform.m02) (vector.y * transform.m12) (vector.z * transform.m22) transform.m32)); } static __inline__ GLKMatrix4 Matrix4MakeFromYawPitchRoll(float yaw, float pitch, float roll) { GLKMatrix4 matrix; GLKQuaternion quaternion; quaternion = QuaternionMakeFromYawPitchRoll(yaw, pitch, roll); return GLKMatrix4MakeWithQuaternion(quaternion); } static __inline__ GLKQuaternion QuaternionMakeFromYawPitchRoll(float yaw, float pitch, float roll) { GLKQuaternion quaternion; quaternion.x = (((float)cos((double)(yaw * 0.5f)) * (float)sin((double)(pitch * 0.5f))) * (float)cos((double)(roll * 0.5f))) (((float)sin((double)(yaw * 0.5f)) * (float)cos((double)(pitch * 0.5f))) * (float)sin((double)(roll * 0.5f))); quaternion.y = (((float)sin((double)(yaw * 0.5f)) * (float)cos((double)(pitch * 0.5f))) * (float)cos((double)(roll * 0.5f))) - (((float)cos((double)(yaw * 0.5f)) * (float)sin((double)(pitch * 0.5f))) * (float)sin((double)(roll * 0.5f))); quaternion.z = (((float)cos((double)(yaw * 0.5f)) * (float)cos((double)(pitch * 0.5f))) * (float)sin((double)(roll * 0.5f))) - (((float)sin((double)(yaw * 0.5f)) * (float)sin((double)(pitch * 0.5f))) * (float)cos((double)(roll * 0.5f))); quaternion.w = (((float)cos((double)(yaw * 0.5f)) * (float)cos((double)(pitch * 0.5f))) * (float)cos((double)(roll * 0.5f))) (((float)sin((double)(yaw * 0.5f)) * (float)sin((double)(pitch * 0.5f))) * (float)sin((double)(roll * 0.5f))); return quaternion; }
That is it .. I will try an package this code in an example later on. For now I just wanted to post it so that I could close a question I made on the Ray Wenderlich forum some time ago.
Hope you find the code useful. If you have questions feel free to contact me via Twitter.