Pixel collision detection for v3.x

I must apologize, I was testing it wrong.

There was indeed a problem with sprite collisions. I have edited the first post with updated code.
It should work correctly now. Let me know if you find any problem.

I also added an opacity threshold (kOpacityThreshold constant in .cpp), to avoid collision detection with almost-invisible pixels.

@sgleizes Thank you so much for this post. I am kind of confusing using RenderTexture and GLProgram in version 3.
But this did not work well with my test, the buffer array values are always zeros . Thus, the result is always false. I am using version 3.2 final.
Could you please have a check at your code ?
I have saved image from TextureRender _rt and the image was all transparent

Thank you for giving feedback, any problem you find prevents unexpected future problems from happening to me :smile:.

I could not reproduce what you’re having though. Could you provide more info about how you instantiate your sprites in your scene and how/where you do the call ?
Please also mention the platform with which you’re testing.

Thank you again.

1 Like

@sgleizes I am sorry that was my mistake in using the wrong file path. Your code works like a charm. You are the real MVP.

@sgleizes I have just came up with an idea about performance of pixel perfect. Instead of testing 1 Sprite with 1 Sprite each time. We can render all the obstacles sprites of which bounding boxes collide with player sprite blue. Do you think it is feasible ?

Thanks a lot for sharing the code. I’ve managed to get this working under cocos2d-js 3.5 using jsb.
However, this code as is did not always worked well. I had to fix the getIntersection calculation.
I’ve also added some debug visualization so you can see the rendered collision.

Please comment out my debug utility SDEBUG.

My fixed code PixelCollision.cpp:

//
//  PixelCollision.cpp
//

#include "PixelCollision.h"

static const auto kVertexShader = "res/Shaders/SolidColorShader.vsh";
static const auto kFragmentShader = "res/Shaders/SolidColorShader.fsh";
static const auto kShaderRedUniform = "u_color_red";
static const auto kShaderBlueUniform = "u_color_blue";
static const auto kOpacityThreshold = 50;

PixelCollision* PixelCollision::s_instance = nullptr;

// Private Constructor being called from within the GetInstance handle
PixelCollision::PixelCollision(void) :
		_glProgram(nullptr),
		_rt(nullptr),
		_pixelReader(nullptr) {
	_glProgram = GLProgram::createWithFilenames(kVertexShader, kFragmentShader);
	_glProgram->addAttribute(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION);
	_glProgram->addAttribute(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORD);
	_glProgram->addAttribute(GLProgram::ATTRIBUTE_NAME_COLOR, GLProgram::VERTEX_ATTRIB_COLOR);

	const Size &size = Director::sharedDirector()->getWinSize();
	_rt = RenderTexture::create(size.width, size.height, kCCTexture2DPixelFormat_RGBA8888);
	_pixelReader = PixelReaderNode::create(Point::ZERO);

	_glProgram->retain();
	_rt->retain();
	_pixelReader->retain();
}

PixelCollision::~PixelCollision(void) {
	_glProgram->release();
	_rt->release();
	_pixelReader->release();
}

PixelCollision* PixelCollision::getInstance(void) {
	if (!s_instance) {
		s_instance = new PixelCollision();
	}
	return s_instance;
}

void PixelCollision::destroyInstance(void) {
	delete s_instance;
	s_instance = nullptr;
}

bool PixelCollision::collidesWithSprite(Sprite *sprite1, Sprite *sprite2) {
	return this->collidesWithSprite(sprite1, sprite2, true);
}

bool PixelCollision::collidesWithSprite(Sprite *sprite1, Sprite *sprite2, bool pp)
{
#ifdef DEBUG_COLLISION_TEST_VISUALIZATION
    // debug
    //
    Scene *cur_n = Director::getInstance()->getRunningScene();
    Sprite *s_blend = dynamic_cast<Sprite*>(cur_n->getChildByTag(11));
    if (s_blend) {
        s_blend->removeFromParent();
    }
    //
    // /debug
#endif

    Rect r1 = Rect(0, 0, sprite1->getContentSize().width, sprite1->getContentSize().height);
    r1 = RectApplyAffineTransform(r1, sprite1->getNodeToWorldAffineTransform());
    //SDEBUG(LEVEL_DEBUG, "dragging", "Collision test: r1_new=%s;", RectStr(r1));


    Rect r2 = Rect(0, 0, sprite2->getContentSize().width, sprite2->getContentSize().height);
    r2 = RectApplyAffineTransform(r2, sprite2->getNodeToWorldAffineTransform());
    //SDEBUG(LEVEL_DEBUG, "dragging", "Collision test: r2_new=%s;", RectStr(r2));

    if (r1.intersectsRect(r2)) {
        if (!pp) {
            SDEBUG(LEVEL_DEBUG, "dragging", "Collision test (intersects/pixelPerfect): ACCEPTED by !pp; rect1=%s; rect2=%s;", RectStr(r1), RectStr(r2));
            return true;
        }

        Rect intersection = this->getIntersection(r1, r2);
        unsigned int numPixels = intersection.size.width * intersection.size.height;

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

        CustomCommand sprite1Command;
        CustomCommand sprite2Command;
        auto oldPosition1 = this->renderSprite(sprite1, sprite1Command, true);
        auto oldPosition2 = this->renderSprite(sprite2, sprite2Command, false);

#ifdef DEBUG_COLLISION_TEST_VISUALIZATION
        // debug
        //
        Node *n;
        n = this->drawRect(intersection, Color4F(255, 255, 0, 1));
        n->visit();
        //
        // /debug
#endif

	    _pixelReader->setReadPoint(intersection.origin);
	    _pixelReader->setReadSize(intersection.size);
        _pixelReader->reset();
	    _pixelReader->visit();

	    auto buffer = _pixelReader->getBuffer();
	    _rt->end();
	    Director::getInstance()->getRenderer()->render();

	    this->resetSprite(sprite1, oldPosition1);
	    this->resetSprite(sprite2, oldPosition2);

	    unsigned int maxIndex = numPixels * 4;
        bool retVal = false;
	    for (unsigned int i = 0; i < maxIndex; i += 4) {
	        if (buffer[i] > 0 && buffer[i + 2] > 0 && buffer[i + 3] > kOpacityThreshold) { // red and blue
                retVal = true;
                break;
            }
	    }
        SDEBUG(LEVEL_DEBUG, "dragging", "Collision test (intersects/pixelPerfect): retVal=%s; rect1=%s; rect2=%s; intersection=%s; numPixels=%d; CC_CONTENT_SCALE_FACTOR=%.2f; s1.scale=%.2f; s2.scale=%.2f;",
            BOOLStr(retVal), RectStr(r1), RectStr(r2), RectStr(intersection), numPixels, CC_CONTENT_SCALE_FACTOR(), sprite1->getScale(), sprite2->getScale());
        
#ifdef DEBUG_COLLISION_TEST_VISUALIZATION
        // debug
        //
        s_blend = CCSprite::createWithTexture(_rt->getSprite()->getTexture());
        s_blend->setTag(11);
        s_blend->setPosition(ccp(0,0));
        s_blend->setAnchorPoint(ccp(0,0));
        cur_n->addChild(s_blend);
        //
        // debug
#endif
        return retVal;
    }
    
	SDEBUG(LEVEL_DEBUG, "dragging", "Collision test (intersects/pixelPerfect): REJECTED by intersectsRect(); rect1=%s; rect2=%s;", RectStr(r1), RectStr(r2));

    return false;
}

Node* PixelCollision::drawRect (Rect r, const Color4F &color)
{
    r.origin.x /= CC_CONTENT_SCALE_FACTOR();
    r.origin.y /= CC_CONTENT_SCALE_FACTOR();
    r.size.width /= CC_CONTENT_SCALE_FACTOR();
    r.size.height /= CC_CONTENT_SCALE_FACTOR();

    Size winSize = r.size;
    Point pos = r.origin;
    auto rectNode = DrawNode::create();
    Vec2 rectangle[4];
    rectangle[0] = Vec2(pos.x+0, pos.y+0);
    rectangle[1] = Vec2(pos.x+winSize.width, pos.y+0);
    rectangle[2] = Vec2(pos.x+winSize.width, pos.y+winSize.height);
    rectangle[3] = Vec2(pos.x+0, pos.y+winSize.height);

    rectNode->drawPolygon(rectangle, 4, Color4F(255, 0, 0, 255), 1, color);
    return rectNode;
}

bool PixelCollision::collidesWithPoint(Sprite *sprite, const Point &point) {
    _rt->beginWithClear(0, 0, 0, 0);
    glColorMask(1, 0, 0, 1);

	Point oldPosition = sprite->getPosition();
	sprite->setPosition(sprite->getParent()->convertToWorldSpace(oldPosition));
    sprite->visit();

	auto readPoint = sprite->getParent()->convertToWorldSpace(point) * CC_CONTENT_SCALE_FACTOR();
	_pixelReader->setReadPoint(readPoint);
	_pixelReader->setReadSize(Size(1, 1));
	_pixelReader->visit();

	auto color = _pixelReader->getBuffer();
	_rt->end();
	Director::getInstance()->getRenderer()->render();
    glColorMask(1, 1, 1, 1);
	sprite->setPosition(oldPosition);

	return color[0] > 0;
}

// Private methods
PixelCollision::PixelReaderNode *PixelCollision::PixelReaderNode::create(const Point &readPoint) {
	auto pixelReader = new PixelReaderNode(readPoint);
	if (pixelReader && pixelReader->init()) {
		pixelReader->autorelease();
		return pixelReader;
	}
	CC_SAFE_DELETE(pixelReader);
	return nullptr;
}

PixelCollision::PixelReaderNode::PixelReaderNode(const Point &readPoint) :
		_readPoint(readPoint),
		_readSize(Size::ZERO),
		_buffer(nullptr) {
	this->setReadSize(Size(1, 1));
}

PixelCollision::PixelReaderNode::~PixelReaderNode(void) {
    free(_buffer);
}

void PixelCollision::PixelReaderNode::draw(Renderer *renderer, const Mat4& transform, uint32_t flags) {
	_readPixelCommand.init(_globalZOrder);
	_readPixelCommand.func = CC_CALLBACK_0(PixelCollision::PixelReaderNode::onDraw, this);
	renderer->addCommand(&_readPixelCommand);
}

void PixelCollision::PixelReaderNode::onDraw(void) {
	glReadPixels(_readPoint.x, _readPoint.y, _readSize.width, _readSize.height,
		GL_RGBA, GL_UNSIGNED_BYTE, _buffer);
}

Rect PixelCollision::getIntersection(const Rect &r1, const Rect &r2) const
{
// <Pp>
    Rect intersection;
  intersection = Rect(std::max(r1.getMinX(),r2.getMinX()), std::max(r1.getMinY(),r2.getMinY()),0,0);

intersection.size.width = std::min(r1.getMaxX(), r2.getMaxX()) - intersection.getMinX();
intersection.size.height = std::min(r1.getMaxY(), r2.getMaxY()) - intersection.getMinY();

intersection.origin.x *= CC_CONTENT_SCALE_FACTOR();
intersection.origin.y *= CC_CONTENT_SCALE_FACTOR();
intersection.size.width *= CC_CONTENT_SCALE_FACTOR();
intersection.size.height *= CC_CONTENT_SCALE_FACTOR();

return intersection;
// </Pp>

/* <ORIG>
	float tempX;
	float tempY;
	float tempWidth;
	float tempHeight;

	if (r1.getMaxX() > r2.getMinX()) {
		tempX = r2.getMinX();
		tempWidth = r1.getMaxX() - r2.getMinX();
	} else {
		tempX = r1.getMinX();
		tempWidth = r2.getMaxX() - r1.getMinX();
	}
	if (r2.getMaxY() < r1.getMaxY()) {
		tempY = r1.getMinY();
		tempHeight = r2.getMaxY() - r1.getMinY();
	} else {
		tempY = r2.getMinY();
		tempHeight = r1.getMaxY() - r2.getMinY();
	}

	return Rect(tempX * CC_CONTENT_SCALE_FACTOR(), tempY * CC_CONTENT_SCALE_FACTOR(),
		tempWidth * CC_CONTENT_SCALE_FACTOR(), tempHeight * CC_CONTENT_SCALE_FACTOR());*/
}

Point PixelCollision::renderSprite(Sprite *sprite, CustomCommand &command, bool red) {
	command.init(sprite->getGlobalZOrder());
	command.func = [=]() {
		sprite->getGLProgramState()->setUniformInt(kShaderRedUniform, red ? 255 : 0);
		sprite->getGLProgramState()->setUniformInt(kShaderBlueUniform, red ? 0 : 255);
	};
	Director::getInstance()->getRenderer()->addCommand(&command);

	sprite->setGLProgram(_glProgram);
	sprite->setBlendFunc(BlendFunc::ADDITIVE);
	Point oldPosition = sprite->getPosition();
	sprite->setPosition(sprite->getParent()->convertToWorldSpace(oldPosition));
	sprite->visit();

	return oldPosition;
}

void PixelCollision::resetSprite(Sprite *sprite, const Point &oldPosition) {
	auto program = ShaderCache::sharedShaderCache()->programForKey(
		GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP);
	sprite->setGLProgram(program);
	sprite->setBlendFunc(BlendFunc::ALPHA_PREMULTIPLIED);
	sprite->setPosition(oldPosition);
}

Hi pepeek,

Thanks for sharing the code.

Can you share code of “PixelCollision.h” ?

Thanks.

"PixelCollision.h"

#ifndef __Brilla__PixelCollision__
#define __Brilla__PixelCollision__

#include <iostream>
#include "cocos2d.h"

USING_NS_CC;

class PixelCollision
{
public:
    static PixelCollision *getInstance(void);
    static void destroyInstance(void);

    bool collidesWithSprite(Sprite *sprite1, Sprite *sprite2, bool pp);
    bool collidesWithSprite(Sprite *sprite1, Sprite *sprite2);
    bool collidesWithPoint(Sprite *sprite, const Point &point);
    Node* drawRect (Rect r, const Color4F &color);

private:
    class PixelReaderNode : public Node {
    public:
        static PixelReaderNode* create(const Point &readPoint);

        PixelReaderNode(const Point &readPoint);
        virtual ~PixelReaderNode(void);

        virtual void draw(cocos2d::Renderer *renderer, const cocos2d::Mat4& transform, uint32_t flags) override;
        inline void reset();

        inline const Point &getReadPoint(void) const;
        inline void setReadPoint(const Point &readPoint);

        inline const Size &getReadSize(void) const;
        inline void setReadSize(const Size &readPoint);

        inline GLubyte *getBuffer(void);

    private:
       void onDraw(void);

        CustomCommand _readPixelCommand;
        Point _readPoint;
        Size _readSize;
        GLubyte *_buffer;
    };

    PixelCollision(void);
    virtual ~PixelCollision(void);

    Rect getIntersection(const Rect &r1, const Rect &r2) const;
    Rect getUnion (const Rect &r1, const Rect &r2) const;
    Point renderSprite(Sprite *sprite, CustomCommand &command, bool red);
    void resetSprite(Sprite *sprite, const Point &oldPosition);

    // Singleton
    static PixelCollision *s_instance;

    GLProgram *_glProgram;
    RenderTexture *_rt;
    PixelReaderNode *_pixelReader;
};

// Inline methods
inline void PixelCollision::PixelReaderNode::reset(void) {
    memset(_buffer, 0, 4 * _readSize.width * _readSize.height);
}

inline const Point &PixelCollision::PixelReaderNode::getReadPoint(void) const {
    return _readPoint;
}

inline void PixelCollision::PixelReaderNode::setReadPoint(const Point &readPoint) {
    _readPoint = readPoint;
}

inline const Size &PixelCollision::PixelReaderNode::getReadSize(void) const {
    return _readSize;
}

inline void PixelCollision::PixelReaderNode::setReadSize(const Size &readSize) {
    if (_readSize.width * _readSize.height < readSize.width * readSize.height) {
        free(_buffer);
        _buffer = (GLubyte*) malloc(4 * readSize.width * readSize.height);
    }
    _readSize = readSize;
}

inline GLubyte *PixelCollision::PixelReaderNode::getBuffer(void) {
    return _buffer;
}

#endif /* defined(__Brilla__PixelCollision__) */

Hi pepeek,

Thank you very much for code.

I will try it and tell you the result of it.

Thanks.

Ty very much for your post @sgleizes
This is a very big help for my game.
In 3.6 version, we don’t need to make a precompile shaders. [I don’t know why. Correct me if I am wrong.]
I just put the shaders in

“res/Shader/…”

So I just call it from this code in update function:

if (PixelCollision::getInstance()->collidesWithSprite(sprite1, sprite2, true))
{
    CCLog("Collision happened! %f", delta); 
}

then I drag first image of grossini.png to second image.
It will display Message: “Collision happened! 0.01500” if its texture collides, not the bounding box.
thank you, thank you.
I will give u a credit in my game later.
[along with cocos2d-x team and @happybirthday]

Hi pepeek.

Thanks your share. I have a problem when using it, please help me.

When i include PixelCollision class to my project. if i check collision in init function, it work fine. but when i check in update game function (60f/s) it cannot detect collision.

globle init:
 Sprite Ship1;
 Sprite Ship2;


init function: 
   Ship1  = Sprite::createWithSpriteFrameName("ship1.png");
    Ship1->setPosition(visibleSize.width/2, visibleSize.height/2);
    GameSpriteBatch->addChild(Ship1);
    
    Ship2 = Sprite::createWithSpriteFrameName("ship2.png");
    Ship2->setPosition(visibleSize.width/2 + 30, visibleSize.height/2 - 40);
    GameSpriteBatch->addChild(Ship2);
    
    if (PixelCollision::getInstance()->collidesWithSprite(Ship1, Ship2)) {
        CCLOG("is collision");
    }

void Game::update(float dt) {
CCLOG("next checking!!!");
if (PixelCollision::getInstance()->collidesWithSprite(Ship1, Ship2)) {
    CCLOG("is collision in update function");
}}

and logs in console:
"is collision
"next checking!!!
"next checking!!!
"next checking!!!
"next checking!!!
"next checking!!!

i cant find the false in my code, please help me. Thanks

u don’t need to declare it in main function.
Only ONCE in update function:

void game::update(float dt)
{

}

@sgleizes
I tried this method and it only works for Sprite not SpriteFrameCache or SpriteFrame.
Is there a way to use it for animation?

Hi gOzaru

Sorry i dont understand the mean of:
Only ONCE in update function

I want to check any collision of objects in update function. but i dont know why the collision just apear just one.

Thanks.

Hi,

It is my full function, and it never collision between Ship1 and Ship2

// on "init" you need to initialize your instance
bool HelloWorld::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Layer::init() )
    {
        return false;
    }
    
visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();

/////////////////////////////
// 2. add a menu item with "X" image, which is clicked to quit the program
//    you may modify it.

// add a "close" icon to exit the progress. it's an autorelease object
auto closeItem = MenuItemImage::create(
                                       "CloseNormal.png",
                                       "CloseSelected.png",
                                       CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));

closeItem->setPosition(Vec2(origin.x + visibleSize.width - closeItem->getContentSize().width/2 ,
                            origin.y + closeItem->getContentSize().height/2));

// create menu, it's an autorelease object
auto menu = Menu::create(closeItem, NULL);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);

GameSpriteBatch = SpriteBatchNode::create("testbatch.png");
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("testbatch.plist");
this->addChild(GameSpriteBatch);
GameSpriteBatch->retain();

Ship1 = Sprite::createWithSpriteFrameName("ship1.png");
Ship1->setPosition(visibleSize.width/2, visibleSize.height/2);
GameSpriteBatch->addChild(Ship1);

Ship2 = Sprite::createWithSpriteFrameName("ship2.png");
Ship2->setPosition(visibleSize.width/2 + 30, visibleSize.height/2 - 35);
GameSpriteBatch->addChild(Ship2);

this->scheduleUpdate();

return true;
}


void HelloWorld::update(float delta) {
    
    if (PixelCollision::getInstance()->collidesWithSprite(Ship1, Ship2)) {
        CCLOG("is collision");
    }
}

Think logically dude.
U need to move the sprite1 so it can collide with sprite2.
Using onTouchMoved.
Set it to true:

void HelloWorld::update(float delta) {

if (PixelCollision::getInstance()->collidesWithSprite(Ship1, Ship2) == true) {
    CCLOG("is collision");
}

}

thank you for you sharing ,its very kind of you.But i find a problem .
//SDEBUG(LEVEL_DEBUG, “dragging”, “Collision test (intersects/pixelPerfect): retVal=%s; rect1=%s; rect2=%s; intersection=%s; numPixels=%d; CC_CONTENT_SCALE_FACTOR=%.2f; s1.scale=%.2f; s2.scale=%.2f;”,
//BOOLStr(retVal), RectStr(r1), RectStr(r2), RectStr(intersection), numPixels, CC_CONTENT_SCALE_FACTOR(), sprite1->getScale(), sprite2->getScale());
what is the SDBUG and BOOLStr

Thank you for this work!
I have to test it :smile:

How is performance wise? specially in low-end mobile devices.
I’m using two ETC1 textures for my sprite atlasheet (RGB + alpha split). It might be useful to use only the alpha texture to perform this test.

Thanks
Dredok

Dear , i find a big problem.It works well when i open then debug model ,but when i close debug, it just work as a boudingBox,I use the 2dx-3.3。

Just like others said it’s working like a bounding box…