Core Animation: 3D Perspective
April 14th, 2008| Check out my new iPad game: America Revealed: Civics. |
Last time, we looked at how Core Animation lets you perform simple animations, including animating along paths.
Now, using Core Animation, let’s look at a way to simulate a 3D perspective. Core Animation is not ideal for 3D graphics—use OpenGL for that—but it’s handy to be able to use 3D perspective at times for simpler things.
For this example, we’ll display a two-room house with a front door.

The house’s seven walls fall neatly on a grid. Walls 1 through 4 run east-west, while 5 through 7 run north-south. Each wall is a layer. All walls are sublayers of the floorLayer, which is in turn a sublayer of the rootLayer.

Set up
-(void)setupHostView {
// Set up the root layer. Nothing unusual here.
rootLayer = [[CALayer layer] retain];
rootLayer.needsDisplayOnBoundsChange = YES;
rootLayer.backgroundColor = CGColorGetConstantColor(kCGColorBlack);
[roomView setLayer:rootLayer];
[roomView setWantsLayer:YES];
// Set up the floor layer. Nothing unusual here.
floorLayer = [[CALayer layer] retain];
floorLayer.delegate = roomView;
floorLayer.bounds = rootLayer.bounds;
floorLayer.frame = rootLayer.frame;
floorLayer.autoresizingMask = kCALayerHeightSizable | kCALayerWidthSizable;
floorLayer.fillMode = rootLayer.fillMode;
floorLayer.position = CGPointMake(400, 200);
[rootLayer addSublayer: floorLayer];
// Finally, apply the 3D perspective. Without this, things look quite odd.
floorLayer.sublayerTransform = [self get3DTransform];
}
Applying the 3D perspective relies on a poorly-documented feature of Core Animation’s CATransform3D structure, a 4-by-4 matrix used to perform matrix transformations. Apple’s documentation says that changes to CATransform3D.m34 “affect the sharpness of the transform.” For our purposes, this means “make things look 3D”.
- (CATransform3D) get3DTransform {
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0 / -2000;
return transform;
}
Creating the walls
Creating the walls is relatively straightforward. Each wall has an origin and a size.
- (CALayer *)makeWallAtOrigin:(CGPoint)origin size:(CGSize)size color:(CGColorRef)color {
// Create the wall with the desired background color.
CALayer *wall = [CALayer layer];
wall.backgroundColor = color;
wall.anchorPoint = CGPointZero;
CGRect frame;
frame.origin = origin;
frame.size = size;
wall.frame = frame;
wall.bounds = frame;
return wall;
}
Walls 3, 4, and 6 lie further back and need to be translated along the z axis.
- (void)moveWall:(CALayer*)wall toDepth:(float)depth {
NSNumber* value = [NSNumber numberWithFloat:depth];
[wall setValue:value forKeyPath:@"transform.translation.z"];
}
North-south walls 5, 6, and 7 need to be rotated.
- (void)rotateWall:(CALayer*)wall withDegrees:(float)degrees {
// Rotation occurs relative to the layer's anchorPoint, which by
// default is in the middle of the layer. So unless you move the
// anchorPoint, rotation will cause the layer to pivot on its center.
// We want rotation to pivot on the wall's left point, so we'll set
// anchorPoint to 0.
wall.anchorPoint = CGPointZero;
float radians = DegreesToRadians(degrees);
NSNumber* value = [NSNumber numberWithFloat:radians];
[wall setValue:value forKeyPath:@"transform.rotation.y"];
}
Putting it all together
- (void)addWalls:(CALayer *)parentLayer {
float w = [self cellWidth];
float h = [self cellHeight];
float x = parentLayer.frame.size.width/2 - (w*5)/2;
float y = parentLayer.frame.size.height/2 - (h*3/2);
// Wall 1
CALayer* wall1 = [self makeWallAtOrigin:CGPointMake(x + w*1, y + h)
withSize:CGSizeMake(w*1, h) color:_lightGrayColor image:image1];
[parentLayer addSublayer:wall1];
// Wall 2
CALayer* wall2 = [self makeWallAtOrigin:CGPointMake(x + w*3, y + h)
withSize:CGSizeMake(w*1, h) color:_lightGrayColor image:image2];
[parentLayer addSublayer:wall2];
// Wall 3
CALayer* wall3 = [self makeWallAtOrigin:CGPointMake(x + w*1, y + h)
withSize:CGSizeMake(w*2, h) color:_darkGrayColor image:image3];
[self moveWall:wall3 toDepth:w*-6];
[parentLayer addSublayer:wall3];
// Wall 4
CALayer* wall4 = [self makeWallAtOrigin:CGPointMake(x + w*1, y + h)
withSize:CGSizeMake(w*3, h) color:_darkGrayColor image:image4];
[self moveWall:wall4 toDepth:w*-10];
[parentLayer addSublayer:wall4];
// Wall 5
CALayer* wall5 = [self makeWallAtOrigin:CGPointMake(x + w*1, y + h)
withSize:CGSizeMake(w*6, h) color:_lightGrayColor image:image5];
[self rotateWall:wall5 withDegrees:90];
[parentLayer addSublayer:wall5];
// Wall 6
CALayer* wall6 = [self makeWallAtOrigin:CGPointMake(x + w*1, y + h)
withSize:CGSizeMake(w*4, h) color:_orangeColor image:image6];
[self moveWall:wall6 toDepth:(w*-6 + 1)];
[self rotateWall:wall6 withDegrees:90];
[parentLayer addSublayer:wall6];
// Wall 7
CALayer* wall7 = [self makeWallAtOrigin:CGPointMake(x + w*4, y + h)
withSize:CGSizeMake(w*10, h) color:_darkGrayColor image:image7];
[self rotateWall:wall7 withDegrees:90];
[parentLayer addSublayer:wall7];
}

Download and try it
Download (requires Leopard): application | source code