Browse Source

WebGLRenderer: Add support for AgX Tone Mapping (#27366)

* Add support for AGX tone mapping

* Update tonemapping example

* Update test

* AGX -> AgX

* glsl lint, suffixes fix

* Adjust tone mapping constant values

* Update src/renderers/shaders/ShaderChunk/tonemapping_pars_fragment.glsl.js

Co-authored-by: Don McCurdy <[email protected]>

* Update src/renderers/shaders/ShaderChunk/tonemapping_pars_fragment.glsl.js

Co-authored-by: Don McCurdy <[email protected]>

* Update OutputPass.js

Add `AgX` support.

* Update OutputShader.js

Add `AgX` support.

* Clamp the values to be > 0.0

* Add rec2020 matrices

* Fix rec2020 conversion matrices

* Update implementation to be based on filament using rec 2020 color space

* Spaces -> tabs

* linting

* Code cleanup

* Support WebGL1, rearrange

* Comments update

* Remove redundant clamp

---------

Co-authored-by: Don McCurdy <[email protected]>
Co-authored-by: Michael Herzog <[email protected]>
Garrett Johnson 1 year ago
parent
commit
e041e082b7

+ 2 - 0
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;
 

+ 4 - 0
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

+ 1 - 0
examples/webgl_tonemapping.html

@@ -49,6 +49,7 @@
 				Reinhard: THREE.ReinhardToneMapping,
 				Cineon: THREE.CineonToneMapping,
 				ACESFilmic: THREE.ACESFilmicToneMapping,
+				AgX: THREE.AgXToneMapping,
 				Custom: THREE.CustomToneMapping
 			};
 

+ 1 - 0
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';
 

+ 84 - 0
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; }
 `;

+ 5 - 1
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;

+ 1 - 0
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' );