Cocos2dx – Spinning Globe by Masking technique

In this post, lets see how we can implement a spinning world in cocos2d-x using masking technique. See the video below for sample output:


Low FPS is because of my Mac Mini; In device, it runs at 60 FPS

Downloads

1. Full Project

1. Create a Cocos2d-x Project

I am not going to discuss about how to setup a cocos2d-x project for iOS here. For info on how to setup cocos2d-x project, refer the official wiki.

All our code will go into `HelloWorld` class that comes with the template.

2. Masking Functionality

The hardest part in achieving our goal would be ‘Masking’ and we resort to Pavel Hancak’s tutorial on cocos2d-x masking. Please check the tutorial on how to mask a sprite. Based on that code, I rewrote the function as below:

CCSprite* HelloWorld::maskedSpriteWithSprite(CCSprite* pTextureSprite, CCSprite* pMaskSprite, float xoffset, float yoffset)
{
    // store the original positions of both sprites
    CCPoint textureSpriteOrigPosition(pTextureSprite->getPosition().x, pTextureSprite->getPosition().y);
    CCPoint maskSpriteOrigPosition(pMaskSprite->getPosition().x, pMaskSprite->getPosition().y);

    // convert the texture sprite position into mask sprite coordinate system
    pTextureSprite->setPosition(ccp(pTextureSprite->getContentSize().width/2 - pMaskSprite->getPosition().x + pMaskSprite->getContentSize().width/2 - xoffset, pTextureSprite->getContentSize().height/2 - pMaskSprite->getPosition().y + pMaskSprite->getContentSize().height/2 + yoffset));

    // position the mask sprite so that the bottom left corner lies on the (o,o) coordinates
    pMaskSprite->setPosition(ccp(pMaskSprite->getContentSize().width/2, pMaskSprite->getContentSize().height/2));

    CCRenderTexture* rt = CCRenderTexture::renderTextureWithWidthAndHeight((int)pMaskSprite->getContentSize().width, (int)pMaskSprite->getContentSize().height);

    ccBlendFunc bfMask = ccBlendFunc();
    bfMask.src = GL_ONE;
    bfMask.dst = GL_ZERO;
    pMaskSprite->setBlendFunc(bfMask);

    // turn off anti-aliasing around the mask sprite
    pMaskSprite->getTexture()->setAliasTexParameters();

    ccBlendFunc bfTexture = ccBlendFunc();
    bfTexture.src = GL_DST_ALPHA;
    bfTexture.dst = GL_ZERO;
    pTextureSprite->setBlendFunc(bfTexture);

    rt->begin();
    pMaskSprite->visit();
    pTextureSprite->visit();
    rt->end();

    // generate the resulting sprite
    CCSprite* pOutcome = CCSprite::spriteWithTexture(rt->getSprite()->getTexture());
    pOutcome->setFlipY(true);

    // restore the original sprite positions
    pTextureSprite->setPosition(textureSpriteOrigPosition);
    pMaskSprite->setPosition(maskSpriteOrigPosition);
    pOutcome->setPosition(maskSpriteOrigPosition);

    return pOutcome;
}

I added two parameters `xoffset` and `yoffset` to the original code. These parameters are to offset the position of underlying map. By varying this offset parameter in gameloop, we move the map linearly over a period of time.

3. Setup Sprites

Add two sprites `map` and `mask`, where

2d world map

`map` contains the full map. Only a part of will be seen at any time

When one edge of the scrolls into the keyhole (mask), we dont want the background to be visible, but the other edge to wrap and fit the gap. For this purpose, we duplicated the map horizontally, and when one edge is within keyhole, the duplicated part will fit in and when the edge crosses the keyhole completely, we reset the map to initial position. (Will see implementation details in subsequent sections).

mask for world map

`mask` picture is like a circular keyhole through which we sees part of a map.

Code to add these two sprites is as follows. Dont add the sprite as child of layer yet.

    
    // in init() function
    map = CCSprite::create("worldmap2d.jpg");
    map->setPosition( ccp(512, 768/2) );
    map->retain();

    mask = CCSprite::create("worldmapmask.png");
    mask->setPosition( ccp(512, 768/2) );
    mask->retain();

Apart from the `map` and `mask`, a star background is added behind.

4. Masking

With Pavel Hancak’s tutorial, masking is now very simple as following:

    // in init() function
    masked = maskedSpriteWithSprite(map, mask, 0, 100);
    addChild(masked);

Only the masked sprite should be added to the `layer`. I found the offsets on trial and error basis 😛 At this point, you should see circular part of the map and not the whole. We masked the map, but still it looks like a flattened map without any depth. To add some depth, add a `shade` picture, above all the sprites as follows

    // in init() function
    shade = CCSprite::create("worldmapshade.png");
    shade->setPosition( ccp(512, 768/2) );
    addChild(shade);
shade over world map

shade to be applied over the world map to create an illusion of depth

5. Spinning Animation

We dont have any real `3D` object here and so we achieve spinning animation by moving the map horizontally behind the keyhole (mask) infinitely. Add this final piece of code and we are done:

    // in init() function
    this->scheduleUpdate();

    void HelloWorld::update(float dt) {
        CCSprite *m;
        xoff += dt * 100;
        if (xoff > 1024) {
            xoff = 0;
        }
        m = maskedSpriteWithSprite(map, mask, xoff, 120);
        masked->setTexture(m->getTexture());
    }

In above code, `xoff` determines the amount of distance that the map should be moved. `Update` is the gamploop, where we increment `xoff` by small amounts in each frame, thus moving the map. When the `xoff` reaches its limit, we reset it. Changing `xoff` doen’t do anything magically. We create a new sprite based on offset position and apply the texture to our `masked` sprite.

Run the project and see the world spinning. Thats the end of the tutorial. Hope it helped :-)

Source can be browsed @ https://github.com/saiy2k/cocos2dx-masking