September 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        










« Creating a 3D viewer for our Apollonian service using iOS – Part 1 | Main | New versions of 123D Catch for web and iPad »

May 09, 2012

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

In the last post, we saw some code to implement a simple 3D viewer of data coming from our Apollonian web-service on iOS. In this post, we’ll add support for touch gestures, as well as a simple message box announcing when the web-service is unavailable.

When compared with Android, iOS provides much higher-level gesture information via its UIKit framework: you basically get callbacks to indicate when the screen has been tapped or swiped, or when pinch or rotate gestures have been performed. This is all very helpful, in the sense that you don’t have to do so much low-level running around in your code – and risk that different apps respond to touch in a different way – but it does come at the expense of flexibility. For instance, if you want to support swipe gestures, you can really only do so in the four main directions (up, down, left, right) and you need a separate recognizer object for at least vertical and horizontal directions, and quite possibly all four.

In the Android version of this app, we had to decide how swipe gestures would be implemented at a fairly low level, but the upside was that we could calculate a more precise direction and use that to adjust the spin of our model. Now I’m sure that it’s possible with iOS to hook into events at a lower level, to essentially do the same as with Android, but there are a couple of reasons I’ve decided not to. The first is that I don’t really need feature parity between the two viewers – as this is mostly research-related – but more importantly I have doubts that I’ll have the same ability to rotate our model in an arbitrary direction, as Dennis Ippel kindly enabled for us in his Rajawali framework (I haven’t seen the same capabilities, necessarily, in iSGL3D, and have less hope of getting them implemented). I’d ideally like to specify a rotation around an axis that’s perpendicular to the swipe direction – perhaps using a quaternion – and then apply that additively to the model.

Again, it’s probably something that I could implement myself, given enough time and effort, but I’ve decided to keep things simple. The addition of the rotate gesture – even if it gets reset when the model starts to spin – seems a reasonable complement to the existing behaviour of spinning in one of four directions, at least.

Before we look at the updated code, here’s a quick video of the application in action on the iOS Simulator:

Unable to display content. Adobe Flash is required.

And with that, on to the code. Here’s the updated Objective-C implementation of our Apollonian Viewer. As you’ll have seen in the video, I’ve gone ahead and added a splash-screen and icons to the project, even if we don’t have much else by way of a UI, still.

Here’s the updated ApollonianViewer.h file:

#import "isgl3d.h"

@interface ApollonianViewer : Isgl3dBasic3DView

{

  @private

  NSMutableArray * _materials;

  Isgl3dNode * _container;

  Isgl3dSphere * _sphereMesh;

  bool _preSpin;

  bool _paused;

  float _rotation;

  bool _spinAroundY;

  int _spinIncrement;

 

  int _initialSpinAmount;

 

  UITapGestureRecognizer * _tapGestureRecognizer;

  UISwipeGestureRecognizer * _swipeLeftGestureRecognizer;

  UISwipeGestureRecognizer * _swipeRightGestureRecognizer;

  UISwipeGestureRecognizer * _swipeUpGestureRecognizer;

  UISwipeGestureRecognizer * _swipeDownGestureRecognizer;

  UIPinchGestureRecognizer *_pinchGestureRecognizer;

  UIRotationGestureRecognizer *_rotationGestureRecognizer;

}

-(void)createSphere

  :(double)radius

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

  level:(int)level;

@end

@interface ApollonianViewer () <UIGestureRecognizerDelegate>

- (void)tapGesture:(UITapGestureRecognizer *)gestureRecognizer;

- (void)swipeGesture:(UISwipeGestureRecognizer *)gestureRecognizer;

- (void)pinchGesture:(UIPinchGestureRecognizer *)gestureRecognizer;

- (void)rotationGesture:(UIRotationGestureRecognizer *)gestureRecognizer;

@end

And here’s the updated ApollonianViewer.m 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]);

  UIAlertView* alert =

    [[[UIAlertView alloc]

      initWithTitle:@"Apollonian Viewer"

      message:

        @"Unable to access the web-service. "

        "Please check you have internet connectivity."

      delegate:self

      cancelButtonTitle:@"Close"

      otherButtonTitles:nil

    ] autorelease];

  [alert show];

}

- (void)alertView

  :(UIAlertView *)alertView

  clickedButtonAtIndex:(NSInteger)buttonIndex

{

  if (buttonIndex == 0) exit (0);  

}

// 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];

    }

   

    // Remove our progress meter

   

    UIActivityIndicatorView * vw =

      (UIActivityIndicatorView *)

        [[Isgl3dDirector sharedInstance].openGLView viewWithTag:1];

    [vw removeFromSuperview];

   

    // Trigger the rotation updates

   

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

  }

}

// Our main scene initialization method

- (id) init

{  

  if ((self = [super init]))

  {

    _preSpin = true;

    _paused = false;

    _rotation = 0.0;

    _initialSpinAmount = 2;

   

    // Create recognizers handling scene-level gestures

   

    // Tap

   

    _tapGestureRecognizer =

      [[UITapGestureRecognizer alloc]

        initWithTarget:self

        action:@selector(tapGesture:)

      ];

    _tapGestureRecognizer.delegate = self;   

    [[Isgl3dDirector sharedInstance]

      addGestureRecognizer:_tapGestureRecognizer

      forNode:nil

    ];

   

    // Swipe

    // (Add a recognizer for each of 4 directions)

    _swipeLeftGestureRecognizer =

      [[UISwipeGestureRecognizer alloc]

        initWithTarget:self

        action:@selector(swipeGesture:)

      ];

    _swipeLeftGestureRecognizer.delegate = self;

    [_swipeLeftGestureRecognizer

      setDirection:UISwipeGestureRecognizerDirectionLeft

    ];   

    [[Isgl3dDirector sharedInstance]

      addGestureRecognizer:_swipeLeftGestureRecognizer

      forNode:nil

    ];

   

    _swipeRightGestureRecognizer =

      [[UISwipeGestureRecognizer alloc]

        initWithTarget:self

        action:@selector(swipeGesture:)

      ];

    _swipeRightGestureRecognizer.delegate = self;

    [_swipeRightGestureRecognizer

      setDirection:UISwipeGestureRecognizerDirectionRight

    ];   

    [[Isgl3dDirector sharedInstance]

      addGestureRecognizer:_swipeRightGestureRecognizer

      forNode:nil

    ];

   

    _swipeUpGestureRecognizer =

      [[UISwipeGestureRecognizer alloc]

        initWithTarget:self

        action:@selector(swipeGesture:)

      ];

    _swipeUpGestureRecognizer.delegate = self;

    [_swipeUpGestureRecognizer

      setDirection:UISwipeGestureRecognizerDirectionUp

    ];   

    [[Isgl3dDirector sharedInstance]

      addGestureRecognizer:_swipeUpGestureRecognizer

      forNode:nil

    ];

    _swipeDownGestureRecognizer =

      [[UISwipeGestureRecognizer alloc]

        initWithTarget:self

        action:@selector(swipeGesture:)

      ];

    _swipeDownGestureRecognizer.delegate = self;

    [_swipeDownGestureRecognizer

      setDirection:UISwipeGestureRecognizerDirectionDown

    ];   

    [[Isgl3dDirector sharedInstance]

      addGestureRecognizer:_swipeDownGestureRecognizer

      forNode:nil

    ];

   

    // Pinch

   

    _pinchGestureRecognizer =

      [[UIPinchGestureRecognizer alloc]

        initWithTarget:self

        action:@selector(pinchGesture:)

      ];

    _pinchGestureRecognizer.delegate = self;   

    [[Isgl3dDirector sharedInstance]

      addGestureRecognizer:_pinchGestureRecognizer

      forNode:nil

    ];

   

    // Rotate

   

    _rotationGestureRecognizer =

      [[UIRotationGestureRecognizer alloc]

        initWithTarget:self

        action:@selector(rotationGesture:)

      ];

    _rotationGestureRecognizer.delegate = self;   

    [[Isgl3dDirector sharedInstance]

      addGestureRecognizer:_rotationGestureRecognizer

      forNode:nil

    ];

   

    // 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 initial 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];

}

// Respond to our timer tick by rotating the model

- (void) tick:(float)dt

{

  // Rotate around the appropriate axis

  if (!_paused && !_preSpin)

  {

    // Reset any rotation around Z, first

   

    if (abs(_container.rotationZ) > 0)

    {

      _container.rotationZ = 0;

      _rotation = 0;

    }

       

    if (_spinAroundY)

    {

      // If spinning around Y, reset any X rotation

     

      if (abs(_container.rotationX) > 0)

        _container.rotationX = 0;

     

      _container.rotationY += _spinIncrement;

    }

    else

    {

      // If spinning around X, reset any Y rotation

     

      if (abs(_container.rotationY) > 0)

        _container.rotationY = 0;

     

      _container.rotationX += _spinIncrement;

    }

  }

}

// Method to specify combination of gesture recognizers

- (BOOL)gestureRecognizer:

(UIGestureRecognizer *)gestureRecognizer

shouldRecognizeSimultaneouslyWithGestureRecognizer:

(UIGestureRecognizer *)otherGestureRecognizer

{

  // If the gesture recognizers are on different views,

  // don't allow simultaneous recognition

 

  if (gestureRecognizer.view != otherGestureRecognizer.view)

    return NO;

 

  // Also stop combination of rotation with other gestures

 

  if ((gestureRecognizer == _rotationGestureRecognizer) ||

      (otherGestureRecognizer == _rotationGestureRecognizer))

    return NO;

 

  return YES;

}

// Action methods for our gestures

// Tap-pause/play

- (void)tapGesture

  :(UITapGestureRecognizer *)gestureRecognizer

{

  // Toggle pause (if we have already had at least one spin)

 

  if (!_preSpin)

    _paused = !_paused;

}

// Swipe-spin

- (void)swipeGesture

  :(UISwipeGestureRecognizer *)gestureRecognizer

{

  switch(gestureRecognizer.direction)

  {   

    case UISwipeGestureRecognizerDirectionDown:

      if (

        _preSpin || _spinAroundY ||

        (!_spinAroundY && _spinIncrement > 0)

      )

      {

        // Reset the axis and spin amount

       

        _spinAroundY = false;

        _spinIncrement = -_initialSpinAmount;

      }

      else

      {

        // Speed up the rate of spin

       

        if (abs(_spinIncrement * 2) < 10)

          _spinIncrement *= 2;

      }

      _preSpin = false;

      _paused = false;

      break;

     

    case UISwipeGestureRecognizerDirectionUp:

      if (

        _preSpin || _spinAroundY ||

        (!_spinAroundY && _spinIncrement < 0)

      )

      {

        // Reset the axis and spin amount

       

        _spinAroundY = false;

        _spinIncrement = _initialSpinAmount;

      }

      else

      {

        // Speed up the rate of spin

       

        if (abs(_spinIncrement * 2) < 10)

          _spinIncrement *= 2;

      }

      _preSpin = false;

      _paused = false;

      break;

     

    case UISwipeGestureRecognizerDirectionLeft:

      if (

        _preSpin || !_spinAroundY ||

        (_spinAroundY && _spinIncrement > 0)

      )

      {

        // Reset the axis and spin amount

       

        _spinAroundY = true;

        _spinIncrement = -_initialSpinAmount;

      }

      else

      {

        // Speed up the rate of spin

       

        if (abs(_spinIncrement * 2) < 10)

          _spinIncrement *= 2;

      }

      _preSpin = false;

      _paused = false;

      break;

     

    case UISwipeGestureRecognizerDirectionRight:

      if (

        _preSpin || !_spinAroundY ||

        (_spinAroundY && _spinIncrement < 0)

      )

      {

        // Reset the axis and spin amount

       

        _spinAroundY = true;

        _spinIncrement = _initialSpinAmount;

      }

      else

      {

        // Speed up the rate of spin

       

        if (abs(_spinIncrement * 2) < 10)

          _spinIncrement *= 2;

      }

      _preSpin = false;

      _paused = false;

      break;

     

    default:

      break;

  }

}

// Pinch-zoom

- (void)pinchGesture

  :(UIPinchGestureRecognizer *)gestureRecognizer

{

  if (

    [gestureRecognizer state] == UIGestureRecognizerStateBegan ||

    [gestureRecognizer state] == UIGestureRecognizerStateChanged

  )

  {

    // Adjust the camera position based on the zoom scale

   

    [self.camera setZ:self.camera.z * (1/gestureRecognizer.scale)];

    [gestureRecognizer setScale:1];

  }

}

// Rotate-rotate :-)

- (void)rotationGesture

  :(UIRotationGestureRecognizer *)gestureRecognizer

{

  if (

    [gestureRecognizer state] == UIGestureRecognizerStateBegan ||

    [gestureRecognizer state] == UIGestureRecognizerStateChanged

  )

  {

    // Adjust the rotation around Z based on the rotation amount

   

    if (_paused || _preSpin)

    {

      _rotation += (gestureRecognizer.rotation * 180.0 / M_PI);

      [_container setRotationZ:_rotation];

      [gestureRecognizer setRotation:0];

    }

  }

}

     

@end

I’ve managed to get into the Apple Developer Program via an Autodesk subscription, but there are some delays with me being able to test the app directly on a physical device. I’m still waiting to do so before I determine what additional UI needs to be implemented (in terms of progress bars, particularly), although I could probably go ahead and add some level-selection capability, at least. I may just move on to other things and revisit this at some point in the future – we’ll see.

blog comments powered by Disqus

Feed/Share

10 Random Posts