Draw specific portion of image with touch in CCRenderTexture

I want to create a game in Cocos2dx in which i have an image which is not visible but when i move my finger on device it start drawing. Only that part of image draws where i move my finger. I have attached a screenshot as a reference to make my question more clear.

Any HELP!!!?

Thanks in Advance

Ignore this. I posted a better solution below.

You can also look at using ClippingNode with the image as a child and a DrawNode as the stencil. Then do as @grimfate suggests to draw the touches into the Drawnode using one of the draw commands whether circles for each touch (drawPoint) or something that connects touches using drawSegment (or bezier curves).

This will produce a hard edged “mask” revealing the image, so if you need soft edge you could also look at using a mask shader that takes in the DrawNode and Image as texture0 and texture1.
http://www.raywenderlich.com/4428/how-to-mask-a-sprite-with-cocos2d-2-0

I figured out the solution you probably want. It’s using a ClippingNode. Here’s some code:

// create the node that will remember where you draw
auto drawNode = DrawNode::create();
// create the node which will use the draw node to determine what is shown, i.e. DrawNode will
// be used as a "mask"
auto clippingNode = ClippingNode::create(drawNode);
addChild(clippingNode);
// without this, touches will be square...
clippingNode->setAlphaThreshold(0.0f);

// add the hidden sprite you want to be revealed
auto sprite = CCSprite::create("SpriteFilename.png");
clippingNode->addChild(sprite);

// match the Sprite and ClippingNode
auto size = sprite->getBoundingBox().size;
clippingNode->setContentSize(size);
sprite->setPosition(Point(size.width/2, size.height/2));

// respond to touches by drawing on the DrawNode
auto touchListener = EventListenerTouchOneByOne::create();
touchListener->onTouchBegan = [drawNode](Touch* touch, Event* event){
  Vec2 p = touch->getLocation();
  drawNode->drawDot(p, 20, Color4F(1.0f, 1.0f, 1.0f, 1.0f));
  return true;
};
touchListener->onTouchMoved = [drawNode](Touch* touch, Event* event){
        Vec2 p = touch->getLocation();
        drawNode->drawDot(p, 20, Color4F(1.0f, 1.0f, 1.0f, 1.0f));
    };
_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);

I typed this out, so hopefully there are no mistakes, but it should be easy enough to figure out and fix if there are any. Here’s an explanation of what is happening:

The DrawNode is provided to the ClippingNode as a mask. What this means is that only the parts of the ClippingNode’s children that are under the filled in parts of the DrawNode will be shown. So, if the ClippingNode contained a sprite and I drew a circle on the DrawNode, you would only see the part of the sprite that was within the area of that circle.

Also, I was lazy in the onTouchBegan method. At the start of the function, you should test if the touch lies within the bounds of the ClippingNode and return false if it doesn’t. That means you need to adjust the function to have access to the ClippingNode (to get the area you can touch) and the DrawNode (so that you can draw on it.)

3 Likes

Thanks Grimfate for your reply it is really very helpful. Do you know how to achieve that with CCRenderTexture and ccBlendFunc?

Create a RenderTexture and add it to your scene. Create a DrawNode and the hidden Sprite and retain them but don’t add them to the scene. Then you can either:

a. Apply a blending function of dst=GL_ZERO and src=GL_DST_ALPHA to sprite, or
b. Apply a blending function of dst=GL_ONE_MINUS_SRC_ALPHA and src=GL_ZERO to the DrawNode, and dst=GL_ONE and src=GL_ONE_MINUS_DST_ALPHA to the sprite.

Then whenever you draw on the DrawNode, update the RenderTexture by using

For A: renderTexture->beginWithClear(0, 0, 0, 0);
For B: renderTexture->beginWithClear(0, 0, 0, 1.0f);
drawNode->visit();
sprite->visit();
renderTexture->end();

The difference between A and B is:

  • A will draw the sprite wherever there is colour. The colour is provided by the DrawNode by drawing on it. This method means that the RenderTexture cannot have any colour on it except where you want the sprite drawn. It must be cleared to black with an alpha of 0 before drawing on it.
  • B will draw the sprite wherever there is no colour, i.e. zero alpha value. The DrawNode with its blending function works by “cutting out” the colour from the RenderTexture before the sprite fills in these parts. This requires the RenderTexture to be cleared to a colour with an alpha value of 1.0f or to have another image rendered on the RenderTexture before the DrawNode is visited.

If you do use B, you can actually use an alpha value less than 1.0f and you will see the sprite underneath. So if you use a clear of black with an alpha of something like 0.8f, you will faintly see the sprite underneath. This looks a little like you are cleaning a dirty surface.

Note: I believe the RenderTextures treat themselves as being down the bottom left of the screen when drawing onto them. What this means is that if you touch the bottom left of the screen, it will draw on the bottom left of the RenderTexture, even if you have moved the RenderTexture to the other side of the screen. This means that when you get the position of a touch, you need to adjust it to pretend your RenderTexture is down the bottom-left of the screen. So, I think that’s probably taking away half the width and height of the RenderTexture from the actual position of the touch.

1 Like

It’s not working

How is it not working? Do you get errors? Does it do something it shouldn’t? Or does it do nothing?

it does something it shouldn’t. Its not what i want

A or B? And what is it doing that you don’t want it to do?

Let me share screenshots with you

For option A, result is

It shows nothing happens at all. I move my finger but it draws nothing.

For option B, result is

I touched the screen and on first touch image is shown completely instead of the part where i touched.

The thing i want to achieve is

This is what i want to get but i don’t know how do that.

Here is some rough code, built using Cocos2d-x v3.3beta0:

Header File (HelloWorldScene.h):

class HelloWorld : public cocos2d::Layer
{
public:
    static cocos2d::Scene* createScene();
    virtual bool init();
    CREATE_FUNC(HelloWorld);
    
private:
    // the "hidden" sprite to slowly reveal
    cocos2d::Sprite* spr;
    // draw node defines where the sprite is revealed
    cocos2d::DrawNode* dn;
    // the render texture used to combine the draw node and the sprite
    cocos2d::RenderTexture* rt;
    
    // updates the render texture, applying the latest version of the draw node
    void updateRT();
};

Source file (HelloWorldScene.cpp):

#include "HelloWorldScene.h"

USING_NS_CC;

Scene* HelloWorld::createScene()
{
    // 'scene' is an autorelease object
    auto scene = Scene::create();
    
    // 'layer' is an autorelease object
    auto layer = HelloWorld::create();

    // add layer as a child to scene
    scene->addChild(layer);

    // return the scene
    return scene;
}

// on "init" you need to initialize your instance
bool HelloWorld::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Layer::init() )
    {
        return false;
    }
    
    Size winSize = Director::getInstance()->getWinSize();
    
    /* Add necessary nodes */
    
    // the node which defines where the to draw the hidden image
    dn = DrawNode::create();
    dn->retain();
    
    // the image to reveal
    spr = Sprite::create("HelloWorld.png");
    spr->retain();
    // fill screen
    spr->setScale(winSize.width / spr->getContentSize().width,
        winSize.height / spr->getContentSize().height);
    // center it on screen
    spr->setPosition(Point(spr->getBoundingBox().size.width / 2,
        spr->getBoundingBox().size.height / 2));

    // create render texture
    auto sprBB = spr->getBoundingBox().size;
    rt = RenderTexture::create(sprBB.width, sprBB.height);
    this->addChild(rt);
    rt->setPosition(Point(sprBB.width / 2, sprBB.height / 2));
    
    /* Set up blending functions (Method B) */
    BlendFunc bf;
    // apply blending function to draw node
    bf.dst = GL_ONE_MINUS_SRC_ALPHA;
    bf.src = GL_ZERO;
    dn->setBlendFunc(bf);
    
    // apply blending function to sprite
    bf.src = GL_ONE_MINUS_DST_ALPHA;
    bf.dst = GL_ONE;
    spr->setBlendFunc(bf);
    
    /* Handle touches */
    auto el = EventListenerTouchOneByOne::create();
    // apply initial touch to drawing node
    el->onTouchBegan = [=](Touch* touch, Event* event){
        Vec2 p = touch->getLocation();
        dn->drawDot(p, 20, Color4F(1.0f, 1.0f, 1.0f, 1.0f));
        this->updateRT();
        return true;
    };
    // apply touch's new location to drawing node
    el->onTouchMoved = [=](Touch* touch, Event* event){
        Vec2 p = touch->getLocation();
        dn->drawDot(p, 20, Color4F(1.0f, 1.0f, 1.0f, 1.0f));
        this->updateRT();
    };
    
    _eventDispatcher->addEventListenerWithSceneGraphPriority(el, this);
    
    // set the render texture to its initial state
    updateRT();
    
    return true;
}

// clears the render texture, cuts out the parts where the drawing node has been
// coloured and then draws the sprite where in the cut out areas
void HelloWorld::updateRT() {
    rt->beginWithClear(0.0f, 0.0f, 0.0f, 1.0f);
    dn->visit();
    spr->visit();
    rt->end();
}

This uses method B and it works for me. It should create a black screen that slowly reveals the image as it is touched. I tested this as an OSX app.

1 Like

It works Grimfate but there is a little problem. RenderTexture’s empty part is black. How can we make it transparent?

Oh, you need method A.

Change the blending function section from

 /* Set up blending functions (Method B) */
    BlendFunc bf;
    // apply blending function to draw node
    bf.dst = GL_ONE_MINUS_SRC_ALPHA;
    bf.src = GL_ZERO;
    dn->setBlendFunc(bf);
    
    // apply blending function to sprite
    bf.src = GL_ONE_MINUS_DST_ALPHA;
    bf.dst = GL_ONE;
    spr->setBlendFunc(bf);

to

 /* Set up blending functions (Method A) */
    BlendFunc bf;    
    // apply blending function to sprite
    bf.src = GL_DST_ALPHA;
    bf.dst = GL_ZERO;
    spr->setBlendFunc(bf);

And change

rt->beginWithClear(0.0f, 0.0f, 0.0f, 1.0f);

to

renderTexture->beginWithClear(0, 0, 0, 0);

It’s turning empty parts white instead of transparent

Can you provide a screenshot of what is happening?

Here’s screenshot

What should be in the white areas instead of white?

Whatever the background is. If i add a background sprite it gets hide by the white color(empty portion) of rendertexture

This happens if the RenderTexture is larger than the sprite and if the sprite isn’t centered. Based on your image, you need to either stretch the image to the same size as the screen, or shrink the RenderTexture to match the size of the sprite.

FYI If you shrink the RenderTexture and position it away from the bottom-left of the screen, you will need to adjust the location of your touches to account for this, because the RenderTexture seems to think it’s down the bottom-left when you draw on it, no matter where on screen it actually is.