How can I render a CGImage in an MTKView while maintaining reasonable color space mapping?

459 views Asked by At

I want to render text on 2D placards drawn in a 3D MetalKit view. My approach is to first render the text into a CGImage and then to load a texture from it using MTKTextureLoader, which I can in turn easily render in the view. My code creates a resulting texture with colors radically different from those in the original (e.g., a red fill renders as turquoise.) The problem at hand is broader than just text; ordinary graphical fill colors are also a casualty.

I've tried a number of tricks, such as forcing everything to SRGB, being very explicit about color spaces, and so forth, to no avail.

Here's a drawInMTKView: that illustrates the problem with a simple red fill color:

- (void) drawInMTKView: (MTKView*) view {
    // "self" is my MetalKitView : MTKView <MTKViewDelegate>
    const CGColorSpaceRef colorSpace = [self colorspace];
    
    CGFloat REDArray[4] = { 1.0, 0.0, 0.0, 1.0 };
    
    // Either of these will fail in the same way
    struct CGColor *REDfillColor = CGColorCreateSRGB(1.0, 0.0, 0.0, 1.0);
    REDfillColor = CGColorCreate(colorSpace, REDArray);
    
    const CGContextRef graphicsContext = CGBitmapContextCreate(
                    NULL,
                    60,
                    14, 8, 0,
                    colorSpace, kCGImageAlphaPremultipliedFirst);
    assert (nil != graphicsContext);
    
    // These don't seem to make any difference
    CGContextSetFillColorSpace(graphicsContext, colorSpace);
    CGContextSetStrokeColorSpace(graphicsContext, colorSpace);
    
    CGContextSetFillColorWithColor(graphicsContext, REDfillColor);
    const CGRect wholeTamale = {
        {0.0, 0.0},
        {60, 14}
    };
    CGContextFillRect(graphicsContext, wholeTamale);
    CGContextFlush(graphicsContext);
    
    const CGImageRef cgImage = CGBitmapContextCreateImage(graphicsContext);
    MTKTextureLoader *const loader = [[MTKTextureLoader alloc] initWithDevice: metalDevice_];
    
    // YES and NO make no difference here
    NSDictionary *loaderOptions = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithInt: NO], MTKTextureLoaderOptionSRGB, nil];

    NSError *error = nil;
    // This texture is turquoise rather than red!
    id<MTLTexture> texture = [loader newTextureWithCGImage: cgImage
                                                   options: loaderOptions
                                                     error: &error];
    assert (nil == error);
    
    // this NSImage is just fine, so the problem is in the
    // texture loading
    NSImage *image2 = [[NSImage alloc] initWithCGImage: cgImage
                                                  size: wholeTamale.size];
    
    return;
}

MacOS Big Sur 11.5, XCode 12.5 (12E262), MacBook Pro. Adthanksvance.

Here is a screen shot per Hamid Yusifli's request below: display of miscolored texture

Note that the ordinary NSImage is fine (though small...): NSimage rendition of the same source

1

There are 1 answers

1
Hamid Yusifli On

Sorry for the late response.

Metal requires all textures to be formatted with a specific MTLPixelFormat value. The pixel format describes the layout of pixel data in the texture. In the sample that you provided you create image with kCGImageAlphaPremultipliedFirst flag.

The AlphaFirst/AlphaLast flags mean is the alpha either in the high order or the low order bits of the pixel. You set these regardless of the endian-ness you are trying to achieve. So if your data is ARGB (or the little endian equivalent BGRA) then the alpha is "first". If you have RGBA (or ABGR) then the alpha is "last".

To fix your color issue, you should change your alpha flag from kCGImageAlphaPremultipliedFirst to kCGImageAlphaPremultipliedLast.

// CGColorSpaceCreateWithName(kCGColorSpaceDisplayP3);
const CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat REDArray[4] = {1.0, 0.0, 0.0, 1.0};

struct CGColor* REDfillColor = CGColorCreateSRGB(1.0, 0.0, 0.0, 1.0);
REDfillColor = CGColorCreate(colorSpace, REDArray);

const CGContextRef graphicsContext = CGBitmapContextCreate(NULL, 60, 14, 8, 0, colorSpace, kCGImageAlphaPremultipliedLast);

assert (nil != graphicsContext);
CGContextSetFillColorSpace(graphicsContext, colorSpace);
CGContextSetStrokeColorSpace(graphicsContext, colorSpace);

CGContextSetFillColorWithColor(graphicsContext, REDfillColor);
const CGRect wholeTamale = { {0.0, 0.0}, {60, 14} };

CGContextFillRect(graphicsContext, wholeTamale);
CGContextFlush(graphicsContext);

const CGImageRef cgImage = CGBitmapContextCreateImage(graphicsContext);

MTKTextureLoader *const loader = [[MTKTextureLoader alloc] initWithDevice:_device];
id<MTLTexture> texture = [loader newTextureWithCGImage:cgImage options:NULL error:NULL];