Sprite kit and colorWithPatternImage
Asked Answered
S

3

6

Do we have any way of repeating an image across an area, like a SKSpriteNode? SKColor colorWithPatternImage doesn't work unfortunately.

Edit:

I did the following categories, it seems to work so far. Using Mac, not tested on iOS. Likely needs some fixing for iOS.

// Add to SKSpriteNode category or something.
+(SKSpriteNode*)patternWithImage:(NSImage*)image size:(const CGSize)SIZE;

// Add to SKTexture category or something.
+(SKTexture*)patternWithSize:(const CGSize)SIZE image:(NSImage*)image;

And the implementations. Put in respective files.

+(SKSpriteNode*)patternWithImage:(NSImage*)imagePattern size:(const CGSize)SIZE {
    SKTexture* texturePattern = [SKTexture patternWithSize:SIZE image:imagePattern];
    SKSpriteNode* sprite = [SKSpriteNode spriteNodeWithTexture:texturePattern];
    return sprite;
}

+(SKTexture*)patternWithSize:(const CGSize)SIZE image:(NSImage*)image {
    // Hopefully this function would be platform independent one day.
    SKColor* colorPattern = [SKColor colorWithPatternImage:image];

    // Correct way to find scale?
    DLog(@"backingScaleFactor: %f", [[NSScreen mainScreen] backingScaleFactor]);
    const CGFloat SCALE = [[NSScreen mainScreen] backingScaleFactor];
    const size_t WIDTH_PIXELS = SIZE.width * SCALE;
    const size_t HEIGHT_PIXELS = SIZE.height * SCALE;
    CGContextRef cgcontextref = MyCreateBitmapContext(WIDTH_PIXELS, HEIGHT_PIXELS);
    NSAssert(cgcontextref != NULL, @"Failed creating context!");
    //  CGBitmapContextCreate(
    //                                                    NULL, // let the OS handle the memory
    //                                                    WIDTH_PIXELS,
    //                                                    HEIGHT_PIXELS,

    CALayer* layer = CALayer.layer;
    layer.frame = CGRectMake(0, 0, SIZE.width, SIZE.height);

    layer.backgroundColor = colorPattern.CGColor;

    [layer renderInContext:cgcontextref];

    CGImageRef imageref = CGBitmapContextCreateImage(cgcontextref);

    SKTexture* texture1 = [SKTexture textureWithCGImage:imageref];
    DLog(@"size of pattern texture: %@", NSStringFromSize(texture1.size));

    CGImageRelease(imageref);

    CGContextRelease(cgcontextref);

    return texture1;
}

Ok this is needed as well. This likely only works on Mac.

CGContextRef MyCreateBitmapContext(const size_t pixelsWide, const size_t pixelsHigh) {
    CGContextRef    context = NULL;
    CGColorSpaceRef colorSpace;
    void *          bitmapData;
    //int             bitmapByteCount;
    size_t             bitmapBytesPerRow;

    bitmapBytesPerRow   = (pixelsWide * 4);// 1
    //bitmapByteCount     = (bitmapBytesPerRow * pixelsHigh);

    colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);// 2
    bitmapData = NULL;

#define kBitmapInfo     kCGImageAlphaPremultipliedLast
//#define kBitmapInfo       kCGImageAlphaPremultipliedFirst
//#define kBitmapInfo       kCGImageAlphaNoneSkipFirst
    // According to https://mcmap.net/q/360414/-implicit-conversion-from-enumeration-type-39-enum-cgimagealphainfo-39-to-different-enumeration-type-39-cgbitmapinfo-39-aka-39-enum-cgbitmapinfo-39 it should be safe to just cast
    CGBitmapInfo bitmapinfo = (CGBitmapInfo)kBitmapInfo; //kCGImageAlphaNoneSkipFirst; //0; //kCGBitmapAlphaInfoMask; //kCGImageAlphaNone; //kCGImageAlphaNoneSkipFirst;
    context = CGBitmapContextCreate (bitmapData,// 4
                                     pixelsWide,
                                     pixelsHigh,
                                     8,      // bits per component
                                     bitmapBytesPerRow,
                                     colorSpace,
                                     bitmapinfo
                                     );
    if (context== NULL)
    {
        free (bitmapData);// 5
        fprintf (stderr, "Context not created!");
        return NULL;
    }
    CGColorSpaceRelease( colorSpace );// 6

    return context;// 7
}
Sardinia answered 21/10, 2013 at 9:39 Comment(6)
i tried to get it working.. must be a bug with colorWithPatternImage and initWithPatternImage, no luck at all.Carnassial
Written for Mac, not tested on iOS. Just sayingSardinia
i tested for iOS and no luck getting anything to work with those methods.Carnassial
That's why I wrote in the post that it's not tested for iOS and would likely need to be fixed for iOS. The context need to be created iOS-wise. Replace MyCreateBitmapContext.Sardinia
Use an image context for IOSPhotochemistry
I guess NSImage would have to be replaced with UIImage... I guess there is no such thing as a crossplatform SKImage. So might need to refactor a bit more...Sardinia
N
3

iOS working code:

CGRect textureSize = CGRectMake(0, 0, 488, 650);
CGImageRef backgroundCGImage = [UIImage imageNamed:@"background.png"].CGImage;

UIGraphicsBeginImageContext(self.level.worldSize); // use WithOptions to set scale for retina display
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawTiledImage(context, textureSize, backgroundCGImage);
UIImage *tiledBackground = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

SKTexture *backgroundTexture = [SKTexture textureWithCGImage:tiledBackground.CGImage];
SKSpriteNode *backgroundNode = [SKSpriteNode spriteNodeWithTexture:backgroundTexture];
[self addChild:backgroundNode];
Nebulose answered 7/11, 2013 at 10:18 Comment(0)
B
2

I found that the above linked sprite kit shader did not work with Xcode 10, so I rolled my own. Here is the shader code:

void main(void) {
    vec2 offset = sprite_size - fmod(node_size, sprite_size) / 2;
    vec2 pixel = v_tex_coord * node_size + offset;
    vec2 target = fmod(pixel, sprite_size) / sprite_size;
    vec4 px = texture2D(u_texture, target);
    gl_FragColor = px;
}

Note that the offset variable is only required if you want the pattern centralised - you can omit it, and its addition in the following line, if you want your tile pattern to start with the tile texture's bottom left corner.

Also note that you will need to manually add the node_size and sprite_size variables to the shader (and update them if they change) as neither of these have standard representations any more.

// The sprite node's texture will be used as a single tile
let node = SKSpriteNode(imageNamed: "TestTile")
let tileShader = SKShader(fileNamed: "TileShader.fsh")

// The shader needs to know the tile size and the node's final size.
tileShader.attributes = [
    SKAttribute(name: "sprite_size", type: .vectorFloat2),
    SKAttribute(name: "node_size", type: .vectorFloat2)
]

// At this point, the node's size is equal to its texture's size.
// We can therefore use it as the sprite size in the shader.
let spriteSize = vector_float2(
    Float(node.size.width),
    Float(node.size.height)
)

// Replace this with the desired size of the node.
// We will set this as the size of the node later.
let size = CGSize(x: 512, y: 256)
let nodeSize = vector_float2(
    Float(size.width),
    Float(size.height)
)

newBackground.setValue(
    SKAttributeValue(vectorFloat2: spriteSize),
    forAttribute: "sprite_size"
)

newBackground.setValue(
    SKAttributeValue(vectorFloat2: nodeSize),
    forAttribute: "node_size"
)

node.shader = tileShader
node.size = size
Budwig answered 16/10, 2018 at 9:47 Comment(0)
A
-1

Yes, it is possible to implement that with a call to CGContextDrawTiledImage(), but that wastes a lot of memory for medium and large size nodes. A significantly improved approach is provided at spritekit_repeat_shader. This blog post provides example GLSL code and BSD licensed source is provided.

Acerb answered 11/6, 2016 at 22:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.