October 2014

Sun Mon Tue Wed Thu Fri Sat
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  










« Creating a 3D viewer for our Apollonian service using Android – Part 3 | Main | Creating a 3D viewer for our Apollonian service using iOS – Part 2 »

May 07, 2012

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

Last week, it was all about Android. 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 learning different programming languages, 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 = [[NSMutableData alloc] init];

  else

    [_receivedData setLength:0];

}

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

- (void)connection:(NSURLConnection *)connection

    didReceiveData:(NSData *)data

{

  // Append the received data to our member

 

  [_receivedData appendData: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 =

    [[NSString alloc]

      initWithData:_receivedData

      encoding:NSUTF8StringEncoding

    ];

  [_receivedData release];

  // 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)

        [self createSphere:radius x:x y:y z:z level:level];

    }

   

    // Trigger the rotation updates

   

    [self schedule:@selector(tick:)];

  }

}

- (id) init

{  

  if ((self = [super init]))

  {

    // 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 =

      [[NSURLConnection alloc]initWithRequest:request delegate:self];

    if (connection)

    {

      _receivedData = [[NSMutableData data] retain];

    }       

   

    // Move the default camera to the desired position

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

   

    // Create a container for our spheres

   

    _container = [self.scene createNode];

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

    // levels. Define the colors for those levels

   

    NSArray * colors =

      [NSArray arrayWithObjects:

        /* 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 = [[NSMutableArray alloc] 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 =

        [[Isgl3dColorMaterial alloc]

          initWithHexColors:col

          diffuse:col

          specular:col

          shininess:0.7

        ];

      [_materials addObject:mat];

    }

    // Create a single sphere mesh

   

    _sphereMesh =

      [[Isgl3dSphere alloc] initWithGeometry:1 longs:9 lats: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:1 y:2 z:-5];

    [self.scene addChild:light];

   

    // Set the scene ambient color

    [self setSceneAmbient:@"000000"];

  }

  return self;

}

// 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:[_materials objectAtIndex: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

 

  [_materials release];

  [_sphereMesh release];

   

  [super dealloc];

}

- (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:

First pass Apollonian Viewer on iOSI’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.

blog comments powered by Disqus

Feed/Share

10 Random Posts