Saturday, May 19, 2012

Create 3D objects inside Cocos2D-x (Part 1 - Introduction)

Part 1 - Introduction
Part 2 - Menus

Cocos2D-x is, as you might infer from its name, a 2d gaming library. But, as it uses OpenGL internally to draw its stuff, we might leverage it to create some 3D objects on the mix.

In this post I'm going to show how to create a  bunch of animated 3D boxes inside a Cocos2D-X scene mixed with 2D sprites.


Although I don't recommend using Cocos2D-x for making a complete 3D game, adding some 3D objects might bring some interesting effects.
You can check the end-result of this simple demo in the following video:



Disclaimer:
I'm not a C++ / openGL expert so I don't guarantee that I've implemented everything in the best way possible. It has some known issues like:
  • doesn't allow you to draw sprites on top of the boxes
  • the gluLookAt call shouldn't be inside the Box class draw method
  • the color shading is hardcoded
  • and so on, so on... 
Anyway, use this code freely it if you want, although this is far from "production ready". It's meant to be a proof of concept.

Steps:
  • Create a new project using the Cocos2D-x template
  • Add the following files to your project:
OpenGLCommon.h
#ifndef _OPENGL_COMMON_H
#define _OPENGL_COMMON_H

typedef struct {    

    GLfloat x;
    GLfloat y;
    GLfloat z;

} Vector3D;

static inline Vector3D vec(const GLfloat x, const GLfloat y, const GLfloat z)
{
    Vector3D v;
    v.x = x;
    v.y = y;
    v.z = z;
    return v;
}

#endif

Box.h
#ifndef _BOX_h
#define _BOX_h

#include "cocos2d.h"
#include "OpenGLCommon.h"

class Box : public cocos2d::CCNode
{
    
public:
    Box();
    virtual void draw();
    static Box* boxWithSizeAndPosition(Vector3D size, Vector3D position);
    void setPosition(Vector3D vector);
    void setSize(Vector3D vector);
    Vector3D getSize();
    
private:
    Vector3D position;
    Vector3D size;
};

#endif

Box.cpp
#include "Box.h"
using namespace cocos2d;

Box::Box()
{
}

Box* Box::boxWithSizeAndPosition(Vector3D size, Vector3D position)
{

    Box *box = new Box();    
    box->setPosition(position);
    box->setSize(size);
    box->autorelease();
    return box;
}

void Box::setPosition(Vector3D vector)
{
    position = vector;
}

void Box::setSize(Vector3D vector)
{
    size = vector;
}

Vector3D Box::getSize()
{
    return size;
}

void Box::draw()
{
    GLfloat maxX = size.x / 2.0f;
    GLfloat minX = maxX * -1;
    GLfloat maxY = size.y / 2.0f;
    GLfloat miny = maxY * -1;

    const GLfloat frontVertices[] = {
  minX, miny, size.z,
        maxX, miny, size.z,
        minX, maxY, size.z,
        maxX, maxY, size.z,
    };

    const GLfloat backVertices[] = {
        minX, miny, 0.0f,
        minX, maxY, 0.0f,
        maxX, miny, 0.0f,
        maxX, maxY, 0.0f,
    };

    const GLfloat leftVertices[] = {
        minX, miny, size.z,
        minX, maxY, size.z,
        minX, miny, 0.0f,
        minX, maxY, 0.0f,
    };

    const GLfloat rightVertices[] = {
        maxX, miny, 0.0f,
        maxX, maxY, 0.0f,
        maxX, miny, size.z,
        maxX, maxY, size.z,
    };

    const GLfloat topVertices[] = {
        minX, maxY, size.z,
        maxX, maxY, size.z,
        minX, maxY, 0.0f,
        maxX, maxY, 0.0f,

    };

    const GLfloat bottomVertices[] = {
        minX, miny, size.z,
        minX, miny, 0.0f,
        maxX, miny, size.z,
        maxX, miny, 0.0f,
    };


    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glDisableClientState(GL_COLOR_ARRAY); 
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);

    glLoadIdentity();

    cocos2d::gluLookAt(0, 0, 5, 0, 0, 0, 0, 1, 0);

    glTranslatef(position.x, position.y, position.z);

    glVertexPointer(3, GL_FLOAT, 0, frontVertices);
    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    glVertexPointer(3, GL_FLOAT, 0, backVertices);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    glVertexPointer(3, GL_FLOAT, 0, leftVertices);
    glColor4f(0.5f, 0.5f, 0.5f, 1.0f);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    glVertexPointer(3, GL_FLOAT, 0, rightVertices);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    glVertexPointer(3, GL_FLOAT, 0, topVertices);
    glColor4f(0.7f, 0.7f, 0.7f, 1.0f);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    glVertexPointer(3, GL_FLOAT, 0, bottomVertices);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    glEnableClientState(GL_COLOR_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisable(GL_CULL_FACE);
    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);

}
  • Also, modify:
HelloWorldScene.h
#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"
#include "Box.h"

class HelloWorld : public cocos2d::CCLayer
{
public:

 virtual bool init();  
    
 virtual void draw();
    
 void tick(cocos2d::ccTime dt);
 static cocos2d::CCScene* scene();
 
 virtual void menuCloseCallback(CCObject* pSender);

 LAYER_NODE_FUNC(HelloWorld);
    
private:
    cocos2d::CCSet *set;
};

#endif // __HELLOWORLD_SCENE_H__

HelloWorldScene.cpp
#include "HelloWorldScene.h"
#include "SimpleAudioEngine.h"

using namespace cocos2d;
using namespace CocosDenshion;

CCScene* HelloWorld::scene()
{
    CCScene *scene = CCScene::node();
 
    HelloWorld *layer = HelloWorld::node();

    scene->addChild(layer);

    return scene;
}

bool HelloWorld::init()
{   
    if ( !CCLayer::init() )
    {
      return false;
    }

    CCMenuItemImage *pCloseItem = CCMenuItemImage::itemFromNormalImage(
          "CloseNormal.png",
          "CloseSelected.png",
          this,
          menu_selector(HelloWorld::menuCloseCallback) );
    pCloseItem->setPosition( ccp(CCDirector::sharedDirector()->getWinSize().width - 20, 20) );

 
    CCMenu* pMenu = CCMenu::menuWithItems(pCloseItem, NULL);
    pMenu->setPosition( CCPointZero );
    this->addChild(pMenu, 1);

    // ask director the window size
    CCSize size = CCDirector::sharedDirector()->getWinSize();
    
    
    // add "HelloWorld" splash screen"
    CCSprite* pSprite = CCSprite::spriteWithFile("HelloWorld.png");
  
    // position the sprite on the center of the screen
    pSprite->setPosition( ccp(size.width/2, size.height/2) );

    // add the sprite as a child to this layer
    this->addChild(pSprite, 0);
        
    set = new CCSet();
    
    Vector3D boxSize = vec(0.5f,0.5f,0.01f);

    set->addObject(Box::boxWithSizeAndPosition(boxSize, vec(1.5f, 1.5f, 0.0f)));
    set->addObject(Box::boxWithSizeAndPosition(boxSize, vec(1.5f, 0.5f, 0.0f)));
    set->addObject(Box::boxWithSizeAndPosition(boxSize, vec(1.5f,-0.5f, 0.0f)));
    set->addObject(Box::boxWithSizeAndPosition(boxSize, vec(1.5f,-1.5f, 0.0f)));
    set->addObject(Box::boxWithSizeAndPosition(boxSize, vec(-1.5f, 1.5f,0.0f)));
    set->addObject(Box::boxWithSizeAndPosition(boxSize, vec(-1.5f, 0.5f,0.0f)));
    set->addObject(Box::boxWithSizeAndPosition(boxSize, vec(-1.5f,-0.5f,0.0f)));
    set->addObject(Box::boxWithSizeAndPosition(boxSize, vec(-1.5f,-1.5f,0.0f)));
    
    CCSetIterator it;
    
    for( it = set->begin(); it != set->end(); it++) 
    {
        this->addChild((Box*)(*it));
    }

    schedule( schedule_selector(HelloWorld::tick) );
    return true;
}

void HelloWorld::tick(ccTime dt)
{
    CCSetIterator it;
    Box* box;
    
    for( it = set->begin(); it != set->end(); it++) 
    {
        box = (Box*)(*it);
        
        Vector3D size = box->getSize();
        
        if(size.z < 2.0)
        {        
            size.z += size.z * 0.05;
            box->setSize(size);
        }
    }
}

void HelloWorld::draw()
{  
}

void HelloWorld::menuCloseCallback(CCObject* pSender)
{
    CCDirector::sharedDirector()->end();

    #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
        exit(0);
    #endif
}

If you have any doubts regarding the code don't be afraid to ask.

10 comments:

  1. Thanks for you Tutorial! very Usefull!
    Instead of Drawing 3d, is there a way to read in a 3d object (.obj file) and then rotate and move it around?
    Thanks

    ReplyDelete
  2. There's an free project called Cocos3D which, among other features, is able to load an .obj file. Unfortunately it's just for Cocos2d, although I've seen people asking for a Cocos2d-x port.

    ReplyDelete
  3. When I run this on win32 the boxes never render... Where in the above code was Box::draw() being called? I tried inserting box->draw() at the end of the tick function, but that just resulted in the default background sprite shrinking steadily until it disappeared (not sure why that happened). Also, why don't you need to call the CCNode super-constructor after the Box constructor? Even though the CCNode constructor doesn't seem to do much on its own, it does call the super-constructor for CCObject, which does take parameters-- shouldn't that make it necessary to construct Box as follows in Box.cpp?

    Box::Box() : CCNode()

    thanks,
    CCJ

    ReplyDelete
  4. The boxes inherit from CCNode. The draw() method is thus automatically called as they're added to the main CCLayer. I don't know why the boxes don't appear in win32. Could be some issue with my code as I've just tested it with iOS.
    Anyway, if you want, I can provide a zip file with my code. Although awful could be useful :P

    ReplyDelete
    Replies
    1. yeah, a zipped xcode project to creswel2@gmail.com would be useful so I can make sure we're building under the same conditions (obviously I'll be testing on a Mac for that purpose). If everything works in the iOS simulator as in your video above I'll dig deeper into the win32 build to see what the disparity may be...
      thanks,
      CCJ

      Delete
  5. hi there, thanks for the tutorial.
    Could you briefly explain why wouldn't you recommend cocos2dx for a complete OpenGL game?
    I know OpenGL has its own template obviously but I would like to know some specific drawbacks.
    Also could you please give some advice for writing OpenGL ES 2.0 (which came out recently) for cocos2dx?
    Thank you very much!

    ReplyDelete
  6. Ams,

    Are you talking about OpenGL ES 2.0 generically in cocos2d-x or 3D specifically. If you want 3D, look no further than this link:

    http://cocos2d-x.org/news/67 (includes sample code really easy to set up)

    If you want to play with OpenGL ES 2.0 stuff like shaders, any Cocos2d tutorial should be enough, namely the ones you can find in raywenderlich.com, like this one:

    http://www.raywenderlich.com/10862/how-to-create-cool-effects-with-custom-shaders-in-opengl-es-2-0-and-cocos2d-2-x

    ReplyDelete
    Replies
    1. Thanks for the reply and the links but you didnt address my first question. and yes I am interested in OpenGL ES 2.0 for cocos2d-x not cocos2d. I couldn't implement the same in cocos2dx by just looking at the Ray's tutorials.
      So 3D in general for iOS will have to be through OpenGL ES and since cocos2dx is written based on it, why would it not be a good idea to use cocos2dx for a complete 3D game? I would like to know specific reasons if possible

      Delete
  7. It's just that Cocos2d-x is a 2D engine at its core. For 3D you would have to do everything from scratch and delve into low-level OpenGL ES programming, as the cocos2d-x API would not help you with any of that. There's nothing wrong with that approach itself and no real drawback, just more work for you.

    I guess the project template of Cocos2d-x would be helpful, and for some 2D overlays like the hud or menus, but not much more.

    Anyway, I don't really know the offerings in the mobile space for cross-platform 3D engines, but there're certainly some options out there.

    ReplyDelete
  8. Thank you for the tutorial..
    But i am getting one error like this..
    Using glVertexPointer it is giving glVertexPointer not declared in the scope..
    I am using cocos2dx 2.0.1
    Please help

    ReplyDelete