shaderscenekitmetalcolor-spacegamma

Metal: How to attach image to shader without color/gamma conversion?


Is it possible to send an image to a metal shader (from scenekit) without any color/gamma correction?

I have a data texture where each channel corresponds to specific values that I want to be able to test against. It's a world map with the green channel mapping to a country index. The country will be rendered red if it's 'active', grey otherwise.

Here's an example of the code I'm using:

sphereNode = SCNNode(geometry: sphereGeometry)
scene.rootNode.addChildNode(sphereNode)

let program = SCNProgram()
program.vertexFunctionName = "sliceVertex"       
program.fragmentFunctionName = "sliceFragment"
sphereNode.geometry?.firstMaterial?.program = program

let imageProperty = SCNMaterialProperty(contents: UIImage(named: "art.scnassets/Textures/earth-data-map-2k.png")!)
mageProperty.mipFilter = SCNFilterMode.none 
sphereNode.geometry?.firstMaterial?.setValue(imageProperty, forKey: "dataTexture")

The fragment shader:

fragment float4 sliceFragment(
    Vertex in [[stage_in]],
    texture2d<float, access::sample> dataTexture [[texture(0)]]
){
    constexpr sampler quadSampler(coord::normalized, filter::linear, address::repeat);
    float4 color = dataTexture.sample(quadSampler, in.texCoords);
    int index = int(color.g * 255.0);
    float active = index == 81 ? 1.0 : 0.0;
    float3 col = mix(float3(color.g,color.g,color.g), float3(1.0,0.0,0.0), active);
    return float4(col.r, col.g, col.b, 1);
}

The problem is that the UIImage that I'm using for the texture seems to be converted into linear space but I want to use the image data unchanged in the shader.


Solution

  • Metal always uses linear RGB within shaders for texture data. If a texture is sRGB rather than linear, reads and samples convert from sRGB to linear and writes convert from linear to sRGB.

    The texture pixel format is what tells Metal whether the data backing the texture is linear or sRGB. Most pixel formats are linear but some have names ending in _sRGB and are sRGB.

    Behind the scenes (no pun intended), SCNMaterialProperty and UIImage will interpret the image data and use the source color profile to pick the pixel format. If the PNG has a color profile that specifies something other than linear or sRGB, it's probably being converted to one of those. If it has no color profile, it's probably assumed to be SRGB.

    A PNG that's representing non-image data should carry a color profile that specifies that it's in linear RGB. If you can't modify the PNG and it's being interpreted as sRGB, you may need to go through CGImage and use copy(colorSpace:) to create a new image object with the same data and override its color profile. You'd get the desired color space using CGColorSpace(.genericRGBLinear).

    Also, you should presumably use filter::nearest for your shader's sampler. It makes no sense to combine adjacent country indexes.

    Better than all that, though, would probably be to use a data file format in your app's resources rather than an image file format. Then pass it to the shader in a buffer rather than a texture.