diff --git a/examples/jsm/postprocessing/OutputPass.js b/examples/jsm/postprocessing/OutputPass.js index b3fe30301fc738..56746e894330c1 100644 --- a/examples/jsm/postprocessing/OutputPass.js +++ b/examples/jsm/postprocessing/OutputPass.js @@ -5,6 +5,7 @@ import { LinearToneMapping, ReinhardToneMapping, CineonToneMapping, + AgXToneMapping, ACESFilmicToneMapping, SRGBTransfer } from 'three'; @@ -59,6 +60,7 @@ class OutputPass extends Pass { else if ( this._toneMapping === ReinhardToneMapping ) this.material.defines.REINHARD_TONE_MAPPING = ''; else if ( this._toneMapping === CineonToneMapping ) this.material.defines.CINEON_TONE_MAPPING = ''; else if ( this._toneMapping === ACESFilmicToneMapping ) this.material.defines.ACES_FILMIC_TONE_MAPPING = ''; + else if ( this._toneMapping === AgXToneMapping ) this.material.defines.AGX_TONE_MAPPING = ''; this.material.needsUpdate = true; diff --git a/examples/jsm/shaders/OutputShader.js b/examples/jsm/shaders/OutputShader.js index a0b5fafe2a19b6..e654d853a8d7f1 100644 --- a/examples/jsm/shaders/OutputShader.js +++ b/examples/jsm/shaders/OutputShader.js @@ -60,6 +60,10 @@ const OutputShader = { gl_FragColor.rgb = ACESFilmicToneMapping( gl_FragColor.rgb ); + #elif defined( AGX_TONE_MAPPING ) + + gl_FragColor.rgb = AgXToneMapping( gl_FragColor.rgb ); + #endif // color space diff --git a/examples/webgl_tonemapping.html b/examples/webgl_tonemapping.html index b19011b0c09361..8a0bfad8ceb7e6 100644 --- a/examples/webgl_tonemapping.html +++ b/examples/webgl_tonemapping.html @@ -49,6 +49,7 @@ Reinhard: THREE.ReinhardToneMapping, Cineon: THREE.CineonToneMapping, ACESFilmic: THREE.ACESFilmicToneMapping, + AgX: THREE.AgXToneMapping, Custom: THREE.CustomToneMapping }; diff --git a/src/constants.js b/src/constants.js index dfa7a9b329bdc4..b734005f3fc13c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -57,6 +57,7 @@ export const ReinhardToneMapping = 2; export const CineonToneMapping = 3; export const ACESFilmicToneMapping = 4; export const CustomToneMapping = 5; +export const AgXToneMapping = 6; export const AttachedBindMode = 'attached'; export const DetachedBindMode = 'detached'; diff --git a/src/renderers/shaders/ShaderChunk/tonemapping_pars_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/tonemapping_pars_fragment.glsl.js index 777da0761e198c..89e1b3c6d9c626 100644 --- a/src/renderers/shaders/ShaderChunk/tonemapping_pars_fragment.glsl.js +++ b/src/renderers/shaders/ShaderChunk/tonemapping_pars_fragment.glsl.js @@ -73,5 +73,89 @@ vec3 ACESFilmicToneMapping( vec3 color ) { } +// Matrices for rec 2020 <> rec 709 color space conversion +// matrix provided in row-major order so it has been transposed +// https://www.itu.int/pub/R-REP-BT.2407-2017 +const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3( + vec3( 1.6605, - 0.1246, - 0.0182 ), + vec3( - 0.5876, 1.1329, - 0.1006 ), + vec3( - 0.0728, - 0.0083, 1.1187 ) +); + +const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3( + vec3( 0.6274, 0.0691, 0.0164 ), + vec3( 0.3293, 0.9195, 0.0880 ), + vec3( 0.0433, 0.0113, 0.8956 ) +); + +// https://iolite-engine.com/blog_posts/minimal_agx_implementation +// Mean error^2: 3.6705141e-06 +vec3 agxDefaultContrastApprox( vec3 x ) { + + vec3 x2 = x * x; + vec3 x4 = x2 * x2; + + return + 15.5 * x4 * x2 + - 40.14 * x4 * x + + 31.96 * x4 + - 6.868 * x2 * x + + 0.4298 * x2 + + 0.1191 * x + - 0.00232; + +} + +// Input and output encoded as Linear-sRGB. +vec3 AgXToneMapping( vec3 color ) { + + // AgX constants + const mat3 AgXInsetMatrix = mat3( + vec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ), + vec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ), + vec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 ) + ); + + // explicit AgXOutsetMatrix generated from Filaments AgXOutsetMatrixInv + const mat3 AgXOutsetMatrix = mat3( + vec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ), + vec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ), + vec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 ) + ); + + const float AgxMinEv = - 12.47393; // log2(pow(2, LOG2_MIN) * MIDDLE_GRAY) + const float AgxMaxEv = 4.026069; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY) + + // AGX Tone Mapping implementation based on Filament, which is in turn based + // on Blender's implementation for rec 2020 colors: + // https://github.com/google/filament/pull/7236 + color = LINEAR_SRGB_TO_LINEAR_REC2020 * color; + color *= toneMappingExposure; + + color = AgXInsetMatrix * color; + + // Log2 encoding + color = max( color, 1e-10 ); // avoid 0 or negative numbers for log2 + color = log2( color ); + color = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv ); + + color = clamp( color, 0.0, 1.0 ); + + // Apply sigmoid + color = agxDefaultContrastApprox( color ); + + // Apply AgX look + // v = agxLook(v, look); + + color = AgXOutsetMatrix * color; + + // Linearize + color = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) ); + + color = LINEAR_REC2020_TO_LINEAR_SRGB * color; + + return color; + +} + vec3 CustomToneMapping( vec3 color ) { return color; } `; diff --git a/src/renderers/webgl/WebGLProgram.js b/src/renderers/webgl/WebGLProgram.js index 0f04631cd656b8..b5a5ee3310ff08 100644 --- a/src/renderers/webgl/WebGLProgram.js +++ b/src/renderers/webgl/WebGLProgram.js @@ -1,7 +1,7 @@ import { WebGLUniforms } from './WebGLUniforms.js'; import { WebGLShader } from './WebGLShader.js'; import { ShaderChunk } from '../shaders/ShaderChunk.js'; -import { NoToneMapping, AddOperation, MixOperation, MultiplyOperation, CubeRefractionMapping, CubeUVReflectionMapping, CubeReflectionMapping, PCFSoftShadowMap, PCFShadowMap, VSMShadowMap, ACESFilmicToneMapping, CineonToneMapping, CustomToneMapping, ReinhardToneMapping, LinearToneMapping, GLSL3, LinearSRGBColorSpace, SRGBColorSpace, LinearDisplayP3ColorSpace, DisplayP3ColorSpace, P3Primaries, Rec709Primaries } from '../../constants.js'; +import { NoToneMapping, AddOperation, MixOperation, MultiplyOperation, CubeRefractionMapping, CubeUVReflectionMapping, CubeReflectionMapping, PCFSoftShadowMap, PCFShadowMap, VSMShadowMap, AgXToneMapping, ACESFilmicToneMapping, CineonToneMapping, CustomToneMapping, ReinhardToneMapping, LinearToneMapping, GLSL3, LinearSRGBColorSpace, SRGBColorSpace, LinearDisplayP3ColorSpace, DisplayP3ColorSpace, P3Primaries, Rec709Primaries } from '../../constants.js'; import { ColorManagement } from '../../math/ColorManagement.js'; // From https://www.khronos.org/registry/webgl/extensions/KHR_parallel_shader_compile/ @@ -120,6 +120,10 @@ function getToneMappingFunction( functionName, toneMapping ) { toneMappingName = 'ACESFilmic'; break; + case AgXToneMapping: + toneMappingName = 'AgX'; + break; + case CustomToneMapping: toneMappingName = 'Custom'; break; diff --git a/test/unit/src/constants.tests.js b/test/unit/src/constants.tests.js index 99e829aa547a44..d0341aead2857c 100644 --- a/test/unit/src/constants.tests.js +++ b/test/unit/src/constants.tests.js @@ -71,6 +71,7 @@ export default QUnit.module( 'Constants', () => { assert.equal( Constants.CineonToneMapping, 3, 'CineonToneMapping is equal to 3' ); assert.equal( Constants.ACESFilmicToneMapping, 4, 'ACESFilmicToneMapping is equal to 4' ); assert.equal( Constants.CustomToneMapping, 5, 'CustomToneMapping is equal to 5' ); + assert.equal( Constants.AgXToneMapping, 6, 'AgXToneMapping is equal to 6' ); assert.equal( Constants.AttachedBindMode, 'attached', 'AttachedBindMode is equal to attached' ); assert.equal( Constants.DetachedBindMode, 'detached', 'DetachedBindMode is equal to detached' );