The Corona Labs Blog
Posted on . Written by


Is this similar to Apple’s “@2x” solution?

With the launch of the iPhone 4, which features a double-resolution 640 x 960 “Retina Display”, Apple introduced a system for automatic high-res asset replacement. Given an image in the application bundle named (for example) foo.png, if a double-sized image named foo@2x.png is also present, the latter image will be loaded automatically whenever the former is called from Objective-C on the iPhone 4.

The important difference is that Apple’s system only handles the specific case where a screen is exactly twice the resolution of the original content size. In other words, the Apple “@2x” method is effectively a subset of Corona’s dynamic image resolution.

With Corona, you are free to adopt Apple’s naming scheme and 1x/2x scaling categories, but you can also use any other asset scaling factor (and file naming scheme) you like. This general solution allows you to also handle Android devices, the iPad — and future mobile screens we don’t know about yet.

A second advantage is that Corona does not require you to know the ultimate resolution of the target device — which may be unknown, especially for the growing range of current and future Android devices. Instead, you can define several resolution thresholds, and Corona will automatically use the best match when it runs on each device.

How does it work?

As noted above, dynamic image resolution works in conjunction with dynamic content scaling (documented in the “Configuring Projects” section of the documentation). For further reference, the dynamic image resolution docs arehere.

To use this feature, you basically need to do two things:

  • Use display.newImageRect() rather than display.newImage() when loading your images
  • Specify one or more scaling thresholds in your project’s config.lua file

The syntax is as follows:

display.newImageRect( [parentGroup,] filename [, baseDirectory] imageWidth, imageHeight )

  • imageWidth is the base image’s width in the base dimensions of the content.
  • imageHeight is the base image’s height in the base dimensions of the content.
  • parentGroup and baseDirectory are optional, and can be omitted; they behave in the same way as their counterparts in display.newImage().

An actual example might look like this:

[cc lang="lua"]myDynamicImage = display.newImageRect( “baseImage.png”, 100, 100 ) [/cc]

…and then your config.lua file might look like this:

[cc lang="lua"]
application =
{
content =
{
width = 320,
height = 480,
scale = “letterbox”,

imageSuffix =
{
["-x15"] = 1.5,
["-x2"] = 2,
},
},
}
[/cc]
Assuming your base image was 100 x 100 pixels, you would then provide the following three images:

baseImage.png (100 x 100 pixels)
baseImage-15.png (150 x 150 pixels)
baseImage-2.png (200 x 200 pixels)

Again, your naming scheme can be anything you like:

[cc lang="lua"]
application =
{
content =
{
width = 320,
height = 480,
scale = “letterbox”,

imageSuffix =
{
["_medium"] = 1.6,
["_large"] = 2,
["_xxl"] = 3,
},
},
}
[/cc]

The important thing is the numerical factors listed alongside each image suffix (1.6, 2 and 3 in the above example). These values determine the scaling thresholds at which Corona will automatically load the alternative images. Of course, it’s unlikely that you’ll need to use values above 2 on today’s mobile devices, but it’s a safe bet that screens will continue to increase in resolution.

The final thing to understand is that the alternative image files are always optional. Whenever you use display.newImageRect(), Corona will automatically drop back to loading the base image if no higher-resolution alternative files are available. Specifically, it will attempt to load the best choice, then fall back to the next best (if applicable), eventually ending up at the base image if all else fails. This means that the base image file must always be present, but the other resolutions need not be — you can decide on a case by case basis whether you want to provide alternatives.

(Why does Corona make you provide the height and width manually in display.newImageRect()? Because doing it automatically would require first loading the base image to find out its size, resulting in two image-loading operations rather than one. Since image loading is a very slow pipeline on mobile devices, it’s better to avoid the overhead by having you specify the known size and avoid the extra loading.)

That’s a lot of choices — what’s the “magic recipe”?

At this point, the simplest (and most common) config.lua recipe simply replicates Apple’s Retina Display solution:

[cc lang="lua"]
application =
{
content =
{
width = 320,
height = 480,
scale = “letterbox”,

imageSuffix =
{
["@2x"] = 2,
},
},
}
[/cc]

In this case, you would then provide two versions of each image:

baseImage.png
baseImage@2x.png

This will automatically swap in the double-resolution images on iPhone 4 and iPad, and use the base image on all other devices.

But wait! In the year since the iPad first appeared, there has been an explosion of 7″ Android tablets, including the Samsung Galaxy Tab, the Barnes and Noble Nook, and a flurry of newly-announced tablets at the recent Consumer Electronics Show.

Many of these devices feature a resolution of 600 x 1024. You’ll note that a width of 600 pixels is almost, but not quite, twice the classic iPhone width of 320 pixels. Therefore, if your content assumes a base size of 320 x 480 (which is still fairly common), the 2x scaling threshold won’t quite kick in on 7″ tablets, and you’ll end up displaying the lower-resolution assets. This seems suboptimal; you’ll more likely want to target high-res assets at all tablets, not just the iPad.

One solution, of course, is to implement three-tier resolution (1x, 1.5x and 2x assets) rather than merely two-tier. The “Hardware/DynamicImageResolution” sample mentioned above is an example of this technique.

But there is an easier way! Rather than creating three sets of images, you can maintain just two sets (normal and double resolution) and then force the higher-resolution images to show up on the 7″ tablets.

To do this, simply tweak the scaling threshold in the config.lua file shown above:

[cc lang="lua"]
application =
{
content =
{
width = 320,
height = 480,
scale = “letterbox”,

imageSuffix =
{
["@2x"] = 1.8,
},
},
}
[/cc]

This is probably the closest thing right now to a “magic recipe” for dynamic image resolution. With this setup, you can create the same set of single- and double-resolution images as in the previous example, while still having the better versions loading automatically on the slightly-less-than-double-size Android tablets.

(Since the base 320 x 480 stage size given above will be upscaled by at least a factor of 1.875 on a 7″ tablet, the specified threshold of “1.8″ rather than “2″ should be triggered on all iOS and Android tablets currently available.)

The secret is that the actual image size of the file loaded with display.newImageRect() is literally irrelevant. Since you are specifying the visible target size manually, Corona can pour any arbitrary bitmap into that space, and the job of config.lua is simply to give it a guide for choosing the best image on each screen size.

Note that because Corona uses hardware scaling in OpenGL, upscaled or downscaled bitmaps will be automatically smoothed — it’s not like the old days of the web, where we all learned to avoid scaling images on the fly due to ugly pixelation in browsers.

That’s cool, but why not do this automatically for all images?

As I discussed in the Flash porting guide, Corona’s high performance is derived from hardware acceleration, which in turn relies on a texture memory buffer on the device. Although recent devices like the iPhone 4 and iPad have doubled this available memory (from around 10MB to 20MB), doubling the resolution of an image actually quadruples the memory used. Therefore, you probably shouldn’t just double all your image resolutions, since you will actually run out of memory much faster on high-res devices.

For this reason, the best practice is to be selective about your high-resolution images. For main character sprites or crisp hard-edged foreground objects (say, platform game elements), selective high resolutions may be worthwhile. On the other hand, a background texture depicting grass, dirt or fluffy clouds may not benefit much from higher dpi, and should therefore be allowed to scale up from a lower-resolution file.

What about sprite sheets?

The display.newImageRect() API is separate from sprite sheets, and does not affect them. This is because sprite sheets are a special case: they’re a method for conserving texture memory by allocating large shared images at the maximum possible size available on the hardware. For most mobile hardware, the largest image size is 1024 x 1024 (or 2048 x 2048 on the iPhone 4 and iPad). A sprite sheet cannot simply be doubled in size, and generally it can’t be increased in resolution at all, without exceeding the hardware limitations on many devices.

Also, due to the cruel mathematics noted above (2x resolution equals 4x memory used), a game that uses sprite sheets to push the graphics memory envelope on a low-end iPhone can’t be globally asset-doubled on the iPhone 4. Finally, Android devices have no reliable correlation between screen size and available texture memory, since these details are freely chosen by individual hardware manufacturers. There is no way to abstract away all of these hardware considerations without severely limiting you at the high end.

Therefore, the best practice for multi-resolution output with sprite sheets is to create alternative sets of high-resolution sprite sheets and data, which will likely mean more sheets for the higher-resolution sprites, with fewer animation frames fitting on each sheet. Then, use platform detection and scaling detection to tell when you’re running on an iPhone 4 and/or iPad, and load the alternative sprite sheets in those specific cases only.

Eventually, we’ll live in a world where graphics memory is essentially unlimited, but for now, these hardware details remain critical.

25 Responses to “Dynamic Image Resolution Made Easy”

  1. Bryan Rieger

    Thanks for this! I was never really sure how image adaptation was being handle in Corona (or what options were available).

    BTW – any chance Corona for Symbian might see life once again? iOS is immensely popular in the west, but Symbian (and Nokia in general) is a very big opportunity in Europe and Asia.

    Reply
  2. Joe Hocking

    [brag]
    I implemented Retina graphics in my game even though I couldn’t see first hand that I did it right (I have a 3Gs.) Last night I saw my game running on a Retina display for the first time and the graphics were perfect. Corona makes this stuff so easy!
    [/brag]

    Reply
  3. BTLJ

    Good post, clear and succinct. Is there a minor typo with the latter two of:

    baseImage.png (100 x 100 pixels)
    baseImage-15.png (150 x 150 pixels)
    baseImage-2.png (200 x 200 pixels)

    which should be:- baseImage-x15 and -x2.png ?

    Reply
  4. sedev

    Hi Evan,

    I was wondering if it is possible to access this dynamic information when using CoronaUI. To serve a higher-res graphic as the ‘default’ in a tableview, for example.

    Thanks,

    Séamus

    Reply
  5. Paul Mackay

    How would the configuration be if you wanted to use a width and height for the Retina display and then define a suffix for a scaled down set of images for iPhone 3? Would it work if the scale factor is a decimal < 1?

    Reply
  6. Fotis

    Hello!

    I have the same question as Paul Mackay.
    Is it is possible to start with large image 1024×768 for ipad and scale down to iphone & iphone4?

    Reply
  7. Marcus

    I too have the same question as Paul Mackay and Fotis. My own testing has shown, that scaling values below 1 apparently don’t work as one think they might.

    Reply
  8. Jerome82

    @Fotis: My understanding from reading the wonderful Evan-posts is that instinctively we all want to start large and then scale down… but the problem is the wasted Memory doing so and that is not ideal. I suggest you read his other post about Content Scaling – it explains it all very well.

    Thanks for the great info Evan!!

    Reply
  9. black

    I have two images assets (hd, non-hd), however when I specify a 1.8 in my config.lua file, the android devices still show the low-res images. any ideas why?

    Reply
  10. Anneli

    Thanks for this brilliant post, Evan. I haven’t felt this relieved about building a game in Corona in days. Things are finally starting to make sense!

    Reply
  11. Michael M

    Hi Evan, thank you very much for a really brilliant post.
    After your explanation I’ve written a module that allows you to use dynamic image resolution and doesn’t require using display.newImageRect() – that is, you don’t have to provide image dimensions. I’ve posted it here:
    http://developer.anscamobile.com/code/different-way-managing-dynamic-image-resolution-and-scaling-font-size
    I would be happy to know what do you think about it and – everybody – I hope you find it useful.

    Michael

    Reply
  12. Ziad Baroudi

    I want to display a Splash screen and this is what I am doing:
    My config.lua looks exactly like the one under “magic recipe”. The image file is called Splash.png is a 320×480 with another, Splash@2x.png containing a 640×960 image.
    Whenever I use display.newImageRect(“Splash.png”, 320, 480), I get the splash screen displaying in a small corner of the screen. Anyone knows why?
    Ta,
    Ziad

    Reply
  13. Ziad Baroudi

    Just wanted to clarify that I have also tried display.contentWidth, display.contentHeight as well as display.viewableContentWidth, display.viewableContentHeight instead of 320, 480.

    Reply
  14. Steve

    Hello,
    I have an image i want to use as a startup screen that will be shown for 5 seconds before my game. I am new to this and do not know how to make my Default image stay on the screen and fit most phones. I do not know how to edit a config.lua file. Do i have to make one in here? I would appreciate a code to show me example if anybody could help me. I want to learn this but I’m struggling

    Thanks

    Reply
  15. Gary

    Is it possible, alternatively, to have a base size of 640×960 and have a scaling value of .5 to load for 3GS and a value of 1 to load for iPhone 4?

    Reply
  16. Nathan

    I am having the same problem as Ziad. I’m also using display.newImageRect and the “magic recipe.” :P I’m trying to do this with my background image, but it makes the image 1/4 the screen size instead of covering the whole screen. Does anyone know why?

    Reply
  17. Michael

    Not using dynamic image selection for sprite sheets citing memory allocation concerns is a copout.

    The maximum size of the image is irrelevant. Not all sprite sheets are 1024 squared.

    Many are just 256, 512, etc, which is perfectly suitable for dynamically selecting higher sprite sets.

    In games with tons of animation you’ve signed developers up for hours of spaghetti spritesheet loading on a very flimsy logical basis.

    Reply
  18. Dave

    So what is the desired size of actual content for the “magic recipe”? Still 380×570?

    Reply
  19. David Gross

    Forcing us to “know” the image size beforehand kills the value for many of us! When will you fix this? You can easily check which file to use by knowing the intended screen, and only loading the correct image.

    Reply

Leave a Reply

  • (Will Not Be Published)