March 16th 2011
During the development of Corrino Software's iPhone/iPad game Pizarro, I ran into problems with some of the inherent limitations of the OpenGL ES implementation in iOS. The iOS devices don't support anti-aliasing for polygons or for lines wider than 1 pixel. I knew Pizarro would require drawing primitives in OpenGL, as opposed to sprites, both for performance and memory reasons. However, without antialiasing, these primitives would be ugly and the lines jagged.
I was off to several false starts. Initially, I tried drawing the circles with CoreGraphics and converting them to OpenGL textures, but the performance was dreadful and brought older devices to their knees. It seemed ridiculous overhead for something that should be so simple: drawing an antialiased circle. After much trial and error, I was ultimately able to overcome -- or, some might say, circumvent -- these problems, and so thought I'd share with the world the clever trick I used.
To start off, it's worth saying a few things about Pizarro, the game I was developing. Pizarro is a very simple game. One or more red balls bounce around in a box. The objective of the game is to touch the screen and hold to create expanding circles to cover the area without having these circles collide with the bouncing balls. When 80% of the surface area has been covered, you advance to the next level. Obviously, the majority of the in-game graphics were circles. It was essential that they look good. The appearance and feel of the game itself depended on it.
As you can see from the screenshot of the final version of Pizarro above, the circles look great and are beautifully antialiased. How did I get them antialiased on iOS, which as of writing has an implementation of OpenGL ES that doesn't support antialiasing for polygons (and by extension, circles)?
I was developing Pizarro for iOS using the excellent, open-source cocos2d
engine. The version of Cocos2D I was using, 0.99.5, included some built-in functions for drawing primitives,
including a function called ccDrawCircle
in the CCDrawingPrimitives
class. All the
primitives functions in the engine only drew the shapes as outlines. I wanted filled circles, not outlines, so
I created a duplicate of the ccDrawCircle
function, which I called ccFillCircle
, where
only one line was changed:
glDrawArrays(GL_LINE_STRIP, 0, segs+additionalSegment);
became
glDrawArrays(GL_TRIANGLE_FAN, 0, segs+additionalSegment);
This gave me filled circles, but they were NOT antialiased and looked quite ugly:
I searched and googled this problem extensively, but found nothing to help me out except implementing antialiasing of the entire OpenGL scene by scaling it, which carried with it severe performance penalties. I wanted Pizarro, a simple, basic game, to run well on all iOS devices, back to the original iPhone, and so I immediately eliminated that option.
I knew from prior experience that the iOS implementation of OpenGL ES didn't support GL_LINE_SMOOTH for lines wider
than 1 pixel and assumed that similar constraints would apply to everything else. However, glancing over the
primitives drawing class in cocos2d, the function ccDrawPoints
caught my eye. What if OpenGL ES supported
drawing antialiased points?
I immediately tried it out and much to my surprise and pleasure I discovered that the iOS OpenGL implementation did indeed support antialiased points. I could draw antialiased filled circles -- points, technically -- with the following code:
glPointSize(circle.size); glEnable(GL_POINT_SMOOTH); glVertexPointer(2, GL_FLOAT, 0, &p); glDrawArrays(GL_POINTS, 0, 1);
This produced much better-looking circles than GL_TRIANGLE_STRIP:
Here's a side-by-side comparison of GL_TRIANGLE_STRIP vs. GL_POINT_SMOOTH:
This effectively solved my problem. However, I quickly discovered that drawing large, antialiased points using GL_POINT_SMOOTH was very expensive in terms of performance. Drawing many large, anti-aliased point at 60 frames per second brought even my iPhone 4 to its knees.
Ultimately, I was able to circumvent this by drawing the points to a CCRenderTexture which is used as a background for the game scene. The OpenGL drawing code for the smooth point would be called only once. The circles that were to expand during the touch phase would still be drawn at 60fps using GL_TRIANGLE_STRIP. So, currently Pizarro's expanding circles look much uglier than the final circle. However, in the heat of gameplay it's not so noticable, especially on the iPhone 4's retina display.
Of course, if you only have a few small circles, you might be able to get away with drawing them using GL_POINT_SMOOTH. The iPhone 4, which is much faster than previous devices, handles it reasonably well.
Another problem I ran into with using GL_POINT_SMOOTH was that when the points reached a certain size, they would no longer be drawn. A bit of googling located the source of the problem: There are point size limitations in OpenGL ES and drawing calls that try to draw points larger than the limit will fail.
My solution to this was to use code to check for the point size. If it was larger than the maximum smooth point size, I used GL_TRIANGLE_FAN. If not, I'd use the GL_POINT_SMOOTH method:
// first, get max smooth point size float maxSmoothPointSize[2]; glGetFloatv(GL_SMOOTH_POINT_SIZE_RANGE, (float *)&maxSmoothPointSize); // If larger than smooth point size, we draw as circle w. GL_TRIANGLE_FAN if (circle.size > maxSmoothPointSize[1]) { ccFillCircle(p, circle.size/2, CC_DEGREES_TO_RADIANS(360), 60, NO); } else // else we use GL_POINT_SMOOTH for anti-aliased "circle" { glPointSize(circle.size * CC_CONTENT_SCALE_FACTOR()); glEnable(GL_POINT_SMOOTH); glVertexPointer(2, GL_FLOAT, 0, &p); glDrawArrays(GL_POINTS, 0, 1); }
I believe the smooth point size limit in the iOS OpenGL implementation is 511. Any attempt to draw a point larger than this will fail. The code above will fall back to the non-antialiased circle drawing method. Of course, this means that very large circles in Pizarro look ugly:
Fortunately, the situation in which a circle can become this big is very unlikely to be realised in Pizarro since collision with the bouncing red ball is difficult to avoid at larger sizes. Still, it's a graphical mar on the game which I'm not entirely happy with. Better support for antialiasing in the OpenGL ES implementation would be a great boon to iOS developers and would eliminate the need for tricks like this one.
Finally, it goes without saying that this only works for filled circles and won't help you at all if you're just looking to draw a circle outline. Depending on your needs, the GL_POINT_SMOOTH technique could be used to draw anti-aliased non-filled circles by drawing a smooth point and then drawing another slightly smaller point in the background-color. This, of course, won't work if you have a textured background. Still, overall, it's a neat trick to have in the iOS development toolchest.
-- Sveinbjorn Thordarson
March 16th 2011