Font artifacts when using CCLabelAtlas

This post is for developers. If you use cocos2d and use CCLabelAtlas to render your fonts, and especially if you use pixel art, you have probably stumbled upon this issue: font artifacts. In order to help any other developers out there that are struggling with the same problem, I decided to share how I solved it.

What are font artifacts?

CCLabelAtlas takes an image file with characters in it and uses that to render a piece of text on screen. What’s nice with CCLabelAtlas is that you can design your own font with any kind of effects baked into the image. You have total control of each letter which I find useful for pixel art.

Now see this:

CCLabelAtlas

See those nasty vertical 1 pixel lines around letters “Y” and “O” of the title text? FONT ARTIFACTS. Your carefully drawn pretty little font is now ruined! Note: this happens on device only, not on simulator. So on the simulator you may think everything is fine (because everything looks fine!) and then you launch your app on a device and see this. Bummer.

Boys and girls: always test on device.

So how do I get rid of them?

I turned to google, looking for help. Turned out that I wasn’t the only one with this problem. Wiser men than I wrote that these artifacts happen because of the way hardware renders textures (I guess that’s why you only see them when running on a device).

But, don’t worry! Only thing you have to do is disable cocos2d’s subpixel rendering by setting the following in ccConfig.h:

#define CC_COCOSNODE_RENDER_SUBPIXEL 0
#define CC_SPRITESHEET_RENDER_SUBPIXEL 0

 

On top of that, projection should be changed from 3D to 2D. In AppDelegate.m:

[director_ setProjection:kCCDirectorProjection2D];

 

Unfortunately, these had no effect at all. And I don’t know why.

I then started playing with these settings and tried all kinds of combinations, aching for a visible result, but to no avail. I was getting desperate and even thought about replacing my own font with a true type font, because true type fonts are rendered with CCLabelTTF which works perfectly with no artifacts whatsoever.

Then I rolled up my sleeves and got to work.

The solution

I remembered that I had similar artifacts with sprites if they were on the spritesheet next to each other without any margin around them. Each letter in my font is 9 x 18 pixels and there were no margins around the letters in the font file, like so:

largearcadefont_example_no_margin

With the earlier sprite experience, I adjusted the font file and added a 1 pixel margin around each letter:

largearcadefont_example_1px_margin

This removed all artifacts from the font!

Downside was that all fonts rendered using CCLabelAtlas would have a 1 pixel margin between letters, and this is not always what we want. In other words I wanted the result as seen in the top picture (no spacing). Therefore I had to modify CCLabelAtlas a little bit to have more control on how fonts are rendered.

Subclassing CCLabelAtlas

I dug into the depths of cocos2d framework and its CCLabelAtlas class. There is a method “updateAtlasValues” which handles the rendering of each letter. To figure out how everything worked, I just played with various values in the method. I got some very interesting results at first (most of them garbage), until I found the right ones for my needs.

I subclassed CCLabelAtlas as PowerLabelAtlas (going with the “power” theme), and added two new parameters to PowerLabelAtlas: spacing and margin. PowerLabelAtlas.h interface looks like this:

@interface PowerLabelAtlas : CCLabelAtlas
{
    NSUInteger _spacing;
    NSUInteger _margin;
}

And here are the modified initialisers in PowerLabelAtlas.m:

+(id) labelWithString:(NSString*)string charMapFile:(NSString*)charmapfile itemWidth:(NSUInteger)w itemHeight:(NSUInteger)h spacing:(NSUInteger)s margin:(NSUInteger)m startCharMap:(NSUInteger)c
{
    // itemWidth: actual width of the character + spacing
    return [[[self alloc] initWithString:string charMapFile:charmapfile itemWidth:w+s itemHeight:h spacing:s margin:m startCharMap:c] autorelease];
}
-(id) initWithString:(NSString*)string charMapFile: (NSString*)filename itemWidth:(NSUInteger)w itemHeight:(NSUInteger)h spacing:(NSUInteger)s margin:(NSUInteger)m startCharMap:(NSUInteger)c
{
    CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage:filename];
    return [self initWithString:string texture:texture itemWidth:w itemHeight:h spacing:s margin:m startCharMap:c];
}

-(id) initWithString:(NSString*) theString texture:(CCTexture2D*)texture itemWidth:(NSUInteger)w itemHeight:(NSUInteger)h spacing:(NSUInteger)s margin:(NSUInteger)m startCharMap:(NSUInteger)c
{
    if ((self=[super initWithTexture:texture tileWidth:w tileHeight:h itemsToRender:[theString length] ]) )
    {
        _mapStartChar = c;
        _spacing = s;
        _margin = m;
        [self setString: theString];
    }
    return self;
}

The “itemWidth” parameter works a little bit differently in PowerLabelAtlas. In CCLabelAtlas, it’s the total width of the letter + spacing. In PowerLabelAtlas, width and spacing are separated, so itemWidth is the width of the letter only and “spacing” parameter tells the method how many pixels there are reserved for spacing in the font file.

Here are the parameters explained:

  • itemWidth: width of the letter in pixels
  • itemHeight: height of the letter in pixels
  • spacing: how many pixels of spacing between letters in the image file
  • margin: how many pixels of margin between letters in the image file

updateAtlasValues method

Now that the initialisers are ready, we have to modify the updateAtlasValues method to take advantage of the new “margin” parameter. I actually modified only one row:

// adjust calculation to use the total width of the item
float left = row*(itemWidthInPixels + _margin)/textureWide; // original: row*itemWidthInPixels/textureWide;

“itemWidthInPixels” is actually width + spacing (from the initialiser: itemWidth:w+s). We add margin, and the sum of these elements is used to create the texture for the letter from the image file. 

So in my case, even though the character size is 9 x 18 pixels, if using CCLabelAtlas, I had to use itemWidth of 10 because of the 1 pixel margin. Because cocos2d uses itemWidth to also space out individual letters in a string, the result is a text with 1 pixel space between letters. With PowerLabelAtlas, I can now define that my itemWidth is 9, spacing is 0 (the letters should be adjacent to each other) and margin is 1.

Here’s the final output:

PowerLabelAtlas

Artifacts and 1 px spacing gone!

Last Words

This post was to show how I solved the font artifact problem. Based on Google search, it’s a quite common problem but none of the results I found helped to solve the problem I was having.

The solution is not 100% perfect. If you look carefully, some of the letters are stretched 1 sub pixel on the x-axis. I guess this is some kind of sorcery happening in OpenGL that I have no clue about. Maybe if I’d play more with the values in updateAtlasValues method I could find a fix. The most important thing for me was to get rid of the artifacts.

I hope you find this article useful. If I managed to help even one developer struggling with a font artifact issue, I’m happy!