Skip to content

Commit 3cd6283

Browse files
committed
kramv - fix display of images to match Figma/Photoshop
These apps do all filtering, blends, and premul in srgb space. This is totally wrong, but to match have to emulate it. The premul code applies the srgbToLinear conversion (not pow2.2) to the alpha before shader premul. pow2.2 is not a good approximation, with 20% of a black to white gradient not matching up. Note that kram already has isPremul and isPremulRGB. These correspond with linear and srgb premul to the texture. See below for how they differ. sRGB data must be held in an RGBA8Unorm (not RGBAUnorm_srgb) buffer to emulate what these tools do. This is counter-intuitive. But premul is directl is then correlated between alpha and rgb. When premul is applied to srgb, the rgb values are < alpha even when rgb = 111 due to engamma of rgb. This affects fromPremul() calls which try to divide out the alpha. isPremul: res = srgbToLinear( srgb ) * a ) build and filter mips in linear space res2 = linearToSrgb( res ) write out res2 to BC7_srgb isPremulRGB: res = srgb * a build and filter mips in srgb space write out res to BC7
1 parent 8f0551f commit 3cd6283

File tree

3 files changed

+128
-10
lines changed

3 files changed

+128
-10
lines changed

Diff for: kramv/KramRenderer.mm

+89-7
Original file line numberDiff line numberDiff line change
@@ -376,27 +376,108 @@ - (void)_loadMetalWithView:(nonnull MTKView *)view
376376
// false is good for srgb -> rgba16f
377377
// true is good for non-srgb -> rgba16f
378378
CGColorSpaceRef viewColorSpace;
379-
MTLPixelFormat format;
379+
MTLPixelFormat format = MTLPixelFormatRGBA16Float;
380380

381381
// This doesn't look like Figma or Photoshop for a rgb,a = 255,0 to 255,1 gradient across a 256px wide rect. The shader is saturating
382382
// the color to 0,1. So can get away with SRGB color space for now.
383383
// This also lines up with Preview.
384384
//viewColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGBLinear);
385385

386386

387+
CAMetalLayer* metalLayer = (CAMetalLayer*)[view layer];
387388

388389
// was using 16f so could sample hdr images from it
389390
// and also so hdr data went out to the display
390-
format = MTLPixelFormatRGBA16Float;
391-
viewColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
392-
//viewColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceLinearSRGB);
391+
uint32_t colorSpaceChoice = 1;
392+
switch(colorSpaceChoice) {
393+
default:
394+
case 0:
395+
// This is best so far
396+
format = MTLPixelFormatRGBA16Float;
397+
viewColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
398+
//viewColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceLinearSRGB);
399+
break;
400+
401+
case 1: {
402+
// Display P3 is a standard made by Apple that covers the same colour space as DCI-P3, but uses the more neutral D65 as a white point instead of the green white of the DCI standard.
403+
// Ideally feed 16-bit color to P3.
404+
405+
// This also works
406+
// 25% larger than srgb
407+
format = MTLPixelFormatRGBA16Float;
408+
409+
// This is industry format
410+
// viewColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceDCIP3);
411+
412+
// This isn't edr
413+
// viewColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceDisplayP3);
414+
415+
// Use this because it exists from 10.14.3+
416+
viewColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearDisplayP3);
417+
418+
// don't set this yet.
419+
// metalLayer.wantsExtendedDynamicRangeContent = YES;
420+
421+
// https://developer.apple.com/videos/play/wwdc2021/10161/
422+
423+
/* Can detect if on HDR display or not
424+
user can mod the brightness, or move to another monitor,
425+
need to listen for notification when this changes.
426+
427+
NSScreen* screen = NSScreen.mainScreen;
428+
429+
// This reports 1
430+
CGFloat val1 = screen.maximumExtendedDynamicRangeColorComponentValue;
431+
432+
// This is 16
433+
CGFloat maxPot = screen.maximumPotentialExtendedDynamicRangeColorComponentValue;
434+
435+
// This is 0
436+
CGFloat maxRef = screen.maximumReferenceExtendedDynamicRangeColorComponentValue;
437+
*/
438+
439+
// M1 monitor
440+
441+
442+
break;
443+
}
444+
case 2:
445+
// This doesn't match wnen srgb is turned off on TestColorGradient
446+
format = MTLPixelFormatRGBA8Unorm_sRGB;
447+
viewColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
448+
449+
// this looks totally wrong
450+
//viewColorSpace = CGColorSpaceCreateWithName(kCGColorLinearSpaceSRGB);
451+
break;
452+
453+
/*
454+
case 3: {
455+
// There is an exrMetadata field on NSView to set as well.
456+
// https://developer.apple.com/documentation/metal/hdr_content/using_color_spaces_to_display_hdr_content?language=objc
457+
458+
// Rec2020 color primaries, with PQ Transfer function.
459+
// Would have to get into Rec2020 colors to set this, also go from 10bit
460+
format = MTLPixelFormatBGR10A2Unorm;
461+
viewColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2100_PQ);
462+
463+
metalLayer.wantsExtendedDynamicRangeContent = YES;
464+
465+
// https://developer.apple.com/documentation/metal/hdr_content/using_system_tone_mapping_on_video_content?language=objc
466+
// must do os version check on this
467+
// 1.0 is 100 nits of output
468+
CAEDRMetadata* edrMetaData = [CAEDRMetadata HDR10MetadataWithMinLuminance: 0.005 maxLuminance: 1000 opticalOutputScale: 100];
469+
metalLayer.EDRMetadata = edrMetaData;
470+
471+
break;
472+
}
473+
*/
474+
}
475+
393476

394-
// This doesn't work with or without srgb color space
395-
//format = MTLPixelFormatRGBA8Unorm_sRGB;
396-
397477
view.colorPixelFormat = format;
398478
view.colorspace = viewColorSpace;
399479

480+
CGColorSpaceRelease(viewColorSpace);
400481

401482
view.depthStencilPixelFormat = MTLPixelFormatDepth32Float_Stencil8;
402483
view.sampleCount = 1;
@@ -1282,6 +1363,7 @@ - (void)_updateGameState
12821363

12831364
uniforms.isNormal = _showSettings->texContentType == TexContentTypeNormal;
12841365
uniforms.doShaderPremul = _showSettings->doShaderPremul;
1366+
uniforms.isSrgbInput = _showSettings->isSRGBShown && isSrgbFormat(_showSettings->originalFormat);
12851367
uniforms.isSigned = _showSettings->isSigned;
12861368
uniforms.isSwizzleAGToRG = _showSettings->isSwizzleAGToRG;
12871369

Diff for: kramv/Shaders/KramShaders.h

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ struct Uniforms {
127127
bool isSDF;
128128
bool isPreview; // render w/lighting, normals, etc
129129
bool isUVPreview; // show uv overlay
130+
bool isSrgbInput;
130131

131132
bool is3DView;
132133
bool isNormalMapPreview; // for isNormal or combined

Diff for: kramv/Shaders/KramShaders.metal

+38-3
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,41 @@ float3 calculateViewDir(float3 worldPos, float3 cameraPosition) {
915915
// This is writing out to 16F and could write snorm data, but then that couldn't be displayed.
916916
// So code first converts to Unorm.
917917

918+
float srgbToLinear(float s) {
919+
s = saturate(s);
920+
//return (s < 0.04044823) ? (s / 12.92)
921+
// : pow((s + 0.055) / 1.055, 2.4);
922+
923+
return (s < 0.04044823) ? (s * ( 1.0 / 12.92 ))
924+
: pow((s + 0.055) * ( 1.0 / 1.055 ), 2.4);
925+
926+
}
927+
928+
void shaderPremul(thread float4& c, bool isSrgbInput) {
929+
float alpha = c.a;
930+
931+
// This is because Figma/PS do all filering/blends in premul srgb.
932+
// So srgbToLinear( srgb * a ) = srgbToLinear( srgb ) * srgbToLinear( a ).
933+
// Emulating srgb blends also takes two alpha or access to dst.
934+
// Shader would need to export srgbToLinear( 1 - a ) for dstColor,
935+
// but that's not correct to blend alpha. Alpha still needs linear 1-a.
936+
937+
// TODO: make this one of the premul options (default for png)
938+
if (isSrgbInput)
939+
{
940+
// can tell difference in last 20% of TestAlphaGradient.png
941+
//c.a = pow(c.a, 2.2); // approx
942+
943+
c.a = srgbToLinear(c.a);
944+
}
945+
946+
c = toPremul(c);
947+
948+
// Note: be careful fromPremul would need to do similar math
949+
// have an alpha that is much smaller in rgb, than the blend alpha.
950+
c.a = alpha;
951+
}
952+
918953
float4 DrawPixels(
919954
ColorInOut in [[stage_in]],
920955
bool facing,
@@ -1000,7 +1035,7 @@ float4 DrawPixels(
10001035

10011036
// to premul, but also need to see without premul
10021037
if (uniforms.doShaderPremul) {
1003-
c = toPremul(c);
1038+
shaderPremul(c, uniforms.isSrgbInput);
10041039
}
10051040
}
10061041

@@ -1050,15 +1085,15 @@ float4 DrawPixels(
10501085
// Note: premul on signed should occur while still signed, since it's a pull to zoer
10511086
// to premul, but also need to see without premul
10521087
if (uniforms.doShaderPremul) {
1053-
c = toPremul(c);
1088+
shaderPremul(c, uniforms.isSrgbInput);
10541089
}
10551090

10561091
sc = c;
10571092
c.xyz = toUnorm(c.xyz);
10581093
}
10591094
else {
10601095
if (uniforms.doShaderPremul) {
1061-
c = toPremul(c);
1096+
shaderPremul(c, uniforms.isSrgbInput);
10621097
}
10631098
}
10641099

0 commit comments

Comments
 (0)