From 8d778e484818eb116471fcd3eea06f2678d1befc Mon Sep 17 00:00:00 2001 From: isiko Date: Thu, 25 May 2023 12:15:00 +0200 Subject: [PATCH] Introduce Split/Combine Channels nodes (#1153) * Add Channel Extrataction Node * Add hacky BlendModes for Inserting Color Channels * Fix Channel Exporter * Add Monochrome Option and Multi Output Node * Fix Input Mapping * Fix Formatting * Split Alpha Extraction to seperate node * Remove unnecessary functionality * Add Alpha Channel as an output to the extract channel node * Fix compilation * Add unpolished 'Combine Channels' Node * Fix Rebasing Issues * Add a bit of polish * Fix Rebase Issues * Switch from 'ColorChannel' to 'RedGreenBlue' I initially added an enum to hold color channels called 'ColorChannel', but while implementing the nodes, there somebody allready added a similar enum so I switched to that type * Add correct names * Add Improvement - Some Performance Improvements - Some Formatting Improvements * Add some improvements Most of this stuff was done by TrueDoctor in my Luchbreak :D * Implement IO Improvements - Converted primary output from split node to a dummy output - Removed primary Input from split node * Fix Formatting * Fix Combine RGB Node (hopefully final :D ) * Swap around Inputs and Outputs Move from ARGB -> RGBA * Improve naming * More naming fixes * Fix Replace -> Into * Rename Replacment -> Insertion * Add blank assist area --------- Co-authored-by: Keavon Chambers Co-authored-by: Dennis Kobert --- .../document_node_types.rs | 109 ++++++++++++++++++ .../node_properties.rs | 34 +++++- node-graph/gcore/src/raster.rs | 5 + node-graph/gcore/src/raster/adjustments.rs | 42 ++++++- node-graph/gcore/src/raster/color.rs | 49 +++++++- node-graph/gstd/src/raster.rs | 52 ++++++++- .../interpreted-executor/src/node_registry.rs | 45 ++++++++ 7 files changed, 331 insertions(+), 5 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index 11da3dfd..8b823e3c 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -371,6 +371,35 @@ fn static_nodes() -> Vec { outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::mask_properties, }, + DocumentNodeType { + name: "Insert Channel", + category: "Image Adjustments", + identifier: NodeImplementation::proto("graphene_std::raster::InsertChannelNode<_, _, _, _>"), + inputs: vec![ + DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), + DocumentInputType::value("Insertion", TaggedValue::ImageFrame(ImageFrame::empty()), true), + DocumentInputType::value("Replace", TaggedValue::RedGreenBlue(RedGreenBlue::Red), false), + ], + outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], + properties: node_properties::insert_channel_properties, + }, + DocumentNodeType { + name: "Combine Channels", + category: "Image Adjustments", + identifier: NodeImplementation::proto("graphene_std::raster::CombineChannelsNode"), + inputs: vec![ + DocumentInputType::value("None", TaggedValue::None, false), + DocumentInputType::value("Red", TaggedValue::ImageFrame(ImageFrame::empty()), true), + DocumentInputType::value("Green", TaggedValue::ImageFrame(ImageFrame::empty()), true), + DocumentInputType::value("Blue", TaggedValue::ImageFrame(ImageFrame::empty()), true), + DocumentInputType::value("Alpha", TaggedValue::ImageFrame(ImageFrame::empty()), true), + ], + outputs: vec![DocumentOutputType { + name: "Image", + data_type: FrontendGraphDataType::Raster, + }], + properties: node_properties::no_properties, + }, DocumentNodeType { name: "Blend", category: "Image Adjustments", @@ -483,6 +512,86 @@ fn static_nodes() -> Vec { outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: node_properties::luminance_properties, }, + DocumentNodeType { + name: "Extract Channel", + category: "Image Adjustments", + identifier: NodeImplementation::proto("graphene_core::raster::ExtractChannelNode<_>"), + inputs: vec![ + DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), + DocumentInputType::value("From", TaggedValue::RedGreenBlue(RedGreenBlue::Red), false), + ], + outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], + properties: node_properties::extract_channel_properties, + }, + DocumentNodeType { + name: "Extract Alpha", + category: "Image Adjustments", + identifier: NodeImplementation::proto("graphene_core::raster::ExtractAlphaNode<>"), + inputs: vec![DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true)], + outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], + properties: node_properties::no_properties, + }, + DocumentNodeType { + name: "Split Channels", + category: "Image Adjustments", + identifier: NodeImplementation::DocumentNode(NodeNetwork { + inputs: vec![0], + outputs: vec![NodeOutput::new(4, 0), NodeOutput::new(1, 0), NodeOutput::new(2, 0), NodeOutput::new(3, 0), NodeOutput::new(4, 0)], + nodes: [ + DocumentNode { + name: "Identity".to_string(), + inputs: vec![NodeInput::Network(concrete!(ImageFrame))], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode")), + ..Default::default() + }, + DocumentNode { + name: "RedNode".to_string(), + inputs: vec![NodeInput::node(0, 0), NodeInput::value(TaggedValue::RedGreenBlue(RedGreenBlue::Red), false)], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::raster::ExtractChannelNode<_>")), + ..Default::default() + }, + DocumentNode { + name: "GreenNode".to_string(), + inputs: vec![NodeInput::node(0, 0), NodeInput::value(TaggedValue::RedGreenBlue(RedGreenBlue::Green), false)], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::raster::ExtractChannelNode<_>")), + ..Default::default() + }, + DocumentNode { + name: "BlueNode".to_string(), + inputs: vec![NodeInput::node(0, 0), NodeInput::value(TaggedValue::RedGreenBlue(RedGreenBlue::Blue), false)], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::raster::ExtractChannelNode<_>")), + ..Default::default() + }, + DocumentNode { + name: "AlphaNode".to_string(), + inputs: vec![NodeInput::node(0, 0)], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::raster::ExtractAlphaNode<>")), + ..Default::default() + }, + DocumentNode { + name: "EmptyOutput".to_string(), + inputs: vec![NodeInput::value(TaggedValue::ImageFrame(ImageFrame::empty()), false)], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode")), + ..Default::default() + }, + ] + .into_iter() + .enumerate() + .map(|(id, node)| (id as NodeId, node)) + .collect(), + + ..Default::default() + }), + inputs: vec![DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true)], + outputs: vec![ + DocumentOutputType::new("Empty", FrontendGraphDataType::Raster), + DocumentOutputType::new("Red", FrontendGraphDataType::Raster), + DocumentOutputType::new("Green", FrontendGraphDataType::Raster), + DocumentOutputType::new("Blue", FrontendGraphDataType::Raster), + DocumentOutputType::new("Alpha", FrontendGraphDataType::Raster), + ], + properties: node_properties::no_properties, + }, DocumentNodeType { name: "Gaussian Blur", category: "Image Filters", 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 5049bb50..df51b029 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 @@ -231,6 +231,26 @@ fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na widgets } +//TODO Use generalized Version of this as soon as it's available +fn color_channel(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + if let &NodeInput::Value { + tagged_value: TaggedValue::RedGreenBlue(mode), + exposed: false, + } = &document_node.inputs[index] + { + let calculation_modes = [RedGreenBlue::Red, RedGreenBlue::Green, RedGreenBlue::Blue]; + let mut entries = Vec::with_capacity(calculation_modes.len()); + for method in calculation_modes { + entries.push(DropdownEntryData::new(method.to_string()).on_update(update_value(move |_| TaggedValue::RedGreenBlue(method), node_id, index))); + } + let entries = vec![entries]; + + widgets.extend_from_slice(&[WidgetHolder::unrelated_separator(), DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder()]); + } + LayoutGroup::Row { widgets }.with_tooltip("Color Channel") +} + //TODO Use generalized Version of this as soon as it's available fn blend_mode(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); @@ -251,7 +271,7 @@ fn blend_mode(document_node: &DocumentNode, node_id: u64, index: usize, name: &s LayoutGroup::Row { widgets }.with_tooltip("Formula used for blending") } -// TODO: Generalize this for all dropdowns ( also see blend_mode ) +// TODO: Generalize this for all dropdowns ( also see blend_mode and channel_extration ) fn luminance_calculation(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); if let &NodeInput::Value { @@ -590,6 +610,18 @@ pub fn luminance_properties(document_node: &DocumentNode, node_id: NodeId, _cont vec![luminance_calc] } +pub fn insert_channel_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let color_channel = color_channel(document_node, node_id, 2, "Into", true); + + vec![color_channel] +} + +pub fn extract_channel_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let color_channel = color_channel(document_node, node_id, 1, "From", true); + + vec![color_channel] +} + pub fn adjust_hsl_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let hue_shift = number_widget(document_node, node_id, 1, "Hue Shift", NumberInput::default().min(-180.).max(180.).unit("°"), true); let saturation_shift = number_widget(document_node, node_id, 2, "Saturation Shift", NumberInput::default().min(-100.).max(100.).unit("%"), true); diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index aa2b58cd..01b3004c 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -140,6 +140,11 @@ pub trait RGB: Pixel { self.blue() } } +pub trait RGBMut: RGB { + fn set_red(&mut self, red: Self::ColorChannel); + fn set_green(&mut self, green: Self::ColorChannel); + fn set_blue(&mut self, blue: Self::ColorChannel); +} pub trait AssociatedAlpha: RGB + Alpha { fn to_unassociated(&self) -> Out; diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index f57c170b..0c353029 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -46,7 +46,7 @@ impl core::fmt::Display for LuminanceCalculation { } impl BlendMode { - pub fn list() -> [BlendMode; 26] { + pub fn list() -> [BlendMode; 29] { [ BlendMode::Normal, BlendMode::Multiply, @@ -74,6 +74,9 @@ impl BlendMode { BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity, + BlendMode::InsertRed, + BlendMode::InsertGreen, + BlendMode::InsertBlue, ] } } @@ -121,6 +124,11 @@ pub enum BlendMode { Saturation, Color, Luminosity, + + // Other Stuff + InsertRed, + InsertGreen, + InsertBlue, } impl core::fmt::Display for BlendMode { @@ -157,6 +165,10 @@ impl core::fmt::Display for BlendMode { BlendMode::Saturation => write!(f, "Saturation"), BlendMode::Color => write!(f, "Color"), BlendMode::Luminosity => write!(f, "Luminosity"), + + BlendMode::InsertRed => write!(f, "Insert Red"), + BlendMode::InsertGreen => write!(f, "Insert Green"), + BlendMode::InsertBlue => write!(f, "Insert Blue"), } } } @@ -178,6 +190,30 @@ fn luminance_color_node(color: Color, luminance_calc: LuminanceCalculation) -> C color.map_rgb(|_| luminance) } +#[derive(Debug, Clone, Copy, Default)] +pub struct ExtractChannelNode { + channel: TargetChannel, +} + +#[node_macro::node_fn(ExtractChannelNode)] +fn extract_channel_node(color: Color, channel: RedGreenBlue) -> Color { + let extracted_value = match channel { + RedGreenBlue::Red => color.r(), + RedGreenBlue::Green => color.g(), + RedGreenBlue::Blue => color.b(), + }; + return color.map_rgb(|_| extracted_value); +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct ExtractAlphaNode; + +#[node_macro::node_fn(ExtractAlphaNode)] +fn extract_alpha_node(color: Color) -> Color { + let alpha = color.a(); + Color::from_rgbaf32(alpha, alpha, alpha, 1.0).unwrap() +} + #[derive(Debug, Clone, Copy, Default)] pub struct LevelsNode { input_start: InputStart, @@ -397,6 +433,10 @@ fn blend_node(input: (Color, Color), blend_mode: BlendMode, opacity: f64) -> Col BlendMode::Saturation => background.blend_saturation(foreground), BlendMode::Color => background.blend_color(foreground), BlendMode::Luminosity => background.blend_luminosity(foreground), + + BlendMode::InsertRed => foreground.with_red(background.r()), + BlendMode::InsertGreen => foreground.with_green(background.g()), + BlendMode::InsertBlue => foreground.with_blue(background.b()), }; background.alpha_blend(target_color.to_associated_alpha(opacity as f32)) diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 14365431..e7917cdf 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -12,7 +12,7 @@ use spirv_std::num_traits::Euclid; use bytemuck::{Pod, Zeroable}; -use super::{Alpha, AssociatedAlpha, Luminance, Pixel, Rec709Primaries, RGB, SRGB}; +use super::{Alpha, AssociatedAlpha, Luminance, Pixel, RGBMut, Rec709Primaries, RGB, SRGB}; #[repr(C)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -86,6 +86,17 @@ impl RGB for Color { self.blue } } +impl RGBMut for Color { + fn set_red(&mut self, red: Self::ColorChannel) { + self.red = red; + } + fn set_green(&mut self, green: Self::ColorChannel) { + self.green = green; + } + fn set_blue(&mut self, blue: Self::ColorChannel) { + self.blue = blue; + } +} impl Pixel for Color { #[cfg(not(target_arch = "spirv"))] @@ -391,6 +402,42 @@ impl Color { Color::from_hsla(hue, saturation, lightness, alpha) } + pub fn with_alpha(&self, alpha: f32) -> Color { + Color { + red: self.red, + green: self.green, + blue: self.blue, + alpha, + } + } + + pub fn with_red(&self, red: f32) -> Color { + Color { + red, + green: self.green, + blue: self.blue, + alpha: self.alpha, + } + } + + pub fn with_green(&self, green: f32) -> Color { + Color { + red: self.red, + green, + blue: self.blue, + alpha: self.alpha, + } + } + + pub fn with_blue(&self, blue: f32) -> Color { + Color { + red: self.red, + green: self.green, + blue, + alpha: self.alpha, + } + } + #[inline(always)] pub fn blend_normal(_c_b: f32, c_s: f32) -> f32 { c_s diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 6f559627..1ee9a996 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -1,6 +1,6 @@ use dyn_any::{DynAny, StaticType}; use glam::{DAffine2, DVec2}; -use graphene_core::raster::{Alpha, BlendMode, BlendNode, Image, ImageFrame, LinearChannel, Luminance, Pixel, RasterMut, Sample}; +use graphene_core::raster::{Alpha, BlendMode, BlendNode, Image, ImageFrame, Linear, LinearChannel, Luminance, Pixel, RGBMut, Raster, RasterMut, RedGreenBlue, Sample}; use graphene_core::transform::Transform; use graphene_core::value::CopiedNode; @@ -174,6 +174,54 @@ fn compute_transformed_bounding_box(transform: DAffine2) -> Bbox { } } +#[derive(Debug, Clone, Copy)] +pub struct InsertChannelNode { + insertion: Insertion, + target_channel: TargetChannel, + _p: PhantomData

, + _s: PhantomData, +} + +#[node_macro::node_fn(InsertChannelNode<_P, _S>)] +fn insert_channel_node< + // _P is the color of the input image. + _P: RGBMut, + _S: Pixel + Luminance, + // Input image + Input: RasterMut, + Insertion: Raster, +>( + mut image: Input, + insertion: Insertion, + target_channel: RedGreenBlue, +) -> Input +where + _P::ColorChannel: Linear, +{ + if insertion.width() == 0 { + return image; + } + + if insertion.width() != image.width() || insertion.height() != image.height() { + log::warn!("Stencil and image have different sizes. This is not supported."); + return image; + } + + for y in 0..image.height() { + for x in 0..image.width() { + let image_pixel = image.get_pixel_mut(x, y).unwrap(); + let insertion_pixel = insertion.get_pixel(x, y).unwrap(); + match target_channel { + RedGreenBlue::Red => image_pixel.set_red(insertion_pixel.l().cast_linear_channel()), + RedGreenBlue::Green => image_pixel.set_green(insertion_pixel.l().cast_linear_channel()), + RedGreenBlue::Blue => image_pixel.set_blue(insertion_pixel.l().cast_linear_channel()), + } + } + } + + image +} + #[derive(Debug, Clone, Copy)] pub struct MaskImageNode { stencil: Stencil, @@ -182,7 +230,7 @@ pub struct MaskImageNode { } #[node_macro::node_fn(MaskImageNode<_P, _S>)] -fn mask_image< +fn mask_imge< // _P is the color of the input image. It must have an alpha channel because that is going to // be modified by the mask _P: Copy + Alpha, diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index d63150ec..0990c18f 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -153,6 +153,49 @@ fn node_registry() -> HashMap, input: ImageFrame, params: []), register_node!(graphene_std::raster::MaskImageNode<_, _, _>, input: ImageFrame, params: [ImageFrame]), register_node!(graphene_std::raster::MaskImageNode<_, _, _>, input: ImageFrame, params: [ImageFrame]), + register_node!(graphene_std::raster::InsertChannelNode<_, _, _, _>, input: ImageFrame, params: [ImageFrame, RedGreenBlue]), + register_node!(graphene_std::raster::InsertChannelNode<_, _, _, _>, input: ImageFrame, params: [ImageFrame, RedGreenBlue]), + vec![( + NodeIdentifier::new("graphene_std::raster::CombineChannelsNode"), + |args| { + use graphene_core::raster::*; + use graphene_core::value::*; + + let channel_r: DowncastBothNode<(), ImageFrame> = DowncastBothNode::new(args[0]); + let channel_g: DowncastBothNode<(), ImageFrame> = DowncastBothNode::new(args[1]); + let channel_b: DowncastBothNode<(), ImageFrame> = DowncastBothNode::new(args[2]); + let channel_a: DowncastBothNode<(), ImageFrame> = DowncastBothNode::new(args[3]); + + let insert_r = InsertChannelNode::new(channel_r.clone(), CopiedNode::new(RedGreenBlue::Red)); + let insert_g = InsertChannelNode::new(channel_g.clone(), CopiedNode::new(RedGreenBlue::Green)); + let insert_b = InsertChannelNode::new(channel_b.clone(), CopiedNode::new(RedGreenBlue::Blue)); + let complete_node = insert_r.then(insert_g).then(insert_b); + let complete_node = complete_node.then(MaskImageNode::new(channel_a.clone())); + + // TODO: Move to FN Node for better performance + let (mut transform, mut bounds) = (DAffine2::ZERO, glam::UVec2::ZERO); + for imf in [channel_a, channel_r, channel_g, channel_b] { + let image = imf.eval(()); + if image.image.width() > bounds.x { + bounds = glam::UVec2::new(image.image.width(), image.image.height()); + transform = image.transform; + } + } + let empty_image = ImageFrame { + image: Image::new(bounds.x, bounds.y, Color::BLACK), + transform, + }; + let final_image = ClonedNode::new(empty_image).then(complete_node); + + let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(final_image)); + Box::pin(any) + }, + NodeIOTypes::new( + concrete!(()), + concrete!(ImageFrame), + vec![value_fn!(ImageFrame), value_fn!(ImageFrame), value_fn!(ImageFrame), value_fn!(ImageFrame)], + ), + )], register_node!(graphene_std::raster::EmptyImageNode<_, _>, input: DAffine2, params: [Color]), register_node!(graphene_std::memo::MonitorNode<_>, input: ImageFrame, params: []), register_node!(graphene_std::memo::MonitorNode<_>, input: graphene_core::GraphicGroup, params: []), @@ -251,6 +294,8 @@ fn node_registry() -> HashMap, params: [LuminanceCalculation]), + raster_node!(graphene_core::raster::ExtractChannelNode<_>, params: [RedGreenBlue]), + raster_node!(graphene_core::raster::ExtractAlphaNode<>, params: []), raster_node!(graphene_core::raster::LevelsNode<_, _, _, _, _>, params: [f64, f64, f64, f64, f64]), register_node!(graphene_std::image_segmentation::ImageSegmentationNode<_>, input: ImageFrame, params: [ImageFrame]), register_node!(graphene_core::raster::IndexNode<_>, input: Vec>, params: [u32]),