Think And Build

iOS

Introduction to 3D drawing in Core Animation (Part 2)

Posted on .

Introduction to 3D drawing in Core Animation (Part 2)

Introduction

In the previous article You’ve learnt the basics of 3D drawing in Core Animation.
In this tutorial we push your knowledge even further creating an interactive scene. We are going to build a Carousel the users will be able to interact with through Pan gestures, defining how the Carousel moves.

You have already seen a preview of it in the previous article, but here is the final result of this tutorial, again:

At this point you can download the code of this article and follow me. It’s the same repository of the previous tutorial, but I have modified it adding a new target for this lesson.

The Carousel – Overview

Let’s dig into the planning of this application just to understand how we can split it into different areas.

We go 3D!

We know we want to draw using a perspective view because we need the 3D effects given by the planes that are farther from the user’s point of view.
To build the carousel we’ll create a 3D hierarchy and, as you read in the previous article, using a CATransformLayer as root for that hierarchy is a good practice.

The planes

The Carousel is composed of a number of Planes. We’ll describe these objects with the class CAGradientLayer. This is a subclass of CALayer that let us define a background using a gradient rather than a plain tint.

The planes should be translated and rotated to be in the shape of a circle (AKA: the Carousel).

Gestures

Intercepting the user’s gesture is extremely simple. We just use a Gesture Recognizer.
Then we keep track of the user actions, converting the gesture data in angular values useful to perform the rotation of the carousel.

Now that you have an overview of the main areas of the project we can start with the fun. Fire up XCode!

Let’s Code!

Expand the group folder TB_3DPlanes and open the file: ViewController.m.

We start from the function viewDidLoad:

[code lang=”objc”]
– (void)viewDidLoad
{
[super viewDidLoad];

//Initialize the TransformLayer
transformLayer = [CATransformLayer layer];
transformLayer.frame = self.view.bounds;
[self.view.layer addSublayer:transformLayer];

angle = 0;
XPanOffset = 0;

//Create 5 planes
[self addPlane];
[self addPlane];
[self addPlane];
[self addPlane];
[self addPlane];

//Force the first animation to set the planes in place
[self animate];

//Initialize the Pan gesture recognizer
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(pan:)];
[self.view addGestureRecognizer:panGesture];
}

At the moment this function has too many information that you can’t fully understand, but it is the core from where the code starts, so just keep on reading and trust me. 🙂
It’s clear that here we are creating a Root layer (transformLayer) using a CATransformLayer, we insert 5 planes into the scene using the function addPlane and we manage the Pan gestures with the function panGesture:

Draw the planes

The function addPlane is straightforward. It is just used to create a CAGradientLayer with some parameters and to attach it as sublayer of transformLayer.

[code lang=”objc”]
/** A simple function to create a CAGradientLayer **/
-(void)addPlane{

CGSize planeSize = CGSizeMake(250, 150);

//Initialize the layer
CAGradientLayer *layer = [CAGradientLayer layer];

//Set the frame and the anchorPoint
layer.frame = CGRectMake(480/2 – planeSize.width/2, 320/2 – planeSize.height/2 -20, planeSize.width, planeSize.height);
layer.anchorPoint = CGPointMake(0.5, 0.5);

//Set borders and cornerRadius
layer.borderColor = [[UIColor colorWithWhite:1.0 alpha:0.3]CGColor];
layer.cornerRadius = 10;
layer.borderWidth = 4;

//Set the gradient color for the plane background
layer.colors = [NSArray arrayWithObjects:
(id)[UIColor purpleColor].CGColor,
(id)[UIColor redColor].CGColor,
nil];
layer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:1.0f],
nil];

//Set the shadow
layer.shadowColor = [[UIColor blackColor]CGColor];
layer.shadowOpacity = 1;
layer.shadowRadius = 20;

//The double side has to be setted if we want to see the plane when its face is turned back
layer.doubleSided = YES;

//Add the plane to the transformLayer
[transformLayer addSublayer:layer];
}

The only property that is worth spending some words on is doubleSide. Setting it to YES, we say that we want its backside to be drawn. So when a plane is Y rotated by a value near to 180°, we still see it into the scene, just rotated in the opposite direction.
To better understand this situation, check what would happen defining doubleSide = NO.

Simple, the planes whose back side is rotated toward us are not drawn.

You may notice that within the viewDidLoad function we add 5 planes but these planes are not positioned in the scene. They are just created at the same position defined by their frame definition in the addPlane function.

Positioning the planes

Just after the planes creation, in the viewDidLoad function, we send the message animate.
The main task of this function is to update the Planes’ positions. The first time we call “animate” the touch events are not fired yet. So its objective is just to position each plane around, shaping the Carousel.

Let’s see the code for this function:

[code lang=”objc”]
/** This function performs the transformation on each plane **/
-(void)animate{

//Define the degree needed for each plane to create a circle
float degForPlane = 360 / [[transformLayer sublayers] count];

//The current angle offset (initially it is 0… it will change through the pan function)
float degX = angle;

for (CALayer *layer in [transformLayer sublayers]) {
//Create the Matrix identity
CATransform3D t = CATransform3DIdentity;
//Setup the perspective modifying the matrix elementat [3][4]
t.m34 = 1.0f / – 1000.0f;

//Perform rotate on the matrix identity
t = CATransform3DRotate(t, degToRad(degX), 0.0f, 1.0f, 0.0f);

//Perform translate on the current transform matrix (identity + rotate)
t = CATransform3DTranslate(t, 0.0f, 0.0f, 250.0f);

//Avoid animations
[CATransaction setAnimationDuration:0.0];

//apply the transoform on the current layer
layer.transform = t;

//Add the degree needed for the next plane
degX += degForPlane;
}
}

The defForPlane variable is the angle that each plane has to be rotated in relation to the previous plane to shape a 360 circle. Let me better explain that with an image:

The 5 planes drawn on the circumference have to be rotated by a value. This value start from 0 and increase by 360/Planes-Count for every plane.

For now, the variable “angle” is equal to zero, we’ll talk about it later.

The animate function cycles through every sublayers of the transformLayer, namely the 5 planes.
The main role of this loop is to apply some transformations to each plane:
The first is the perspective, as we did in the previous article we simply set a value for the property m34 to be sure that the plane will be drawn using a 3D depth.
The other 2 transformations require a bit of calculations.
We’ve already spoken about the rotation needed by every plane but we have skipped a crucial point.

If we only add a rotation to every plane we obtain this:

As you can see, a translation is needed on each plane to move them on the circumference. Now the planes are positioned at the right positions.

The last step is to apply the transformations to the planes, just sending them to the property transform of the current plane.
The degX variable is then incremented by degForPlane to determine the rotation needed by the next plane.

Pan gesture

The function pan:is responsible of managing the pan gestures. Here’s the code of the function:

[code lang=”Objc”]
-(void)pan:(UIPanGestureRecognizer*)gesture{

//Get the current translation on the X
float xOffset = [gesture translationInView:self.view].x;

//When gesture begin, reset the offset
if(gesture.state == UIGestureRecognizerStateBegan){
XPanOffset = 0;
}

//the distance covered since the last gesture event (I slow down a bit the final rotation multiplying by 0.5)
float movedBy = xOffset * 0.5 – XPanOffset;

//Calculate the offset from the previous gesture event
XPanOffset += movedBy;

//Add the offset to the current angle
angle += movedBy;

//Update the plane
[self animate];

}

A pan gesture is continuous, it means that starting from the beginning of the gesture event, the function pan is called as soon as the finger moves and until all fingers are lifted.
To convert this information into something useful for the carousel animation, we want to get the portion of movement on the X axis from the previous position.
We use the variable XPanOffset as position buffer. When the gesture changes, we update XPanOffset adding the distance covered since the previous position.
This distance is contained in the variable movedBy that is then added to the current angle variable.
Calling the animate function will result in rotating the planes in relation to the new angle value.

That’s it! 🙂

This is a simple example of what you can do with 3D using just Core Animation.

Now have fun with your experiments and feel free to poke me on twitter for your questions and suggestions!

Yari D'areglia

Yari D'areglia

https://www.thinkandbuild.it

Senior iOS developer @ Neato Robotics by day, game developer and wannabe artist @ Black Robot Games by night.

Navigation