diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 8e38e61405..1590a0ae5d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1329,9 +1329,8 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node let use_classic_value = get_document_node(node_id, context) .ok() .and_then(|document_node| document_node.input(UseClassicInput).and_then(|input| input.as_value())) - .and_then(|tagged| if let TaggedValue::Bool(value) = tagged { Some(*value) } else { None }); - let includes_use_classic = use_classic_value.is_some(); - let use_classic_value = use_classic_value.unwrap_or(false); + .and_then(|tagged| if let TaggedValue::Bool(value) = tagged { Some(*value) } else { None }) + .unwrap_or(false); let brightness_min = if use_classic_value { -100. } else { -150. }; let brightness_max = if use_classic_value { 100. } else { 150. }; @@ -1364,11 +1363,12 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node NumberInput::default().mode_increment().unit("%").min(contrast_min).max(100.), ); - let mut layout = vec![brightness, contrast]; - if includes_use_classic { - // TODO: When we no longer use this function in the temporary "Brightness/Contrast Classic" node, remove this conditional pushing and just always include this - let use_classic = bool_widget(ParameterWidgetsInfo::new(node_id, UseClassicInput, true, context), CheckboxInput::default()); - layout.push(LayoutGroup::row(use_classic)); + let use_classic = bool_widget(ParameterWidgetsInfo::new(node_id, UseClassicInput, true, context), CheckboxInput::default()); + + let mut layout = vec![brightness, contrast, LayoutGroup::row(use_classic)]; + if use_classic_value { + let number_input = NumberInput::default().mode_increment().min(0.).max(255.); + layout.push(spectrum_slider_row(node_id, context, ClassicPivotInput, bw_track(), Color::WHITE, 0., 255., 127., number_input)); } layout diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 13847c6215..2635368393 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -477,12 +477,10 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ aliases: &[ "graphene_raster_nodes::adjustments::BrightnessContrastNode", "graphene_core::raster::adjustments::BrightnessContrastNode", + "graphene_raster_nodes::adjustments::brightness_contrast_classic", + "graphene_raster_nodes::adjustments::BrightnessContrastClassicNode", ], }, - NodeReplacement { - node: graphene_std::raster_nodes::adjustments::brightness_contrast_classic::IDENTIFIER, - aliases: &["graphene_raster_nodes::adjustments::BrightnessContrastClassicNode"], - }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::channel_mixer::IDENTIFIER, aliases: &[ @@ -2207,6 +2205,31 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], inputs_count = 3; } + // The removed "Brightness/Contrast Classic" node had no Use Classic input, so its three inputs become the unified node with the toggle on + if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster::brightness_contrast::IDENTIFIER) && inputs_count == 3 { + let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); + document.network_interface.replace_implementation(node_id, network_path, &mut node_template); + let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; + for (index, input) in old_inputs.iter().take(3).enumerate() { + document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input.clone(), network_path); + } + document + .network_interface + .set_input(&InputConnector::node_at_index(*node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), network_path); + inputs_count = 4; + } + + // Brightness/Contrast gained the classic algorithm's pivot, whose default of 127 matches what PSD adjustment layers store + if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster::brightness_contrast::IDENTIFIER) && inputs_count == 4 { + let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); + document.network_interface.replace_implementation(node_id, network_path, &mut node_template); + let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; + for (index, input) in old_inputs.iter().take(4).enumerate() { + document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input.clone(), network_path); + } + inputs_count = 5; + } + // Levels' Midtones became the gamma value it encoded, and each channel gained its own record after the composite one if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster::levels::IDENTIFIER) && inputs_count == 6 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); diff --git a/node-graph/nodes/raster/src/adjustments.rs b/node-graph/nodes/raster/src/adjustments.rs index 9aca9f9a32..3b77ad70b2 100644 --- a/node-graph/nodes/raster/src/adjustments.rs +++ b/node-graph/nodes/raster/src/adjustments.rs @@ -1,7 +1,6 @@ #![allow(clippy::too_many_arguments)] use crate::adjust::Adjust; -use crate::cubic_spline::CubicSplines; use core::fmt::Debug; #[cfg(feature = "std")] use core_types::list::{Item, List}; @@ -82,11 +81,7 @@ pub enum DesaturateMethod { #[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))] fn desaturate>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] input: Item, method: Item, @@ -130,11 +125,7 @@ fn desaturate>( #[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))] fn gamma_correction>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] input: Item, #[default(2.2)] @@ -156,11 +147,7 @@ fn gamma_correction>( #[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))] fn extract_channel>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] input: Item, channel: Item, @@ -183,11 +170,7 @@ fn extract_channel>( #[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))] fn make_opaque>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] input: Item, ) -> Item { @@ -196,35 +179,98 @@ fn make_opaque>( input } -// TODO: Remove this once GPU shader nodes are able to support the non-classic algorithm -// TODO: Maybe re-add the "Raster: Adjustment" category to make this user-facing if we care to make this not just for testing -#[node_macro::node(name("Brightness/Contrast Classic"), category(""), properties("brightness_contrast_properties"), shader_node(PerPixelAdjust))] -fn brightness_contrast_classic>( - _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] - #[gpu_image] - input: Item, - brightness: Item, - contrast: Item, -) -> Item { - let mut input = input; - let brightness = brightness.into_element(); - let contrast = contrast.into_element(); +/// Remaps a gamma-space channel through the stages of a Levels adjustment: the input range, the midtones gamma, and the output range. +fn apply_levels(value: f32, input_shadows: f32, input_highlights: f32, inverse_gamma: f32, output_minimum: f32, output_maximum: f32) -> f32 { + let highlights_minus_shadows = (input_highlights - input_shadows).clamp(f32::EPSILON, 1.); + let value = ((value - input_shadows).max(0.) / highlights_minus_shadows).min(1.); + let value = value.powf(inverse_gamma); - let brightness = brightness / 255.; + value * (output_maximum - output_minimum) + output_minimum +} - let contrast = contrast / 100.; - let contrast = if contrast > 0. { (contrast * core::f32::consts::FRAC_PI_2 - 0.01).tan() } else { contrast }; +/// The classic Brightness/Contrast algorithm: a Levels remap around the pivot, adding the brightness before a positive +/// contrast stretch and after a negative contrast squeeze. +fn brightness_contrast_classic(value: f32, brightness: f32, contrast: f32, pivot: f32) -> f32 { + // Full contrast is a hard step, sending values at the pivot or above to white (with half a 16-bit step of slack for float ties) + if contrast >= 1. { + return if value + brightness >= pivot - 1. / 65536. { 1. } else { 0. }; + } - let offset = brightness * contrast + brightness - contrast / 2.; + let result = if contrast > 0. { + let input_shadows = pivot * contrast - brightness; + apply_levels(value, input_shadows, input_shadows + 1. - contrast, 1., 0., 1.) + } else { + let output_minimum = brightness - pivot * contrast; + apply_levels(value, 0., 1., 1., output_minimum, output_minimum + 1. + contrast) + }; - input.element_mut().adjust(|color| color.map_gamma_rgb(|c| (c + c * contrast + offset).clamp(0., 1.))); + result.clamp(0., 1.) +} - input +/// One brightness curve of the current algorithm, for a magnitude in 0..100: a line of slope 2^(b/110) up to an output of 0.5, +/// continued by a cubic Hermite segment that eases into (1, 1). +struct BrightnessCurve { + slope: f32, + knee: f32, + end_slope: f32, +} + +impl BrightnessCurve { + fn new(brightness: f32) -> Self { + let slope = 2_f32.powf(brightness / 110.); + let knee = 0.5 / slope; + let end_slope = (1. / (1. + 12. * (slope - 1.))).max(0.1); + + Self { slope, knee, end_slope } + } + + /// Evaluates the Hermite segment at its parameter t in 0..1, returning the value and its derivative with respect to x. + fn hermite(&self, t: f32) -> (f32, f32) { + let length = 1. - self.knee; + let start_tangent = length * self.slope; + let end_tangent = length * self.end_slope; + let t2 = t * t; + let t3 = t2 * t; + + let value = (2. * t3 - 3. * t2 + 1.) * 0.5 + (t3 - 2. * t2 + t) * start_tangent + (-2. * t3 + 3. * t2) + (t3 - t2) * end_tangent; + let derivative = ((6. * t2 - 6. * t) * 0.5 + (3. * t2 - 4. * t + 1.) * start_tangent + (-6. * t2 + 6. * t) + (3. * t2 - 2. * t) * end_tangent) / length; + + (value, derivative) + } + + fn forward(&self, x: f32) -> f32 { + if x < self.knee { + return self.slope * x; + } + + let t = ((x - self.knee) / (1. - self.knee)).min(1.); + self.hermite(t).0.min(1.) + } + + /// Inverts the curve, solving the monotone Hermite segment with a bracketed Newton iteration. + fn inverse(&self, y: f32) -> f32 { + if y <= 0.5 { + return y / self.slope; + } + + let mut low = 0.; + let mut high = 1.; + let mut t = (y - 0.5) * 2.; + for _ in 0..8 { + let (value, derivative) = self.hermite(t); + let error = value - y; + if error > 0. { + high = t; + } else { + low = t; + } + + let step = t - error / (derivative * (1. - self.knee)); + t = if step >= low && step <= high { step } else { (low + high) * 0.5 }; + } + + self.knee + t * (1. - self.knee) + } } // Aims for interoperable compatibility with: @@ -233,81 +279,49 @@ fn brightness_contrast_classic>( // // Some further analysis available at: // https://geraldbakker.nl/psnumbers/brightness-contrast.html -#[node_macro::node(name("Brightness/Contrast"), category("Raster: Adjustment"), properties("brightness_contrast_properties"), cfg(feature = "std"))] +// +// TODO: A Lab-only mode once Graphite supports the CIE Lab color space. +#[node_macro::node(name("Brightness/Contrast"), category("Raster: Adjustment"), properties("brightness_contrast_properties"), shader_node(PerPixelAdjust))] fn brightness_contrast>( - _ctx: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + _: impl Ctx, + #[implementations(Raster, Color, Gradient)] #[gpu_image] input: Item, brightness: Item, contrast: Item, use_classic: Item, + #[default(127.)] classic_pivot: Item, ) -> Item { - let use_classic = use_classic.into_element(); - if use_classic { - return brightness_contrast_classic(_ctx, input, brightness, contrast); - } - let mut input = input; let brightness = brightness.into_element(); - let contrast = contrast.into_element(); + let contrast = contrast.into_element() / 100.; + let use_classic = use_classic.into_element(); + let classic_pivot = classic_pivot.into_element() / 255.; - const WINDOW_SIZE: usize = 1024; + // Beyond a magnitude of 100, the curve for 100 is applied first and the curve for the remainder after it + let magnitude = brightness.abs().min(150.); + let first_curve = BrightnessCurve::new(magnitude.min(100.)); + let second_curve = BrightnessCurve::new((magnitude - 100.).max(0.)); - // Brightness LUT - let brightness_is_negative = brightness < 0.; - // We clamp the brightness before the two curve X-axis points `130 - brightness * 26` and `233 - brightness * 48` intersect. - // Beyond the point of intersection, the cubic spline fitting becomes invalid and fails an assertion, which we need to avoid. - // See the intersection of the red lines at x = 103/22*100 = 468.18182 in the graph: https://www.desmos.com/calculator/ekvz4zyd9c - let brightness = (brightness.abs() / 100.).min(103. / 22. - 0.00001); - let brightness_curve_points = CubicSplines { - x: [0., 130. - brightness * 26., 233. - brightness * 48., 255.].map(|x| x / 255.), - y: [0., 130. + brightness * 51., 233. + brightness * 10., 255.].map(|x| x / 255.), - }; - let brightness_curve_solutions = brightness_curve_points.solve(); - let mut brightness_lut: [f32; WINDOW_SIZE] = core::array::from_fn(|i| { - let x = i as f32 / (WINDOW_SIZE as f32 - 1.); - brightness_curve_points.interpolate(x, &brightness_curve_solutions) - }); - // Special handling for when brightness is negative - if brightness_is_negative { - brightness_lut = core::array::from_fn(|i| { - let mut x = i; - while x > 1 && brightness_lut[x] > i as f32 / WINDOW_SIZE as f32 { - x -= 1; + input.element_mut().adjust(|color| { + color.map_gamma_rgb(|c| { + if use_classic { + return brightness_contrast_classic(c, brightness / 255., contrast, classic_pivot); } - x as f32 / WINDOW_SIZE as f32 - }); - } - // Contrast LUT - // Unlike with brightness, the X-axis points `64` and `192` don't intersect at any contrast value, because they are constants. - // So we don't have to worry about clamping the contrast value to avoid invalid cubic spline fitting. - // See the graph: https://www.desmos.com/calculator/iql9vsca56 - let contrast = contrast / 100.; - let contrast_curve_points = CubicSplines { - x: [0., 64., 192., 255.].map(|x| x / 255.), - y: [0., 64. - contrast * 30., 192. + contrast * 30., 255.].map(|x| x / 255.), - }; - let contrast_curve_solutions = contrast_curve_points.solve(); - let contrast_lut: [f32; WINDOW_SIZE] = core::array::from_fn(|i| { - let x = i as f32 / (WINDOW_SIZE as f32 - 1.); - contrast_curve_points.interpolate(x, &contrast_curve_solutions) + // Negative brightness runs the same curves in reverse + let brightened = if brightness >= 0. { + second_curve.forward(first_curve.forward(c)) + } else { + first_curve.inverse(second_curve.inverse(c)) + }; + + // Contrast pushes away from (or pulls toward) the midpoint, most strongly at the quarter tones + let contrasted = brightened + 0.76 * contrast * (2. * brightened - 1.) * brightened.min(1. - brightened); + contrasted.clamp(0., 1.) + }) }); - // Composed brightness and contrast LUTs - let combined_lut = brightness_lut.map(|brightness| { - let index_in_contrast_lut = (brightness * (contrast_lut.len() - 1) as f32).round() as usize; - contrast_lut[index_in_contrast_lut] - }); - let lut_max = (combined_lut.len() - 1) as f32; - - input.element_mut().adjust(|color| color.map_gamma_rgb(|c| combined_lut[(c * lut_max).round() as usize])); - input } @@ -611,11 +625,7 @@ fn points_to_transfer_curve( #[node_macro::node(name("Black & White"), category("Raster: Adjustment"), properties("black_and_white_properties"), shader_node(PerPixelAdjust))] fn black_and_white>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] image: Item, #[default(Color::BLACK)] tint: Item, @@ -891,11 +901,7 @@ fn lightness_toward_max_or_min(rgb: [f32; 3], amount: f32) -> [f32; 3] { #[node_macro::node(name("Hue/Saturation"), category("Raster: Adjustment"), properties("hue_saturation_properties"), shader_node(PerPixelAdjust))] fn hue_saturation>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] input: Item, hue: Item, @@ -1113,11 +1119,7 @@ fn hue_saturation>( #[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))] fn invert>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] input: Item, ) -> Item { @@ -1197,11 +1199,7 @@ async fn gradient_map + Send>( #[node_macro::node(category("Raster: Adjustment"), properties("vibrance_properties"), shader_node(PerPixelAdjust))] fn vibrance>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] image: Item, vibrance: Item, @@ -1395,11 +1393,7 @@ pub enum DomainWarpType { #[node_macro::node(category("Raster: Adjustment"), properties("channel_mixer_properties"), shader_node(PerPixelAdjust))] fn channel_mixer>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] image: Item, @@ -1537,11 +1531,7 @@ pub enum SelectiveColorChoice { #[node_macro::node(category("Raster: Adjustment"), properties("selective_color_properties"), shader_node(PerPixelAdjust))] fn selective_color>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] image: Item, @@ -1708,11 +1698,7 @@ fn selective_color>( #[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))] fn posterize>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] input: Item, #[default(4)] @@ -1742,11 +1728,7 @@ fn posterize>( #[node_macro::node(category("Raster: Adjustment"), properties("exposure_properties"), shader_node(PerPixelAdjust))] fn exposure>( _: impl Ctx, - #[implementations( - Raster, - Color, - Gradient, - )] + #[implementations(Raster, Color, Gradient)] #[gpu_image] input: Item, exposure: Item, @@ -1964,6 +1946,73 @@ mod tests { } } + /// Runs the node on one gamma-space gray value (0..255) and returns the gamma-space result on the same scale. + fn run_brightness_contrast(value: f32, brightness: f32, contrast: f32, use_classic: bool) -> f32 { + let pixel = Color::from_gamma_srgb_channels(value / 255., value / 255., value / 255., 1.); + let result = brightness_contrast((), Item::new_from_element(pixel), brightness.into(), contrast.into(), use_classic.into(), 127_f32.into()); + result.into_element().to_gamma_srgb_channels()[0] * 255. + } + + #[test] + fn brightness_contrast_curves_brightness_and_pivots_contrast_at_the_midpoint() { + for (value, brightness, contrast, expected) in [ + (16., 100., 0., 30.), + (64., 100., 0., 120.), + (128., 100., 0., 209.), + (192., 100., 0., 245.), + (128., 20., 0., 145.), + (64., -100., 0., 34.), + (128., -100., 0., 68.), + (192., -100., 0., 111.), + (240., -100., 0., 177.), + (64., 150., 0., 162.), + (128., 150., 0., 239.), + (128., -150., 0., 50.), + (240., -150., 0., 131.), + (32., 0., 100., 14.), + (64., 0., 100., 40.), + (192., 0., 100., 216.), + (64., 0., -50., 76.), + (64., 0., 25., 58.), + (64., 50., 30., 81.), + (128., 50., 30., 178.), + (192., 50., 30., 233.), + (64., -60., -20., 48.), + (128., -60., -20., 92.), + (192., -60., -20., 139.), + ] { + let actual = run_brightness_contrast(value, brightness, contrast, false); + assert!( + (actual - expected).abs() <= 1., + "{value} at brightness {brightness}, contrast {contrast}: expected {expected}, got {actual}" + ); + } + } + + #[test] + fn brightness_contrast_classic_remaps_levels_around_the_pivot() { + for (value, brightness, contrast, expected) in [ + (0., 0., -50., 64.), + (100., 0., -50., 114.), + (255., 0., -50., 191.), + (64., 0., 50., 1.), + (100., 0., 50., 73.), + (200., 0., 50., 255.), + (50., 40., 40., 65.), + (100., 40., 40., 148.), + (50., -40., -40., 41.), + (200., -40., -40., 131.), + (126., 0., 100., 0.), + (128., 0., 100., 255.), + ] { + let actual = run_brightness_contrast(value, brightness, contrast, true); + assert!( + (actual - expected).abs() <= 1., + "{value} at brightness {brightness}, contrast {contrast}: expected {expected}, got {actual}" + ); + } + } + /// Runs Levels with composite and red records given as [black, white, gamma, output black, output white] with 0..255 points /// on one gamma-space gray value (0..255), returning the red and green results on the same scale. fn run_levels(value: f32, composite: [f32; 5], red: [f32; 5]) -> [f32; 2] { diff --git a/node-graph/nodes/raster/src/cubic_spline.rs b/node-graph/nodes/raster/src/cubic_spline.rs deleted file mode 100644 index f57f699beb..0000000000 --- a/node-graph/nodes/raster/src/cubic_spline.rs +++ /dev/null @@ -1,123 +0,0 @@ -#[derive(Debug)] -pub struct CubicSplines { - pub x: [f32; 4], - pub y: [f32; 4], -} - -impl CubicSplines { - pub fn solve(&self) -> [f32; 4] { - let (x, y) = (&self.x, &self.y); - - // Build an augmented matrix to solve the system of equations using Gaussian elimination - let mut augmented_matrix = [ - [ - 2. / (x[1] - x[0]), - 1. / (x[1] - x[0]), - 0., - 0., - // | - 3. * (y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])), - ], - [ - 1. / (x[1] - x[0]), - 2. * (1. / (x[1] - x[0]) + 1. / (x[2] - x[1])), - 1. / (x[2] - x[1]), - 0., - // | - 3. * ((y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])) + (y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1]))), - ], - [ - 0., - 1. / (x[2] - x[1]), - 2. * (1. / (x[2] - x[1]) + 1. / (x[3] - x[2])), - 1. / (x[3] - x[2]), - // | - 3. * ((y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1])) + (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2]))), - ], - [ - 0., - 0., - 1. / (x[3] - x[2]), - 2. / (x[3] - x[2]), - // | - 3. * (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2])), - ], - ]; - - // Gaussian elimination: forward elimination - for row in 0..4 { - let pivot_row_index = (row..4) - .max_by(|&a_row, &b_row| { - augmented_matrix[a_row][row] - .abs() - .partial_cmp(&augmented_matrix[b_row][row].abs()) - .unwrap_or(core::cmp::Ordering::Equal) - }) - .unwrap(); - - // Swap the current row with the row that has the largest pivot element - augmented_matrix.swap(row, pivot_row_index); - - // Eliminate the current column in all rows below the current one - for row_below_current in row + 1..4 { - assert!(augmented_matrix[row][row].abs() > f32::EPSILON); - - let scale_factor = augmented_matrix[row_below_current][row] / augmented_matrix[row][row]; - for col in row..5 { - augmented_matrix[row_below_current][col] -= augmented_matrix[row][col] * scale_factor - } - } - } - - // Gaussian elimination: back substitution - let mut solutions = [0.; 4]; - for col in (0..4).rev() { - assert!(augmented_matrix[col][col].abs() > f32::EPSILON); - - solutions[col] = augmented_matrix[col][4] / augmented_matrix[col][col]; - - for row in (0..col).rev() { - augmented_matrix[row][4] -= augmented_matrix[row][col] * solutions[col]; - augmented_matrix[row][col] = 0.; - } - } - - solutions - } - - pub fn interpolate(&self, input: f32, solutions: &[f32]) -> f32 { - if input <= self.x[0] { - return self.y[0]; - } - if input >= self.x[self.x.len() - 1] { - return self.y[self.x.len() - 1]; - } - - // Find the segment that the input falls between - let mut segment = 1; - while self.x[segment] < input { - segment += 1; - } - let segment_start = segment - 1; - let segment_end = segment; - - // Calculate the output value using quadratic interpolation - let input_value = self.x[segment_start]; - let input_value_prev = self.x[segment_end]; - let output_value = self.y[segment_start]; - let output_value_prev = self.y[segment_end]; - let solutions_value = solutions[segment_start]; - let solutions_value_prev = solutions[segment_end]; - - let output_delta = solutions_value_prev * (input_value - input_value_prev) - (output_value - output_value_prev); - let solution_delta = (output_value - output_value_prev) - solutions_value * (input_value - input_value_prev); - - let input_ratio = (input - input_value_prev) / (input_value - input_value_prev); - let prev_output_ratio = (1. - input_ratio) * output_value_prev; - let output_ratio = input_ratio * output_value; - let quadratic_ratio = input_ratio * (1. - input_ratio) * (output_delta * (1. - input_ratio) + solution_delta * input_ratio); - - let result = prev_output_ratio + output_ratio + quadratic_ratio; - result.clamp(0., 1.) - } -} diff --git a/node-graph/nodes/raster/src/lib.rs b/node-graph/nodes/raster/src/lib.rs index cd982d4890..58efebdbd9 100644 --- a/node-graph/nodes/raster/src/lib.rs +++ b/node-graph/nodes/raster/src/lib.rs @@ -3,7 +3,6 @@ pub mod adjust; pub mod adjustments; pub mod blending_nodes; -pub mod cubic_spline; pub mod fullscreen_vertex; /// required by shader macro