Saturday, June 2, 2012

Create 3D objects inside Cocos2D-x (Part 2 - Menus)

Part 1 - Introduction
Part 2 - Menus

In my previous post I've created a very rough experiment showing some 3D boxes on top of the default Cocos2d-X template.

In this post I'm going to create something a little bit more useful: a level selection mechanism in 3D. This demo uses some additional concepts like:
  • Texturing
  • Changing the POV (Point-of-view) based on the device accelerometer
  • Click-detection
When launching the application we have the following screen:

Tilting the device changes the perspective.

When a box is clicked it changes color and collapses to the ground.

Here's a video showing the selection behavior (without the accelerometer part because the emulator doesn't support it).

Note: don't mind the frame-rate in the video. My 2007 Mac-Mini struggles a little bit running the emulator while video-recording. In my iPad I have steady 60 FPS.

The texture on top of the box is dynamically generated by combining 3 different layers:
  • The base color (which changes if the box is selected)
  • The metal frame
  • The level number 
The following image represents this overlay.

The code to generate the top texture is:

CCTexture2D* Box::createTopTexture(GLfloat r, GLfloat g, GLfloat b, const char* level) 
    float textureSize = 256;
    CCRenderTexture *rt = CCRenderTexture::renderTextureWithWidthAndHeight(textureSize, textureSize);
    rt->beginWithClear(r, g, b, 1.0);
    std::string file = (std::string(level) + std::string(".png"));
    CCSprite *levelImage = CCSprite::spriteWithFile(file.c_str());
    levelImage->setBlendFunc((ccBlendFunc){GL_ONE, GL_ONE_MINUS_SRC_ALPHA});
    levelImage->setPosition(ccp(textureSize/2, textureSize/2));
    CCSprite *frame = CCSprite::spriteWithFile("frame.png");
    frame->setBlendFunc((ccBlendFunc){GL_ONE, GL_ONE_MINUS_SRC_ALPHA});
    frame->setPosition(ccp(textureSize/2, textureSize/2));

    return rt->getSprite()->getTexture();

The "selected" version also uses the same method but passing a yellow background color as an argument.

Changing the POV
The secret to this functionality is the gluLookAt function. This function is already included inside Cocos2d-x (and Cocos2d) and was borrowed from an OpenGL library called the GLU - OpenGL Utility Library.

I call it like this in my demo:

gluLookAt(camera.x, camera.y, camera.z, 0, 0, 0, 0, 1, 0)


The first 3 arguments represent the point where the camera is located.
The following 3 are always (0,0,0) and represent the point where the camera is pointed to.
The following 3 represent the upwards vector of the camera. It will always be (0,1,0) in this case.

The camera position varies with the accelerometer. This is achieved with this code:
void HelloWorld::didAccelerate(CCAcceleration* pAccelerationValue)
    float x = pow(pAccelerationValue->x * 2.0, 3);
    float y = pow(pAccelerationValue->y * 1.5, 2);

    if(pAccelerationValue->y < 0)
        y *= -1;

    CCSetIterator it;
    Box* box;
    for( it = set->begin(); it != set->end(); it++) 
        box = (Box*)(*it);
        box->setCamera(vec(x, y, 6.0f));
The math applied to the acceleration value was empirical and tried to limit the effect when the device is at its default position (ie., not tilted). When tilting the effect becomes exponentially more pronounced.

Click Detection
This is probably the most complex topic of the lot. Determining which object has been clicked on a 3D space (called as picking) involves ray casting a vector and determining the intersection with the objects in the 3d  world. As I said in my previous post, I'm not an OpenGL expert, so I struggled a lit bit with this.

Fortunately I had an idea to simplify matters by making some assumptions. Well, I only want to check the click on the top of the boxes, which is a 2d plane. So, if I convert the world coordinates of the top-left/bottom-right corners of that plane to screen coordinates, I can easily test if they were clicked. Obviously this has a little error, especially when the screen is tilted and some of the boxes are not frontally facing the screen, but is negligible.

To convert from world-coordinates I use the gluUnProject which, although also belongs to GLU, is not included in Cocos2d-X. I found an implementation of it on the web and included it on the project. I'm sorry for not crediting anyone for that but I don't recall where I got it from.

So, I just unproject the corners of the boxes and know exactly where they are in screen space. For each box I store the screen coordinates of its corners. Then, when handling touches I just do this:
void HelloWorld::ccTouchesBegan(CCSet *touches, CCEvent *pEvent)
    CCSetIterator it;
    CCTouch* touch;
    for( it = touches->begin(); it != touches->end(); it++) 
        touch = (CCTouch*)(*it);
        CCPoint winPos = touch->locationInView();
        winPos = CCDirector::sharedDirector()->convertToGL(winPos);
        //CCLog("x: %f   y: %f", winPos.x, winPos.y);
        CCSetIterator it;
        Box* box;
        for( it = set->begin(); it != set->end(); it++) 
            box = (Box*)(*it);
            if(winPos.x > box->getTopLeft().x && 
               winPos.x < box->getBottomRight().x && 
               winPos.y > box->getBottomRight().y && 
               winPos.y < box->getTopLeft().y)

I've included a zip with the full XCode project. You can download it here.


  1. Dear Pedro thanks again for the tutorial,
    I downloaded your whole project and tried to run on my latest cocos2d-x 2.0.2 and I have a bad access debug time error at:
    levelImage->setBlendFunc((ccBlendFunc){GL_ONE, GL_ONE_MINUS_SRC_ALPHA});
    I could also send a screenshot if needed.

  2. Well, it won't work in Cocos2d-x 2.0.2. Cocos2d-x 2.x uses OpenGL ES 2.0, while version 1.0.13 uses OpenGL ES 1.0. They're not compatible.

    Fortunately someone already wrote a great tutorial on displaying 3D objects using Cocos2d-x 2.0.x, which you can check here:

    1. Awesome thanks!
      looking forward for your ES 2.0 cocos2dx versions hopefully