在macOS上,Metal中的sRGB颜色像素格式会产生奇怪的结果

5
背景:我正在尝试解决iTerm2中的问题:https://gitlab.com/gnachman/iterm2/issues/6703#note_71933498 iTerm2对于所有纹理(包括MTKView的可绘制纹理)都使用默认的像素格式MTLPixelFormatBGRA8Unorm。在我的机器上,如果我的纹理函数返回颜色C,那么在“以sRGB显示”模式下,数字色彩计应用程序将显示屏幕上的颜色为C。我不知道这为什么有效!它应该将我的纹理函数的原始颜色值转换为sRGB,导致它们改变,对吧?例如,当我的片段函数为窗口的背景颜色返回(0, 0.16, 0.20, 1)时,我看到的就是这样:

Proof that a texture with non-sRGB pixel format takes sRGB values from the fragment function

这很奇怪,但我可以接受直到有人抱怨它在他们的设备上不起作用(请参阅上面链接的问题)。对于他们来说,我的片段函数返回的值通过通用 -> sRGb 转换并且看起来不正确。这是一个黑苹果,所以可能是驱动程序错误或其他原因,但这促使我更深入地了解了这个问题。

如果我在所有地方将像素格式更改为MTLPixelFormatBGRA8Unorm_sRGB,那么我得到的颜色就无法解释:

enter image description here

片段函数保持不变,返回(0, 0.16, 0.2, 1)。它正在渲染到我使用MTLPixelFormatBGRA8Unorm_sRGB像素格式创建的纹理上(如果有影响的话,不是可绘制的,尽管可绘制具有相同的像素格式),我可以在GPU调试器中看到这个纹理被分配了你在上面看到的过亮的值。在这个截图中,数字颜色计显示了“中间纹理”缩略图的颜色:

enter image description here

这是调试目的修改过的片段函数:
``` fragment float4 iTermBackgroundColorFragmentShader(iTermBackgroundColorVertexFunctionOutput in [[stage_in]]) { return float4(0, 0.16, 0.20, 1); } ```
我还尝试了对 Apple 的 示例应用程序 (MetalTexturedMesh) 进行修改,使用 sRGB 像素格式并从其片段着色器返回相同的颜色,结果得到了相同的结果。这是我所做的更改。我只能得出结论,我不理解 Metal 如何定义像素格式,并且找不到任何合理的解释来解释我所看到的行为。

您的纹理实际字节值是多少?(通过GPU调试器的预览显示和颜色计会增加混淆的机会。)当配置为显示绝对值而非百分比时,这些值与颜色计显示的值相比如何? - Ken Thomases
我从GPU调试器中将纹理导出为PNG格式,颜色值为(0, 110, 124)。在sRGB模式下使用数字颜色计算器得到(3, 110, 123)。不确定为什么会略有不同,但仍然与我的预期相差甚远:(0, 41, 51)。 - George
不要使用GPU调试器。在应用程序代码中调用MTLTexture的一个-getBytes:...方法并转储它。在此之前不要忘记将纹理与CPU同步。另外,当您说您希望(0, 0.16, 0.20)变为(0, 41, 51)时,这是使用非sRGB像素格式吗? - Ken Thomases
BGRA的原始字节是7C 6E 00 FF。这肯定不是数字颜色计报告的内容,但它也与我的预期相去甚远。我本来希望得到33 29 00 ff。由于纹理函数是硬编码为return float4(0, 0.16, 0.20, 1);并且像素格式是sRGB,我希望蓝色通道中的0.2会产生0.2*255=51(十进制)= 0x33,而不是0x7c。我还可以看到它明显比在没有金属的情况下绘制的相同颜色(例如,在-drawRect:中)更亮。 - George
1个回答

4
原始字节为BGRA格式的7C 6E 00 FF。这显然不是数字色彩计报告的结果,但它也与我的预期相差很远。
实际上,这就是色彩计报告的结果。这个字节序列对应于(0, 0.43, 0.49)。
我本来希望看到的是33 29 00 ff。由于纹理函数硬编码为return float4(0, 0.16, 0.20, 1);,并且像素格式为sRGB,我期望蓝色通道中的0.2会产生0.2*255=51(十进制)=0x33,而不是0x7c。
您的预期是错误的。在着色器内部,颜色始终是线性的,从不是sRGB的。如果一个纹理是sRGB的,则从中读取会将其从sRGB转换为线性,并且写入它会将其从线性转换为sRGB。
根据您着色器中的线性值(0, 0.16, 0.2),在Metal Shading Language规范(PDF)第7.7.7节中描述的转换为sRGB会产生浮点数(0, 0.44, 0.48)和RGBA8Unorm格式的(0, 111, 124),即(0, 0x6f, 0x7c)。这非常接近您得到的结果。因此,对于sRGB,结果似乎是正确的。
规范中给出的从线性到sRGB的转换算法如下:
if (isnan(c)) c = 0.0;
if (c > 1.0)
    c = 1.0;
else if (c < 0.0)
    c = 0.0;
else if (c < 0.0031308)
    c = 12.92 * c;
else
    c = 1.055 * powr(c, 1.0/2.4) - 0.055;

sRGB转线性的转换算法如下:
if (c <= 0.04045)
    result = c / 12.92;
else
    result = powr((c + 0.055) / 1.055, 2.4);

我还可以看到,与没有金属的相同颜色(例如在-drawRect:中)绘制相比,它明显更亮。您是如何构建要绘制的颜色的呢?请注意,通用(也称为校准)RGB颜色空间不是线性的,它具有约1.8的伽玛值。核心图形有kCGColorSpaceGenericRGBLinear,它是线性的。谜团是为什么使用非sRGB纹理时会看到更暗的颜色。着色器中的硬编码颜色应始终表示相同的颜色。纹理的像素格式不应影响其最终显示方式(在舍入误差内)。也就是说,它从线性转换为纹理的像素格式,并且在显示时从纹理的像素格式转换为显示颜色配置文件。这是可传递的,因此结果应与直接从线性到显示颜色配置文件一样。您确定您没有从该纹理中提取字节,然后将其解释为sRGB吗?或者可能使用了-newTextureViewWithPixelFormat:...

好的,我认为期望一个返回float4(0, 0.16, 0.2, 1)的片段函数在纹理中产生特定的值是错误的。我关心的是它看起来与以下代码相同:- (void)drawRect:(NSRect)rect { [[NSColor colorWithSRGBRed:0 green:0.16 blue:0.20 alpha:1] set]; NSRectFill(rect); }但显然这并没有发生。 - George
好的,所以一个sRGB颜色(0, 0.16, 0.2)对应于线性颜色(0, 0.022, 0.033)。您希望您的着色器返回该值。您可以使用我在答案中添加的算法计算任意sRGB值的计算。或者,您可以手动设置sRGB纹理中的sRGB字节,将其传递到您的着色器中,并在着色器中从该纹理中读取颜色。这将使Metal进行转换。 - Ken Thomases

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接