multitouch

Fun with Unity19 May

Astute readers will already know that I am using Unity3d to build a new iPhone game: Snowferno. You might also already know that I do lots of work with multi-touch stuff, and recently have begun to marry the two technologies and use Unity to build multi-touch apps.

This is actually pretty fantastic as it allows you to build MT apps very quickly and they still look cool, and mostly the bugs and stability are handled by someone else (ie the unity guys) so it streamlines the process quite a bit.

Anyway, the reason for this particular post was just to put up some fun stuff I have been doing with Unity this past weekend. Sandor, the man behind all the good ideas, wanted to do some cool stuff with MT and needed some groovy ripples on water effect for a project he is working on. SO I went out to try and find a nice simple solution to do some very basic water simulation.

After finding lots of super duper complicated ways to do realistic fluid simulations that were doing my head in, or lots of crazy custom shaders to do ripple effects (also doing my head in) I found this guys site, which details an old-school method to simulate water ripples.

Old school is more my speed, and this algorithm basically creates a height map that behaves kinda sorta like water might behave. (which is really close enough for me).

So, first up i do what I always do, which is build a test app in the format which I am most accustomed at the moment, which, this week is as a cocoa app. Here are a few screenshots:

picture-23picture-22

Basically it is just the algorithm above applied to a black and white bitmap. pretty standard stuff. If you want, here is the xcode project:

BBRippleTest.zip

Anyhow, once I worked out all the kinks in cocoa, I moved to the slightly less familiar world of unity scripting. I started with javascript, but the various variable casting that needed to go on (casting floats to ints and back again) were not working the way that I thought they should (I really dont know what javascript is doing) and it was pissing me off, so I switched to using C# to script the object in Unity. Having never used C# before a few of the syntax bits were a bit foreign to me, but at it's heart it is really just C, so that was much nicer in terms of casting variable types and other esoteric stuff like that.

Still shots of the actual effect dont look like anything because you need to motion of the ripples to be able to see it, but you can see the mesh deformation here in unity:
picture-27

picture-28

picture-29

In unity, what I did was build a large mesh (in the case of the pics it was 128 x 128 vertices. (well, technically it was 128x128 sections in cheetah3d, which ends up being 127x127 vertices and something like a kasquillion triangles) but the script works with any planar set of vertices as long as they are evenly spaced.

For anyone interested here is the unity project:

UnityRippleTest

I removed some of the auto-generated stuff that Unity adds into the projects to keep the file size down a bit. The downside to this pruning is that if you want to see it in action, you need to reattach the script and the texture to the mesh.
picture-30

Then you can adjust the various settings:
picture-31

The rows and cols need to match the number of sections the mesh has, (it will actually be one less than the total number of vertices across the plane. It seems a bit odd to do it this way, but that made making the meshes in cheetah simpler (ie mesh in cheetah has X x Y sections, script has X x Y rows)) if the cols dont match it will error out.

The splash force can be anything from 10 - 65000 ish.. it is effectively how hard you hit the mesh when you want to make a splash. ultimately if you have the wave height capped, it will boil down to how long you want the waves to propigate for. I find values in the 1000 - 10000 range seem to make pleasing results.

the max wave height is a cap on the wave height. Again, between 1 and 2 seem to make nice waves, anything over 4 or 5 gets a bit wacky.

The dampner is how quickly the waves get killed. numbers close to 1 make the waves propagate for longer. if you set it below 0.9 the waves barely go anywhere. there is actually a pretty big difference to be found in the very upper limits of the dampner. For instance, set the waves height to 2, the force to 10000 and the dampner to 0.99. Do a splash.. this will keep the mesh moving for about 3 - 4 seconds before it is still again. Now change the dampner to 0.9999 and it will go for about 4 times as long.

Anyhow, here is a short video of the whole process, which mostly survived the youtube compression: (it looks slightly better in high quality, but not much... I could have done a better job of making a high def video, but: too lazy)

Oh, and the tunes in that video are from the Snowferno soundtrack. Brent at Fatlab Music wrote that one, and he gave me special permission to use it for my silly little demo video here, so thanks for that Brent!!

Cheers!
-B

Oh and BTW, there is absolutely no implied warranty or whatever for any of the code above, so if you load it up and your computer gives your cat the pig flu because of it, I cannot be held responsible.

code, iPhone, multitouch, openAL, openSoundControl

WWDC 200918 May

I will be there.

If you are also going to be there, and you want to meet up and get a beer, or talk about anything on my blog or anything else really, then drop me a line at support@benbritten.com, or follow my rarely updated twitter: http://twitter.com/benbritten. I plan to use it during WWDC to help people find me and to help me find people. Otherwise I rarely use it, so dont go looking for profound wisdom from my twitter feed.

code, iPhone

Texture Atlases and Core Animation11 May

I have been trying to write a blog post about texture atlases forever now, but I keep getting distracted and then I forget I have a draft up here waiting for me.

SO! I am going to be doing a series of posts about some basic openGL|ES stuff on the iPhone and how to make your cocoa-based core animation application (or game) perform better. Ultimately I will tell you that you should switch to openGL, but for now I am going to keep it simple and talk about core animation layers and texture atlasing. (and for now, we will only be talking about 2d stuff at 1:1 rez, so no mip mapping of the atlases for those of you who are reading ahead, as that gets more complicated than we need right now)

Why do I want to share this information with you? Well, the first game I did on the iPhone: Snowdude was a big ole 2D sprit animation game. I originally did the entire thing in Core Animation and it got about 4 fps, which was not enough :-) So I switched to openGL and could easily get 30 or 40 (though I only needed to do 12 because that was what all the sprite animations were built at), so this is the story of that transition.

The first hurdle we will cross will not require me to get into openGL just yet, but talk about how to optimize your sprite animations, even within CA, so if you only need a teensy bit of performance, you might be able to just get away with this and not actually make a full leap into openGL.

So, lets say I have a sweet animation of a dude face planting that looks like so: (Oh and BTW big mad props to the Lycette Bros makers of the aforementioned Snowdude for letting me use their raw graphics for examples)

faceplant

10 frames, so 10 images.

if I want to turn this into an animation in the easiest, cocoa-tacular fashion I would do something like so:

 
	UIImageView * spritely = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"frame01.png"]];
	spritely.animationImages =  [NSArray arrayWithObjects:[UIImage imageNamed:@"frame01.png"],
[UIImage imageNamed:@"frame02.png"],
[UIImage imageNamed:@"frame03.png"],
[UIImage imageNamed:@"frame04.png"],
[UIImage imageNamed:@"frame05.png"],
[UIImage imageNamed:@"frame06.png"],
[UIImage imageNamed:@"frame07.png"],
[UIImage imageNamed:@"frame08.png"],
[UIImage imageNamed:@"frame09.png"],
[UIImage imageNamed:@"frame10.png"],
nil];
 
	spritely.animationDuration = 10.0/fps;
	[myBigView addSubview:spritely];
	[spritely startAnimating];
 

this is super duper easy. But it is slow. (well, it isnt particularly slow for a few animations, but get a couple dozen of any decent size and your performance will kinda suck)

Well, I dont know exactly what apple is doing on the backend but very likely the CA stuff is built on top of the openGL stuff. So basically what happens every time the frame needs to be redrawn is that you have to bind a new texture then render that texture out to the screen buffer. Binding textures can be slow unless they are already resident in vram, but even then if you can skip binding a texture and just do a draw call every frame then you will have significant gains) (technical note: the CA stuff is actually kinda half built on openGL and half raw core graphics (and half pixie dust) basically the way the tech dudes explained it is that CA goes out to whichever subsystem is best for the task at hand. That said; no matter what the backend is doing, the texture still needs to be bound to the screenbuffer somehow, and that takes time)

So, how do we skip that texture binding step? Well, we have to abandon the UIImage view. (well, you could subclass it and do all this in a uiimage sublclass, but for our purposes we are going to keep it nice and simple and just make a generic uiview subclass)

But first! we need to make an Atlas!

OK, what is a texture atlas? (and why do we care?)

If you want the very detailed explanation then go read the atlasing whitepaper by NVidia. (which is far more detail than I am going to go into)

A simple explanation of an atlas is just this: a big image with lots of little images inside it. Like so:
faceplant

hey! that looks an awful lot like the stuff above! and it is in fact, the same thing, but now I am calling it an atlas instead of an example of 10 images. So that is it, a texture atlas is just a bunch of images in a bigger image. How does this help us? Well it means that the larger image only needs to be loaded as a texture once and then you just draw out different bits of it to the video buffer.

How do we use this new fangled atlas thinger?

First, we need to make a new UIView subclass and add a nice CALayer that we can use to store our images:

 
// our standard frame init method
- (id)initWithFrame:(CGRect)frame
{
	if (self = [super initWithFrame:frame]) {
		// our main layer, holds the main image
		imageLayer = [[CALayer layer] retain];
		imageLayer.frame = self.bounds;
		imageLayer.opaque = YES; // this speeds stuff up if you don't need transparency
		[self.layer addSublayer:imageLayer];
	}
	return self;
}
 
// set our big atlas image
-(void)setAtlasImage:(UIImage*)myAtlasImage
{
        // this is basically binding the big image to our layer
        imageLayer.contents = (id)myAtlasImage.CGImage;
	imageLayer.masksToBounds = YES;
       // this is the magic code here, this tells the layer to only display a
       // subset of the larger image.
	imageLayer.contentsRect = [self rectForFrameIndex:0];
}
 

OK, so we build a simple view with a single sublayer. When we set the atlas image for our new animated view, we set the contents to the entire image.

Now if we just did that, you would see the entire atlas (smashed into whatever frame you had defined when you alloced the view). This is not what you want, so we add the contentsRect definition, which allows us to tell the layer to just display one sub rectangle of the entire contents.

The contentsRect is the basis of the whole idea of the texture atlas. So how do we figure that out?

The contentsRect uses normalized coordinates. If you havent done much with contentsRect or the few other CA properties that inexplicable require normalized coords, then it can be kinda confusing at first, but really it is very easy (and a nice way to handle things).

So, lets have a look at our image and see how to generate some normalized rectangle coordinates for our frames:

The sample image above is 480 x 236 pixels (which is a strange size, i know, more on that later, but the upshot is that this is a much smaller version of the raw graphics (to discourage people from stealing them :-) so the example frame sizes are a bit odd... you will get over it I am sure)

A single frame is 121 x 80 (ish) pixels.

OK, quick tangent: this image is a smaller version of the images that the animator built for snow dude. Keen readers will notice some nice hot pink crop marks, those are not part of the animation, but are in there so that I can feed that image into an atlas generator program that I built and it strips out the proper frames for each animation based on the crop marks. For today, we are going to pretend those are part of the animations, and I will talk about optimizing your atlases in another post)

OK, where were we? Oh yes, contentsRect. So if frame 1 is in the upper left, then it is bounded by:

 
// remember that CALayers have 0,0 at the lower left
CGRect frame1 = CGRectMake( 0.0, 156.0, 121.0, 80.0);
 

Great! almost there, now we just need to normalize that rectangle. That means that all the values need to be between 0 and 1. so we divide all the widths by 480 (the total width of the image) and all the heights by 236 (to total height of the image). So something like this:

 
// remember that CALayers have 0,0 at the lower left
-(CGrect)rectForFrameIndex:(NSInteger)frameIndex
{
	NSInteger column = frameIndex % 4;
	NSInteger row = frameIndex / 4;
	row += 1;
 
	CGFloat x = frameIndex * column;
	CGFloat y = atlasHeight - (row * frameHeight);
	CGFloat normalizedX = x/atlasWidth;
	CGFloat normalizedY = y/atlasHeight;
	CGFloat normalizedWidth = frameWidth/atlasWidth;
	CGFloat normalizedHeight = frameHeight/atlasHeight;
	CGRect frameRect = CGRectMake( normalizedX, normalizedY, normalizedWidth, normalizedHeight);
}
 

so, our frame 1 will look something like this rect:

 
// remember that CALayers have 0,0 at the lower left
CGRect frame1 = CGRectMake( 0.0, 0.6610, 0.2520, 0.3389);
 

OK, that was pretty easy, but why do we need normalized coordinates? Well, the short answer is: you need them cuz the methods require them. The long(er) answer is that if you start to use openGL or even doing mipmapping then they become super handy. (in otherwords that normalized frame would work whether i loaded the example atlas that is 480x236 or the original atlas which is 3 times bigger, and you wouldnt have to recalculate the contentsRect. and later, when we start talking about texture binding and UV coordinate space, it will all make so much more sense)

OK! Now all we need is a timer that switches frames for us and we are all set!

Something like this will do the trick:

 
-(void)timerFire:(id)sender
{
	// we want this to be immediate
	[CATransaction begin];
	[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
 
	imageLayer.contentsRect = thisFrame;
 
	[CATransaction commit];
	frameIndex ++;
}
 

What is all the CATransaction stuff? well, for better or worse, whenever you do something in core animation that CAN be animated it will be, unless you tell it not to. since we want the next frame to show up immediately, we do not want to animate the contentsRect transition (otherwise your new frame will slide in and the old one will slide out). So we use the CATransaction system to shut off the animations.

AndFinally something to call this animation timer routine:

 
[NSTimer scheduledTimerWithTimeInterval:1.0/fps target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
 

And that is it. Super basic texture atlasing.
The pros of this approach: better than loading individual images into your CALayer and animating them that way (or via a UIImageView)
This also makes memory management a bit simple (since you only have to worry about a single image instead of 10)
And the best thing is, this way is halfway to openGL, which is where we want to be.

This particular implementation has lots of drawbacks, like all your frames need to be the same size, and they need to be packed into the atlas in the right way and the right order. Also we are calculating the frame rect over and over again for each frame (this is very quick, but if you have lots of sprites then it seems like a waste when you could just cache them) And because each frame has to be the same size, then we are actually wasting quite a bit of memory with all that whitespace. (turns out transparent pixels take up just as much memory as colored pixels! who knew?!?)

That is OK, we will talk about all this in the next post when i get into openGL.

Cheers!
-B

multitouch

Snowferno website is live!11 May

Today the Snowferno website went live! The game is nearing beta status, and it is looking to be very fun!

Anyhow, checkout the snowferno website: www.snowferno.com!

Also, we are going to be doing a development blog to talk about how we got to where we are as well as what we are doing as we finish up the game. The first post (by me :-) is here. I will probably cross-post blurbs from the devlopment blog here just to get maximum coverage (in other words: please buy our game once it is out!! :-)

Cheers!
-B

iPhone

Snowferno! Some early details!06 May

Hey All,

I have been extra busy lately, and finally we have some stuff to put up about our upcoming iPhone game: Snowferno!

(by 'us' I mean myself and the very talented Mike and Brent from Fatlab Music)
snowfernotitle

It is an accelerometer controlled 3d puzzle game. And it is all about a snowball that has to travel through the inferno in order to survive the summer :-)

Here is an early teaser trailer:


edit: updated to a slightly newer version :-)

I will be posting more details as we finish things up. We are going to be doing a development blog over at the game's site: Snowferno.com once that is ready to go (should be any day now :-)

It is a fun game, and the music is incredible, so look for it when it comes out!

We hope to have it on the app store around july sometime. (depending on approval delays etc..)

Cheers!
-B

About

meMy full name is Ben Britten Smith.

I go by Ben Britten because Ben Smith is a bit too common and using my full name is a mouthful.

I live in Melbourne, Australia and service clients all over the globe.

Contact

Have some questions?

Feel free to contact me directly at support@benbritten.com with any questions you might have about any of the applications I support.

Thanks!

PHVsPjxsaT48c3Ryb25nPndvb19hYm91dDwvc3Ryb25nPiAtIGFib3V0LXdpZGdldDwvbGk+PGxpPjxzdHJvbmc+d29vX2FkX2JlbG93X2ltYWdlPC9zdHJvbmc+IC0gaHR0cDovL2JlbmJyaXR0ZW4uY29tL3dwLWNvbnRlbnQvdGhlbWVzL3ZpYnJhbnRjbXMvaW1hZ2VzL2FkNDY4LmpwZzwvbGk+PGxpPjxzdHJvbmc+d29vX2FkX2JlbG93X3VybDwvc3Ryb25nPiAtIGh0dHA6Ly93d3cud29vdGhlbWVzLmNvbTwvbGk+PGxpPjxzdHJvbmc+d29vX2FsdF9zdHlsZXNoZWV0PC9zdHJvbmc+IC0gYmVuYnJpdHRlbi5jc3M8L2xpPjxsaT48c3Ryb25nPndvb19ibG9ja19pbWFnZTwvc3Ryb25nPiAtIGh0dHA6Ly9iZW5icml0dGVuLmNvbS93cC1jb250ZW50L3RoZW1lcy92aWJyYW50Y21zL2ltYWdlcy9hZDMzNi5qcGc8L2xpPjxsaT48c3Ryb25nPndvb19ibG9ja191cmw8L3N0cm9uZz4gLSBodHRwOi8vd3d3Lndvb3RoZW1lcy5jb208L2xpPjxsaT48c3Ryb25nPndvb19ibG9nPC9zdHJvbmc+IC0gdHJ1ZTwvbGk+PGxpPjxzdHJvbmc+d29vX2Jsb2djYXQ8L3N0cm9uZz4gLSAvY2F0ZWdvcnkvYmxvZy88L2xpPjxsaT48c3Ryb25nPndvb19jYXRfbWVudTwvc3Ryb25nPiAtIGZhbHNlPC9saT48bGk+PHN0cm9uZz53b29fY29udGFjdDwvc3Ryb25nPiAtIGNvbnRhY3Q8L2xpPjxsaT48c3Ryb25nPndvb19jdXN0b21fY3NzPC9zdHJvbmc+IC0gPC9saT48bGk+PHN0cm9uZz53b29fY3VzdG9tX2Zhdmljb248L3N0cm9uZz4gLSBodHRwOi8vYmVuYnJpdHRlbi5jb20vZmF2aWNvbi5pY288L2xpPjxsaT48c3Ryb25nPndvb19mZWF0cGFnZXM8L3N0cm9uZz4gLSA1NDk8L2xpPjxsaT48c3Ryb25nPndvb19mZWVkYnVybmVyX3VybDwvc3Ryb25nPiAtIDwvbGk+PGxpPjxzdHJvbmc+d29vX2dvb2dsZV9hbmFseXRpY3M8L3N0cm9uZz4gLSA8L2xpPjxsaT48c3Ryb25nPndvb19ncmF2YXRhcjwvc3Ryb25nPiAtIHRydWU8L2xpPjxsaT48c3Ryb25nPndvb19sYXlvdXQ8L3N0cm9uZz4gLSBkZWZhdWx0LnBocDwvbGk+PGxpPjxzdHJvbmc+d29vX2xvZ288L3N0cm9uZz4gLSA8L2xpPjxsaT48c3Ryb25nPndvb19tYW51YWw8L3N0cm9uZz4gLSBodHRwOi8vd3d3Lndvb3RoZW1lcy5jb20vc3VwcG9ydC90aGVtZS1kb2N1bWVudGF0aW9uL3ZpYnJhbnRjbXMvPC9saT48bGk+PHN0cm9uZz53b29fbmF2X2V4Y2x1ZGU8L3N0cm9uZz4gLSAyLDgyLDU0OSw1NTMsNTY3LDUzMiw1MzQsNTM3LDgzMjwvbGk+PGxpPjxzdHJvbmc+d29vX3Nob3J0bmFtZTwvc3Ryb25nPiAtIHdvbzwvbGk+PGxpPjxzdHJvbmc+d29vX3Nob3dfYWQ8L3N0cm9uZz4gLSBmYWxzZTwvbGk+PGxpPjxzdHJvbmc+d29vX3Nob3dfbXB1PC9zdHJvbmc+IC0gZmFsc2U8L2xpPjxsaT48c3Ryb25nPndvb19zdGVwczwvc3Ryb25nPiAtIDEuLCAyLiwgMy48L2xpPjxsaT48c3Ryb25nPndvb190YWJiZXI8L3N0cm9uZz4gLSBmYWxzZTwvbGk+PGxpPjxzdHJvbmc+d29vX3RoZW1lbmFtZTwvc3Ryb25nPiAtIFZpYnJhbnRDTVM8L2xpPjwvdWw+