May 07, 2012

Creating a 3D viewer for our Apollonian service using iOS – Part 1

Last week, it was allaboutAndroid. This week, I’ve started taking the plunge into the world of iOS. I’ve been using a Mac for some time – mainly to wean myself away from being so Windows-centric, but also with a view to working more with AutoCAD for Mac from a development perspective – but this was the first time I’d actually forced myself to write anything for either OS X or iOS.

It all came as a bit of a shock, initially, even though I was generally aware of the strangeness of Objective-C with respect to its message-passing syntax. So while I enjoy learningdifferentprogramminglanguages, I found I really struggled with Objective-C. But anyway – obviously lots of people have managed to get their heads around it (and many of this blog’s readers will have done so, I’m sure), so at least there is a fair amount of help available out there on the web.

Someone has already commented on the fact that you can use C# to build apps for iOS and Android directly – such as with a toolkit like Xamarin or an engine like Unity3D – but the point of this series of posts is as much about driving my own learning as it is about presenting my readers with easy options (sorry for being selfish, but that’s just how it is). And I think there’s value in seeing the “native” approach across a variety of platforms – while knowing that options exist allowing you to maintain a largely platform-independent codebase to target them.

And so on to my deep-ish dive into iOS…

My first challenge was identifying a decent (and free) 3D engine for our viewer – all of which seem to be based on OpenGL ES, much the same as for Android. I started by looking at Cocos3D (which is based on the apparently very popular Cocos2D), but ended up discarding it as it didn’t appear to have a sphere primitive available (which was a bit of a deal-breaker for me, given the problem space ;-).

I moved on to look at iSGL3D, which certainly appeared to provide what I was looking for from a 3D engine. I spent some time looking at its online tutorials, which were reasonably comprehensive, before trying the “tests” provided with the framework and building my first basic app with the Xcode 4 template. I went through a little unnecessary thrashing, as I pulled down the latest file versions directly from GitHub before realising I really needed to install the latest stable build (version 1.2.3 at the time of writing).

But, that aside, the process was reasonably straightforward. I modified the contents of the “Hello World” files created by the Xcode template (renaming them, too, of course), to be as follows…

It’s worth noting that – in an effort to make the code a more familiar and consistent with the other code I post here – I’ve thrown away the book on Objective-C coding conventions (much as I did for Java, last week). That’s partly for the benefit of this blog’s readers, but also for my own sanity. ;-)

Firstly, the ApollonianViewer.h header file:

#import "isgl3d.h"

@interface ApollonianViewer : Isgl3dBasic3DView

{

@private

NSMutableArray * _materials;

Isgl3dNode * _container;

Isgl3dSphere * _sphereMesh;

}

-(void)createSphere

:(double)radius

x:(double)x y:(double)y z:(double)z

level:(int)level;

@end

And now the main ApollonianViewer.m implementation file:

#import "ApollonianViewer.h"

@implementation ApollonianViewer

// Our data member for the received data

NSMutableData * _receivedData = NULL;

// A response has been received from our web-service call

- (void)connection:(NSURLConnection *)connection

didReceiveResponse:(NSURLResponse *)response

{

// Initialise our member variable receiving data

if (_receivedData == NULL)

_receivedData = [[NSMutableDataalloc] init];

else

[_receivedDatasetLength:0];

}

// Data has been received from our web-service call

- (void)connection:(NSURLConnection *)connection

didReceiveData:(NSData *)data

{

// Append the received data to our member

[_receivedDataappendData:data];

}

// The web-service connection failed

- (void)connection:(NSURLConnection *)connection

didFailWithError:(NSError *)error

{

// Report an error in the log

NSLog(@"Connection failed: %@", [error description]);

}

// The call to our web-service has completed

- (void)connectionDidFinishLoading

:(NSURLConnection *)connection

{

// Release the connection

[connection release];

// Get the response string from our data member then

// release it

NSString *responseString =

[[NSStringalloc]

initWithData:_receivedData

encoding:NSUTF8StringEncoding

];

[_receivedDatarelease];

// Extract JSON data from our response string

NSData *jsonData =

[responseString

dataUsingEncoding:NSUTF8StringEncoding];

// Extract an array from our JSON data

NSError *e = nil;

NSArray *jsonArray =

[NSJSONSerialization

JSONObjectWithData: jsonData

options: NSJSONReadingMutableContainers

error: &e

];

if (!jsonArray)

{

NSLog(@"Error parsing JSON: %@", e);

}

else

{

// Loop through our JSON array, extracting spheres

for (NSDictionary *item in jsonArray)

{

// We'll need this data for each sphere

double x, y, z, radius;

int level;

// We use a single NSNumber to extract the data

NSNumber *num;

num = [item objectForKey:@"X"];

x = [num doubleValue];

num = [item objectForKey:@"Y"];

y = [num doubleValue];

num = [item objectForKey:@"Z"];

z = [num doubleValue];

num = [item objectForKey:@"R"];

radius = [num doubleValue];

num = [item objectForKey:@"L"];

level = [num intValue];

// Only create spheres for those at the edge of the

// outer sphere

double length = sqrt(x*x + y*y + z*z);

if (length + radius > 0.99f)

[selfcreateSphere:radius x:x y:y z:z level:level];

}

// Trigger the rotation updates

[selfschedule:@selector(tick:)];

}

}

- (id) init

{

if ((self = [superinit]))

{

// Set up our web-service call

NSURL *url =

[NSURL

URLWithString:

@"http://apollonian.cloudapp.net/api/spheres/1/7"

];

NSMutableURLRequest *request =

[NSMutableURLRequest

requestWithURL:url

cachePolicy:NSURLRequestUseProtocolCachePolicy

timeoutInterval:60.0

];

[request setHTTPMethod:@"GET"];

NSURLConnection *connection =

[[NSURLConnectionalloc]initWithRequest:request delegate:self];

if (connection)

{

_receivedData = [[NSMutableDatadata] retain];

}

// Move the default camera to the desired position

[self.camerasetPosition:iv3(0, 0, -5)];

// Create a container for our spheres

_container = [self.scenecreateNode];

// We'll maintain an array of materials for our

// levels. Define the colors for those levels

NSArray * colors =

[NSArrayarrayWithObjects:

/* white */@"FFFFFF",

/* red */@"FF0000",

/* yellow */@"FFFF00",

/* green */@"00FF00",

/* cyan */@"00FFFF",

/* blue */@"0000FF",

/* magenta */@"FF00FF",

/* dark gray */@"A9A9A9",

/* gray */@"808080",

/* light gray */@"D3D3D3",

/* white */@"FFFFFF",

nil

];

// Create and populate the array of materials

_materials = [[NSMutableArrayalloc] init];

for (int i=0; i < 12; i++)

{

// Anything we don't have a color for will be white

NSString *col =

(i <= 10) ? [colors objectAtIndex:i] : @"FFFFFF";

// For simplicity, make the colors the same for

// ambient, diffuse and specular lighting

Isgl3dColorMaterial * mat =

[[Isgl3dColorMaterialalloc]

initWithHexColors:col

diffuse:col

specular:col

shininess:0.7

];

[_materialsaddObject:mat];

}

// Create a single sphere mesh

_sphereMesh =

[[Isgl3dSpherealloc] initWithGeometry:1longs:9lats:9];

// Create a directional white light and add it to the scene

Isgl3dLight * light =

[Isgl3dLight

lightWithHexColor:@"A0A0A0"

diffuseColor:@"E9E9E9"

specularColor:@"C0C0C0"

attenuation:0

];

light.lightType = DirectionalLight;

light.position = iv3(4, 0, 8);

[light setDirection:1y:2z:-5];

[self.sceneaddChild:light];

// Set the scene ambient color

[selfsetSceneAmbient:@"000000"];

}

returnself;

}

// Create a single sphere at the desired position with

// the desired radius and level

- (void)createSphere

:(double)radius

x:(double)x y:(double)y z:(double)z

level:(int)level

{

// Create the sphere based on our single mesh

Isgl3dMeshNode * sphere =

[_container

createNodeWithMesh:_sphereMesh

andMaterial:[_materialsobjectAtIndex:level]

];

// Position and scale it

sphere.position = iv3(x, y, z);

[sphere setScale:radius];

}

- (void) dealloc

{

// Make sure we release our materials and sphere mesh

[_materialsrelease];

[_sphereMeshrelease];

[superdealloc];

}

- (void) tick:(float)dt

{

// Rotate around the y axis

_container.rotationY += 2;

}

@end

The app currently does a fair amount less that its Android counterpart – I haven’t implemented any kind of UI, including progress bars, touch gestures, etc. – but there were actually some things that just worked more smoothly: rather than worrying about threading issues, the call to the web-service seemed to execute asynchronously by default, and the code adding spheres worked well, once I’d determined I needed to control the lifetime of my supporting objects rather than allowing them to be garbage-collected at the whim of the iOS runtime. It’s not clear to me how much of this is down to the iSGL3D runtime vs. Objective-C/iOS, but I was pleasantly surprised, either way.

I expect I’ll hit more significant challenges, further down the line, but my initial impression is that the above code is actually impressively functional for the amount there is: it was pretty simple to access a REST web-service and decode the JSON results, for instance, and the iSGL3D coding was also relatively straightforward.

And while looking at the syntax still gives me a headache, at least my nose has stopped bleeding. ;-)

Here’s a screenshot of the app working on the iPad 5.1 Simulator:

I’d really like to see this working on the iPad itself, as before I start tweaking the lighting, etc. to get better results, I’d like to see what, if anything, is due to the lack of GPU-accelerated graphics in the simulator (assuming that’s the case). It seems that to do so I’ll need to sign up for the iOS Developer Progrram at $99 per year, which I find a little annoying but to some degree understandable.

In fairness, the Android simulator can’t even run OpenGL ES 2.0 code, at the time of writing, so being forced to pay to test on a physical device would probably have raised the barrier of entry high enough to put me off working with Android completely. At least there is some option for getting started for free on iOS.

Aside from testing on a physical device, I also want to implement some kind of rudimentary UI – much as I did for Android – so I’ll be working on that before I end up posting the full project.