From c7d14c2a7be2adbebc10b96fb190fea6128cf0da Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 11 Apr 2023 01:52:44 -0700 Subject: [PATCH] Fix Levels and Exposure adjustment nodes (#1112) * Step Imaginate resolution by 64 * Fix the Exposure node * Change comments and variable names --- .../node_properties.rs | 12 ++-- node-graph/gcore/src/raster/adjustments.rs | 62 ++++++++++++------- node-graph/gcore/src/raster/color.rs | 3 +- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 62dca617..dc37dd0b 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -1007,13 +1007,7 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte }) .disabled(transform_not_connected) .on_update(update_value( - move |checkbox_input: &CheckboxInput| { - if checkbox_input.checked { - TaggedValue::OptionalDVec2(Some(vec2)) - } else { - TaggedValue::OptionalDVec2(None) - } - }, + move |checkbox_input: &CheckboxInput| TaggedValue::OptionalDVec2(if checkbox_input.checked { Some(vec2) } else { None }), node_id, resolution_index, )) @@ -1021,6 +1015,8 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte WidgetHolder::unrelated_separator(), NumberInput::new(Some(vec2.x)) .label("W") + .min(64.) + .step(64.) .unit(" px") .disabled(dimensions_is_auto && !transform_not_connected) .on_update(update_value( @@ -1032,6 +1028,8 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte WidgetHolder::related_separator(), NumberInput::new(Some(vec2.y)) .label("H") + .min(64.) + .step(64.) .unit(" px") .disabled(dimensions_is_auto && !transform_not_connected) .on_update(update_value( diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 41d89ac0..a657b2bc 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -195,33 +195,42 @@ pub struct LevelsNode { // From https://stackoverflow.com/questions/39510072/algorithm-for-adjustment-of-image-levels #[node_macro::node_fn(LevelsNode)] fn levels_node(color: Color, input_start: f64, input_mid: f64, input_end: f64, output_start: f64, output_end: f64) -> Color { - // Input Range + // Input Range (Range: 0-1) let input_shadows = (input_start / 100.) as f32; let input_midtones = (input_mid / 100.) as f32; let input_highlights = (input_end / 100.) as f32; - // Output Range + // Output Range (Range: 0-1) let output_minimums = (output_start / 100.) as f32; let output_maximums = (output_end / 100.) as f32; - // Midtones interpolation factor between minimums and maximums + // Midtones interpolation factor between minimums and maximums (Range: 0-1) let midtones = output_minimums + (output_maximums - output_minimums) * input_midtones; - // Gamma correction + // Gamma correction (Range: 0.01-10) let gamma = if midtones < 0.5 { - 1. / (1. + (9. * (1. - midtones * 2.))).min(9.99) + // Range: 0-1 + let x = 1. - midtones * 2.; + // Range: 1-10 + 1. + 9. * x } else { - 1. / ((1. - midtones) * 2.).max(0.01) + // Range: 0-0.5 + let x = 1. - midtones; + // Range: 0-1 + let x = x * 2.; + // Range: 0.01-1 + x.max(0.01) }; - // Input levels - let color = color.map_rgb(|channel| (channel - input_shadows) / (input_highlights - input_shadows)); + // Input levels (Range: 0-1) + let highlights_minus_shadows = (input_highlights - input_shadows).max(f32::EPSILON).min(1.); + let color = color.map_rgb(|c| (c - input_shadows).max(0.) / highlights_minus_shadows); - // Midtones - let color = color.map_rgb(|channel| channel.powf(gamma)); + // Midtones (Range: 0-1) + let color = color.gamma(gamma); - // Output levels - color.map_rgb(|channel| channel * (output_maximums - output_minimums) + output_minimums) + // Output levels (Range: 0-1) + color.map_rgb(|c| c * (output_maximums - output_minimums) + output_minimums) } #[derive(Debug, Clone, Copy, Default)] @@ -423,14 +432,14 @@ fn vibrance_node(color: Color, vibrance: f64) -> Color { let scale = 1. + scale * (1. - channel_difference); let luminance_initial = color.to_linear_srgb().luminance_srgb(); - let altered_color = color.map_rgb(|channel| (channel * scale - channel_reduction)).to_linear_srgb(); + let altered_color = color.map_rgb(|c| c * scale - channel_reduction).to_linear_srgb(); let luminance = altered_color.luminance_srgb(); - let altered_color = altered_color.map_rgb(|channel| channel * luminance_initial / luminance); + let altered_color = altered_color.map_rgb(|c| c * luminance_initial / luminance); let channel_max = altered_color.r().max(altered_color.g()).max(altered_color.b()); let altered_color = if Color::linear_to_srgb(channel_max) > 1. { let scale = (1. - luminance) / (channel_max - luminance); - altered_color.map_rgb(|channel| (channel - luminance) * scale + luminance) + altered_color.map_rgb(|c| (c - luminance) * scale + luminance) } else { altered_color }; @@ -445,7 +454,7 @@ fn vibrance_node(color: Color, vibrance: f64) -> Color { // Near -0% vibrance we mostly use `altered_color`. // Near -100% vibrance, we mostly use half the desaturated luminance color and half `altered_color`. let factor = -slowed_vibrance; - altered_color.map_rgb(|channel| channel * (1. - factor) + luminance * factor) + altered_color.map_rgb(|c| c * (1. - factor) + luminance * factor) }; // TODO: Remove conversion to linear when the whole node graph uses linear color @@ -500,15 +509,20 @@ pub struct ExposureNode { gamma_correction: GammaCorrection, } -// Based on https://stackoverflow.com/questions/12166117/what-is-the-math-behind-exposure-adjustment-on-photoshop +// Based on https://geraldbakker.nl/psnumbers/exposure.html #[node_macro::node_fn(ExposureNode)] fn exposure(color: Color, exposure: f64, offset: f64, gamma_correction: f64) -> Color { - let multiplier = 2_f32.powf(exposure as f32); - color - // TODO: Fix incorrect behavior of offset - .map_rgb(|channel: f32| channel + offset as f32) - // TODO: Fix incorrect behavior of exposure - .map_rgb(|channel: f32| channel * multiplier) - // TODO: While gamma correction is correct on its own, determine and implement the correct order of these three operations + // TODO: Remove conversion to linear when the whole node graph uses linear color + let color = color.to_linear_srgb(); + + let result = color + // Exposure + .map_rgb(|c: f32| c * 2_f32.powf(exposure as f32)) + // Offset + .map_rgb(|c: f32| c + offset as f32) + // Gamma correction .gamma(gamma_correction as f32) + .map_rgb(|c: f32| c.clamp(0., 1.)); + + result.to_gamma_srgb() } diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 8fb33ce7..1c555984 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -563,8 +563,7 @@ impl Color { pub fn gamma(&self, gamma: f32) -> Color { // From https://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-6-gamma-correction/ let inverse_gamma = 1. / gamma; - let per_channel = |channel: f32| channel.powf(inverse_gamma); - self.map_rgb(per_channel) + self.map_rgb(|c: f32| c.powf(inverse_gamma)) } pub fn to_linear_srgb(&self) -> Self {