Intermediate 3D Graphics - iOS Game Development Cookbook (2014)

iOS Game Development Cookbook (2014)

Chapter 9. Intermediate 3D Graphics

Once you’ve got OpenGL drawing basic 3D content, you’ll likely want to create more complicated scenes. In this chapter, you’ll learn how to load meshes from files, how to compose complex objects by creating parent-child relationships between objects, how to position a camera in 3D space, and more.

This chapter builds on the basic 3D graphics concepts covered in Chapter 8, and makes use of the component-based layout shown in Creating a Component-Based Game Layout.

Loading a Mesh

Problem

You want to load meshes from files, so that you can store 3D objects in files.

Solution

First, create an empty text file called MyMesh.json. Put the following text in it:

{

"vertices":[

{

"x":-1, "y":-1, "z":1

},

{

"x":1, "y":-1, "z":1

},

{

"x":-1, "y":1, "z":1

},

{

"x":1, "y":1, "z":1

},

{

"x":-1, "y":-1, "z":-1

},

{

"x":1, "y":-1, "z":-1

},

{

"x":-1, "y":1, "z":-1

},

{

"x":1, "y":1, "z":-1

}

],

"triangles":[

[0,1,2],

[2,3,1],

[4,5,6],

[6,7,5]

]

}

NOTE

JSON is but one of many formats for storing 3D mesh data; many popular 3D modeling tools, such as the open source Blender, support it.

This mesh creates two parallel squares, as seen in Figure 9-1.

The mesh.

Figure 9-1. The mesh

Next, create a new subclass of NSObject, called Mesh. Put the following code in Mesh.h:

#import <GLKit/GLKit.h>

typedef struct {

GLKVector3 position;

GLKVector2 textureCoordinates;

GLKVector3 normal;

} Vertex;

typedef struct {

GLuint vertex1;

GLuint vertex2;

GLuint vertex3;

} Triangle;

@interface Mesh : NSObject

+ (Mesh*) meshWithContentsOfURL:(NSURL*)url error:(NSError**)error;

// The name of the OpenGL buffer containing vertex data

@property (readonly) GLuint vertexBuffer;

// The name of the OpenGL buffer containing the triangle list

@property (readonly) GLuint indexBuffer;

// A pointer to the loaded array of vector info

@property (readonly) Vertex* vertexData;

// The number of vertices

@property (readonly) NSUInteger vertexCount;

// A pointer to the triangle list data

@property (readonly) Triangle* triangleData;

// The number of triangles

@property (readonly) NSUInteger triangleCount;

@end

Next, put the following methods into Mesh.m. The first of these, meshWithContentsOfURL:error:, attempts to load an NSDictionary from disk, and then tries to create a new Mesh object with its contents:

+ (Mesh *)meshWithContentsOfURL:(NSURL *)url

error:(NSError *__autoreleasing *)error {

// Load the JSON text into memory

NSData* meshJSONData = [NSData dataWithContentsOfURL:url options:0

error:error];

if (meshJSONData == nil)

return nil;

// Convert the text into an NSDictionary,

// then check to see if it's actually a dictionary

NSDictionary* meshInfo =

[NSJSONSerialization JSONObjectWithData:meshJSONData options:0

error:error];

if ([meshInfo isKindOfClass:[NSDictionary class]] == NO)

return nil;

// Finally, attempt to create a mesh with this dictionary

return [[Mesh alloc] initWithMeshDictionary:meshInfo];

}

The next method, initWithMeshDictionary:, checks the contents of the provided NSDictionary and loads the mesh information into memory. It then prepares OpenGL buffers so that the mesh information can be rendered:

- (id)initWithMeshDictionary:(NSDictionary*)dictionary

{

self = [super init];

if (self) {

// Get the arrays of vertices and triangles, and ensure they're arrays

NSArray* loadedVertexDictionary = dictionary[@"vertices"];

NSArray* loadedTriangleDictionary = dictionary[@"triangles"];

if ([loadedVertexDictionary isKindOfClass:[NSArray class]] == NO) {

NSLog(@"Expected 'vertices' to be an array");

return nil;

}

if ([loadedTriangleDictionary isKindOfClass:[NSArray class]] == NO) {

NSLog(@"Expected 'triangles' to be an array");

return nil;

}

// Calculate how many vertices and triangles we have

_vertexCount = loadedVertexDictionary.count;

_triangleCount = loadedTriangleDictionary.count;

// Allocate memory to store the vertices and triangles in

_vertexData = calloc(sizeof(Vertex), _vertexCount);

_triangleData = calloc(sizeof(Triangle), _triangleCount);

if (_vertexData == NULL || _triangleData == NULL) {

NSLog(@"Couldn't allocate memory!");

return nil;

}

// For each vertex in the list, read information about it and store it

for (int vertex = 0; vertex < _vertexCount; vertex++) {

NSDictionary* vertexInfo = loadedVertexDictionary[vertex];

if ([vertexInfo isKindOfClass:[NSDictionary class]] == NO) {

NSLog(@"Vertex %i is not a dictionary", vertex);

return nil;

}

// Store the vertex data in memory, at the correct position:

// Position:

_vertexData[vertex].position.x = [vertexInfo[@"x"] floatValue];

_vertexData[vertex].position.y = [vertexInfo[@"y"] floatValue];

_vertexData[vertex].position.z = [vertexInfo[@"z"] floatValue];

// Texture coordinates

_vertexData[vertex].textureCoordinates.s =

[vertexInfo[@"s"] floatValue];

_vertexData[vertex].textureCoordinates.t =

[vertexInfo[@"t"] floatValue];

// Normal

_vertexData[vertex].normal.x = [vertexInfo[@"nx"] floatValue];

_vertexData[vertex].normal.y = [vertexInfo[@"ny"] floatValue];

_vertexData[vertex].normal.z = [vertexInfo[@"nz"] floatValue];

}

// Next, for each triangle in the list, read information and store it

for (int triangle = 0; triangle < _triangleCount; triangle++) {

NSArray* triangleInfo = loadedTriangleDictionary[triangle];

if ([triangleInfo isKindOfClass:[NSArray class]] == NO) {

NSLog(@"Triangle %i is not an array", triangle);

return nil;

}

// Store the index of each referenced vertex

_triangleData[triangle].vertex1 =

[triangleInfo[0] unsignedIntegerValue];

_triangleData[triangle].vertex2 =

[triangleInfo[1] unsignedIntegerValue];

_triangleData[triangle].vertex3 =

[triangleInfo[2] unsignedIntegerValue];

// Check to make sure that the vertices referred to exist

if (_triangleData[triangle].vertex1 >= _vertexCount) {

NSLog(@"Triangle %i refers to an unknown vertex %i", triangle,

_triangleData[triangle].vertex1);

return nil;

}

if (_triangleData[triangle].vertex2 >= _vertexCount) {

NSLog(@"Triangle %i refers to an unknown vertex %i", triangle,

_triangleData[triangle].vertex2);

return nil;

}

if (_triangleData[triangle].vertex3 >= _vertexCount) {

NSLog(@"Triangle %i refers to an unknown vertex %i", triangle,

_triangleData[triangle].vertex3);

return nil;

}

}

// We've now loaded all of the data into memory. Time to create

// buffers and give them to OpenGL!

glGenBuffers(1, &_vertexBuffer);

glGenBuffers(1, &_indexBuffer);

glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);

glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * _vertexCount, _vertexData,

GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer);

glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Triangle) * _triangleCount,

_triangleData, GL_STATIC_DRAW);

}

return self;

}

The final method, dealloc, releases the resources that are created in the init method:

- (void)dealloc {

// We're going away, so we need to tell OpenGL to get rid of the

// data we uploaded

glDeleteBuffers(1, &_vertexBuffer);

glDeleteBuffers(1, &_indexBuffer);

// Now free the memory that we allocated earlier

free(_vertexData);

free(_triangleData);

}

Discussion

In this solution, we’re creating a Mesh object, which represents a loaded mesh. One of the advantages of this approach is that you can load a mesh once, and then reuse it multiple times.

At minimum, a mesh is a collection of vertices combined with information that links the vertices up into polygons. This means that when you load a mesh, you need to get the positions of every vertex, as well as information describing which vertices make up which polygons.

The specific format that you use to store your mesh on disk can be anything you like. In this solution, we went with JSON, since it’s easy to read and write, and iOS has a built-in class designed for reading it. However, it’s not the only format around, and not necessarily the best one for your uses. Other popular mesh formats include Wavefront OBJ, which is a text-based format, and Autodesk FBX, which is binary and supports a number of handy features like embedded animations and hierarchical meshes.

As we discussed in Drawing a Square Using OpenGL, to draw a mesh, you first need to load the information for its vertices and how the vertices are linked together into memory. The actual format for how this information is stored in memory is up to you, since you describe to OpenGL where to find the specific types of information that it needs.

When you load a mesh and give it to OpenGL to draw, you first allocate a chunk of memory, fill it with information, and then create an OpenGL buffer. You then tell OpenGL to fill the buffer with the information you’ve loaded. This happens twice: once for the vertices, and again for the list of indices that describe how the vertices are linked into triangles.

In this solution, the format for vertices is a structure that looks like this:

typedef struct {

GLKVector3 position;

GLKVector2 textureCoordinates;

} Vertex;

However, a mesh is almost always going to contain more than one vertex, and it isn’t possible to know how many vertices you’re going to be dealing with at compile time. To deal with this, the memory that contains the vertices needs to be allocated, using the calloc function. This function takes two parameters, the size of each of the pieces of memory you want to allocate and the number of pieces:

_vertexData = calloc(sizeof(Vertex), _vertexCount);

So, to create the memory space that contains the list of vertices, you need to know how big each vertex is, and how many vertices you need. To find out the size of any structure, you use the sizeof function, and to find out the number of vertices, you check the file that you’re loading.

Once this memory has been allocated, you can work with it: for each vertex, you read information about it and fill in the data. The same process is done for the triangle list.

When you allocate memory using calloc (or any of its related methods, including malloc and realloc), you need to manually free it, using the free function. You do this in your Mesh object’s dealloc method, which is called when the Mesh is in the process of being removed from memory.

WARNING

It’s very important that any memory that you allocate using calloc or malloc gets freed with free. If you don’t do this, you end up with a memory leak: memory that’s been allocated but never freed and is never referred to again. Memory leaks waste memory, which is something you really don’t want in the memory-constrained environment of iOS.

Parenting Objects

Problem

You want to attach objects to other objects, so that multiple animations can combine.

Solution

The code in this solution uses the component-based architecture discussed in Creating a Component-Based Game Layout, though the idea can also be applied to hierarchy-based architectures (see Creating an Inheritance-Based Game Layout). In this solution, we’ll be creating a Transformcomponent.

Create a new class named Transform, and put the following contents in Transform.h:

@interface Transform : Component

@property (weak) Transform* parent;

@property (strong, readonly) NSSet* children;

- (void) addChild:(Transform*)child;

- (void) removeChild:(Transform*)child;

// Position relative to parent

@property (assign) GLKVector3 localPosition;

// Rotation angles relative to parent, in radians

@property (assign) GLKVector3 localRotation;

// Scale relative to parent

@property (assign) GLKVector3 localScale;

// The matrix that maps local coordinates to world coordinates

@property (readonly) GLKMatrix4 localToWorldMatrix;

// Vectors relative to us

@property (readonly) GLKVector3 up;

@property (readonly) GLKVector3 forward;

@property (readonly) GLKVector3 left;

// Position in world space

@property (readonly) GLKVector3 position;

// Rotation in world space

@property (readonly) GLKQuaternion rotation;

// Scale, taking into account parent object's scale

@property (readonly) GLKVector3 scale;

@end

And in Transform.m:

#import "Transform.h"

@interface Transform () {

NSMutableSet* _children;

}

@end

@implementation Transform

@dynamic position;

@dynamic rotation;

@dynamic scale;

@dynamic up;

@dynamic left;

@dynamic forward;

- (id)init

{

self = [super init];

if (self) {

// The list of children

_children = [NSMutableSet set];

// By default, we're scaled to 1 on all 3 axes

_localScale = GLKVector3Make(1, 1, 1);

}

return self;

}

// Add a transform as a child of us

- (void)addChild:(Transform *)child {

[_children addObject:child];

child.parent = self;

}

// Remove a transform from the list of children

- (void)removeChild:(Transform *)child {

[_children removeObject:child];

child.parent = nil;

}

// Rotate a vector by our local axes

- (GLKVector3)rotateVector:(GLKVector3)vector {

GLKMatrix4 matrix = GLKMatrix4Identity;

matrix = GLKMatrix4RotateX(matrix, self.localRotation.x);

matrix = GLKMatrix4RotateY(matrix, self.localRotation.y);

matrix = GLKMatrix4RotateZ(matrix, self.localRotation.z);

return GLKMatrix4MultiplyVector3(matrix, vector);

}

- (GLKVector3)up {

return [self rotateVector:GLKVector3Make(0, 1, 0)];

}

- (GLKVector3)forward {

return [self rotateVector:GLKVector3Make(0, 0, 1)];

}

- (GLKVector3)left {

return [self rotateVector:GLKVector3Make(1, 0, 0)];

}

// Create a matrix that represents our position, rotation, and scale in

// world space

- (GLKMatrix4)localToWorldMatrix {

// First, get the identity matrix

GLKMatrix4 matrix = GLKMatrix4Identity;

// Next, get the matrix of our parent

if (self.parent)

matrix = GLKMatrix4Multiply(matrix, self.parent.localToWorldMatrix);

// Translate it

matrix = GLKMatrix4TranslateWithVector3(matrix, self.localPosition);

// Rotate it

matrix = GLKMatrix4RotateX(matrix, self.localRotation.x);

matrix = GLKMatrix4RotateY(matrix, self.localRotation.y);

matrix = GLKMatrix4RotateZ(matrix, self.localRotation.z);

// And scale it!

matrix = GLKMatrix4ScaleWithVector3(matrix, self.localScale);

return matrix;

}

// Get a quaternion that describes our orientation in world space

- (GLKQuaternion)rotation {

// First, get the identity quaternion (i.e. no rotation)

GLKQuaternion rotation = GLKQuaternionIdentity;

// Now, multiply this rotation with its parent, if it has one

if (self.parent)

rotation = GLKQuaternionMultiply(rotation, self.parent.rotation);

// Finally, rotate around our local axes

GLKQuaternion xRotation = GLKQuaternionMakeWithAngleAndVector3Axis(

self.localRotation.x, GLKVector3Make(1, 0, 0));

GLKQuaternion yRotation = GLKQuaternionMakeWithAngleAndVector3Axis(

self.localRotation.y, GLKVector3Make(0, 1, 0));

GLKQuaternion zRotation = GLKQuaternionMakeWithAngleAndVector3Axis(

self.localRotation.z, GLKVector3Make(0, 0, 1));

rotation = GLKQuaternionMultiply(rotation, xRotation);

rotation = GLKQuaternionMultiply(rotation, yRotation);

rotation = GLKQuaternionMultiply(rotation, zRotation);

return rotation;

}

// Get our position in world space

- (GLKVector3)position {

GLKVector3 position = self.localPosition;

if (self.parent)

position = GLKVector3Add(position, self.parent.position);

return position;

}

// Get our scale in world space

- (GLKVector3)scale {

GLKVector3 scale = self.localScale;

if (self.parent)

scale = GLKVector3Multiply(scale, self.parent.scale);

return scale;

}

To get the model-view matrix for an object at a given position, you just ask for its localToWorldMatrix, which you can then provide to a GLKBaseEffect’s transform.modelViewMatrix property.

Discussion

It’s often the case that you’ll have an object (called the “child” object) that needs to be attached to another object (called the “parent”), such that when the parent moves, the child moves with it. You can make this happen by allowing objects to keep a reference to their parent and use the position, orientation, and scale of the parent when calculating their own position, orientation, and scale:

my world position = parent's position + my local position

my world rotation = parent's rotation + my local rotation

my world scale = parent's scale + my local scale

When you do this, you can have a long chain of parents: one can be the parent of another, which can be the parent of yet another, and so on.

The easiest way to represent this is by making each object calculate a matrix, which is a single value that represents the transform of an object (i.e., the position, rotation, and scale of the object). Matrices are useful, both because a single matrix represents all three operations and because matrices can be combined. So, if you get the transform matrix of the parent and multiply that by your own transform matrix, you end up with a matrix that combines the two.

It just so happens that a transform matrix is exactly what’s needed when you render a mesh: the model-view matrix, which converts the coordinates of vertices in a mesh to world space, is a transform matrix.

Animating a Mesh

Problem

You want to animate objects by moving them over time.

Solution

The code in this solution uses the component-based architecture discussed in Creating a Component-Based Game Layout, though the idea can also be applied to hierarchy-based architectures (see Creating an Inheritance-Based Game Layout). In this solution, we’ll be creating an Animationcomponent.

Add a new class called Animation, and put the following contents in Animation.h:

#import "Component.h"

#import <GLKit/GLKit.h>

// Animates a property on "object"; t is between 0 and 1

typedef void (^AnimationBlock)(GameObject* object, float t);

@interface Animation : Component

@property (assign) float duration;

- (void) startAnimating;

- (void) stopAnimating;

- (id) initWithAnimationBlock:(AnimationBlock)animationBlock;

@end

And in Animation.m:

#import "Animation.h"

@implementation Animation {

AnimationBlock _animationBlock;

float _timeElapsed;

BOOL _playing;

}

- (void)startAnimating {

_timeElapsed = 0;

_playing = YES;

}

- (void)stopAnimating {

_playing = NO;

}

- (void) update:(float)deltaTime {

// Don't do anything if we're not playing

if (_playing == NO)

return;

// Don't do anything if the duration is zero or less

if (self.duration <= 0)

return;

// Increase the amount of time that this animation's been running for

_timeElapsed += deltaTime;

// Go back to the start when time elapsed > duration

if (_timeElapsed > self.duration)

_timeElapsed = 0;

// Dividing the time elapsed by the duration returns a value between 0 and 1

float t = _timeElapsed / self.duration;

// Finally, call the animation block

if (_animationBlock) {

_animationBlock(self.gameObject, t);

}

}

- (id)initWithAnimationBlock:(AnimationBlock)animationBlock

{

self = [super init];

if (self) {

_animationBlock = animationBlock;

_duration = 2.5;

}

return self;

}

@end

To use the animation component, you create one and provide a block that performs an animation:

GameObject* myObject = ... // a GameObject

// Create an animation that rotates around the y-axis

Animation* rotate =

[[Animation alloc] initWithAnimationBlock:^(GameObject *object, float t) {

float angle = 2*M_PI*t;

object.transform.localRotation = GLKVector3Make(0, angle, 0);

}];

// The animation takes 10 seconds to complete

rotate.duration = 10;

// Add the animation to the object

[myObject addComponent:rotate];

// Kick off the animation

[rotate startAnimating];

Discussion

An animation is a change in value over time. When you animate an object’s position, you’re changing the position from a starting point to the ending point.

An animation can take as long as you want to complete, but it’s often very helpful to think of the animation’s time scale as going from 0 (animation start) to 1 (animation end). This means that the animation’s duration can be kept separate from the animation itself.

You can use this value as part of an easing equation, which smoothly animates a value over time.

If t is limited to the range of 0 to 1, you can write the equation for a linear progression from value v1 to value v2 like this:

result = v1 + (v2 - v1) * t;

Imagine that you want to animate the x coordinate of an object from 1 to 5. If you plot this position on a graph, where the y-axis is the position of the object and the x-axis is time going from 0 to 1, the coordinates looks like Table 9-1.

Table 9-1. Plot points to animate x

Position

t

1

0.00

2

0.25

3

0.50

4

0.75

5

1.00

This equation moves smoothly from the first value to the second, and maintains the same speed the entire way.

There are other equations that you can use:

// Ease-in - starts slow, reaches full speed by the end

result = v1 + (v2 - v1) * pow(t,2);

// Ease-out - starts at full speed, slows down toward the end

result = v1 + (v1 - v2) * t * (t-2)

You can also create more complex animations by combining multiple animations, using parenting (see Parenting Objects).

Batching Draw Calls

Problem

You have a number of objects, all of which have the same texture, and you want to improve rendering performance.

Solution

This solution uses the Mesh class discussed in Loading a Mesh.

If you have a large number of objects that can be rendered with the same GLKEffect (but with varying positions and orientations), create a new vertex buffer and, for each copy of the object, copy each vertex into the buffer.

For each vertex you copy in, multiply the vertex by the object’s transform matrix (see Parenting Objects). Next, create an index buffer and copy each triangle from each object into it. (Because they’re triangles, they don’t get transformed.)

When you render the contents of these buffers, all copies of the object will be rendered at the same time.

Discussion

The glDrawElements function is the slowest function involved in OpenGL. When you call it, you kick off a large number of complicated graphics operations: data is fetched from the buffers, vertices are transformed, shaders are run, and pixels are written into the frame buffer. The more frequently you call glDrawElements, the more work needs to happen, and as discussed in Improving Your Frame Rate, the more work you do, the lower your frame rate’s going to be.

To reduce the number of calls to glDrawElements and improve performance, it’s better to group objects together and render them at the same time. If you have a hundred crates, instead of drawing a crate 100 times, you draw 100 crates once.

There are a couple of limitations when you use this technique, though:

§ All objects have to use the same texture and lights, because they’re all being drawn at the same time.

§ More space in memory gets taken up, because you have to store a duplicate set of vertices for each copy of the object.

§ If any of the objects are moving around, you have to dynamically create a new buffer and fill it with vertex data every frame, instead of creating a buffer once and reusing it.

Creating a Movable Camera Object

Problem

You want your camera to be an object that can be moved around the scene like other objects.

Solution

This solution builds on the component-based architecure discussed in Creating a Component-Based Game Layout and uses the Transform component from Parenting Objects.

Create a new subclass of Component called Camera, and put the following contents in Camera.h:

#import "Component.h"

#import <GLKit/GLKit.h>

@interface Camera : Component

// Return a matrix that maps world space to view space

- (GLKMatrix4) viewMatrix;

// Return a matrix that maps view space to eye space

- (GLKMatrix4) projectionMatrix;

// Field of view, in radians

@property (assign) float fieldOfView;

// Near clipping plane, in units

@property (assign) float nearClippingPlane;

// Far clipping plane, in units

@property (assign) float farClippingPlane;

// Clear screen contents and get ready to draw

- (void) prepareToDraw;

// The color to erase the screen with

@property (assign) GLKVector4 clearColor;

@end

And in Camera.m:

#import "Camera.h"

#import "Transform.h"

#import "GameObject.h"

@implementation Camera

// By default, start with a clear color of black (RBGA 1,1,1,0)

- (id)init

{

self = [super init];

if (self) {

self.clearColor = GLKVector4Make(0.0, 0.0, 0.0, 1.0);

}

return self;

}

- (void) prepareToDraw {

// Clear the contents of the screen

glClearColor(self.clearColor.r, self.clearColor.g, self.clearColor.b,

self.clearColor.a);

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

}

// Return a matrix that maps world space to view space

- (GLKMatrix4)viewMatrix {

Transform* transform = self.gameObject.transform;

// The camera's position is its transform's position in world space

GLKVector3 position = transform.position;

// The camera's target is right in front of it

GLKVector3 target = GLKVector3Add(position, transform.forward);

// The camera's up direction is the transform's up direction

GLKVector3 up = transform.up;

return GLKMatrix4MakeLookAt(position.x, position.y, position.z,

target.x, target.y, target.z,

up.x, up.y, up.z);

}

// Return a matrix that maps view space to eye space

- (GLKMatrix4)projectionMatrix {

// We'll assume that the camera is always rendering into the entire screen

// (i.e., it's never rendering to just a subsection if it).

// This means the aspect ratio of the camera is the screen's aspect ratio.

float aspectRatio = [UIScreen mainScreen].bounds.size.width /

[UIScreen mainScreen].bounds.size.height;

return GLKMatrix4MakePerspective(self.fieldOfView, aspectRatio,

self.nearClippingPlane,

self.farClippingPlane);

}

@end

Discussion

The idea of a “camera” in OpenGL is kind of the reverse of how people normally think of viewing a scene. While it’s easy to think of a camera that moves around in space, looking at objects, what’s really going on in OpenGL is that objects get rearranged to be in front of the viewer. That is, when the camera “moves forward,” what’s really happening is that objects are moving closer to the camera.

When objects in your 3D scene are rendered, they get transformed through a variety of coordinate spaces. The first space that vertices begin in is model space, where all vertices are defined relative to the point of the model. Because we don’t want all the meshes to be drawn on top of one another, they need to be transformed into world space, in which all vertices are defined relative to the origin point of the world.

Once they’re in world space, they need to be rearranged such that they’re in front of the camera. This is done by calculating a view matrix, which you do by taking the position of the camera in world space, the position of a point it should be looking toward, and a vector defining which direction is “up” for the camera, and calling GLKMatrix4MakeLookAt.

The view matrix rearranges vertices so that the camera’s position is at the center of the coordinate space, and arranges things so that the camera’s “up” and “forward” directions become coordinate space’s y- and z-axes.

So, when you “position” a camera, what you’re really doing is preparing a matrix that arranges your vertices in a way that puts the camera in the center, with everything else arranged around it.