Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AgX Tone Mapping: Add Required Gamut Mapping #27413

Merged
merged 1 commit into from
Jan 2, 2024

Conversation

WestLangley
Copy link
Collaborator

This PR adds a gamut mapping step to the AgX tone mapping algorithm. This is required when mapping from the larger Rec.2020 gamut to the smaller Rec.709 gamut. The following image highlights out-of-gamut pixels, which are now avoided.

Screenshot 2023-12-20 at 3 32 11 PM

It remains to determine if the simple clamp() introduced in this PR is adequate for our purposes.

Copy link

📦 Bundle size

Full ESM build, minified and gzipped.

Filesize dev Filesize PR Diff
670.7 kB (166.8 kB) 670.7 kB (166.8 kB) +37 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Filesize dev Filesize PR Diff
451.5 kB (109.7 kB) 451.6 kB (109.7 kB) +37 B

const float AgxMaxEv = 4.026069; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY)
// LOG2_MIN = -10.0
// LOG2_MAX = +6.5
// MIDDLE_GRAY = 0.18
Copy link
Collaborator Author

@WestLangley WestLangley Dec 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 stops below middle gray. These comments help clarify the formulas that follow.

@mrdoob mrdoob added this to the r160 milestone Dec 21, 2023
Comment on lines +161 to +163
// Gamut mapping. Simple clamp for now.
color = clamp( color, 0.0, 1.0 );

Copy link
Collaborator

@gkjohnson gkjohnson Dec 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't done in our other color conversion functions (see P3 to Linear sRGB conversion). Is it something we should add there, as well?

That aside - is it important that the values be clamped to in-gamut after this conversion? It's very likely for the Linear sRGB values to come into this function with values far greater that 1.0 and some effects such as screen space bloom depend on those larger-than 1 values being output to the render target.

edit

I see now that AgX is clamped, as well. I guess I'm in part confused by this line in your original post:

This is required when mapping from the larger Rec.2020 gamut to the smaller Rec.709 gamut.

Is it important that we clamp that values after the color space conversion or after tone mapping? In tone mapping we seem to be clamping these values. In other color space conversions we are not.

Copy link
Collaborator

@donmccurdy donmccurdy Dec 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't done in our other color conversion functions (see P3 to Linear sRGB conversion). Is it something we should add there, as well?

I prefer to think of the color conversion functions themselves as open-domain-to-open-domain. But certainly any particular invocation of the functions needs to decide whether to clamp, gamut map, or keep the full domain.

... some effects such as screen space bloom depend on those larger-than 1 values being output to the render target

I'm trying hard to encourage users to put HDR effects like bloom before tone-mapping. See forum discussion. But yes, a lot of people have done this. My preference would mainly be consistency, whichever result that means. It looks like our existing tone mappers mostly do clamp before color space conversion.

@mrdoob mrdoob modified the milestones: r160, r161 Dec 22, 2023
@WestLangley
Copy link
Collaborator Author

The OETF which follows requires code values in [0, 1] currently.

This PR must be merged with r160.

@mrdoob
Copy link
Owner

mrdoob commented Dec 22, 2023

Happy to make a .1 release with this once everyone is happy with the PR 👌

@Mugen87 Mugen87 modified the milestones: r161, r160 Dec 23, 2023
@gkjohnson
Copy link
Collaborator

The OETF which follows requires code values in [0, 1] currently.

Can you please elaborate? I'm happy to have this PR merged based on consistency with the other tonemapping functions but I'd like to understand what you're suggesting and the reasoning for it.

I assuming the OETF after tone mapping you're referring to is the color space conversion step that happens in the <colorspace_fragment> shader block (ie p3 to linear srgb, etc). Is that right? If that's the case there are plenty of other scenarios where colors are being converted without being clamped (like when tone mapping isn't used and in the Color class). Are you suggesting that this is incorrect, as well?

@WestLangley
Copy link
Collaborator Author

If you are asking "why do we not clamp elsewhere", then that is a separate discussion unrelated to this PR. Certainly, when we map from a smaller gamut to a larger one, clamping is not required. When we map from a larger gamut to a smaller one, some form of gamut mapping is appropriate, unless the app accommodates out-of-gamut values. Clamping is the simplest form of gamut mapping.

The output of tone mapping is a code value between 0 and the maximum code value the monitor supports. The AgX tone mapping algorithm -- and all of the current three.js tone mapping algorithms -- only support LDR monitors. Hence, the peak code value is 1. At least, that is my understanding.

It is true, the monitor hardware likely clamps inputs to [0, 1] anyway, but the clamping here is just a stand-in for gamut mapping.

@sobotka
Copy link

sobotka commented Dec 25, 2023

Someone reached out and asked if I had any opinion on this matter. Apologies if this barges into the PR, or if this is the totally inappropriate place to do so.

Is it important that we clamp that values after the color space conversion or after tone mapping?

In the technical sense, both. Once the transform of open domain to open domain values is achieved, the interstitial log encoding stage asserts valid values for the working space in the zero to one hundred percent range. However, most log encoders will permit values beyond this range, and it will cause picture breakups that are nightmarish. I can explain if anyone cares.

Certainly, when we map from a smaller gamut to a larger one, clamping is not required.

Broadly correct, but it is worth noting that the primaries given are based on the three primary coordinates in CIE xy and an achromatic centroid value. It is quite possible that a “colourspace” can lead to tristimulus coordinates outside of the gamut volume, despite being “smaller”. Generally, the clamp is the brute force picture formation detail that I chose to make the original AgX demonstration. There was a good reason to not muddle it up beyond this brute force approach.

It is true, the monitor hardware likely clamps inputs to [0, 1] anyway, but the clamping here is just a stand-in for gamut mapping.

This brings up another detail in that once the picture is formed and everyone is happy, we have an issue reformulating the picture for other output mediums; we end up in an inception-like loop where the Grassmann additivity based transformation to another destination medium will lead to values outside of the display medium, and this can and will lead to visual cognition WTFs. The cheek ridge on the kids in the Red XMas picture is a terrific example here, where the destination transform to BT.709 causes out of domain values at the upper end of the medium limits. This leads to a very peculiar “lustre” along that cheek ridge line.

I never openly published one particular path I was had used in private repositories, but the results of the approach were very positive and of low cycle cost. The goal of a luminance constancy across the mediums can reduce some manifestations of errors. Remember, clipping a negative inadvertently will always increase energy / current in the resulting tristimulus.

  1. Calculate origin luminance from the tristimulus in the origin encoding. Let this be luminance_origin.
  2. Clamp the destination working space encoding at zero.
  3. Calculate the destination luminance post clamp. This is the luminance that the destination encoding can achieve at maximal purity. Let this be luminance_destination.
  4. Gain the destination RGB current by a brute force luminance_origin / luminance_destination.

The attenuation of purity in the Nightclub beam of the upper left will cause a cognitive hiccup when the basic brute force clamp is applied. It is nerfed slightly, although not ideally, via the luminance gaining technique outlined above. Notice how in most frustum dimensions we cognize a “cleft”? This is the byproduct of the clamp adding energy to the negative lobed values. As a result, the purity attenuation has a “gap” along the range, which amplifies the region with the clamp’s attenuation.

image

Keep kicking ass folks!

@mrdoob
Copy link
Owner

mrdoob commented Dec 26, 2023

Random question before it's too late...

Would be okay to name the constant from AgXToneMapping to AGXToneMapping?

After some searches I was unable to find the meaning of AgX 🤔

@sobotka
Copy link

sobotka commented Dec 26, 2023

Random question before it's too late...

Would be okay to name the constant from AgXToneMapping to AGXToneMapping?

Not sure if that was directed at me.

AgX is a pseudo-chemical term for the class of silver halides used in creative chemical film. Ag being silver, and the X as a holder for any of the other elements that are combined with it.

It originated as an experiment in picture formation that focused on the purity dimension in relation to our visual cognition’s underlying mechanic to scise a picture and parse it into meaningful mental assemblies.

@gkjohnson
Copy link
Collaborator

Thank you for the explanations!

Would be okay to name the constant from AgXToneMapping to AGXToneMapping?

I originally named it "AGXToneMapping" but all the resources online referred to it as AgX tone mapping. I feel like for the sake of consistency with the rest of the ecosystem we should retain the capitalization as-is.

@Mugen87 Mugen87 modified the milestones: r160, r161 Jan 2, 2024
@Mugen87 Mugen87 merged commit b3aa4c6 into mrdoob:dev Jan 2, 2024
12 checks passed
@WestLangley WestLangley deleted the dev-agx_gamut_mapping branch January 5, 2024 01:33
AdaRoseCannon pushed a commit to AdaRoseCannon/three.js that referenced this pull request Jan 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants