diff --git a/examples/src/examples/gaussian-splatting/viewer.controls.mjs b/examples/src/examples/gaussian-splatting/viewer.controls.mjs index d5439ec6215..d8e6e545561 100644 --- a/examples/src/examples/gaussian-splatting/viewer.controls.mjs +++ b/examples/src/examples/gaussian-splatting/viewer.controls.mjs @@ -132,6 +132,17 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { precision: 2 }) ), + jsx( + LabelGroup, + { text: 'midtones' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'data.colorEnhance.midtones' }, + min: -1, + max: 1, + precision: 2 + }) + ), jsx( LabelGroup, { text: 'vibrance' }, diff --git a/examples/src/examples/gaussian-splatting/viewer.example.mjs b/examples/src/examples/gaussian-splatting/viewer.example.mjs index acc23d15ffa..7edde857179 100644 --- a/examples/src/examples/gaussian-splatting/viewer.example.mjs +++ b/examples/src/examples/gaussian-splatting/viewer.example.mjs @@ -179,6 +179,7 @@ assetListLoader.load(() => { enabled: false, shadows: 0, highlights: 0, + midtones: 0, vibrance: 0, dehaze: 0 } @@ -210,6 +211,7 @@ assetListLoader.load(() => { cameraFrame.colorEnhance.enabled = data.get('data.colorEnhance.enabled'); cameraFrame.colorEnhance.shadows = data.get('data.colorEnhance.shadows'); cameraFrame.colorEnhance.highlights = data.get('data.colorEnhance.highlights'); + cameraFrame.colorEnhance.midtones = data.get('data.colorEnhance.midtones'); cameraFrame.colorEnhance.vibrance = data.get('data.colorEnhance.vibrance'); cameraFrame.colorEnhance.dehaze = data.get('data.colorEnhance.dehaze'); diff --git a/examples/src/examples/graphics/post-processing.controls.mjs b/examples/src/examples/graphics/post-processing.controls.mjs index c35f3b80894..eca4f5fed97 100644 --- a/examples/src/examples/graphics/post-processing.controls.mjs +++ b/examples/src/examples/graphics/post-processing.controls.mjs @@ -200,6 +200,17 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { precision: 2 }) ), + jsx( + LabelGroup, + { text: 'midtones' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'data.colorEnhance.midtones' }, + min: -1, + max: 1, + precision: 2 + }) + ), jsx( LabelGroup, { text: 'vibrance' }, diff --git a/examples/src/examples/graphics/post-processing.example.mjs b/examples/src/examples/graphics/post-processing.example.mjs index 4e29b1438f1..5f470098eaf 100644 --- a/examples/src/examples/graphics/post-processing.example.mjs +++ b/examples/src/examples/graphics/post-processing.example.mjs @@ -261,6 +261,7 @@ assetListLoader.load(() => { if (cameraFrame.colorEnhance.enabled) { cameraFrame.colorEnhance.shadows = data.get('data.colorEnhance.shadows'); cameraFrame.colorEnhance.highlights = data.get('data.colorEnhance.highlights'); + cameraFrame.colorEnhance.midtones = data.get('data.colorEnhance.midtones'); cameraFrame.colorEnhance.vibrance = data.get('data.colorEnhance.vibrance'); cameraFrame.colorEnhance.dehaze = data.get('data.colorEnhance.dehaze'); } @@ -320,6 +321,7 @@ assetListLoader.load(() => { enabled: false, shadows: 0, highlights: 0, + midtones: 0, vibrance: 0, dehaze: 0 }, diff --git a/examples/src/examples/graphics/sky.controls.mjs b/examples/src/examples/graphics/sky.controls.mjs index f510a59edf5..3a32586a59d 100644 --- a/examples/src/examples/graphics/sky.controls.mjs +++ b/examples/src/examples/graphics/sky.controls.mjs @@ -126,6 +126,17 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { precision: 2 }) ), + jsx( + LabelGroup, + { text: 'midtones' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'data.colorEnhance.midtones' }, + min: -1, + max: 1, + precision: 2 + }) + ), jsx( LabelGroup, { text: 'vibrance' }, diff --git a/examples/src/examples/graphics/sky.example.mjs b/examples/src/examples/graphics/sky.example.mjs index 3683f68dd84..9469d2bf5b9 100644 --- a/examples/src/examples/graphics/sky.example.mjs +++ b/examples/src/examples/graphics/sky.example.mjs @@ -176,6 +176,7 @@ assetListLoader.load(() => { cameraFrame.colorEnhance.enabled = data.get('data.colorEnhance.enabled'); cameraFrame.colorEnhance.shadows = data.get('data.colorEnhance.shadows'); cameraFrame.colorEnhance.highlights = data.get('data.colorEnhance.highlights'); + cameraFrame.colorEnhance.midtones = data.get('data.colorEnhance.midtones'); cameraFrame.colorEnhance.vibrance = data.get('data.colorEnhance.vibrance'); cameraFrame.colorEnhance.dehaze = data.get('data.colorEnhance.dehaze'); cameraFrame.update(); @@ -190,6 +191,7 @@ assetListLoader.load(() => { enabled: false, shadows: 0, highlights: 0, + midtones: 0, vibrance: 0, dehaze: 0 }); diff --git a/scripts/esm/camera-frame.mjs b/scripts/esm/camera-frame.mjs index 5ef0588de1b..deac6566776 100644 --- a/scripts/esm/camera-frame.mjs +++ b/scripts/esm/camera-frame.mjs @@ -317,6 +317,14 @@ class ColorEnhance { */ highlights = 0; + /** + * @visibleif {enabled} + * @range [-1, 1] + * @precision 3 + * @step 0.01 + */ + midtones = 0; + /** * @visibleif {enabled} * @range [-1, 1] @@ -568,6 +576,7 @@ class CameraFrame extends Script { if (colorEnhance.enabled) { dstColorEnhance.shadows = colorEnhance.shadows; dstColorEnhance.highlights = colorEnhance.highlights; + dstColorEnhance.midtones = colorEnhance.midtones; dstColorEnhance.vibrance = colorEnhance.vibrance; dstColorEnhance.dehaze = colorEnhance.dehaze; } diff --git a/src/extras/render-passes/camera-frame.js b/src/extras/render-passes/camera-frame.js index abe61ce828c..784044f95f8 100644 --- a/src/extras/render-passes/camera-frame.js +++ b/src/extras/render-passes/camera-frame.js @@ -159,6 +159,9 @@ import { CameraFrameOptions, RenderPassCameraFrame } from './render-pass-camera- * @property {number} vibrance - The vibrance (smart saturation), -1 to 1 range. Positive values boost * saturation of less-saturated colors more than already-saturated ones. Negative values desaturate. * Defaults to 0. + * @property {number} midtones - The midtone adjustment, -1 to 1 range. Positive values brighten + * midtones, negative values darken midtones, with shadows and highlights more strongly preserved + * than by a linear exposure change. Defaults to 0. * @property {number} dehaze - The dehaze adjustment, -1 to 1 range. Positive values remove atmospheric * haze, increasing clarity and contrast. Negative values add a haze effect. Defaults to 0. */ @@ -331,6 +334,7 @@ class CameraFrame { shadows: 0, highlights: 0, vibrance: 0, + midtones: 0, dehaze: 0 }; @@ -541,6 +545,7 @@ class CameraFrame { composePass.colorEnhanceShadows = colorEnhance.shadows; composePass.colorEnhanceHighlights = colorEnhance.highlights; composePass.colorEnhanceVibrance = colorEnhance.vibrance; + composePass.colorEnhanceMidtones = colorEnhance.midtones; composePass.colorEnhanceDehaze = colorEnhance.dehaze; } diff --git a/src/extras/render-passes/render-pass-compose.js b/src/extras/render-passes/render-pass-compose.js index 629f01abe5a..2ff5a4b1eec 100644 --- a/src/extras/render-passes/render-pass-compose.js +++ b/src/extras/render-passes/render-pass-compose.js @@ -77,6 +77,8 @@ class RenderPassCompose extends RenderPassShaderQuad { colorEnhanceDehaze = 0; + colorEnhanceMidtones = 0; + _taaEnabled = false; _sharpness = 0.5; @@ -127,6 +129,7 @@ class RenderPassCompose extends RenderPassShaderQuad { this.colorLUTParams = new Float32Array(4); this.colorLUTParamsId = scope.resolve('colorLUTParams'); this.colorEnhanceParamsId = scope.resolve('colorEnhanceParams'); + this.colorEnhanceMidtonesId = scope.resolve('colorEnhanceMidtones'); } set debug(value) { @@ -385,6 +388,7 @@ class RenderPassCompose extends RenderPassShaderQuad { if (this._colorEnhanceEnabled) { this.colorEnhanceParamsId.setValue([this.colorEnhanceShadows, this.colorEnhanceHighlights, this.colorEnhanceVibrance, this.colorEnhanceDehaze]); + this.colorEnhanceMidtonesId.setValue(this.colorEnhanceMidtones); } const lutTexture = this._colorLUT; diff --git a/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose-color-enhance.js b/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose-color-enhance.js index 3a8fcfcae23..7c141ed1371 100644 --- a/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose-color-enhance.js +++ b/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose-color-enhance.js @@ -1,6 +1,7 @@ export default /* glsl */` #ifdef COLOR_ENHANCE uniform vec4 colorEnhanceParams; // x=shadows, y=highlights, z=vibrance, w=dehaze + uniform float colorEnhanceMidtones; vec3 applyColorEnhance(vec3 color) { float maxChannel = max(color.r, max(color.g, color.b)); @@ -19,6 +20,20 @@ export default /* glsl */` color *= pow(2.0, colorEnhanceParams.y * highlightWeight); } + // Midtones - localized exposure in log-luminance space + if (colorEnhanceMidtones != 0.0) { + const float pivot = 0.18; + const float widthStops = 1.25; + const float maxStops = 2.0; + float y = max(dot(color, vec3(0.2126, 0.7152, 0.0722)), 1e-6); + + // 0 at pivot, +/-1 one stop away from pivot + float d = log2(y / pivot); + float w = exp(-(d * d) / (2.0 * widthStops * widthStops)); + float stops = colorEnhanceMidtones * maxStops * w; + color *= exp2(stops); + } + // Vibrance - skip if zero (coherent branch) if (colorEnhanceParams.z != 0.0) { float minChannel = min(color.r, min(color.g, color.b)); diff --git a/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose-color-enhance.js b/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose-color-enhance.js index b033f03a01b..44899c9d011 100644 --- a/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose-color-enhance.js +++ b/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose-color-enhance.js @@ -1,6 +1,7 @@ export default /* wgsl */` #ifdef COLOR_ENHANCE uniform colorEnhanceParams: vec4f; // x=shadows, y=highlights, z=vibrance, w=dehaze + uniform colorEnhanceMidtones: f32; fn applyColorEnhance(color: vec3f) -> vec3f { var colorOut = color; @@ -20,6 +21,20 @@ export default /* wgsl */` colorOut *= pow(2.0, uniform.colorEnhanceParams.y * highlightWeight); } + // Midtones - localized exposure in log-luminance space + if (uniform.colorEnhanceMidtones != 0.0) { + let pivot = 0.18; + let widthStops = 1.25; + let maxStops = 2.0; + let y = max(dot(colorOut, vec3f(0.2126, 0.7152, 0.0722)), 1e-6); + + // 0 at pivot, +/-1 one stop away from pivot + let d = log2(y / pivot); + let w = exp(-(d * d) / (2.0 * widthStops * widthStops)); + let stops = uniform.colorEnhanceMidtones * maxStops * w; + colorOut *= exp2(stops); + } + // Vibrance - skip if zero (coherent branch) if (uniform.colorEnhanceParams.z != 0.0) { let minChannel = min(colorOut.r, min(colorOut.g, colorOut.b));