Universal OpenGL Apps
November 28th, 2010
I recently took part in the 360iDev Game Jam remotely from home. The game jam is always a lot of fun, even if you can’t get yourself to the 360iDev conference itself (which you should totally do, by the way, if you can. It’s an amazing conference). I created a game in 8 hours which I’m calling “Dirty Diapers”, you can see the details of its creation on the Game Jam site. There’s a video of the game in action at the end of that page.
The game is about keeping a room full of babies happy. Over time, babies become unhappy because they either want to be fed, changed, or rocked. It’s your job to drag an action up from the bottom of the screen to make the baby happy again. As time goes on, they get more unhappy more frequently.
I think it turned out quite well, so I’ve decided to try to get it into the store at some point. Right now I’m extremely busy with some contract work, but I’m trying to find a few hours here and there to finish it up and submit it.
One of things I decided to try with this game was to build it as a universal app (that is, one app that runs on iPhone/iPod touch, and iPad, instead of separate apps for iPhone and iPad). The game is 100% OpenGL on the rendering side, so I needed to figure out how to set up my frame buffers for a universal app. I thought I’d go through that today. (Holy moly, a technical post instead of an indie lifestyle post! Hold on to your hats!)
Project Setup
The first thing you need to do for a universal app is set up your Xcode project so that it knows to build the app as universal. I went to Jeff LaMarche’s excellent blog and followed his instructions to do this part (and read the follow-up post for a few more details).
Once you’ve done that, you should have a project that can at least compile for iPad or iPhone. Good.
Frame Buffer Setup
Now that you’ve got your app building for multiple devices, you’re going to need to do some work to support multiple screen resolutions. As of this writing, there are three different resolutions to worry about:
- iPad: 1024 x 768
- iPhone/iPod touch (non-retina display): 320 x 480
- iPhone/iPod touch (retina display): 640 x 960
However, Apple does some clever stuff to help you with the retina displays. If you query the size of the screen:
CGRect screenFrame = [[UIScreen mainScreen] applicationFrame]; |
You’ll get 320 x 480 on both retina and non-retina displays. This is because they measure the screen in “points” instead of pixels now. On a retina display, there are 2 pixels for every point. The nice thing is that it lets you treat all iPhone/iPod touch screens the same way, with the same positioning code.
Ok, so we’ve got these different screen sizes, we need to tell OpenGL to create different sized frame buffers. Somewhere in your code you’ve probably got something like this, which creates a render buffer from an EAGLDrawable CAEAGLLayer:
glBindRenderbufferOES(GL_RENDERBUFFER_OES, buffer.m_colorBufferHandle); [oglContext renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:drawable]; glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_WIDTH_OES, &buffer.m_width); glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_HEIGHT_OES, &buffer.m_height); |
If you build and run, what you’ll find is that your render buffer will be created with the proper size for iPad and non-retina displays, but you’ll end up with a 320 x 480 render buffer for retina displays (which need 640 x 960), and your retina display game will look awful. This is because the CAEAGLLayer is telling OpenGL that it’s dimensions are 320 x 480 on the retina display.
In order to remedy this, we need to make use of two new UIView properties found in iOS 4.0 and up called: scale and contentScaleFactor.
scale
The new scale property will tell you the amount the point values of the UIView are being scaled. If you’re running on a non-retina display and query a view’s scale, it will be 1.0. If you’re running on an retina display, you’ll find that it’s 2.0. This gives you a way to test which kind of display we’re working with, and we’ll use this later.
contentScaleFactor
The contentScaleFactor is a different beast. This will always be 1.0. However, if you’re running OpenGL in a view on a retina display, you need to set this value to match the scale value of the view. This will tell the CAEAGLLayer to update its dimensions to match the actual pixel size of the screen, allowing you to create an appropriately sized render buffer.
Now, if you’re planning to support iOS versions prior to 4.0, these properties won’t be available, so you need to do runtime checks for them.
This is what I do inside my EAGLView’s -layoutSubviews method to set the contentScaleFactor on the view:
// Set the scale factor to be the same as the main screen if ([self respondsToSelector: NSSelectorFromString(@"contentScaleFactor")]) { [self setContentScaleFactor:[[UIScreen mainScreen] scale]]; } |
Now, when I tell the render buffer to create itself based on the size of the layer, it will create a 640 x 960 render buffer on a retina display, but a 320 x 480 buffer on a non-retina display. Hooray! But, we’re not quite there yet…
Viewport Setup
We’ve got our render buffer set up to create itself at the proper resolution now. It will match the screen dimension and contentScaleFactor of the EAGLView, which is what we want. Now we need to tell OpenGL to create an appropriately sized viewport. This code is for a 2D game, so adjust as necessary for a 3D game.
You’ll need to create a viewport that matches the frame buffer size for the given screen. Something like this:
glViewport(0, 0, buffer.m_width, buffer.m_height); |
For a 2D game, you then need to set up an orthogonal projection. There’s a good chance that you have a line in your code somewhere that looks like this:
glOrthof(0.0f, 480.f, 320.f, 0.0f, -1.0f, 1.0f); |
Clearly this isn’t going to work. Now, there are two options here for creating the transform:
1) Create the transform based on the UIScreen size
The code for this is simple:
CGRect appFrame = [[UIScreen mainScreen] applicationFrame]; glOrthof(0.0f, appFrame.size.width, appFrame.size.height, 0.0f, -1.0f, 1.0f); |
However, let’s look at what this does. On iPad, it creates a 1024 x 768 sized viewport. Great, that’s what we want. On a non-retina display, it creates a 320 x 480 viewport. Also good. But, on a retina display, it also creates a 320 x 480 display. Now, this is OK. The framebuffer is 640 x 960, so the pixels are there. So what does this mean?
The Good
You only need to write two code paths for positioning sprites on the screen: one for iPad, and one for iPhone, and let OpenGL handle matching the render buffer to the viewport appropriately for you. The only caveat here is that you’ll need to manually scale all your sprites’ sizes by 50%. If you do this, you’ll scale them down to the correct point size, and then OpenGL will render them at 100% resolution when it pushes the textures through the render pipeline.
Confused? Think about it this way: you’ve got a view port of 320 x 480 but a render buffer of 640 x 960. Take a sprite that’s 100 pixels x 100 pixels and render it as a 100 x 100 point sprite in the viewport. This is actually 200 x 200 pixels in the render buffer. So in order to render it as 100 x 100 pixels, you need to scale your vertices down by 50% to a 50 x 50 point sprite.
The Bad
You’ll need to write some code into your sprite system (and bitmap font system) to scale things to 50% if you’re on a retina display.
This is the approach I use. At app startup, I get the screen scaling factor, and then use it to scale all my sprites:
if ([[UIScreen mainScreen] respondsToSelector: NSSelectorFromString(@"scale")]) { ScreenScaleFactor = [[UIScreen mainScreen] scale]; InverseScaleFactor = 1.f / ScreenScaleFactor; } |
2) Create the transform based on the layer size
The alternative is to create a different viewport size based on the actual pixel size of the screen. You can do this by getting the size of your created render buffer. Something like this:
glOrthof(0.0f, frameBuffer.m_width, frameBuffer.m_height, 0.0f, -1.0f, 1.0f); |
This will give you a viewport that matches the screen size in pixels, not points.
The Good
The advantage here is that you don’t need to do any sprite scaling. You just render everything at 100%.
The Bad
You’ll need to have three different code paths for sprite positioning, based on the size of the screen, as you’ll now have three different sized viewports to worry about.
Art Work
You’ll probably want to have different sized artwork for non-retina displays than for iPad and retina displays. For Dirty Diapers I’ve got two game texture atlases: one for iPad and retina displays, and one for non-retina displays. The non-retina atlas is identical to the larger one, it’s just scaled down 50% in Photoshop. You could scale your graphics in OpenGL, but it will never look as good. If you’re like me, the imperfections will just drive you nuts.
So I have two texture atlases, but there are 3 possible screen sizes to worry about (see the next section). I do a check like this to determine which texture atlas to load:
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad || ScreenSizeConsts::ScreenScaleFactor > 1.01f) { // Load texture atlas for iPad/Retina display } else { // Load texture atlas for Original iPhone display } |
If you want to load 3 different textures (iPad, non-retina, retina), you can use the @2x.png file naming convention (depending on how you’re loading your textures) and just check for iPad or not, but now you’re dealing with 3 sets of textures. The choice is yours. If you’re loading textures manually and the @2x trick doesn’t work, you can split things out like this:
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { // Load texture atlas for iPad display } else if (ScreenSizeConsts::ScreenScaleFactor > 1.01f) { // Load texture atlas for Retina display } else { // Load texture atlas for Original iPhone display } |
Finally, Positioning
Now you’ve got your different artwork loading, you’ve got your render buffer set up, and your viewport set up. Now you just need to tell your objects to render in different positions, based on the screen. To do this, I set up a bunch of member variables for positioning code (for static items), and then on init, check the device and screen scale factors and adjust them accordingly. This is necessary for 2D art layout, but if you’re working with a viewport into a 3D world, you may not need to do nearly as much work here.
That’s it…
There you go. I hope you were able to follow along and I wasn’t too confusing. If you’ve got better ways of doing any of this, feel free to post suggestions in the comments.
Owen