Question

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?

Était-ce utile?

La 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;
}

Autres conseils

I have a general purpose function I built up from various sources (that I'm ashamed to say I can't reference here). What it does is take a CCSprite, create a stroke that you can put behind it and return in a CCRenderTexture. If the sprite passed in has children (as yours might) I see no reason why it wouldn't do what you want, but I haven't tried.

Here it is in case it works:

@implementation Cocosutil

+(CCRenderTexture*) createStrokeForSprite:(CCSprite*)sprite  size:(float)size  color:(ccColor3B)cor
{
    CCRenderTexture* rt = [CCRenderTexture renderTextureWithWidth:sprite.texture.contentSize.width+size*2  height:sprite.texture.contentSize.height+size*2];
    CGPoint originalPos = [sprite position];
    ccColor3B originalColor = [sprite color];
    BOOL originalVisibility = [sprite visible];
    [sprite setColor:cor];
    [sprite setVisible:YES];
    ccBlendFunc originalBlend = [sprite blendFunc];
    [sprite setBlendFunc:(ccBlendFunc) { GL_SRC_ALPHA, GL_ONE }];
    CGPoint bottomLeft = ccp(sprite.texture.contentSize.width * sprite.anchorPoint.x + size, sprite.texture.contentSize.height * sprite.anchorPoint.y + size);
    CGPoint positionOffset = ccp(sprite.texture.contentSize.width * sprite.anchorPoint.x - sprite.texture.contentSize.width/2,sprite.texture.contentSize.height * sprite.anchorPoint.y - sprite.texture.contentSize.height/2);
    CGPoint position = ccpSub(originalPos, positionOffset);

    [rt begin];
    for (int i=0; i<360; i+=30)
    {
        [sprite setPosition:ccp(bottomLeft.x + sin(CC_DEGREES_TO_RADIANS(i))*size, bottomLeft.y + cos(CC_DEGREES_TO_RADIANS(i))*size)];
        [sprite visit];
    }
    [rt end];
    [sprite setPosition:originalPos];
    [sprite setColor:originalColor];
    [sprite setBlendFunc:originalBlend];
    [sprite setVisible:originalVisibility];
    [rt setPosition:position];
    return rt;
}

@end

and here is code where I use it:

- (id) initWithSprite:(CCSprite*)sprite color:(ccColor3B)color strokeSize:(float)strokeSize strokeColor:(ccColor3B)strokeColor {
    self = [super init];

    if (self != nil) {
        strokeColor_ = strokeColor;
        strokeSize_ = strokeSize;
        CCRenderTexture *stroke = [CocosUtil createStrokeForSprite:sprite size:strokeSize color:strokeColor];
        [self addChild:stroke z:kZStroke tag:kStroke];
        [self addChild:sprite z:kZLabel tag:kLabel];
        [self setContentSize:[sprite contentSize]];
    }    

    return self;
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top