It’s a beautiful Whit Monday, here in Switzerland – and will probably be a beautiful Memorial Day, when I arrive in San Francisco, this afternoon. I’m heading across the pond to attend an internal technical conference and spend time with other members of my team over the coming fortnight.
I have a couple of posts to finish up – one on WinRT, another on iOS, now that I’m able to deploy that version to my iPad 2 – before I wrap up the somewhat epic “cloud & mobile” series with a summary post. I’ll see what I can get done on the trip over, but as I haven’t taken pains to implement local caching, I’m going to be dependent on a local network connection to do much of what I need to do to finish up the remaining coding tasks.
Maybe that’s an excuse to kick back (such as is possible, in an economy seat) and enjoy the extended double-holiday, watching a few movies on the (for once!) direct flight from ZRH to SFO.
After tackling the implementation of a basic 3D viewer for our Apollonian web-service using a variety of technology stacks – AutoCAD, Unity3D, Android, iOS & HTML5/WebGL – I felt as though I really needed to give it a try with WinRT, the new runtime powering Windows 8.
All of the previous stacks had some “object” layer I could use above the base graphics engine – Rajawali provided it for Android/OpenGL ES, iSGL3D for iOS and Three.js for HTML5/WebGL – but for WinRT all bets were off. The general guidance for developing Metro-style 3D applications (typically that means games) is to use DirectX 11 (i.e. Direct3D) from C++.
I found two managed wrappers to allow me to use DirectX from C#: it seems SlimDX is the established player, and SharpDX the new kid on the block (although as it is apparently auto-generated using DirectX headers it has broad coverage, already). I went with SharpDX, as it was the only one that worked – at the time of writing – with WinRT to allow creation of Metro-style apps. Both SlimDX and SharpDX have pretty sparse documentation and samples, unfortunately, but you take what you can get. And neither is intended to provide an object framework that I’d been spoiled with on other platforms: perhaps they’re out there or in development (there seems to be some excitement around ANX.Framework, which is apparently an open source replacement for Microsoft’s popular XNA, but it seems a ways off being useable).
So I had to roll up my sleeves and wade into low-level graphics implementation – something I’d explicitly avoided when working with OpenGL ES.
The first thing to take care of was building the polygons representing a sphere: something I managed to find in this C++ DirectX sample. I combined that with much of the code in the MiniCube and MiniCubeXaml SharpDX samples to create a Metro-style 3D viewer for our Apollonian models.
That’s the quick version of the story: the longer version is a bit more detailed, as I had to learn all about buffers and shaders. The Direct3D pipeline is such that you need to create a vertex shader, which takes data in the form of buffers that get loaded onto the GPU and outputs data to be passed on to the pixel shader, which then calculates the color of a specific vertex based on the data it receives and lighting, etc. Nasty, low-level stuff (for me, at least). I had to spend some time understanding the basics of HLSL, the C-like language you use to write these shaders before they get compiled into code the GPU can execute.
And it took me some time to find out that the Direct3D feature level supported by my graphics card – probably influenced by the Parallels virtualization layer between Windows 8 and my Mac hardware – influenced the way I had to compile the shaders: the app now targets a minimum of Level 9.3, which presumably means that compiling for a higher version would give better performance on different hardware.
Anyway, by this point I had a simple, shaded, rotating sphere. Then I had to learn about – and implement – instancing, as one sphere wasn’t enough and we clearly didn’t want to have to generate a mesh for each of our spheres. This took me some time to get right – my app would work well until I tried to access some of the instance-specific data in my vertex shader, at which point I’d just get no graphics at all. Eventually I understood that there was a different version of the InputElement() constructor that took arguments allowing definition of instance-specific data – we needed this to define the position, radius and colour of each sphere.
While I was hitting this particular wall, I was excited to find out about the new Graphics Debugger in Visual Studio 11. This is enabled automatically for C++ projects, but when working with managed code – via SlimDX or SharpDX – it takes more work to get working. I found a few forumposts explaining an approach involving creating a separate EXE and then a C++ project that launches it rather than the debugger. Neither of which helped me diagnose the issue – or even led to the Graphics Debugger displaying any useful data, as it didn’t let me capture a frame, for some reason – but it was an interesting process.
Once I finally had instancing working, it was then a simple matter of plugging together some code to access our web-service and extract the sphere information from our JSON results.
Here’s the resulting application in action:
Here’s the VS11 project. The main two source files are SimpleSphere.fx – the HLSL source for the vertex and pixel shaders…
float4x4 worldViewProj;
struct VS_IN
{
float4 pos : POSITION;
float3 norm : NORMAL;
float3 instancePos : TEXCOORD0;
float4 col : TEXCOORD1;
float rad : TEXCOORD2;
};
struct PS_IN
{
float4 pos : SV_POSITION;
float3 norm : NORMAL;
float3 worldPos : POSITION0;
float4 col : COLOR0;
float rad : TEXCOORD0;
};
PS_IN VS( VS_IN input )
{
PS_IN output = (PS_IN)0;
float4 temp = input.pos;
temp.w = 1.0f;
// Update the position of the vertices based on the
// data for this particular instance
temp.xyz *= input.rad;
temp.x += input.instancePos.x;
temp.y += input.instancePos.y;
temp.z += input.instancePos.z;
output.worldPos = temp.xyz / temp.w;
temp = mul(temp, worldViewProj);
output.pos = temp;
output.norm = input.norm;
output.col = input.col;
output.rad = input.rad;
return output;
}
float4 PS( PS_IN input ) : SV_Target
{
float3 LightDirection = float3(0, 0, 1);
float3 AmbientColor = float3(0.43, 0.31, 0.24);
float3 LightColor = 1 - AmbientColor;
float SpotRadius = 50;
// Basic ambient (Ka) and diffuse (Kd) lighting from above
I didn’t especially enjoy having to get down and dirty with Direct3D to write this app – I’d much rather make use of a higher-level framework, where one’s available – but I’m reasonably happy with the results. In the next post, we’ll add some touch-based UI enhancements, although I suspect that given the low-level nature of the current implementation I probably won’t end up with the same level of touch UI as was able easy to write for Android or even iOS. We’ll see,
To finish off our look at developing an HTML5-based 3D viewer for our Apollonian web-service, today’s post integrates the trackball capability of the Three.js library. Many thanks to Jeff Geer for once again pointing me in the right direction on this. :-)
The trackball capability allows you not to worry about manual implementation of 3D navigation inside your viewer: you simply set up some basic parameters to indicate the size of your model and the speed with which you want navigation to occur – as well as the keys for rotate, zoom and pan – and then you just let it fly.
Here’s the updated file for you to try in either Chrome or Firefox (or elsewhere with WebGL enabled), with the A, S and D keys mapped to rotate, zoom and pan, respectively.
Here’s a video of the trackball capability in action:
// On Firefox we need event.which rather than keyCode
var code = event.keyCode ? event.keyCode : event.which;
switch (String.fromCharCode(code))
{
case'0':
case'1':
case'2':
case'3':
case'4':
case'5':
case'6':
case'7':
case'8':
case'9':
var level = code - '0'.charCodeAt();
populateWithLevel(level == 0 ? 10 : level);
break;
}
}
);
populateWithLevel(10);
}
function populateWithLevel(level)
{
// Make sure we're not already changing levels
if (changingLevel)
return;
changingLevel = true;
// If we already have a level loaded, remove the
// root from the scene and delete it
if (root != null)
{
scene.remove(root);
delete root;
}
// Make sure CORS is enabled
jQuery.support.cors = true;
// Call our web-service with the appropriate level
$.ajax(
{
url:
'http://apollonian.cloudapp.net/api/spheres/1/' +
level,
crossDomain: true,
data: {},
dataType: "json",
error: function(err)
{
alert(err.statusText);
},
success: function(data)
{
// Create the spheres' materials
var materials =
[
new THREE.MeshLambertMaterial({ color: 0x000000 }),
new THREE.MeshLambertMaterial({ color: 0xFF0000 }),
new THREE.MeshLambertMaterial({ color: 0xFFFF00 }),
new THREE.MeshLambertMaterial({ color: 0x00FF00 }),
new THREE.MeshLambertMaterial({ color: 0x00FFFF }),
new THREE.MeshLambertMaterial({ color: 0x0000FF }),
new THREE.MeshLambertMaterial({ color: 0xFF00FF }),
new THREE.MeshLambertMaterial({ color: 0xA9A9A9 }),
new THREE.MeshLambertMaterial({ color: 0x808080 }),
new THREE.MeshLambertMaterial({ color: 0xD3D3D3 }),
new THREE.MeshLambertMaterial({ color: 0xFFFFFF }),
new THREE.MeshLambertMaterial({ color: 0xFFFFFF })
];
// Set up the sphere vars
var rootRad = 0.01, segs = 9, rings = 9;
// Create our root object
var sphereGeom =
new THREE.SphereGeometry(rootRad, segs, rings);
// Create the mesh from the geometry
root =
new THREE.Mesh(sphereGeom, materials[0]);
scene.add(root);
// Process each sphere, adding it to the scene
$.each(
data,
function (i, item)
{
// Get shortcuts to our JSON data
var x = item.X, y = item.Y, z = item.Z,
rad = item.R, level = item.L;
var length = Math.sqrt(x * x + y * y + z * z);
// Only add spheres near the edge of the outer one
// (and only the front half if not animating)
if (
length + rad > 0.99 &&
(animateWithWebGL || z > 0)
)
{
// Create the mesh from the geometry
var sphere =
new THREE.Mesh(sphereGeom, materials[level]);
sphere.position.x = x;
sphere.position.y = y;
sphere.position.z = z;
var scaledRad = rad / rootRad;
sphere.scale.x = scaledRad;
sphere.scale.y = scaledRad;
sphere.scale.z = scaledRad;
root.add(sphere);
}
}
);
// Draw!
renderer.render(scene, camera);
changingLevel = false;
}
}
);
}
function animate()
{
requestAnimationFrame(animate);
trackball.update();
}
function render()
{
renderer.render(scene, camera);
}
</script>
</body>
</html>
I was able to strip out all the code enabling zoom/spin, although I left the piece in that lets you change levels with the number keys. Once again Three.js and WebGL have really impressed me: this is very interesting technology, and really feels comparable with a native, GPU-enabled 3D graphics experience.
Over the next few posts, we’re going to wrap up this series by implementing a Metro-style 3D viewer using WinRT and DirectX. Fun, fun, fun! :-)
Brian has mentioned it, over on his blog, too. Those of you working with Inventor, in particular, should be sure to bookmark Brian & Wayne’s Mod the Machine blog as well as the Manufacturing DevBlog (after a period of relatively low activity, things have definitely picked up over at MtM).
It’s great to see these blogs launched, and I know they’ll be a great resource for developers. I’m already finding that the availability of AutoCAD-related developer content on another Autodesk blog is freeing me up to tackle quite different topics (which are more aligned with my new role but also a little more forward-looking).
If you do have topics related to our products that you’d like to see addressed, be sure to keep letting me know. I can very easily pass them on to the ADN team, should it make more sense for someone in that team to tackle them.
In the last post, we created another, basic 3D viewer for the data from our Apollonian web-service – this time using HTML5 via Three.js. In this post, we’ll extend the code to listen for keyboard events and manipulate the model according to user-input, as well as enabling feature detection of WebGL (allowing the same implementation to call into the canvas rendering code when WebGL isn’t present).
We’re introducing keyboard-based commands to enable zoom, rotate (meaning spin, although not continuously) and the change of levels:
+
Zoom in
-
Zoom out
, or %
Rotate left
. or /
Rotate right
& or a
Rotate up
( or z
Rotate down
0 – 9
Change level (0 == 10)
You’ll see some strange choices for some of the rotate keys, above: on browsers other than Chrome, the keypress event gets called with symbols representing the arrow keys (left: ‘%’ right: ‘/’ up: ‘&’ down: ‘(‘). But because of an issue with Chrome that means its doesn’t receive the keypress event for arrow keys, we’re also checking for other characters, too (left: ‘,’ right ‘.’ up: ‘a’ down: ‘z”).
For changing levels: we might also have confirmed the change of level with the user via a message-box, but in the end I felt it wasn’t too disruptive to just do it.
Before we see the code, here’s a quick demo video and a link to the page:
[Errata: I mistakenly said “level 5” in the below video instead of “level 10”, and I also should have reinforced that the view was really using WebGL rather than basic HTML5 Canvas.]
// On Firefox we need event.which rather than keyCode
var code = event.keyCode ? event.keyCode : event.which;
switch (String.fromCharCode(code))
{
case'-': // Zoom out
zoomScale -= 0.1;
break;
case'+': // Zoom in
zoomScale += 0.1;
break;
case'=': // Reset view
zoomScale = 1;
root.rotation.x = 0;
root.rotation.y = 0;
break;
case'\'': // Rotate right
case'.':
yRotation = rotInc;
break;
case'%': // Rotate left
case',':
yRotation = -rotInc;
break;
case'&': // Rotate up
case'a':
xRotation = -rotInc;
break;
case'(': // Rotate down
case'z':
xRotation = rotInc;
break;
case'0':
case'1':
case'2':
case'3':
case'4':
case'5':
case'6':
case'7':
case'8':
case'9':
var level = code - '0'.charCodeAt();
populateWithLevel(level == 0 ? 10 : level);
break;
}
}
);
populateWithLevel(10);
}
function populateWithLevel(level)
{
// Make sure we're not already changing levels
if (changingLevel)
return;
changingLevel = true;
// If we already have a level loaded, remove the
// root from the scene and delete it
if (root != null)
{
scene.remove(root);
delete root;
}
// Make sure CORS is enabled
jQuery.support.cors = true;
// Call our web-service with the appropriate level
$.ajax(
{
url:
'http://apollonian.cloudapp.net/api/spheres/1/' +
level,
crossDomain: true,
data: {},
dataType: "json",
error: function(err)
{
alert(err.statusText);
},
success: function(data)
{
// Create the spheres' materials
var materials =
[
new THREE.MeshLambertMaterial({ color: 0x000000 }),
new THREE.MeshLambertMaterial({ color: 0xFF0000 }),
new THREE.MeshLambertMaterial({ color: 0xFFFF00 }),
new THREE.MeshLambertMaterial({ color: 0x00FF00 }),
new THREE.MeshLambertMaterial({ color: 0x00FFFF }),
new THREE.MeshLambertMaterial({ color: 0x0000FF }),
new THREE.MeshLambertMaterial({ color: 0xFF00FF }),
new THREE.MeshLambertMaterial({ color: 0xA9A9A9 }),
new THREE.MeshLambertMaterial({ color: 0x808080 }),
new THREE.MeshLambertMaterial({ color: 0xD3D3D3 }),
new THREE.MeshLambertMaterial({ color: 0xFFFFFF }),
new THREE.MeshLambertMaterial({ color: 0xFFFFFF })
];
// Set up the sphere vars
var rootRad = 0.01, segs = 9, rings = 9;
// Create our root object
var sphereGeom =
new THREE.SphereGeometry(rootRad, segs, rings);
// Create the mesh from the geometry
root =
new THREE.Mesh(sphereGeom, materials[0]);
scene.add(root);
// Process each sphere, adding it to the scene
$.each(
data,
function (i, item)
{
// Get shortcuts to our JSON data
var x = item.X, y = item.Y, z = item.Z,
rad = item.R, level = item.L;
var length = Math.sqrt(x * x + y * y + z * z);
// Only add spheres near the edge of the outer one
// (and only the front half if not animating)
if (
length + rad > 0.99 &&
(animateWithWebGL || z > 0)
)
{
// Create the mesh from the geometry
var sphere =
new THREE.Mesh(sphereGeom, materials[level]);
sphere.position.x = x;
sphere.position.y = y;
sphere.position.z = z;
var scaledRad = rad / rootRad;
sphere.scale.x = scaledRad;
sphere.scale.y = scaledRad;
sphere.scale.z = scaledRad;
root.add(sphere);
}
}
);
// Draw!
renderer.render(scene, camera);
changingLevel = false;
}
}
);
}
function animate()
{
requestAnimationFrame(animate);
render();
}
function render()
{
if (root != null)
{
// Apply rotations around X and/or Y
if (xRotation != 0)
{
root.rotation.x += xRotation;
xRotation = 0;
}
if (yRotation != 0)
{
root.rotation.y += yRotation;
yRotation = 0;
}
}
// Apply the zoom
camera.position.z = 4 / zoomScale;
camera.lookAt(scene.position);
renderer.render(scene, camera);
}
</script>
</body>
</html>
A few things to note:
The hasWebGL() function returns true if the browser supports the use of WebGL. We use the result of this function to determine whether to create and use a WebGL or a Canvas renderer.
We use jQuery to assign a handler to the document’s keypress() event.
We need to check event.keyCode in most browsers but event.which on Firefox.
When changing levels we set a flag: if we kick off multiple level changes – very easy to do with the longer-loading levels – then we run the risk of stomping on our geometry list… safest to protect it with a rudimentary critical section.
That’s about all there is to it. The application itself works surprisingly well on WebGL-enabled browsers. If I use it for long enough, I can usually crash the page in Chrome (as you’ll see from the demo video), but in Firefox the page is rock solid (which leads me to be believe it’s a browser issue rather than a problem with the code).
Since publishing the last post (and this one was already pretty well baked, too), Jeff Geer has pointed me at the trackball capability in Three.js, which allows you to more simply create a user-interface to control 3D orientation of your model. In the next post we’ll take a look at integrating that – probably alongside some of the existing keypress-enabled level changing – to see what that provides.
After looking at how to bring data from our Apollonian web-service into Unity3D, Android and iOS over the last few weeks, it seemed natural to extend this series to cover HTML. A big thanks to Jeff Geer for suggesting Three.js, which is the HTML5-based framework I ended up adopting for this project.
I like Three.js for a number of reasons: just as jQuery (another library I’ve used in this project, to good effect) attempts to abstract away the messiness inherent in supporting multiple browsers, Three.js does the same for the world of browser-resident 3D.
The other main reason I like Three.js is that it’s open source. I know, I know – I work for a proprietary software vendor… :-) But unless there’s a trusted, proprietary vendor providing platform technology, I really do prefer to have the peace of mind of having access to the framework’s source. And for my adjacent, experimental projects, I tend not to want to spend money on fully-fledged toolkits/frameworks, either, which means I gravitate to open source tools, or at least those that have versions available for free.
In terms of getting started with Three.js, the online tutorial deals with creating a simple scene with a sphere in it, so that was just perfect. :-)
The coding was reasonably straightforward: the main issue – on all fronts – was to enable cross-domain scripting of our web-service. On the server side, we had done so using a crossdomain.xml file when we first needed to call it from Unity, but we needed to go one step further, this time: we had to modify the Web.config file of our ASP.NET project and redeploy the project to Windows Azure. Not a big deal, but it needed doing.
The only difference between the two HTML files is the value of the animateWithWebGL flag, which obviously causes a slightly different code-path to be followed in a few places.
To see them both in action on my system, here’s a quick video:
To compare how the canvas-based viewer looks on different OS/browser combinations, here are those I’ve tested. The results are mostly pretty decent, although Safari on OS X had something very strange going on (whereas it worked fine in the iOS emulator on the same system).
In the next post, we’re going to make a few enhancements to this HTML5-based viewer: we’ll add support for keyboard events – to enable zoom, rotate and changing levels, in particular – as well as implementing feature detection to use WebGL automatically when available or to default to the use of the canvas renderer, otherwise.
As announced previously on this blog, the Autodesk Developer Network (ADN) team is in the process of launching a series of DevBlogs, publishing material publicly that would previously only have been made available to ADN members. I see some fantastic material being posted there: more than I can keep up with, personally, but I expect that will settle down once the team has created posts from the bulk of the DevNotes published on the ADN web-site.
If you’re [considering] developing for Revit, Navisworks or our other AEC and BIM technologies, be sure to bookmark this page and visit often.
On a somewhat related note, I also wanted to remind you that there’s now only a month to go until this year’s AEC and Manufacturing DevCamps. If you’ve held off from registering, be sure to do so soon: time is running out!
It’s been a while since I’ve talked about our “reality capture” (or 3D reconstruction) technologies, but I still like to play around with them, when I get the chance. My previous projects have all been with the PC version of 123D Catch (previously known as the Photo Scene Editor for Project Photofly), but I’ve just spent a few hours messing around with the new web and iPad versions of this tool (Shaan has already spent some time talking about these new versions over on his blog, in case).
Thinking about it, these two new versions have been made possible by exactly the principle we’ve been looking at in the posts over the last few weeks: the technology that currently powers Photofly was originally desktop-based, but was migrated to work in the cloud. The Photo Scene Editor was the first client to make use of this service, before it was even properly exposed via a RESTful web-service API. These new clients – one browser-based, one for camera-enabled iPad devices – were made possible by the exposure and formalisation of this web-service (something that I’ll talk more about in future posts, no doubt).
But anyway – that’s hopefully a helpful example of how the cloud can help scale and broaden the use of technology, but is a little besides the point of this post. This post is really just about how cool this technology is. :-)
To step through the differences between the versions of 123D Catch, I decided to capture a scene that I’ve been meaning to, for some time:
The male members of my immediate family (i.e. my sons and I – and probably only because my daughter is still a little young) are huge LEGO fans. The above creation – with its associated, epic storyline – was developed over a number of weeks (maybe even months) by my 7-year-old. Rather than let it get recycled (the good and bad thing about LEGO), I decided to preserve it digitally for posterity (and eventual re-creation in case of accident, like that would ever happen ;-).
On the subject of LEGO – although this is more related to 3D printing than it is to 3D capture – I just found out about the ingenious Free Universal Construction Kit, which allows you to 3D print connecting pieces to combine LEGO with other building block systems – several of which I’ve never heard of: Duplo, Fischertechnik, Gears! Gears! Gears!, K’Nex, Krinkles (Bristle Blocks), Lincoln Logs, Tinkertoys, Zome, and Zoob. I love the idea, although a) apparently today’s domestic 3D printing technology – at a precision of around 100 microns – may not be enough for some block systems (LEGO is manufactured at a precision of 10 microns, apparently) and b) we really don’t have any sets of the systems the kit allows you to connect with. Which clearly defeats the object for my household, at least.
OK, back to today’s topic…
I started with the iPad version of 123D Catch – which it seems is currently only available in North America (one of my iTunes accounts is US-based, which means I could download and try the app). As you might expect, the iPad version provides a very streamlined experience: it allows you to take photos directly using the iPad’s camera (you’ll need at least an iPad 2, of course, but “the new iPad” will apparently give better results), and after a little bit of meta-data entry (name, category, tags, description) it goes ahead and uploads the scene for processing. Once the scene has been created, you can view in directly on the iPad – and it has a cute “gyro” capability to automatically adjust the view as you move/tilt the device – or you can email the scene to open it up in the desktop version (which is particularly helpful if you want to edit the scene, stitching in images, etc.).
I found that the quality of the camera on my iPad 2 wasn’t really good enough to capture models adequately, especially in low-light conditions. And as this version has a limit of 40 images, you’re less likely to capture the level of detail that you might want from a complex model.
Here’s an image captured from the iPad 2, to give you an idea of the quality:
I ended up switching across to the web version (which also has some pretty cool, browser-based visualization that must use something like WebGL – something we’ll take a look at next week, incidentally) to create a scene based on images I took with a more capable digital camera. Which was all well and good, until I realised that I’d quickly taken 256 MB (purely a coincidence – my inner geek wasn’t especially aiming for that number :-) of photos, while the web version limits uploads to 200 MB.
Rather than resize the images (which I vaguely remember might affect the EXIF data that helps Photofly reconstruct the 3D scene – although I’m probably confusing that with rotation or cropping) or reduce the set of images selectively, I went ahead and uploaded what I could:
Something to note: I created a few different photo scenes with different subsets of the images, to see what happened. While the desktop version can tell which images have already been uploaded to the web-service, it seems the web version does not (which means more time is spent for repeated attempts, and may well be part of the reason for the 200 MB upload limit).
Once the upload was completed, I received a message stating the scene was being processed:
Once the process has completed, you’ll see the status in the capture’s description update to state “the capture is ready”:
For some reason – while the capture has worked for me with other sets of images – these particular images (or the quantity of them, perhaps) have not resulted in a working set (and that’s even true for the set that is flagged as ready). Given the lateness of the hour, I’m going to have to leave it there, at this stage, and take another look at some point in the future.
So with that, on to my results with the desktop version…
I uploaded the full set of photos, and waited for the processing to complete:
Once completed, I had my scene, which was pretty close to what I was looking for:
I suspect that with images taken with better lighting – and some editing of the scene, to clean things up – the results would be even better. And that may also be true of the web-based process. But this will already serve as a pretty good reminder of a child’s creativity, which is ultimately what I was looking for.
Well, that’s it for my whirlwind look at the three available versions of 123D Catch. If you’re interested, go ahead and check out the version that suits you best – and be sure to let us know where you’d like us to go with this technology.
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:
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.
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.
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
// 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.