Gradient texture has wrong scale on retina Mac

2020-05-03 11:09发布

问题:

I wrote a gradient generating function as a category on SKTexture. It works well on 1x screens but retina renders the texture too big, double width and double height ie wrong scale. I have been trying to get it right by changing between pixels and points, but can't get it right. Can someone help me please?

+(SKTexture*)gradientWithSize:(const CGSize)SIZE colors:(NSArray*)colors {
    // Hopefully this function would be platform independent one day.

    // 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(SIZE.width, SIZE.height, SCALE);
    NSAssert(cgcontextref != NULL, @"Failed creating context!");
    //  CGBitmapContextCreate(
    //                                                    NULL, // let the OS handle the memory
    //                                                    WIDTH_PIXELS,
    //                                                    HEIGHT_PIXELS,

    CAGradientLayer* gradient = CAGradientLayer.layer;
    //gradient.contentsScale = SCALE;
    gradient.frame = CGRectMake(0, 0, SIZE.width, SIZE.height);

    NSMutableArray* convertedcolors = [NSMutableArray array];
    for (SKColor* skcolor in colors) {
        [convertedcolors addObject:(id)skcolor.CGColor];
    }
    gradient.colors = convertedcolors;
    [gradient renderInContext:cgcontextref];

    CGImageRef imageref = CGBitmapContextCreateImage(cgcontextref);
    DLog(@"imageref pixel size: %zu %zu", CGImageGetWidth(imageref), CGImageGetHeight(imageref));

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

    CGImageRelease(imageref);

    CGContextRelease(cgcontextref);

    return texture1;
}
CGContextRef MyCreateBitmapContext(const size_t POINTS_W, const size_t POINTS_H, const CGFloat SCALE) {
    CGContextRef    context = NULL;
    CGColorSpaceRef colorSpace;
    void *          bitmapData;
    //int             bitmapByteCount;
    size_t             bitmapBytesPerRow;

    const size_t PIXELS_W = POINTS_W * SCALE;
    const size_t PIXELS_H = POINTS_H * SCALE;

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

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

#define kBitmapInfo     kCGImageAlphaPremultipliedLast
    //#define kBitmapInfo       kCGImageAlphaPremultipliedFirst
    //#define kBitmapInfo       kCGImageAlphaNoneSkipFirst
    // According to http://stackoverflow.com/a/18921840/129202 it should be safe to just cast
    CGBitmapInfo bitmapinfo = (CGBitmapInfo)kBitmapInfo; //kCGImageAlphaNoneSkipFirst; //0; //kCGBitmapAlphaInfoMask; //kCGImageAlphaNone; //kCGImageAlphaNoneSkipFirst;
    context = CGBitmapContextCreate (bitmapData,// 4
                                     PIXELS_W,
                                     PIXELS_H,
                                     8,      // bits per component
                                     bitmapBytesPerRow,
                                     colorSpace,
                                     bitmapinfo
                                     );
    if (context == NULL) {
        free (bitmapData);// 5
        fprintf (stderr, "Context not created!");
        return NULL;
    }
    CGColorSpaceRelease( colorSpace );// 6

    // TEST!!
//  CGContextClipToRect(context, CGRectMake(0, 0, POINTS_W, POINTS_H));
    CGContextClipToRect(context, CGRectMake(0, 0, PIXELS_W, PIXELS_H));
    CGContextScaleCTM(context, SCALE, SCALE);

    return context;// 7
}

So, to some sum it up, I expect to call +(SKTexture*)gradientWithSize:(const CGSize)SIZE colors:(NSArray*)colors using points for SIZE.

回答1:

Updated the code as below based on LearnCocos2D's suggestion. When to scale and not to is confusing (because in the docs, CGBitmapContextCreate explicitly takes width and height in PIXELS) but this seems to have the size right also on retina screens. Still I don't know if the rendered resolution is really retina - but this is very hard to confirm using eye because after all this is a gradient... Still I'm using the same context creating functions for a texture repeater (to enable to use colorWithPatternImage with Sprite Kit). Maybe I have the time later to try a detailed texture repeated using this new version... Also leaving gradient.contentsScale = SCALE; on or off doesn't make a visible difference.

+(SKTexture*)gradientWithSize:(const CGSize)SIZE colors:(NSArray*)colors {
    // Hopefully this function would be platform independent one day.

    CGContextRef cgcontextref = MyCreateBitmapContext(SIZE.width, SIZE.height);
    NSAssert(cgcontextref != NULL, @"Failed creating context!");

    CAGradientLayer* gradient = CAGradientLayer.layer;
    DLog(@"gradient.contentScale: %f", gradient.contentsScale);
    //gradient.contentsScale = SCALE;
    DLog(@"gradient.contentScale: %f", gradient.contentsScale);
    gradient.frame = CGRectMake(0, 0, SIZE.width, SIZE.height);

    NSMutableArray* convertedcolors = [NSMutableArray array];
    for (SKColor* skcolor in colors) {
        [convertedcolors addObject:(id)skcolor.CGColor];
    }
    gradient.colors = convertedcolors;
    [gradient renderInContext:cgcontextref];

    CGImageRef imageref = CGBitmapContextCreateImage(cgcontextref);
    DLog(@"imageref pixel size: %zu %zu", CGImageGetWidth(imageref), CGImageGetHeight(imageref));

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

    CGImageRelease(imageref);

    CGContextRelease(cgcontextref);

    return texture1;
}
CGContextRef MyCreateBitmapContext(const size_t POINTS_W, const size_t POINTS_H/*, const CGFloat SCALE*/) {
    CGContextRef    context = NULL;
    CGColorSpaceRef colorSpace;
    void *          bitmapData;
    size_t             bitmapBytesPerRow;

    bitmapBytesPerRow   = (POINTS_W * 4);

    colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
    bitmapData = NULL;

#define kBitmapInfo     kCGImageAlphaPremultipliedLast
    // According to https://stackoverflow.com/a/18921840/129202 it should be safe to just cast
    CGBitmapInfo bitmapinfo = (CGBitmapInfo)kBitmapInfo;
    context = CGBitmapContextCreate (bitmapData,
                                     POINTS_W,
                                     POINTS_H,
                                     8,
                                     bitmapBytesPerRow,
                                     colorSpace,
                                     bitmapinfo
                                     );
    if (context == NULL) {
        free (bitmapData);
        fprintf (stderr, "Context not created!");
        return NULL;
    }
    CGColorSpaceRelease(colorSpace);

    return context;
}

Results below. The left is just a yellow rectangle created using SKSpriteNode. The right is a rectangle created by using the texture generated from the above function. The two rectangles are at the same position and have same point sizes, just different anchorPoints.

SKColor* color1 = SKColor.blueColor;
SKColor* color2 = [SKColor colorWithCalibratedRed:1 green:0 blue:0 alpha:0.5];
SKTexture* textureGradient = [SKTexture gradientWithSize:SIZE colors:@[color1, color2]];
SKSpriteNode* spriteGradient = [SKSpriteNode spriteNodeWithTexture:textureGradient];

1x resolution

2x resolution (retina)