Pixel Collision Detection v4.x

Hello everyone, my previous game which is using cocos v3x was using pixel collision detection from this post : Pixel collision detection for v3.x . Its works nice and properly.
But now i want to upgrade my game into v4, so far i know cocos remove GlProgram to Program. So i tried to upgrade it by looking cocos2dx reference v4, but im ran out idea how to do it in v4. I mean the new Program doesnt have class that supposed to be as the GlProgram class be, Its totaly change. i’ve took a look at cocos2dx reference v4 and still cant make pixel detection in v4.
Any advice would be appreciated ? Thanks

Collision.h
#ifndef PIXEL_COLLISION_H_
#define PIXEL_COLLISION_H_

#include <iostream>
#include "../../Implement.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 cocos2d::Point &point);
	
private:
	class PixelReaderNode : public Node {
	public:
		static PixelReaderNode* create(const cocos2d::Point &readPoint);

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

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

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

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

		inline GLubyte *getBuffer(void);

	private:
		void onDraw(void);

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

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

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

	// Singleton
	static PixelCollision *s_instance;

	cocos2d::backend::Program *_glProgram;
	RenderTexture *_rt;
	PixelReaderNode *_pixelReader;
};

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

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

inline void PixelCollision::PixelReaderNode::setReadPoint(const cocos2d::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 /* PIXEL_COLLISION_H_ */

Collision.cpp
#include “Collision.h”

static const auto kVertexShader = "shaders/SolidColorShader.vsh";
static const auto kFragmentShader = "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) {

	if (!FileUtils::getInstance()->isFileExist(EducaRes::additionalPath + kVertexShader)) return;
	if (!FileUtils::getInstance()->isFileExist(EducaRes::additionalPath + kFragmentShader)) return;

	//_glProgram = GLProgram::createWithFilenames(EducaRes::additionalPath + kVertexShader, kFragmentShader);
	//_glProgram->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION);
	//_glProgram->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORD);
	//_glProgram->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_COLOR, GLProgram::VERTEX_ATTRIB_COLOR);

	const Size &size = Director::getInstance()->getWinSize();
	_rt = RenderTexture::create(size.width, size.height, cocos2d::backend::PixelFormat::RGBA8888);
	_pixelReader = PixelReaderNode::create(cocos2d::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) {
	Rect r1 = sprite1->getBoundingBox();
	Rect r2 = sprite2->getBoundingBox();

	if (r1.intersectsRect(r2)) {
		if (!pp) {
			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);

		_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;
		for (unsigned int i = 0; i < maxIndex; i += 4) {
			if (buffer[i] > 0 && buffer[i + 2] > 0 && buffer[i + 3] > kOpacityThreshold) { // red and blue
				return true;
			}
		}
	}
	return false;
}

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

	auto oldColor = sprite->getColor();
	auto prnt = sprite->getParent();
	cocos2d::Point oldPosition = prnt->convertToWorldSpace(prnt->convertToNodeSpace(sprite->getPosition()));///sprite->getPosition();
	sprite->setScale(prnt->getScaleX());
	sprite->setColor(Color3B::WHITE);
	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->setColor(oldColor);
	sprite->setPosition(oldPosition);
	sprite->setScale(1.0f);

	return color[0] > 0;
}

// Private methods
PixelCollision::PixelReaderNode *PixelCollision::PixelReaderNode::create(const cocos2d::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 cocos2d::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 {
	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());
}

cocos2d::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->set(_glProgram);
	sprite->setBlendFunc(BlendFunc::ADDITIVE);
	cocos2d::Point oldPosition = sprite->getPosition();
	sprite->setPosition(sprite->getParent()->convertToWorldSpace(oldPosition));
	sprite->visit();

	return oldPosition;
}

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

Hi @justL
I have come across a number of posts related to the glProgram upgrade in v4.
One example is this V4 - Shaders, ProgramState, and Program
Another place to look at will be the cpp tests.

I see that your code is using glReadPixels, which you will need a cross-platform method on v4 to support for both GL & Metal. And doing a search in the codes, you will find that it is found in the TextureBackend::getBytes function. This will require some adjustments to your code as the result is not immediate (for Metal), but will come from a callback which is triggered in the midst of render() function.

As for your usage of glColorMask, performing a search shows that it is being set by BlendDescriptor, which you can set the properties in the PipelineDescriptor of your RenderCommand.

2 Likes

Anyone managed to soccessfully implement it?

Did you find a solution ?

bump. Just keeping it alive in case anyone implemented a solution…

hey @chumbyfuzz, sorry for my late reply. and yes i found the solution for it.

Cool! Are you going to keep it a secret?