Friday, August 31, 2012

Freehand drawing with Cocos2d-x and Box2d

After going on holidays and recharging my batteries I'm ready for some more blogging action.

This will be a fun one. Basically I want to:
  • Allow the user to draw a freehand shape on the screen
  • After releasing the click/gesture the shape should materialize itself into a dynamic physics body.
  • The generated bodies should be affected by gravity and will collide with each other
I'll be using cocos2d-x 2.0.1 with Box2D for the physics.

(Update: 24 March 2013: As some people were having trouble making it work I've replaced the current download with a complete XCode project using the latest Cocos2d-x version (cocos2d-x 2.1.2))

Anyway, a video is worth more than 10.000 words (heck, if a picture is worth 1000 words a video is certainly more). Here's the final effect:





There are a lot of challenges to address here so let's get started. I'll use the default "Cocos2d-x with box2d" template as a starting point.



Step 1. Remove unwanted code.

We'll just use the HelloWorld class, and thus remove the "addNewSpriteAtPosition" method, and change the touch event so that nothing happens... yet :)

Step 2. Freehand drawing on the screen:

The idea is as follows:
  1. Create the concept of a brush
  2. Also, create the concept of a full-screen canvas
  3. When touching/clicking the screen, paint the canvas with that brush.
Now, let's map this to Cocos2d-x concepts:
  1. The brush will be a CCSprite
  2. The canvas will be a CCRenderTexture
  3. To paint a CCSprite (or any other CCNode for that mater) onto a CCRenderTexture you call the Visit method of the sprite during the gesture/cursor movement.
Here's the image sprite used for the brush

Source-code:
Inside the constructor:
    target = CCRenderTexture::create(s.width, s.height, 
                                     kCCTexture2DPixelFormat_RGBA8888);
    target->retain();
    target->setPosition(ccp(s.width / 2, s.height / 2));
    
    this->addChild(target);
    
    brush = CCSprite::create("largeBrush.png");
    brush->retain();

The "brush" and "target" are declared as:
cocos2d::CCRenderTexture *target;
cocos2d::CCSprite *brush;

Then add the ccTouchesMoved method with this implementation.
void HelloWorld::ccTouchesMoved(CCSet* touches, CCEvent* event)
{
    CCTouch *touch = (CCTouch *)touches->anyObject();
    CCPoint start = touch->locationInView();
    start = CCDirector::sharedDirector()->convertToGL(start);
    CCPoint end = touch->previousLocationInView();
    end = CCDirector::sharedDirector()->convertToGL(end);

    target->begin();

    float distance = ccpDistance(start, end);

    for (int i = 0; i < distance; i++)
    {
        float difx = end.x - start.x;
        float dify = end.y - start.y;
        float delta = (float)i / distance;
        brush->setPosition(
            ccp(start.x + (difx * delta), start.y + (dify * delta)));

        brush->visit();
    }

    target->end();
}

The for cicle is used to make the line continuous. Otherwise, if the cursor moved too fast, there would be gaps in the line.

Here's the end-result while drawing.


Nice.

Step 3. Create a static physic object matching the drawing

Our matching physics object will be composed of several rectangles. Each rectangle will be a fixture of the body with constant height, based on the brush image size. This will make the physics object and drawing match as closely as possible.

Let's begin by creating an helper method to add a rectangle to a box2d body.
void HelloWorld::addRectangleBetweenPointsToBody(b2Body *body, CCPoint start, CCPoint end)
{   
    float distance = sqrt( pow(end.x - start.x, 2) + pow(end.y - start.y, 2));
    
    float sx=start.x;
    float sy=start.y;
    float ex=end.x;
    float ey=end.y;
    float dist_x=sx-ex;
    float dist_y=sy-ey;
    float angle= atan2(dist_y,dist_x);
    
    float px= (sx+ex)/2/PTM_RATIO - body->GetPosition().x;
    float py = (sy+ey)/2/PTM_RATIO - body->GetPosition().y;
    
    float width = abs(distance)/PTM_RATIO;
    float height = _brush.boundinbog.size.height /PTM_RATIO;
    
    b2PolygonShape boxShape;
    boxShape.SetAsBox(width / 2, height / 2, b2Vec2(px,py),angle);
    
    b2FixtureDef boxFixtureDef;
    boxFixtureDef.shape = &boxShape;
    boxFixtureDef.density = 5;
    
    body->CreateFixture(&boxFixtureDef);
}

The physics boxes are created simultaneously with the drawing. The following image shows the physic bodies composed of small rectangles (I've hidden the yellow drawing).



Step 4. Convert the static physic objects into dynamic objects

We already have physic objects, but they're static. I want them to materialize into dynamic objects when releasing the click/gesture so that they're affected by gravity. After implementing this behavior we get this.


I've drawn the Hello word. After releasing the gesture the physics objects tumble down but the drawing remains static. We're just missing the final piece of the puzzle.

Step 5. Make the sprites match the physics object while moving/rotating

Here comes the hard part: make the sprite match the box2d object and keep them matched while the physics object moves and rotates.

The idea is simple:
  • Iterate the box2d body fixtures to get the top-left and bottom-right corners coordinates.
  • Take a rectangular snapshot of the CCRenderTexture (using the above coordinates) to create a new sprite. 
  • Associate the sprite with the physics body.
  • Then, and this is essential, change the sprite anchor point to match the box2d body starting point.
Afterwards everything should work like in the video (except the color and radius of the brush, which I changed).



I'm also including the source-code (updated on 24/03/2013) for this example. Don't mind the quality of the code as it has some duplication and is not exactly polished. Anyway, feel free to use it.

You can get a zip file here

23 comments:

  1. Hi,

    Thanks for the code, I tried to compile it using Cocos2d-x 2.0.3 as simulator iPad 5.0 simulator, but this line in "void HelloWorld::ccTouchesEnded(CCSet* touches, CCEvent* event)" keep getting this compile error: "No matching function for call to 'create'" - any idea how to resolve that? Cheers

    CCSprite *sprite = CCSprite::create(tex, bodyRectangle);

    ReplyDelete
    Replies
    1. Hi, I got the previous problem fixed by checking CCSprite.h and changed it to "createWithTexture", it now compiles and runs on simulator, but whenever I tap on the screen, it died in "/libs/cocos2dx/cocoa/CCGeometry.cpp" with this error:

      Assertion failed: (width >= 0.0f && height >= 0.0f), function setRect, file /Users/qq/Documents/iOS.Dev.Projects/TestCocod2d-x/TestCocod2d-x/libs/cocos2dx/cocoa/CCGeometry.cpp, line 148.


      void CCRect::setRect(float x, float y, float width, float height)
      {
      // Only support that, the width and height > 0
      CCAssert(width >= 0.0f && height >= 0.0f, "width and height of Rect must not less than 0.");

      origin.x = x;
      origin.y = y;

      size.width = width;
      size.height = height;
      }

      Is it possible for you to upload your full working project? Thanks!

      Delete
    2. Hi,

      My working project only had those files (besides Cocos2d-x and box2d libraries). The main difference is that it targeted cocos2d-x2.0.1 instead of 2.0.3.

      I've probably deleted it as I made it specifically for this post, but I'll try to create it again, updated to cocos2d-x2.0.3

      Delete
    3. i got the same error, CCGeometry.cpp, line 148.
      T_T"

      Delete
    4. Hi,

      I haven't updated it yet to version 2.0.3

      Delete
  2. Cool, will wait for your update then.
    By the way, like to clarify which platform your work was running under? Mine was running on Xcode 4.5 under Mac OS X 10.7.4

    ReplyDelete
  3. hi Pedro Sousa

    i tied the code and i got some question.

    it is that

    in HelloWorld::ccTouchesEnded

    =====================================================
    for(int i=0; i < plataformPoints.size()-1; i++)
    {
    CCPoint start = plataformPoints[i];
    CCPoint end = plataformPoints[i+1];
    addRectangleBetweenPointsToBody(newBody,start,end);
    }
    =======================================================

    i try cclog plataformPoints.size and found the value is 1.
    so addRectangleBetweenPointsToBody(newBody,start,end);
    did't run.

    and i found
    plataformPoints.push_back(location);
    is in ccTouchesBegan, that make plataformPoints.size is 1.

    am i miss something?

    ReplyDelete
  4. Hello..first thank you very much for nice example..
    I have one issue in the free hand drawing..
    Drawing comes after some time of touches move..
    I am using cocos2dx 0.13.0
    but i think that is not the problem of version..
    Do you know what should be the issue

    ReplyDelete
    Replies
    1. Ok its the version problem..May be some ccrendertexture performance issues on old versions..
      The same stuff regarding rendertexture working fine on 2.x
      but not on 0.13.0

      Delete
    2. Viraj, I'm actually surprised that it ran at all on 0.13, as it used OpenGL ES 1.0 :)

      Delete
    3. Now it is working very well on both the versions as the community solved the opengl context saving problem..
      http://www.cocos2d-x.org/boards/6/topics/19675
      Thank you again..

      And yes is it possible to undo and redo drawing as per this approach or do you have some idea about it..

      Delete
  5. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Ouch, sorry about that. At the time that I've made this blogpost my project was like a sandbox littered with misc stuff, thus the "incomplete" zip with just the meaningful classes.

      I'll try to create a working project as soon as possible.

      Delete
  6. Look I really need your help, I need something like this for part of my game I have to make for a course project. I cannot get it to work. Is there anyway you can help me, I'm running out of time and resources.
    Thanks

    ReplyDelete
  7. @brian:

    I've created a complete XCode project with the latest cocos2d-x version. I'm uploading it now and will update the post with it.

    ReplyDelete
  8. I don't understand this code :
    float anchorX = newBody->GetPosition().x * PTM_RATIO - bodyRectangle.origin.x;
    float anchorY = bodyRectangle.size.height - (s.height - bodyRectangle.origin.y - newBody->GetPosition().y * PTM_RATIO);

    sprite->setAnchorPoint(ccp(anchorX / bodyRectangle.size.width, anchorY / bodyRectangle.size.height));

    Can you give me a explanation,please

    ReplyDelete
  9. Anyone can give me idea for undo function for drawing using CCRenderTexture as given in this post.
    how can i remove most recent sprite from a point from CCRenderTexture.

    ReplyDelete
  10. I try to compile it with Cocos2dx3 and had problems . It works but the line is not continuous. You can upload the compiled project with Cocos2dx3??

    ReplyDelete
    Replies
    1. Hi. I'm not really planning on using Cocod2dX3 so updating this code is not very likely to happen. Sorry

      Delete
    2. Yes, i have this problem too, It seems like the _brush->visit(); isn't called in each iteration of the loop for .

      Delete
  11. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. Not really, thanks anyway. I've been mostly away from iphone development for some time now.

      Delete