I figured it out - it was due to an error on my part in calculating the offset. Sorry about the confusion.
Pixel Perfect Collision Detection using Color Blending
(117 posts) (40 voices)-
Posted 3 months ago #
-
Thanks Dani - I didn't change very much of your code :) - I just dynamically created the render texture, and matched its size to that of the intersection rectangle.
After fixing the error in calculating the offset, my fps is just ~50fps on a first gen iPhone when the bounding boxes constantly overlap.
The fps should be better on 3gs - I don't have one so I can't check. On 4th Gen iPod it is 60 (for checks every frame when bounding boxes overlap).On older devices this test is probably ideal for projectiles, or objects which will stop checking after one positive instance of collision, or in cases where bounding boxes won't overlap for too many consecutive frames. I'm not aware of any other technique which is more efficient or easier to use for sprites of irregular shapes and orientations.
Posted 3 months ago # -
Hi,
I am trying to make a collision test between 2 sprites. One of them is retrieved from a tile map array using the code
CCSprite *tiledsprite = [colLayer tileAt:ccp(pos1X,pos1Y)];
And that's the problem since trying to make the collision test inside a CCRenderTexture offscreen, everytime I call
[tiledsprite visit];
I get the the following assertion error
NSAssert(!usesBatchNode_, @"If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");
This works as it expected for the mentioned reasons, but I still need to render this tiledsprite inside the CCRenderTexture offscreen.
Is there anyway to overcome this problem and render it as normal sprite ?
Regards,
Posted 2 months ago # -
@Some Guy: hey, would you mind posting the updated/fixed code please?
It would be interesting to do an OpenGL ES 2.0 version, it should not need many changes hopefully... added to a long TODO list lol.
Posted 2 months ago # -
I havent tried this, but in stead of using glReadPixels, and manually checking for collisions, I wonder if it would be possible to render the collision result to a single pixel texture, using additive blending. If black were no collision, anything but a fully black pixel, would be a collision.
Posted 2 months ago # -
@Birkemose Here is the link to the example for you to play and improve:
Pixel Perfect Collision Detection Example
I can't understand your single pixel approach, but hope you can improve this function, it seems it's becoming very useful for some users!
Regards!
Posted 1 month ago # -
@Dani, @sdancer4
The code I use is the same as posted by Some Guy in page 3, and include Dani's CCSpriteBatchNode extension to use a spritebatchnode directly. In theory, Creating separate sprites may be a good idea too since you are moving the sprites position when you create the render texture - since any other collision check could be false / true since the position of the original sprite would be changing. But in practice, using SpriteBatchNode I have not had any false positives or negatives.
Posted 1 month ago # -
The post on the previous page by Some Guy is the latest / fixed. (He forgot password to his account). Only the function to offset the sprites for the intersection test wasn't there. I posted the full code below
-(BOOL) isCollisionBetweenSpriteA:(CCSprite*)spr1 spriteB:(CCSprite*)spr2 pixelPerfect:(BOOL)pp
{
BOOL isCollision = NO;CGRect intersection = CGRectIntersection([spr1 boundingBox], [spr2 boundingBox]);
// Look for simple bounding box collision
if (!CGRectIsEmpty(intersection))
{
// If we're not checking for pixel perfect collisions, return true
if (!pp) {return YES;}CGPoint spr1OldPosition = spr1.position;
CGPoint spr2OldPosition = spr2.position;spr1.position = CGPointMake(spr1.position.x - intersection.origin.x, spr1.position.y - intersection.origin.y);
spr2.position = CGPointMake(spr2.position.x - intersection.origin.x, spr2.position.y - intersection.origin.y);intersection = CGRectIntersection([spr1 boundingBox], [spr2 boundingBox]);
// Assuming that the spritebatchnode of both sprites is the same, I just use one. If each sprite has a different sprite batch node as parent you should modify the code to get the spriteBatchNode for each sprite and visit them.
CCSpriteBatchNode* _sbnMain =(CCSpriteBatchNode*) spr1.parent;//NOTE: We are assuming that the spritebatchnode is always at 0,0
// Get intersection info
unsigned int x = (intersection.origin.x)* CC_CONTENT_SCALE_FACTOR();
unsigned int y = (intersection.origin.y)* CC_CONTENT_SCALE_FACTOR();
unsigned int w = intersection.size.width* CC_CONTENT_SCALE_FACTOR();
unsigned int h = intersection.size.height* CC_CONTENT_SCALE_FACTOR();
unsigned int numPixels = w * h;// * CC_CONTENT_SCALE_FACTOR();// create render texture and make it visible for testing purposes
int renderWidth = w+1;
int renderHeight = h+1;if(renderWidth<16)
{
renderWidth = 16;
}if(renderHeight < 16)
{
renderHeight = 16;
}CCRenderTexture* _rt = [[CCRenderTexture alloc] initWithWidth:renderWidth height:renderHeight pixelFormat:kCCTexture2DPixelFormat_RGBA8888];
//rt is always going to be at 0,0 - can't change it.
_rt.position = CGPointMake(0, 0);
[[RIGameScene sharedGameScene]addChild:_rt];
_rt.visible = YES;//NSLog(@"\nintersection = (%u,%u,%u,%u), area = %u",x,y,w,h,numPixels);
// Draw into the RenderTexture
[_rt beginWithClear:0 g:0 b:0 a:0];// Render both sprites: first one in RED and second one in GREEN
glColorMask(1, 0, 0, 1);
[_sbnMain visitSprite:spr1];
glColorMask(0, 1, 0, 1);
[_sbnMain visitSprite:spr2];
glColorMask(1, 1, 1, 1);// Get color values of intersection area
ccColor4B *buffer = malloc( sizeof(ccColor4B) * numPixels );
glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, buffer);[_rt end];
// Read buffer
unsigned int step = 1;
for(unsigned int i=0; i<numPixels; i+=step)
{
ccColor4B color = buffer[i];if (color.r > 0 && color.g > 0)
{
isCollision = YES;
break;
}
}// Free buffer memory
free(buffer);spr1.position = spr1OldPosition;
spr2.position = spr2OldPosition;[_rt release];
[[RIGameScene sharedGameScene]removeChild:_rt cleanup:YES];
}
return isCollision;
}
And Dani's extension to CCSpriteBatchNode is:
CCSpriteBatchNodeExtended.h
#import <Foundation/Foundation.h>
#import "cocos2d.h"@interface CCSpriteBatchNode(drawSpritesPPCD)
-(void) drawSprite:(CCSprite*)spr;
-(void) visitSprite:(CCSprite*)spr;@end
CCSpriteBatchNodeExtended.m
#import "CCSpriteBatchNodeExtended.h"static SEL selUpdate = NULL;
@implementation CCSpriteBatchNode(drawSpritesPPCD)
//+(void) initialize
//{
// if ( self == [CCSpriteBatchNode class] ) {
// selUpdate = @selector(updateTransform);
// }
//}-(void) drawSprite:(CCSprite*)spr
{
[super draw];// Optimization: Fast Dispatch
if( textureAtlas_.totalQuads == 0 )
return;CCSprite *child;
ccArray *array = descendants_->data;NSUInteger i = array->num;
id *arr = array->arr;unsigned int index = 0;
if (i > 0)
{
while (i-- > 0)
{
child = *arr++;// fast dispatch
if (child == spr)
{
index = [child atlasIndex];
child->updateMethod(child, selUpdate);
}
}// Default GL states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Needed states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Unneeded states: -BOOL newBlend = blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST;
if( newBlend )
glBlendFunc( blendFunc_.src, blendFunc_.dst );[textureAtlas_ drawNumberOfQuads:1 fromIndex:index];
if( newBlend )
glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST);
}
}-(void) visitSprite:(CCSprite*)spr
{
if (!visible_)
return;glPushMatrix();
if ( grid_ && grid_.active) {
[grid_ beforeDraw];
[self transformAncestors];
}[self transform];
[self drawSprite:spr];
if ( grid_ && grid_.active)
[grid_ afterDraw:self];glPopMatrix();
}@end
Posted 1 month ago # -
Arg! How do I highlight the code? :S
Posted 1 month ago # -
@Pteriedaktyl: You can use backticks to open and close code. By the way, I have needed 10 seconds to type your nick, haha!
Posted 1 month ago # -
I guess its to late to edit the code. So I reformatted below. @Dani. You could just say Pter instead. :)
-(BOOL) isCollisionBetweenSpriteA:(CCSprite*)spr1 spriteB:(CCSprite*)spr2 pixelPerfect:(BOOL)pp { BOOL isCollision = NO; CGRect intersection = CGRectIntersection([spr1 boundingBox], [spr2 boundingBox]); // Look for simple bounding box collision if (!CGRectIsEmpty(intersection)) { // If we're not checking for pixel perfect collisions, return true if (!pp) {return YES;} CGPoint spr1OldPosition = spr1.position; CGPoint spr2OldPosition = spr2.position; spr1.position = CGPointMake(spr1.position.x - intersection.origin.x, spr1.position.y - intersection.origin.y); spr2.position = CGPointMake(spr2.position.x - intersection.origin.x, spr2.position.y - intersection.origin.y); intersection = CGRectIntersection([spr1 boundingBox], [spr2 boundingBox]); // Assuming that the spritebatchnode of both sprites is the same, I just use one. If each sprite has a different sprite batch node as parent you should modify the code to get the spriteBatchNode for each sprite and visit them. CCSpriteBatchNode* _sbnMain =(CCSpriteBatchNode*) spr1.parent; //NOTE: We are assuming that the spritebatchnode is always at 0,0 // Get intersection info unsigned int x = (intersection.origin.x)* CC_CONTENT_SCALE_FACTOR(); unsigned int y = (intersection.origin.y)* CC_CONTENT_SCALE_FACTOR(); unsigned int w = intersection.size.width* CC_CONTENT_SCALE_FACTOR(); unsigned int h = intersection.size.height* CC_CONTENT_SCALE_FACTOR(); unsigned int numPixels = w * h;// * CC_CONTENT_SCALE_FACTOR(); // create render texture and make it visible for testing purposes int renderWidth = w+1; int renderHeight = h+1; if(renderWidth<16) { renderWidth = 16; } if(renderHeight < 16) { renderHeight = 16; } CCRenderTexture* _rt = [[CCRenderTexture alloc] initWithWidth:renderWidth height:renderHeight pixelFormat:kCCTexture2DPixelFormat_RGBA8888]; //rt is always going to be at 0,0 - can't change it. _rt.position = CGPointMake(0, 0); [[RIGameScene sharedGameScene]addChild:_rt]; _rt.visible = YES; //NSLog(@"\nintersection = (%u,%u,%u,%u), area = %u",x,y,w,h,numPixels); // Draw into the RenderTexture [_rt beginWithClear:0 g:0 b:0 a:0]; // Render both sprites: first one in RED and second one in GREEN glColorMask(1, 0, 0, 1); [_sbnMain visitSprite:spr1]; glColorMask(0, 1, 0, 1); [_sbnMain visitSprite:spr2]; glColorMask(1, 1, 1, 1); // Get color values of intersection area ccColor4B *buffer = malloc( sizeof(ccColor4B) * numPixels ); glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, buffer); [_rt end]; // Read buffer unsigned int step = 1; for(unsigned int i=0; i<numPixels; i+=step) { ccColor4B color = buffer[i]; if (color.r > 0 && color.g > 0) { isCollision = YES; break; } } // Free buffer memory free(buffer); spr1.position = spr1OldPosition; spr2.position = spr2OldPosition; [_rt release]; [[RIGameScene sharedGameScene]removeChild:_rt cleanup:YES]; } return isCollision; }And Dani's extension to CCSpriteBatchNode is:
CCSpriteBatchNodeExtended.h
#import <Foundation/Foundation.h> #import "cocos2d.h" @interface CCSpriteBatchNode(drawSpritesPPCD) -(void) drawSprite:(CCSprite*)spr; -(void) visitSprite:(CCSprite*)spr; @endCCSpriteBatchNodeExtended.m
#import "CCSpriteBatchNodeExtended.h" static SEL selUpdate = NULL; @implementation CCSpriteBatchNode(drawSpritesPPCD) //+(void) initialize //{ // if ( self == [CCSpriteBatchNode class] ) { // selUpdate = @selector(updateTransform); // } //} -(void) drawSprite:(CCSprite*)spr { [super draw]; // Optimization: Fast Dispatch if( textureAtlas_.totalQuads == 0 ) return; CCSprite *child; ccArray *array = descendants_->data; NSUInteger i = array->num; id *arr = array->arr; unsigned int index = 0; if (i > 0) { while (i-- > 0) { child = *arr++; // fast dispatch if (child == spr) { index = [child atlasIndex]; child->updateMethod(child, selUpdate); } } // Default GL states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY // Needed states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY // Unneeded states: - BOOL newBlend = blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST; if( newBlend ) glBlendFunc( blendFunc_.src, blendFunc_.dst ); [textureAtlas_ drawNumberOfQuads:1 fromIndex:index]; if( newBlend ) glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST); } } -(void) visitSprite:(CCSprite*)spr { if (!visible_) return; glPushMatrix(); if ( grid_ && grid_.active) { [grid_ beforeDraw]; [self transformAncestors]; } [self transform]; [self drawSprite:spr]; if ( grid_ && grid_.active) [grid_ afterDraw:self]; glPopMatrix(); } @endPosted 1 month ago # -
@Dani
I unfortunately don't have time to look into it, but the idea would be to have a "collision result", where no collisions would be an all black image. You could even use RGB to "code" different types of collisions. And then, in stead of reading all pixels, and manually scanning them ( which probably is the most time costly operation of pixel collision ), the idea was to down-sample the image into a 1*1 image. This could be done manually, or maybe just with mipmapping. At least it would be fully HW accelerated.
If the resulting pixel was anything but pure black, it would mean there were a collision.Posted 1 month ago # -
Interesting thread. Can anyone comment on how expensive this sort of collision detection is? I have some fairly complex sprites, and bounding boxes aren't quite ideal. I'm looking for alternatives, and this thread has been popular for a while.
Posted 1 month ago # -
If you're referring to the pasted code above, most of the time, you would do a bounding box test first - so if there is no overlap in bounding box, then it is only as expensive as doing a pure bounding box test.
When the bounding box overlaps, thats the only time you do the pixel perfect test.
So it really depends on how often the bounding boxes of sprites will overlap, as well as how many sprites are doing the pixel perfect test. If you have a scenario where bounding boxes will overlap almost all the time(or worse) , and their are multiple sprites whose bounding boxes are overlapping , then performance may suffer.
In my case, I have around 50 sprites on screen at the same time, but most of the time the bounding box doesn't overlap - so the pixel perfect test is rarely executed and I don't have any performance impact. On an iPod 4g, if there is a constant bounding box overlap between two sprites I still get 60fps.
You really need to performance test it for your scenario to have a realistic idea, since each of our scenarios are different.
Posted 1 month ago # -
hi all!
has someone already tried to use it under cocos2d 2.0?
for me it doesn't work.
it is reporting collisions, even if no pixel collision happend.i'm grateful for any tip!
sven
Posted 1 month ago # -
Did you try the sample project? Or were you trying to integrate with your own code?
(I haven't tried 2.0 yet, only 1.x).Posted 1 month ago # -
hi pteriedaktyl,
i have integrated it in my own code.
the following error message was shown:OpenGL error 0x0502 in -[CCSprite draw] 532
after using cocos2d 1.X everything works as expected.
sven
Posted 1 month ago # -
For cocos2d v2.0 (early versions) I used batch nodes and the category looks something like this:
CCSpriteBatchNodeExtended.h
@interface CCSpriteBatchNode(drawSpritesPPCD) -(void) drawSprite:(CCSprite*)spr; -(void) visitSprite:(CCSprite*)spr; @endCCSpriteBatchNodeExtended.m
#import "CCSpriteBatchNodeExtended.h" @implementation CCSpriteBatchNode(drawSpritesPPCD) -(void) drawSprite:(CCSprite*)spr { CC_PROFILER_START(@"CCSpriteBatchNode - draw"); [super draw]; // Optimization: Fast Dispatch if( textureAtlas_.totalQuads == 0 ) return; [spr performSelector:@selector(updateTransform)]; ccGLBlendFunc( blendFunc_.src, blendFunc_.dst ); ccGLUseProgram( shaderProgram_->program_ ); ccGLUniformModelViewProjectionMatrix( shaderProgram_ ); [textureAtlas_ drawNumberOfQuads:1 fromIndex:[spr atlasIndex]]; CC_PROFILER_STOP(@"CCSpriteBatchNode - draw"); } -(void) visitSprite:(CCSprite*)spr { CC_PROFILER_START_CATEGORY(kCCProfilerCategoryBatchSprite, @"CCSpriteBatchNode - visit"); NSAssert(parent_ != nil, @"CCSpriteBatchNode should NOT be root node"); if (!visible_) return; kmGLPushMatrix(); if ( grid_ && grid_.active) { [grid_ beforeDraw]; [self transformAncestors]; } [self transform]; [self drawSprite:spr]; if ( grid_ && grid_.active) [grid_ afterDraw:self]; kmGLPopMatrix(); CC_PROFILER_STOP_CATEGORY(kCCProfilerCategoryBatchSprite, @"CCSpriteBatchNode - visit"); } @endThe idea is to pass the sprite as parameter and draw it only:
[textureAtlas_ drawNumberOfQuads:1 fromIndex:[spr atlasIndex]];All you have to do is modify the new CCSprite or CCSpriteBatchNode code that comes with the latest cocos2d 2.0 version, in order to draw one sprite only.
Hope this helps.
Posted 1 month ago # -
hi dani,
thanks for your answer.
I currently use CCSprite for drawing. As far as i understood the source of this class it only draws one sprite.
How must i change to CCSprite source?Thanks in advance.
Sven
Posted 1 month ago # -
Hhhmmm, then I suppose you don't need to change the code, only call the visit method. I cannot test it at the moment. If you have the time, try to start a new clean test project with cocos2d v2.0 and add two sprites and the collision function, to ensure that your error comes from the visit method.
Posted 1 month ago # -
Hi all,
it was my fault! for test purposes i have set CC_SPRITE_DEBUG_DRAW to 1.
since i set it to 0 everything works perfect.sorry about the confusion!
Sven
Posted 1 month ago # -
Its ok man. Writing code is a strange and terrible thing. There are very few outsiders that understand what its like. Sometimes, I see it compiling in my dreams. It never crashes, never hangs.... it just runs... optimizing itself and checking for boundary conditions.... its beautiful.... then I realize its just a dream.
Posted 4 weeks ago # -
Maybe the dream is reality and what you think is reality is actually a nightmare.
Or, maybe you're just an unfortunate character in my dream and I'm toying with you and your code. So many exciting possibilities!
Posted 4 weeks ago #
Reply
You must log in to post.