Core Animation: 3D Perspective

April 14th, 2008
New iPad puzzle game:
Art Scrambles

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.

plot.png

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.

hierarchy.png

Set up

1. Setting things 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”.

2. Applying 3D perspective.
- (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.

3. Creating a wall.
- (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.

4. Move a wall to the desired depth.
- (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.

5. Rotate a wall.
- (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

6. Add all walls to the scene.
- (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];
}

rooms.png

Download and try it

Download (requires Leopard): application | source code