cocos2d-iphoneccspritestrokeccnode

Outline (stroke) for non-rectangular CCNode in cocos2d


I need to create an outline like this dynamically:

Red 2px outline

Not for a CCSprite, but for multiple animated CCSprites united in one CCNode. I'm thinking about:

  1. copying CCNode's content to a texture (like canvasBitmapData.draw(sourceDisplayObject) in AS3)
  2. creating CCSprite with the resulting texture
  3. tinting the sprite to outline color and scaling it up a bit
  4. placing the sprite behind other sprites in the node

I have no idea how to perform step 1. And maybe it is faster to draw "true stroke" around the texture's opaque pixels instead of tint-scale in step 3?


Solution

  • I totally forgot to post an answer for this question. Here's the code for a very smooth stroke. It's not fast but worked great for a couple of big sprites on the first iPad.

    The idea is to draw tiny colored and blurred balls around the sprite and place them onto their own texture. It can be used both for CCNode and CCSprite. The code also shifts anchor points because the resulting sprites will have a bit larger width and height.

    Resulting outline (body and 2 hands, about 0.3s on iPad1):

    outline

    White balls examples:

    CCNode category, for Cocos2d-iPhone 2.1:

    @implementation CCNode (Outline)
    
    - (CCSprite*) outline
    {
        return [self outlineRect:CGRectMake(0, 0, self.contentSize.width, self.contentSize.height)];
    }
    
    - (CCSprite*) outlineRect:(CGRect)rect
    {
        NSInteger gap = dscale(4);
        CGPoint positionShift = ccp(gap - rect.origin.x, gap - rect.origin.y);
        CGSize canvasSize = CGSizeMake(rect.size.width + gap * 2, rect.size.height + gap * 2);
    
        CCRenderTexture* renderedSpriteTexture = [self renderTextureFrom:self shiftedFor:positionShift onCanvasSized:canvasSize];
        CGSize textureSize = renderedSpriteTexture.sprite.contentSize;
        CGSize textureSizeInPixels = renderedSpriteTexture.sprite.texture.contentSizeInPixels;
    
        NSInteger bitsPerComponent = 8;
        NSInteger bytesPerPixel = (bitsPerComponent * 4) / 8;
        NSInteger bytesPerRow = bytesPerPixel * textureSizeInPixels.width;
        NSInteger myDataLength = bytesPerRow * textureSizeInPixels.height;
    
        NSMutableData* buffer = [[NSMutableData alloc] initWithCapacity:myDataLength];
        Byte* bytes = (Byte*)[buffer mutableBytes];
    
        [renderedSpriteTexture begin];
        glReadPixels(0, 0, textureSizeInPixels.width, textureSizeInPixels.height, GL_RGBA, GL_UNSIGNED_BYTE, bytes);
        [renderedSpriteTexture end];
    
        //SEE ATTACHMENT TO GET THE FILES
        NSString* spriteFrameName;
        if (IS_IPAD) spriteFrameName = (CC_CONTENT_SCALE_FACTOR() == 1) ? @"10f.png" : @"20f.png";
        else spriteFrameName = (CC_CONTENT_SCALE_FACTOR() == 1) ? @"5f.png" : @"10f.png";
    
        CCSprite* circle = [CCSprite spriteWithSpriteFrameName:spriteFrameName];
        circle.anchorPoint = ccp(0.48, 0.48);
        float retinaScale = (CC_CONTENT_SCALE_FACTOR() == 1) ? 1.0 : 0.5;
    
        CCRenderTexture* strokeTexture = [CCRenderTexture renderTextureWithWidth:textureSize.width height:textureSize.height pixelFormat:kCCTexture2DPixelFormat_RGBA8888];
        [strokeTexture beginWithClear:0 g:0 b:0 a:0];
        for (NSInteger x = 0; x < textureSizeInPixels.width; x++)
        {
            for (NSInteger y = 0; y < textureSizeInPixels.height; y++)
            {
                NSInteger idx = y * bytesPerRow + x * bytesPerPixel + 3;
                NSInteger w = 1;
                if (bytes[idx] <= 254)
                {
                    BOOL shouldBeStroked = NO;
                    for (NSInteger nx = -w; nx <= w; nx++)
                    {
                        for (NSInteger ny = -w; ny <= w; ny++)
                        {
                            if (x + nx < 0 || y + ny < 0 || x + nx >= textureSizeInPixels.width || y + ny >= textureSizeInPixels.height)
                                continue;
    
                            if (bytes[idx + nx * bytesPerPixel + ny * bytesPerRow] == 255)
                            {
                                shouldBeStroked = YES;
                                break;
                            }
                        }
                    }
    
                    if (shouldBeStroked == YES)
                    {
                        circle.position = ccp(x * retinaScale, y * retinaScale);
                        [circle visit];
                    }
                }
            }
        }
        [strokeTexture end];
    
        CCSprite* resultSprite = [CCSprite spriteWithTexture:strokeTexture.sprite.texture];
        [resultSprite.texture setAntiAliasTexParameters];
        resultSprite.flipY = YES;
    
        if ([self isKindOfClass:[CCSprite class]]) {
            CGPoint oldAnchorInPixels = ccp(roundf(self.contentSize.width * self.anchorPoint.x), roundf(self.contentSize.height * self.anchorPoint.y));
            resultSprite.anchorPoint = ccp((oldAnchorInPixels.x + gap) / resultSprite.contentSize.width, (oldAnchorInPixels.y + gap) / resultSprite.contentSize.height);
            resultSprite.position = self.position;
        } else { //CCNode
            resultSprite.anchorPoint = CGPointZero;
            resultSprite.position = ccpAdd(self.position, ccp(rect.origin.x - gap, rect.origin.y - gap));
        }
        return resultSprite;
    }
    
    - (CCRenderTexture*) renderTextureFrom:(CCNode*)node shiftedFor:(CGPoint)posShift onCanvasSized:(CGSize)size
    {
        SoftAssertion(!CGSizeEqualToSize(size, CGSizeZero), @"node has zero size");
    
        BOOL isSprite = [node isMemberOfClass:[CCSprite class]];
        CGPoint apSave = node.anchorPoint;
        CGPoint posSave = node.position;
        BOOL wasVisible = node.visible;
    
        CCRenderTexture* rtx = [CCRenderTexture renderTextureWithWidth:size.width
                                                                height:size.height
                                                           pixelFormat:kCCTexture2DPixelFormat_RGBA8888];
        [rtx beginWithClear:0 g:0 b:0 a:0];
    
        node.anchorPoint = CGPointZero;
        node.position = posShift;
        node.visible = YES;
    
        if (isSprite) [node visit];
        else [[self cloneCCNode:node] visit];
    
        node.anchorPoint = apSave;
        node.position = posSave;
        node.visible = wasVisible;
    
        [rtx end];
        return rtx;
    }
    
    - (CCNode*) cloneCCNode:(CCNode*)source
    {
        CCNode* clone = [CCNode node];
    
        void (^copyCCNodeProperties)(CCNode*, CCNode*) = ^(CCNode* source, CCNode* clone)
        {
            clone.visible = source.visible;
            clone.rotation = source.rotation;
            clone.position = source.position;
            clone.anchorPoint = source.anchorPoint;
            clone.zOrder = source.zOrder;
            clone.tag = source.tag;
        };
    
        for (CCNode* srcSubnode in source.children) {
    
            CCNode* subNode;
    
            if ([srcSubnode isMemberOfClass:[CCSprite class]]) {
                CCSprite* srcSprite = (CCSprite*)srcSubnode;
                subNode = [CCSprite spriteWithTexture:srcSprite.texture];
                CCSprite* subSprite = (CCSprite*)subNode;
                subSprite.flipX = srcSprite.flipX;
                subSprite.flipY = srcSprite.flipY;
                subSprite.displayFrame = srcSprite.displayFrame;
                subSprite.opacity = srcSprite.opacity;
            }
            else if ([srcSubnode isMemberOfClass:[CCLabelTTF class]]) {
                CCLabelTTF* srcLabel = (CCLabelTTF*)srcSubnode;
                subNode = [CCLabelTTF labelWithString:srcLabel.string fontName:srcLabel.fontName fontSize:srcLabel.fontSize dimensions:srcLabel.dimensions hAlignment:srcLabel.horizontalAlignment vAlignment:srcLabel.verticalAlignment];
                CCSprite* subLabel = (CCSprite*)subNode;
                subLabel.flipX = srcLabel.flipX;
                subLabel.flipY = srcLabel.flipY;
                subLabel.color = srcLabel.color;
            }
            else {
                subNode = [self cloneCCNode:srcSubnode];
            }
    
            copyCCNodeProperties(srcSubnode, subNode);
            [clone addChild:subNode];
        }
        copyCCNodeProperties(source, clone);
    
        return clone;
    }