Touch detection on non-transparent pixels


#1

Hi,
I know this topic was quite a few times here on the forums, but all the topics are pretty old and it doesn’t work for me.

For example I’ve found this:

But hit test is always failing (red all the time), maybe I’m doing it wrong?

bool HelloWorld::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
    
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();
    
    root = Node::create();
    addChild(root);

    FileUtils::getInstance()->addSearchPath("res");
    sprite = Sprite::create("image.png");
    sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));
    sprite->setColor(Color3B::RED);
    root->addChild(sprite);
    
    auto listener = EventListenerTouchOneByOne::create();
    listener->setSwallowTouches(true);
    
    listener->onTouchBegan = [&](Touch* touch, Event* event){
        Point locationInNode = root->convertToNodeSpace(touch->getLocation());
        if(HitDetectHelper::hitTest(sprite, locationInNode)){
            sprite->setColor(Color3B::GREEN);
            return true;
        }
        else{
            sprite->setColor(Color3B::RED);
        }
        
        return false;
    };
    
    listener->onTouchMoved = [&](Touch* touch, Event* event){
        Point locationInNode = root->convertToNodeSpace(touch->getLocation());
        if(HitDetectHelper::hitTest(sprite, locationInNode)){
            sprite->setColor(Color3B::GREEN);
        }
        else{
            sprite->setColor(Color3B::RED);
        }
    };
    
    listener->onTouchEnded = [&](Touch* touch, Event* event){
        sprite->setColor(Color3B::RED);
    };
    
    listener->onTouchCancelled = [&](Touch* touch, Event* event){
        sprite->setColor(Color3B::RED);
    };
    
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
    
    return true;
}

Does anyone have a working example as for 3.14?


#2

In your code, change
Point locationInNode = root->convertToNodeSpace(touch->getLocation());
to
Point locationInNode = sprite->convertToNodeSpace(touch->getLocation());

Also, HitDetectHelper doesn’t seem to check if the point is in the sprite’s bounding box. I believe checking the bounding box is cheaper than checking the pixel value, so doing the bounding box test first means you can skip the pixel test if the touch is outside of the sprite. Then check the pixel value if the point is inside the bounding box.


#3

Thanks for the hint, sure it should be sprite->convertToNodeSpace. However it still doesn’t work for me. Checking bounding box is a sure thing, but it’s just for testing purposes.


#4

I don’t understand.
I made a simple test:

std::string str = "";
for(int j = sprite->getContentSize().height; j >= 0; j--){
    for(int i = 0; i < sprite->getContentSize().width; i++){
        bool h = HitDetectHelper::hitTest(sprite, Vec2(i, j));
        str += h ? "#" : " ";
    }
    str += "\n";
}

It printed me all hit tests for each x, y and after that a little ascii “image” with # on non transparent pixels:

However when I call this in “onTouchBegan” the “image” looks like this:


#5

Is that first ascii image correct? And is the code to draw the ascii image the same in onTouchBegan? Does it take the touch location into account?


#6

I started a new project, copied in the header and class for HitDetectHelper, copied your code into HelloWorldScene, changed image.png to HelloWorld.png (because I don’t have your image), and changed Point locationInNode = root->convertToNodeSpace(touch->getLocation()); to Point locationInNode = sprite->convertToNodeSpace(touch->getLocation()); in both onTouchBegan and onTouchMoved, and the sprite changes to green when the touch is inside a non-transparent part of it.

I’m not sure if you are aware, but just in case this could be your problem, because you return false from onTouchBegan if you touch outside the sprite or touch a transparent part inside the sprite, onTouchMoved is never called if you didn’t touch a non-transparent part of the sprite when the touch began. So if your touch begins outside the sprite, it will never turn green, even if the touch moves inside it.


#7

Here’s the image:

Yes, the code to draw the ascii image is the same in onTouchBegan. It doesn’t take touch location into account, it just scans whole image.

Latest code:

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

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()
{
    if ( !Layer::init() )
    {
        return false;
    }
    
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();
    
    root = Node::create();
    addChild(root);

    FileUtils::getInstance()->addSearchPath("res");
    sprite = Sprite::create("image.png");
    sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));
    sprite->setColor(Color3B::RED);
    root->addChild(sprite);
    
    auto listener = EventListenerTouchOneByOne::create();
    listener->setSwallowTouches(true);
    
    std::string str = "";
    for(int j = sprite->getContentSize().height; j >= 0; j--){
        for(int i = 0; i < sprite->getContentSize().width; i++){
            bool h = HitDetectHelper::hitTest(sprite, Vec2(i, j));
            str += h ? "#" : " ";
        }
        str += "\n";
    }
    
    CCLOG("%s", str.c_str());
    
    listener->onTouchBegan = [&](Touch* touch, Event* event){
        Point locationInNode = sprite->convertToNodeSpace(touch->getLocation());
        Point roundedLocation = Point(round(locationInNode.x), round(locationInNode.y));
        bool h = HitDetectHelper::hitTest(sprite, roundedLocation);
        CCLOG("onTouchBegan(%f, %f): %s", roundedLocation.x, roundedLocation.y, h ? "true" : "false");
        if(h){
            sprite->setColor(Color3B::GREEN);
        }
        else{
            sprite->setColor(Color3B::RED);
        }
        
        std::string str = "";
        for(int j = sprite->getContentSize().height; j >= 0; j--){
            for(int i = 0; i < sprite->getContentSize().width; i++){
                bool h = HitDetectHelper::hitTest(sprite, Vec2(i, j));
                str += h ? "#" : " ";
            }
            str += "\n";
        }
        
        CCLOG("%s", str.c_str());
        
        
        return true;
    };
    
    listener->onTouchMoved = [&](Touch* touch, Event* event){
        Point locationInNode = sprite->convertToNodeSpace(touch->getLocation());
        Point roundedLocation = Point(round(locationInNode.x), round(locationInNode.y));
        bool h = HitDetectHelper::hitTest(sprite, roundedLocation);
        CCLOG("onTouchMoved(%f, %f): %s", roundedLocation.x, roundedLocation.y, h ? "true" : "false");
        if(h){
            sprite->setColor(Color3B::GREEN);
        }
        else{
            sprite->setColor(Color3B::RED);
        }
    };
    
    listener->onTouchEnded = [&](Touch* touch, Event* event){
        sprite->setColor(Color3B::RED);
    };
    
    listener->onTouchCancelled = [&](Touch* touch, Event* event){
        sprite->setColor(Color3B::RED);
    };
    
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
    
    return true;
}

#8

Is this code working for you? Because this code works for me. I copied and pasted your new code and the ascii draws correctly when touch begins and the sprite will turn green when the sprite is touched or the touch moves into the sprite.


#9

No it’s not.
As I said first ascii image is fine, but when I touch screen ascii image is cropped (see image above). I don’t know what causes this. Do you run on device?
I’m running this app on iPad Air with iOS 10.2.1.


#10

I am trying this on MacOS, but I will try it on my iPad Air tomorrow if you have not solved it by then.


#11

@piotrros I have just tested this with cocos2d-x 3.13.1 (it was 3.14 or 3.14.1 on MacOS) on an iPad Air 2, running iOS 10.3, and the code is working for me; the sprite turns green when touch is isn’t the non-transparent areas of the sprite, using both your sprite and the HelloWorld.png sprite.

I’m not sure what else could be affecting this. Could you post your AppDelegate.cpp file? Is there any other code being run between the HelloWorld::init() and the first touch?


#12

No, I don’t have anything special there.
Try it out:
Test.zip (4.8 MB)


#13

OK, I have finally been able to produce results similar to yours. I achieved this on the iPhone 6 simulator. For me, the top of sprite was cut off in the ascii and so touching the sprite would turn it green, unless it was at the top of the sprite.

I have isolated the problem to 2 areas. First, it is caused by a combination of the RenderTexture begin/end calls together with manually calling the Renderer's render function. I’m not sure why this is yet.

And the second area is the ResolutionPolicy. When I disabled the ascii code, I noticed when I first touched the screen that the “debug text”, i.e. the FPS counter, would move from near the bottom of the screen to the bottom. So I tried changing the ResolutionPolicy from ResolutionPolicy::NO_BORDER to ResolutionPolicy::EXACT_FIT and it seemed to fix the problem.

So, in AppDelegate.cpp, try changing
glview->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, ResolutionPolicy::NO_BORDER);
to
glview->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, ResolutionPolicy::EXACT_FIT);

I will keep looking into this tomorrow, as I still don’t know exactly what is causing this. Changing the ResolutionPolicy seems like it makes things work the way they should, but it isn’t an actual fix, because you might want to use the “no border” policy.


#14

EXACT_FIT fixed the problem. But I’m using NO_BORDER in all my projects.


#15

Ok, finally I think I have figured it out. I discovered that the reason why it was not working properly was because the Sprite was being culled, and I was about to give up because I wasn’t sure how to fix it (besides disabling culling), but thankfully I tried one last thing which I think has fixed the issue:

Solution

In HitDetectHelper, change
_sharedRenderTexture->setVirtualViewport(Point(0.0f, 0.0f), Rect(0.0f, 0.0f, winSize.width, winSize.height), Rect(0.0f, 0.0f, winSizeInPixels.width, winSizeInPixels.height));
to
_sharedRenderTexture->setVirtualViewport(Director::getInstance()->getVisibleOrigin(), Rect(0.0f, 0.0f, winSize.width, winSize.height), Rect(0.0f, 0.0f, winSizeInPixels.width, winSizeInPixels.height));

And change
sprite->setPosition(-point.x, -point.y);
to
sprite->setPosition(Vec2(-point.x, -point.y) + Director::getInstance()->getVisibleOrigin());

What I believe the first part is doing (as I have never used setVirtualViewport() before) is changing the location of where the bottom-left of the RenderTexture's image captures, and then the Sprite is moved to that new bottom-left point. This means the Sprite is being drawn inside the “visible area” and isn’t culled anymore.

Hopefully this works for you.

Explanation

  • The NO_BORDERS RenderPolicy appears to cause a visible origin that is not (0, 0). (I’m actually unfamiliar with what “visible origin” is, but I’m guessing it is when your design resolution is a different ratio than the window/screen resolution, and so it tells you where on the screen the rendering actually begins.)
  • As you may or may not be aware, by default when rendering to a RenderTexture, the bottom-left of the RenderTexture is (0, 0) in world space, i.e. the bottom-left of the screen. So, for example, if you wanted to copy a Sprite using a RenderTexture, you would need to move the Sprite so that it’s bottom-left corner is at (0, 0) in world space.
  • Because of this, when using a RenderTexture to capture part of a Sprite, that part of the Sprite needs to be moved to (0, 0). This is how HitDetectHelper does it: It moves the touched location of the Sprite to (0, 0), renders it to the RenderTexture, moves the Sprite back (so that it looks like it was never moved at all), and then reads the bottom-left pixel of the RenderTexture image.
  • Cocos has something called “culling”. I’m pretty sure what this means is that if the engine detects that a Sprite is not on the screen, then it will skip drawing it to improve performance. (Why bother drawing something that isn’t on screen?)
  • The problem is that when HitDetectHelper is not working properly that is because it is moving the Sprite past the visible origin, and because the culling process takes the visible origin into account, the engine doesn’t draw it. Because the Sprite is not drawn, it doesn’t appear on the RenderTexture, and thus the pixel being read remains blank.

Hopefully that is understandable.


#16

Wow, you rock! Thanks for help! Yes, I understood the explanation perfectly.
So the way how it works is pretty messy.
How do you think will it be hard to change Sprite to Node? I mean I can have a node with several children or have something else like sprite skeleton. In order to make any sense of using it in my app is to be able to support all of these features.
In the nutshell the idea is to make it as universal as possible. I have a container of something and it doesn’t matter what’s inside.


Pixel perfect touch or how to determine if a pixel of a sprite is transparent
#17

Basically switch the parameter of the function from Sprite* to Node* and remove the if statement block that checks sprite->getBatchNode(), and I would expect it to work with any Node. Basically anything that can be rendered to a RenderTexture can be used, which happens to be Node* and everything that derives from it.

That said, I’m not sure of the importance of that if statement block. It might be necessary for Sprites that have batch nodes, so it might be a good idea to check if the Node is a Sprite and, if it is, to cast the Node as a Sprite and then use the if statement block with the cast Sprite.


#18

Looks like it’s working nice with multiple children. In my test I’ve added 2 sprites in different location, one was flipped and spine model. All these were inside one node, where I was performing hit test.

I wanted to create github page for this, but I’m not an author of this algorithm so I’ll just post final code here:

source:

//
//  HitDetectHelper.cpp
//
//  Created by Alexander Wong on 1/21/15.
//
//

#include "HitDetectHelper.h"

USING_NS_CC;

#pragma mark - HitDetectHelper

RenderTexture* HitDetectHelper::_sharedRenderTexture = NULL;
HitDetectHelper::HitDetectHelperNode* HitDetectHelper::_helperNode = NULL;

#pragma mark - Pixel Test

bool HitDetectHelper::hitTest(Node* node, Touch* touch, float extraWidth, float extraHeight){
    if(!hitTestJustBounds(node, touch, extraWidth, extraHeight)){
        return false;
    }
    return hitTestJustPixels(node, touch);
}

bool HitDetectHelper::hitTestJustBounds(cocos2d::Node* node, cocos2d::Touch* touch, float extraWidth, float extraHeight){
    return Rect(0, 0, node->getContentSize().width + extraWidth, node->getContentSize().height + extraHeight).containsPoint(node->convertToNodeSpace(touch->getLocation()));
}

bool HitDetectHelper::hitTestJustPixels(Node* node, cocos2d::Touch* touch)
{
    Point point = node->convertToNodeSpace(touch->getLocation());
    if (!_sharedRenderTexture) {
        _sharedRenderTexture = RenderTexture::create(1, 1);
        _sharedRenderTexture->retain();
        
        _sharedRenderTexture->setKeepMatrix(true);
        Size winSize = Director::getInstance()->getWinSize();
        Size winSizeInPixels = Director::getInstance()->getWinSizeInPixels();
        _sharedRenderTexture->setVirtualViewport(Director::getInstance()->getVisibleOrigin(), Rect(0.0f, 0.0f, winSize.width, winSize.height), Rect(0.0f, 0.0f, winSizeInPixels.width, winSizeInPixels.height));
        _sharedRenderTexture->setAutoDraw(false);
    }
    
    if (!_helperNode) {
        _helperNode = HitDetectHelperNode::create(Point::ZERO);
        _helperNode->retain();
    }
    else {
        _helperNode->reset();
    }
    
    _sharedRenderTexture->beginWithClear(0.0f, 0.0f, 0.0f, 0.0f);
    
    if(auto sprite = dynamic_cast<Sprite* >(node)){
        if (sprite->getBatchNode()) {
            Sprite* tempSprite = Sprite::createWithTexture(sprite->getTexture());
            tempSprite->setTextureRect(sprite->getTextureRect(), sprite->isTextureRectRotated(), sprite->getContentSize());
            sprite = tempSprite;
        }
    }
    
    // store transforms
    Point position = node->getPosition();
    Point anchorPoint = node->getAnchorPoint();
    float rotationX = node->getRotationSkewX();
    float rotationY = node->getRotationSkewY();
    float skewX = node->getSkewX();
    float skewY = node->getSkewY();
    
    // unset transforms
    node->setAnchorPoint(Point::ANCHOR_BOTTOM_LEFT);
    node->setPosition(Vec2(-point.x, -point.y) + Director::getInstance()->getVisibleOrigin());
    node->setRotationSkewX(0.0f);
    node->setRotationSkewY(0.0f);
    node->setSkewX(0.0f);
    node->setSkewY(0.0f);
    
    // draw node
    node->visit();
    
    // add command to read pixel
    _helperNode->visit();
    
    // revert transforms
    node->setAnchorPoint(anchorPoint);
    node->setPosition(position);
    node->setRotationSkewX(rotationX);
    node->setRotationSkewY(rotationY);
    node->setSkewX(skewX);
    node->setSkewY(skewY);
    
    _sharedRenderTexture->end();
    
    Director::getInstance()->getRenderer()->render(); // render texture fix
    
    uint8_t p0 = _helperNode->getPixelValue(0);
    uint8_t p1 = _helperNode->getPixelValue(1);
    uint8_t p2 = _helperNode->getPixelValue(2);
    uint8_t p3 = _helperNode->getPixelValue(3);
    
    return p0 || p1 || p2 || p3;
}

#pragma mark - HitDetectHelperNode

#pragma mark - Reset

void HitDetectHelper::HitDetectHelperNode::reset()
{
    _pixelRead = false;
    
    _pixelBuffer[0] = 0;
    _pixelBuffer[1] = 0;
    _pixelBuffer[2] = 0;
    _pixelBuffer[3] = 0;
}

#pragma mark - Pixel Buffer

uint8_t HitDetectHelper::HitDetectHelperNode::getPixelValue(unsigned int index)
{
    CCASSERT(_pixelRead, "Pixel not read yet.");
    CCASSERT(index < 4, "Out of bounds.");
    
    return _pixelBuffer[index];
}

#pragma mark - Draw

void HitDetectHelper::HitDetectHelperNode::draw(Renderer *renderer, const Mat4& transform, uint32_t flags)
{
    _customCommand.init(_globalZOrder);
    _customCommand.func = CC_CALLBACK_0(HitDetectHelper::HitDetectHelperNode::readPixel, this);
    renderer->addCommand(&_customCommand);
}

#pragma mark - Read Pixel

void HitDetectHelper::HitDetectHelperNode::readPixel()
{
    _pixelRead = true;
    
    glReadPixels(_pixelPoint.x, _pixelPoint.y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, _pixelBuffer);
}

#pragma mark - Constructors

HitDetectHelper::HitDetectHelperNode* HitDetectHelper::HitDetectHelperNode::create(const Point& pixelPoint)
{
    HitDetectHelper::HitDetectHelperNode* node = new HitDetectHelper::HitDetectHelperNode();
    if (node && node->init(pixelPoint)) {
        node->autorelease();
    }
    else {
        CC_SAFE_DELETE(node);
    }
    return node;
}

HitDetectHelper::HitDetectHelperNode::HitDetectHelperNode()

// instance variables
: _pixelRead(false)

{
    
}

bool HitDetectHelper::HitDetectHelperNode::init(const Point &pixelPoint)
{
    _pixelPoint = pixelPoint;
    
    reset();
    
    return Node::init();
}

header:

//
//  HitDetectHelper.h
//
//  Created by Alexander Wong on 1/21/15.
//
//

#ifndef __HitDetectHelper__
#define __HitDetectHelper__

#include "cocos2d.h"

class HitDetectHelper
{
    class HitDetectHelperNode;
    
private:
    
    static cocos2d::RenderTexture* _sharedRenderTexture;
    static HitDetectHelperNode* _helperNode;
    
#pragma mark - Hit Test
    
public:
    
    static bool hitTest(cocos2d::Node* node, cocos2d::Touch* touch, float extraWidth = 0.0f, float extraHeight = 0.0f);
    static bool hitTestJustBounds(cocos2d::Node* node, cocos2d::Touch* touch, float extraWidth = 0.0f, float extraHeight = 0.0f);
    static bool hitTestJustPixels(cocos2d::Node* node, cocos2d::Touch* touch);
    
private:
    
    CC_DISALLOW_IMPLICIT_CONSTRUCTORS(HitDetectHelper)
    
#pragma mark - HitDetectHelperNode
    
private:
    
    class HitDetectHelperNode : public cocos2d::Node
    {
        
    private:
        
        cocos2d::CustomCommand _customCommand;
        uint8_t _pixelBuffer[4];
        
#pragma mark - Properties
        
        CC_SYNTHESIZE(bool, _pixelRead, PixelRead);
        CC_SYNTHESIZE(cocos2d::Point, _pixelPoint, PixelPoint);
        
#pragma mark - Reset
        
    public:
        
        void reset();
        
#pragma mark - Pixel Buffer
        
    public:
        
        uint8_t getPixelValue(unsigned int index);
        
#pragma mark - Draw
        
    public:
        
        virtual void draw(cocos2d::Renderer *renderer, const cocos2d::Mat4& transform, uint32_t flags) override;
        
#pragma mark - Read Pixel
        
    private:
        
        void readPixel();
        
#pragma mark - Constructors
        
    public:
        
        static HitDetectHelperNode* create(const cocos2d::Point& pixelPoint);
        
    CC_CONSTRUCTOR_ACCESS:
        
        HitDetectHelperNode();
        virtual ~HitDetectHelperNode() {};
        
        bool init(const cocos2d::Point& pixelPoint);
        
    private:
        
        CC_DISALLOW_COPY_AND_ASSIGN(HitDetectHelperNode);
        
    };
    
};

#endif /* defined(__HitDetectHelper__) */

#19

I’ve found a bug in code. Pixel checking doesn’t get scale into account. Here’s fixed code:

source:

//
//  HitDetectHelper.cpp
//
//  Created by Alexander Wong on 1/21/15.
//
//

#include "HitDetectHelper.h"

USING_NS_CC;

#pragma mark - HitDetectHelper

RenderTexture* HitDetectHelper::_sharedRenderTexture = NULL;
HitDetectHelper::HitDetectHelperNode* HitDetectHelper::_helperNode = NULL;

#pragma mark - Pixel Test

bool HitDetectHelper::hitTest(Node* node, Touch* touch, float extraWidth, float extraHeight){
if(!hitTestJustBounds(node, touch, extraWidth, extraHeight)){
    return false;
}
else{
    if(extraWidth > 0 || extraHeight > 0){
        return true;
    }
}
return hitTestJustPixels(node, touch);
}

bool HitDetectHelper::hitTestJustBounds(cocos2d::Node* node, cocos2d::Touch* touch, float extraWidth, float extraHeight){
return Rect(0, 0, node->getContentSize().width + extraWidth, node->getContentSize().height + extraHeight).containsPoint(node->convertToNodeSpace(touch->getLocation()));
}

bool HitDetectHelper::hitTestJustPixels(Node* node, cocos2d::Touch* touch)
{
Point point = node->convertToNodeSpace(touch->getLocation());
if (!_sharedRenderTexture) {
    _sharedRenderTexture = RenderTexture::create(1, 1);
    _sharedRenderTexture->retain();
    
    _sharedRenderTexture->setKeepMatrix(true);
    Size winSize = Director::getInstance()->getWinSize();
    Size winSizeInPixels = Director::getInstance()->getWinSizeInPixels();
    _sharedRenderTexture->setVirtualViewport(Director::getInstance()->getVisibleOrigin(), Rect(0.0f, 0.0f, winSize.width, winSize.height), Rect(0.0f, 0.0f, winSizeInPixels.width, winSizeInPixels.height));
    _sharedRenderTexture->setAutoDraw(false);
}

if (!_helperNode) {
    _helperNode = HitDetectHelperNode::create(Point::ZERO);
    _helperNode->retain();
}
else {
    _helperNode->reset();
}

_sharedRenderTexture->beginWithClear(0.0f, 0.0f, 0.0f, 0.0f);

if(auto sprite = dynamic_cast<Sprite* >(node)){
    if (sprite->getBatchNode()) {
        Sprite* tempSprite = Sprite::createWithTexture(sprite->getTexture());
        tempSprite->setTextureRect(sprite->getTextureRect(), sprite->isTextureRectRotated(), sprite->getContentSize());
        sprite = tempSprite;
    }
}

// store transforms
Point position = node->getPosition();
Point anchorPoint = node->getAnchorPoint();
float rotationX = node->getRotationSkewX();
float rotationY = node->getRotationSkewY();
float skewX = node->getSkewX();
float skewY = node->getSkewY();
float scaleX = node->getScaleX();
float scaleY = node->getScaleY();

// unset transforms
node->setAnchorPoint(Point::ANCHOR_BOTTOM_LEFT);
node->setPosition(Vec2(-point.x, -point.y) + Director::getInstance()->getVisibleOrigin());
node->setRotationSkewX(0.0f);
node->setRotationSkewY(0.0f);
node->setSkewX(0.0f);
node->setSkewY(0.0f);
node->setScaleX(1.0f);
node->setScaleY(1.0f);

// draw node
node->visit();

// add command to read pixel
_helperNode->visit();

// revert transforms
node->setAnchorPoint(anchorPoint);
node->setPosition(position);
node->setRotationSkewX(rotationX);
node->setRotationSkewY(rotationY);
node->setSkewX(skewX);
node->setSkewY(skewY);
node->setScaleX(scaleX);
node->setScaleY(scaleY);

_sharedRenderTexture->end();

Director::getInstance()->getRenderer()->render(); // render texture fix

uint8_t p0 = _helperNode->getPixelValue(0);
uint8_t p1 = _helperNode->getPixelValue(1);
uint8_t p2 = _helperNode->getPixelValue(2);
uint8_t p3 = _helperNode->getPixelValue(3);

return p0 || p1 || p2 || p3;
}

#pragma mark - HitDetectHelperNode

#pragma mark - Reset

void HitDetectHelper::HitDetectHelperNode::reset()
{
_pixelRead = false;

_pixelBuffer[0] = 0;
_pixelBuffer[1] = 0;
_pixelBuffer[2] = 0;
_pixelBuffer[3] = 0;
}

#pragma mark - Pixel Buffer

uint8_t HitDetectHelper::HitDetectHelperNode::getPixelValue(unsigned int index)
{
CCASSERT(_pixelRead, "Pixel not read yet.");
CCASSERT(index < 4, "Out of bounds.");

return _pixelBuffer[index];
}

#pragma mark - Draw

void HitDetectHelper::HitDetectHelperNode::draw(Renderer *renderer, const Mat4& transform, uint32_t flags)
{
_customCommand.init(_globalZOrder);
_customCommand.func = CC_CALLBACK_0(HitDetectHelper::HitDetectHelperNode::readPixel, this);
renderer->addCommand(&_customCommand);
}

#pragma mark - Read Pixel

void HitDetectHelper::HitDetectHelperNode::readPixel()
{
_pixelRead = true;

glReadPixels(_pixelPoint.x, _pixelPoint.y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, _pixelBuffer);
}

#pragma mark - Constructors

HitDetectHelper::HitDetectHelperNode* HitDetectHelper::HitDetectHelperNode::create(const Point& pixelPoint)
{
HitDetectHelper::HitDetectHelperNode* node = new HitDetectHelper::HitDetectHelperNode();
if (node && node->init(pixelPoint)) {
    node->autorelease();
}
else {
    CC_SAFE_DELETE(node);
}
return node;
}

HitDetectHelper::HitDetectHelperNode::HitDetectHelperNode()

// instance variables
: _pixelRead(false)

{

}

bool HitDetectHelper::HitDetectHelperNode::init(const Point &pixelPoint)
{
_pixelPoint = pixelPoint;

reset();

return Node::init();
}

header:

//
//  HitDetectHelper.h
//
//  Created by Alexander Wong on 1/21/15.
//
//

#ifndef __HitDetectHelper__
#define __HitDetectHelper__

#include "cocos2d.h"

class HitDetectHelper
{
    class HitDetectHelperNode;
    
private:
    
    static cocos2d::RenderTexture* _sharedRenderTexture;
    static HitDetectHelperNode* _helperNode;
    
#pragma mark - Hit Test
    
public:
    
    static bool hitTest(cocos2d::Node* node, cocos2d::Touch* touch, float extraWidth = 0.0f, float extraHeight = 0.0f);
    static bool hitTestJustBounds(cocos2d::Node* node, cocos2d::Touch* touch, float extraWidth = 0.0f, float extraHeight = 0.0f);
    static bool hitTestJustPixels(cocos2d::Node* node, cocos2d::Touch* touch);
    
private:
    
    CC_DISALLOW_IMPLICIT_CONSTRUCTORS(HitDetectHelper)
    
#pragma mark - HitDetectHelperNode
    
private:
    
    class HitDetectHelperNode : public cocos2d::Node
    {
        
    private:
        
        cocos2d::CustomCommand _customCommand;
        uint8_t _pixelBuffer[4];
        
#pragma mark - Properties
        
        CC_SYNTHESIZE(bool, _pixelRead, PixelRead);
        CC_SYNTHESIZE(cocos2d::Point, _pixelPoint, PixelPoint);
        
#pragma mark - Reset
        
    public:
        
        void reset();
        
#pragma mark - Pixel Buffer
        
    public:
        
        uint8_t getPixelValue(unsigned int index);
        
#pragma mark - Draw
        
    public:
        
        virtual void draw(cocos2d::Renderer *renderer, const cocos2d::Mat4& transform, uint32_t flags) override;
        
#pragma mark - Read Pixel
        
    private:
        
        void readPixel();
        
#pragma mark - Constructors
        
    public:
        
        static HitDetectHelperNode* create(const cocos2d::Point& pixelPoint);
        
    CC_CONSTRUCTOR_ACCESS:
        
        HitDetectHelperNode();
        virtual ~HitDetectHelperNode() {};
        
        bool init(const cocos2d::Point& pixelPoint);
        
    private:
        
        CC_DISALLOW_COPY_AND_ASSIGN(HitDetectHelperNode);
        
    };
    
};

#endif /* defined(__HitDetectHelper__) */