@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 .
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.
@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
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âŚ
Hi - I was looking at this line in PixelCollision::collidesWithSprite, and sometimes collisions donât work. Is this because my sprites do not have the same parents ? Sometimes it seems that r1.intersectsRect(r2) returns false, even though they appear to overlap on screen.
Is this a bug ? I am using cocos2d-x 3.13.1