From e34e6373b612e89efcc8e085941059c0f15d498f Mon Sep 17 00:00:00 2001 From: Omar Emara Date: Thu, 24 Jul 2025 13:41:56 +0200 Subject: [PATCH] Compositor: Replace Composite node with Group Output node This patch replaces the Composite node with the Group Output node as the primary compositor output. The old node was removed and versioned. This was done for consistency with Geometry Nodes and in preparation for more generic use of the compositor in VSE modifiers, layered compositing, NPR multi-stage compositing, and more. The Group Output node relies on the node tree interface, so we now have a default interface of a single input and a single output. For now, only the first input is considered while the rest are ignored, just like the Geometry Nodes design. Furthermore, the input is required to be of type color. Warnings and errors are issues if any of those are not met, also similar to Geometry Nodes. This introduces a new limitation: Composite outputs can no longer exist in node groups, since they obviously then act as their respective group outputs. A refactor for the compositor scheduler is needed to simplify the logic after this change, but this will be done in a separate patch. Pull Request: https://projects.blender.org/blender/blender/pulls/142232 --- .../addons_core/node_wrangler/operators.py | 2 +- scripts/startup/bl_operators/clip.py | 12 +- .../startup/bl_ui/node_add_menu_compositor.py | 10 +- .../blender/blenkernel/BKE_blender_version.h | 2 +- .../blenkernel/BKE_node_legacy_types.hh | 2 +- .../blenloader/intern/versioning_260.cc | 2 +- .../blenloader/intern/versioning_270.cc | 2 +- .../blenloader/intern/versioning_430.cc | 2 +- .../blenloader/intern/versioning_450.cc | 4 +- .../blenloader/intern/versioning_500.cc | 77 +++++++ .../blenloader/intern/versioning_common.cc | 9 + .../blenloader/intern/versioning_common.hh | 5 + source/blender/compositor/COM_context.hh | 2 +- source/blender/compositor/intern/context.cc | 2 +- source/blender/compositor/intern/scheduler.cc | 26 ++- .../engines/compositor/compositor_engine.cc | 2 +- .../blender/editors/space_node/node_edit.cc | 22 +- .../blender/makesrna/intern/rna_nodetree.cc | 1 - source/blender/nodes/NOD_composite.hh | 5 +- source/blender/nodes/composite/CMakeLists.txt | 2 +- .../nodes/composite/node_composite_tree.cc | 2 +- .../nodes/node_composite_composite.cc | 161 --------------- .../nodes/node_composite_group_output.cc | 195 ++++++++++++++++++ .../composite/nodes/node_composite_viewer.cc | 6 +- source/blender/nodes/intern/node_common.cc | 4 +- .../nodes/intern/socket_usage_inference.cc | 1 - source/blender/render/intern/pipeline.cc | 57 ++--- source/blender/render/intern/render_types.h | 8 +- 28 files changed, 369 insertions(+), 256 deletions(-) delete mode 100644 source/blender/nodes/composite/nodes/node_composite_composite.cc create mode 100644 source/blender/nodes/composite/nodes/node_composite_group_output.cc diff --git a/scripts/addons_core/node_wrangler/operators.py b/scripts/addons_core/node_wrangler/operators.py index e88690e052d..69333e90b8d 100644 --- a/scripts/addons_core/node_wrangler/operators.py +++ b/scripts/addons_core/node_wrangler/operators.py @@ -1964,7 +1964,7 @@ class NWLinkToOutputNode(Operator): 'LINESTYLE': 'ShaderNodeOutputLineStyle'} output_type = { 'ShaderNodeTree': shader_outputs[context.space_data.shader_type], - 'CompositorNodeTree': 'CompositorNodeComposite', + 'CompositorNodeTree': 'NodeGroupOutput', 'TextureNodeTree': 'TextureNodeOutput', 'GeometryNodeTree': 'NodeGroupOutput', }[tree_type] diff --git a/scripts/startup/bl_operators/clip.py b/scripts/startup/bl_operators/clip.py index 182950e30ab..d3c88ea1655 100644 --- a/scripts/startup/bl_operators/clip.py +++ b/scripts/startup/bl_operators/clip.py @@ -784,7 +784,7 @@ class CLIP_OT_setup_tracking_scene(Operator): # Create nodes. rlayer_fg = self._findOrCreateNode(tree, 'CompositorNodeRLayers') rlayer_bg = tree.nodes.new(type='CompositorNodeRLayers') - composite = self._findOrCreateNode(tree, 'CompositorNodeComposite') + output = self._findOrCreateNode(tree, 'NodeGroupOutput') movieclip = tree.nodes.new(type='CompositorNodeMovieClip') distortion = tree.nodes.new(type='CompositorNodeMovieDistortion') @@ -831,7 +831,7 @@ class CLIP_OT_setup_tracking_scene(Operator): tree.links.new(shadowcatcher.outputs["Image"], alphaover.inputs[1]) - tree.links.new(alphaover.outputs["Image"], composite.inputs["Image"]) + tree.links.new(alphaover.outputs["Image"], output.inputs[0]) tree.links.new(alphaover.outputs["Image"], viewer.inputs["Image"]) # Place nodes. @@ -862,11 +862,11 @@ class CLIP_OT_setup_tracking_scene(Operator): alphaover.location = shadowcatcher.location alphaover.location += Vector((250.0, -250.0)) - composite.location = alphaover.location - composite.location += Vector((300.0, -100.0)) + output.location = alphaover.location + output.location += Vector((300.0, -100.0)) - viewer.location = composite.location - composite.location += Vector((0.0, 200.0)) + viewer.location = output.location + output.location += Vector((0.0, 200.0)) # Ensure no nodes were created on the position of existing node. self._offsetNodes(tree) diff --git a/scripts/startup/bl_ui/node_add_menu_compositor.py b/scripts/startup/bl_ui/node_add_menu_compositor.py index 9bb52e1a7b4..ad9c497cf2b 100644 --- a/scripts/startup/bl_ui/node_add_menu_compositor.py +++ b/scripts/startup/bl_ui/node_add_menu_compositor.py @@ -20,6 +20,7 @@ class NODE_MT_category_compositor_input(Menu): layout = self.layout layout.menu("NODE_MT_category_compositor_input_constant") layout.separator() + node_add_menu.add_node_type(layout, "NodeGroupInput") node_add_menu.add_node_type(layout, "CompositorNodeBokehImage") node_add_menu.add_node_type(layout, "CompositorNodeImage") node_add_menu.add_node_type(layout, "CompositorNodeImageInfo") @@ -27,9 +28,6 @@ class NODE_MT_category_compositor_input(Menu): node_add_menu.add_node_type(layout, "CompositorNodeMask") node_add_menu.add_node_type(layout, "CompositorNodeMovieClip") - if is_group: - layout.separator() - node_add_menu.add_node_type(layout, "NodeGroupInput") layout.separator() layout.menu("NODE_MT_category_compositor_input_scene") @@ -71,15 +69,11 @@ class NODE_MT_category_compositor_output(Menu): is_group = (len(snode.path) > 1) layout = self.layout - node_add_menu.add_node_type(layout, "CompositorNodeComposite") + node_add_menu.add_node_type(layout, "NodeGroupOutput") node_add_menu.add_node_type(layout, "CompositorNodeViewer") layout.separator() node_add_menu.add_node_type(layout, "CompositorNodeOutputFile") - if is_group: - layout.separator() - node_add_menu.add_node_type(layout, "NodeGroupOutput") - node_add_menu.draw_assets_for_catalog(layout, self.bl_label) diff --git a/source/blender/blenkernel/BKE_blender_version.h b/source/blender/blenkernel/BKE_blender_version.h index b93fc826d76..ee836a032e8 100644 --- a/source/blender/blenkernel/BKE_blender_version.h +++ b/source/blender/blenkernel/BKE_blender_version.h @@ -27,7 +27,7 @@ /* Blender file format version. */ #define BLENDER_FILE_VERSION BLENDER_VERSION -#define BLENDER_FILE_SUBVERSION 40 +#define BLENDER_FILE_SUBVERSION 41 /* Minimum Blender version that supports reading file written with the current * version. Older Blender versions will test this and cancel loading the file, showing a warning to diff --git a/source/blender/blenkernel/BKE_node_legacy_types.hh b/source/blender/blenkernel/BKE_node_legacy_types.hh index bab32c1f180..524a8d2f195 100644 --- a/source/blender/blenkernel/BKE_node_legacy_types.hh +++ b/source/blender/blenkernel/BKE_node_legacy_types.hh @@ -167,7 +167,7 @@ #define CMP_NODE_HUE_SAT 219 #define CMP_NODE_IMAGE 220 #define CMP_NODE_R_LAYERS 221 -#define CMP_NODE_COMPOSITE 222 +#define CMP_NODE_COMPOSITE_DEPRECATED 222 #define CMP_NODE_OUTPUT_FILE 223 #define CMP_NODE_TEXTURE_DEPRECATED 224 #define CMP_NODE_TRANSLATE 225 diff --git a/source/blender/blenloader/intern/versioning_260.cc b/source/blender/blenloader/intern/versioning_260.cc index d871eb188a9..5b9fe56c05d 100644 --- a/source/blender/blenloader/intern/versioning_260.cc +++ b/source/blender/blenloader/intern/versioning_260.cc @@ -669,7 +669,7 @@ static const char *node_get_static_idname(int type, int treetype) return "CompositorNodeImage"; case CMP_NODE_R_LAYERS: return "CompositorNodeRLayers"; - case CMP_NODE_COMPOSITE: + case CMP_NODE_COMPOSITE_DEPRECATED: return "CompositorNodeComposite"; case CMP_NODE_OUTPUT_FILE: return "CompositorNodeOutputFile"; diff --git a/source/blender/blenloader/intern/versioning_270.cc b/source/blender/blenloader/intern/versioning_270.cc index 2bc611444d9..bd52a8c332c 100644 --- a/source/blender/blenloader/intern/versioning_270.cc +++ b/source/blender/blenloader/intern/versioning_270.cc @@ -478,7 +478,7 @@ void blo_do_versions_270(FileData *fd, Library * /*lib*/, Main *bmain) FOREACH_NODETREE_BEGIN (bmain, ntree, id) { if (ntree->type == NTREE_COMPOSIT) { LISTBASE_FOREACH (bNode *, node, &ntree->nodes) { - if (ELEM(node->type_legacy, CMP_NODE_COMPOSITE, CMP_NODE_OUTPUT_FILE)) { + if (ELEM(node->type_legacy, CMP_NODE_COMPOSITE_DEPRECATED, CMP_NODE_OUTPUT_FILE)) { node->id = nullptr; } } diff --git a/source/blender/blenloader/intern/versioning_430.cc b/source/blender/blenloader/intern/versioning_430.cc index 49d45f0f8a2..2f0f5bb52c6 100644 --- a/source/blender/blenloader/intern/versioning_430.cc +++ b/source/blender/blenloader/intern/versioning_430.cc @@ -483,7 +483,7 @@ void blo_do_versions_430(FileData * /*fd*/, Library * /*lib*/, Main *bmain) continue; } LISTBASE_FOREACH_MUTABLE (bNode *, node, &ntree->nodes) { - if (ELEM(node->type_legacy, CMP_NODE_VIEWER, CMP_NODE_COMPOSITE)) { + if (ELEM(node->type_legacy, CMP_NODE_VIEWER, CMP_NODE_COMPOSITE_DEPRECATED)) { node->flag &= ~NODE_PREVIEW; } } diff --git a/source/blender/blenloader/intern/versioning_450.cc b/source/blender/blenloader/intern/versioning_450.cc index 0bc56cc3649..b6fee2be3f0 100644 --- a/source/blender/blenloader/intern/versioning_450.cc +++ b/source/blender/blenloader/intern/versioning_450.cc @@ -2695,7 +2695,7 @@ static void do_version_composite_viewer_remove_alpha(bNodeTree *node_tree) /* Find links going into the composite and viewer nodes. */ LISTBASE_FOREACH (bNodeLink *, link, &node_tree->links) { - if (!ELEM(link->tonode->type_legacy, CMP_NODE_COMPOSITE, CMP_NODE_VIEWER)) { + if (!ELEM(link->tonode->type_legacy, CMP_NODE_COMPOSITE_DEPRECATED, CMP_NODE_VIEWER)) { continue; } @@ -2708,7 +2708,7 @@ static void do_version_composite_viewer_remove_alpha(bNodeTree *node_tree) } LISTBASE_FOREACH (bNode *, node, &node_tree->nodes) { - if (!ELEM(node->type_legacy, CMP_NODE_COMPOSITE, CMP_NODE_VIEWER)) { + if (!ELEM(node->type_legacy, CMP_NODE_COMPOSITE_DEPRECATED, CMP_NODE_VIEWER)) { continue; } diff --git a/source/blender/blenloader/intern/versioning_500.cc b/source/blender/blenloader/intern/versioning_500.cc index 2ef5e0563b5..340789553ea 100644 --- a/source/blender/blenloader/intern/versioning_500.cc +++ b/source/blender/blenloader/intern/versioning_500.cc @@ -1172,6 +1172,50 @@ static void do_version_convert_gp_jitter_values(Brush *brush) } } +/* The Composite node was removed and a Group Output node should be used instead, so we need to + * make the replacement. But first note that the Group Output node relies on the node tree + * interface, so we ensure a default interface with a single input and output. This is only for + * root trees used as scene compositing node groups, for other node trees, we remove all composite + * nodes since they are no longer supported inside groups. */ +static void do_version_composite_node_in_scene_tree(bNodeTree &node_tree, bNode &node) +{ + blender::bke::node_tree_set_type(node_tree); + + /* Remove inactive nodes. */ + if (!(node.flag & NODE_DO_OUTPUT)) { + version_node_remove(node_tree, node); + return; + } + + bNodeSocket *old_image_input = blender::bke::node_find_socket(node, SOCK_IN, "Image"); + + /* Find the link going into the Image input of the Composite node. */ + bNodeLink *image_link = nullptr; + LISTBASE_FOREACH (bNodeLink *, link, &node_tree.links) { + if (link->tosock == old_image_input) { + image_link = link; + } + } + + bNode *group_output_node = blender::bke::node_add_node(nullptr, node_tree, "NodeGroupOutput"); + group_output_node->parent = node.parent; + group_output_node->location[0] = node.location[0]; + group_output_node->location[1] = node.location[1]; + + bNodeSocket *image_input = static_cast(group_output_node->inputs.first); + BLI_assert(blender::StringRef(image_input->name) == "Image"); + copy_v4_v4(image_input->default_value_typed()->value, + old_image_input->default_value_typed()->value); + + if (image_link) { + version_node_add_link( + node_tree, *image_link->fromnode, *image_link->fromsock, *group_output_node, *image_input); + blender::bke::node_remove_link(&node_tree, *image_link); + } + + version_node_remove(node_tree, node); +} + void do_versions_after_linking_500(FileData *fd, Main *bmain) { if (!MAIN_VERSION_FILE_ATLEAST(bmain, 500, 9)) { @@ -1197,6 +1241,39 @@ void do_versions_after_linking_500(FileData *fd, Main *bmain) } } + if (!MAIN_VERSION_FILE_ATLEAST(bmain, 500, 41)) { + LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) { + bNodeTree *node_tree = version_get_scene_compositor_node_tree(bmain, scene); + if (node_tree) { + /* Add a default interface for the node tree. See the versioning function below for more + * details. */ + node_tree->tree_interface.clear_items(); + node_tree->tree_interface.add_socket( + DATA_("Image"), "", "NodeSocketColor", NODE_INTERFACE_SOCKET_INPUT, nullptr); + node_tree->tree_interface.add_socket( + DATA_("Image"), "", "NodeSocketColor", NODE_INTERFACE_SOCKET_OUTPUT, nullptr); + + LISTBASE_FOREACH_BACKWARD_MUTABLE (bNode *, node, &node_tree->nodes) { + if (node->type_legacy == CMP_NODE_COMPOSITE_DEPRECATED) { + do_version_composite_node_in_scene_tree(*node_tree, *node); + } + } + } + } + FOREACH_NODETREE_BEGIN (bmain, node_tree, id) { + blender::bke::node_tree_set_type(*node_tree); + if (node_tree->type == NTREE_COMPOSIT) { + LISTBASE_FOREACH_BACKWARD_MUTABLE (bNode *, node, &node_tree->nodes) { + if (node->type_legacy == CMP_NODE_COMPOSITE_DEPRECATED) { + /* See do_version_composite_node_in_scene_tree. */ + version_node_remove(*node_tree, *node); + } + } + } + } + FOREACH_NODETREE_END; + } + /** * Always bump subversion in BKE_blender_version.h when adding versioning * code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check. diff --git a/source/blender/blenloader/intern/versioning_common.cc b/source/blender/blenloader/intern/versioning_common.cc index 463f537ceb5..766d3a3f8c2 100644 --- a/source/blender/blenloader/intern/versioning_common.cc +++ b/source/blender/blenloader/intern/versioning_common.cc @@ -658,6 +658,15 @@ bool all_scenes_use(Main *bmain, const blender::Span engines) return true; } +bNodeTree *version_get_scene_compositor_node_tree(Main *bmain, Scene *scene) +{ + if (!MAIN_VERSION_FILE_ATLEAST(bmain, 500, 4)) { + return scene->nodetree; + } + + return scene->compositing_node_group; +} + static bool blendfile_or_libraries_versions_atleast(Main *bmain, const short versionfile, const short subversionfile) diff --git a/source/blender/blenloader/intern/versioning_common.hh b/source/blender/blenloader/intern/versioning_common.hh index bbc7c9386e3..04356d75430 100644 --- a/source/blender/blenloader/intern/versioning_common.hh +++ b/source/blender/blenloader/intern/versioning_common.hh @@ -255,3 +255,8 @@ static void adjust_fcurve_key_frame_values(FCurve *fcurve, /* Recalculate the automatic handles of the FCurve after adjustments. */ BKE_fcurve_handles_recalc(fcurve); } + +/* Gets the compositing node tree of the given scene. The deprecated nodetree member is returned + * for older versions before reusable node trees were introduced in bd61e69be5, while the new + * compositing_node_group is returned otherwise. */ +bNodeTree *version_get_scene_compositor_node_tree(Main *bmain, Scene *scene); diff --git a/source/blender/compositor/COM_context.hh b/source/blender/compositor/COM_context.hh index 8d1ad6e53df..51e604343b8 100644 --- a/source/blender/compositor/COM_context.hh +++ b/source/blender/compositor/COM_context.hh @@ -106,7 +106,7 @@ class Context { /* True if the compositor should treat viewers as composite outputs because it has no concept of * or support for viewers. */ - virtual bool treat_viewer_as_composite_output() const; + virtual bool treat_viewer_as_compositor_output() const; /* Populates the given meta data from the render stamp information of the given render pass. */ virtual void populate_meta_data_for_pass(const Scene *scene, diff --git a/source/blender/compositor/intern/context.cc b/source/blender/compositor/intern/context.cc index 3b182d01348..1fea3e5e4df 100644 --- a/source/blender/compositor/intern/context.cc +++ b/source/blender/compositor/intern/context.cc @@ -20,7 +20,7 @@ namespace blender::compositor { -bool Context::treat_viewer_as_composite_output() const +bool Context::treat_viewer_as_compositor_output() const { return false; } diff --git a/source/blender/compositor/intern/scheduler.cc b/source/blender/compositor/intern/scheduler.cc index 13ea938644b..94f9a5384d3 100644 --- a/source/blender/compositor/intern/scheduler.cc +++ b/source/blender/compositor/intern/scheduler.cc @@ -11,8 +11,6 @@ #include "NOD_derived_node_tree.hh" -#include "BKE_node_legacy_types.hh" - #include "COM_context.hh" #include "COM_scheduler.hh" #include "COM_utilities.hh" @@ -39,8 +37,8 @@ static bool is_tree_context_muted(const DTreeContext &tree_context) } /* Add the active viewer node in the given tree context to the given stack. If viewer nodes are - * treated as composite outputs, this function will also add either the viewer or the composite - * node since composite nodes were skipped in add_output_nodes such that viewer nodes take + * treated as compositor outputs, this function will also add either the viewer or the group output + * node since group output nodes were skipped in add_output_nodes such that viewer nodes take * precedence. */ static bool add_viewer_nodes_in_context(const Context &context, const DTreeContext *tree_context, @@ -58,14 +56,14 @@ static bool add_viewer_nodes_in_context(const Context &context, } } - /* If we are not treating viewers as composite outputs, there is nothing else to do. Otherwise, - * the active composite node might be added. */ - if (!context.treat_viewer_as_composite_output()) { + /* If we are not treating viewers as compositor outputs, there is nothing else to do. Otherwise, + * the active group output node might be added. */ + if (!context.treat_viewer_as_compositor_output()) { return false; } /* No active viewers exist in this tree context, add the active Composite node if one exist. */ - for (const bNode *node : tree_context->btree().nodes_by_type("CompositorNodeComposite")) { + for (const bNode *node : tree_context->btree().nodes_by_type("NodeGroupOutput")) { if (node->flag & NODE_DO_OUTPUT && !node->is_muted()) { node_stack.push(DNode(tree_context, node)); return true; @@ -101,7 +99,7 @@ static void add_file_output_nodes(const DTreeContext &tree_context, Stack } /* Add the output nodes whose result should be computed to the given stack. This includes File - * Output, Composite, and Viewer nodes. Viewer nodes are a special case, as only the nodes that + * Output, Group Output, and Viewer nodes. Viewer nodes are a special case, as only the nodes that * satisfies the requirements in the add_viewer_nodes_in_context function are added. First, the * active context is searched for viewer nodes, if non were found, the root context is searched. * For more information on what contexts mean here, see the DerivedNodeTree::active_context() @@ -117,13 +115,13 @@ static void add_output_nodes(const Context &context, add_file_output_nodes(root_context, node_stack); } - /* Add the active composite node in the root tree if needed, but only if we are not treating - * viewer outputs as composite ones. That's because in cases where viewer nodes will be treated - * as composite outputs, viewer nodes will take precedence, so this is handled as a special case + /* Add the active group output node in the root tree if needed, but only if we are not treating + * viewer outputs as compositor ones. That's because in cases where viewer nodes will be treated + * as compositor outputs, viewer nodes will take precedence, so this is handled as a special case * in the add_viewer_nodes_in_context function instead and no need to add it here. */ if (bool(context.needed_outputs() & OutputTypes::Composite)) { - if (!context.treat_viewer_as_composite_output()) { - for (const bNode *node : root_context.btree().nodes_by_type("CompositorNodeComposite")) { + if (!context.treat_viewer_as_compositor_output()) { + for (const bNode *node : root_context.btree().nodes_by_type("NodeGroupOutput")) { if (node->flag & NODE_DO_OUTPUT && !node->is_muted()) { node_stack.push(DNode(&root_context, node)); break; diff --git a/source/blender/draw/engines/compositor/compositor_engine.cc b/source/blender/draw/engines/compositor/compositor_engine.cc index 86c798f21a2..9886b059cd2 100644 --- a/source/blender/draw/engines/compositor/compositor_engine.cc +++ b/source/blender/draw/engines/compositor/compositor_engine.cc @@ -81,7 +81,7 @@ class Context : public compositor::Context { /* The viewport compositor does not support viewer outputs, so treat viewers as composite * outputs. */ - bool treat_viewer_as_composite_output() const override + bool treat_viewer_as_compositor_output() const override { return true; } diff --git a/source/blender/editors/space_node/node_edit.cc b/source/blender/editors/space_node/node_edit.cc index a3e606d8cec..969a529e52b 100644 --- a/source/blender/editors/space_node/node_edit.cc +++ b/source/blender/editors/space_node/node_edit.cc @@ -604,7 +604,12 @@ void ED_node_composit_default_init(const bContext *C, bNodeTree *ntree) BLI_assert(ntree != nullptr && ntree->type == NTREE_COMPOSIT); BLI_assert(BLI_listbase_count(&ntree->nodes) == 0); - bNode *composite = blender::bke::node_add_static_node(C, *ntree, CMP_NODE_COMPOSITE); + ntree->tree_interface.add_socket( + DATA_("Image"), "", "NodeSocketColor", NODE_INTERFACE_SOCKET_INPUT, nullptr); + ntree->tree_interface.add_socket( + DATA_("Image"), "", "NodeSocketColor", NODE_INTERFACE_SOCKET_OUTPUT, nullptr); + + bNode *composite = blender::bke::node_add_node(C, *ntree, "NodeGroupOutput"); composite->location[0] = 200.0f; composite->location[1] = 0.0f; @@ -619,7 +624,7 @@ void ED_node_composit_default_init(const bContext *C, bNodeTree *ntree) bNode *viewer = blender::bke::node_add_static_node(C, *ntree, CMP_NODE_VIEWER); viewer->location[0] = 200.0f; - viewer->location[1] = -60.0f; + viewer->location[1] = -80.0f; /* Viewer and Composite nodes are linked to Render Layer's output image socket through a reroute * node. */ @@ -833,19 +838,6 @@ void ED_node_set_active( BKE_main_ensure_invariants(*bmain, ntree->id); } } - else if (node->type_legacy == CMP_NODE_COMPOSITE) { - if (was_output == 0) { - for (bNode *node_iter : ntree->all_nodes()) { - if (node_iter->type_legacy == CMP_NODE_COMPOSITE) { - node_iter->flag &= ~NODE_DO_OUTPUT; - } - } - - node->flag |= NODE_DO_OUTPUT; - BKE_ntree_update_tag_active_output_changed(ntree); - BKE_main_ensure_invariants(*bmain, ntree->id); - } - } else if (do_update) { BKE_main_ensure_invariants(*bmain, ntree->id); } diff --git a/source/blender/makesrna/intern/rna_nodetree.cc b/source/blender/makesrna/intern/rna_nodetree.cc index 8edaea77fce..cd2a32d4551 100644 --- a/source/blender/makesrna/intern/rna_nodetree.cc +++ b/source/blender/makesrna/intern/rna_nodetree.cc @@ -10618,7 +10618,6 @@ static void rna_def_nodes(BlenderRNA *brna) define("CompositorNode", "CompositorNodeCombRGBA"); define("CompositorNode", "CompositorNodeCombYCCA", def_cmp_ycc); define("CompositorNode", "CompositorNodeCombYUVA"); - define("CompositorNode", "CompositorNodeComposite"); define("CompositorNode", "CompositorNodeConvertColorSpace", def_cmp_convert_color_space); define("CompositorNode", "CompositorNodeCornerPin", def_cmp_cornerpin); define("CompositorNode", "CompositorNodeCrop"); diff --git a/source/blender/nodes/NOD_composite.hh b/source/blender/nodes/NOD_composite.hh index 6bb64bed651..4fb4ea2e3fb 100644 --- a/source/blender/nodes/NOD_composite.hh +++ b/source/blender/nodes/NOD_composite.hh @@ -98,5 +98,8 @@ namespace blender::nodes { compositor::NodeOperation *get_group_input_compositor_operation(compositor::Context &context, DNode node); +compositor::NodeOperation *get_group_output_compositor_operation(compositor::Context &context, + DNode node); +void get_compositor_group_output_extra_info(blender::nodes::NodeExtraInfoParams ¶meters); -} +} // namespace blender::nodes diff --git a/source/blender/nodes/composite/CMakeLists.txt b/source/blender/nodes/composite/CMakeLists.txt index ff6110b54f0..276742e390c 100644 --- a/source/blender/nodes/composite/CMakeLists.txt +++ b/source/blender/nodes/composite/CMakeLists.txt @@ -37,7 +37,6 @@ set(SRC nodes/node_composite_colorbalance.cc nodes/node_composite_colorcorrection.cc nodes/node_composite_common.cc - nodes/node_composite_composite.cc nodes/node_composite_convert_color_space.cc nodes/node_composite_cornerpin.cc nodes/node_composite_crop.cc @@ -61,6 +60,7 @@ set(SRC nodes/node_composite_gamma.cc nodes/node_composite_glare.cc nodes/node_composite_group_input.cc + nodes/node_composite_group_output.cc nodes/node_composite_hue_sat_val.cc nodes/node_composite_huecorrect.cc nodes/node_composite_id_mask.cc diff --git a/source/blender/nodes/composite/node_composite_tree.cc b/source/blender/nodes/composite/node_composite_tree.cc index 6e4008d8b68..a89240c1171 100644 --- a/source/blender/nodes/composite/node_composite_tree.cc +++ b/source/blender/nodes/composite/node_composite_tree.cc @@ -203,7 +203,7 @@ void ntreeCompositTagRender(Scene *scene) { if (sce_iter->compositing_node_group) { for (bNode *node : sce_iter->compositing_node_group->all_nodes()) { - if (node->id == (ID *)scene || node->type_legacy == CMP_NODE_COMPOSITE) { + if (node->id == (ID *)scene) { BKE_ntree_update_tag_node_property(sce_iter->compositing_node_group, node); } } diff --git a/source/blender/nodes/composite/nodes/node_composite_composite.cc b/source/blender/nodes/composite/nodes/node_composite_composite.cc deleted file mode 100644 index dd60ea7fa84..00000000000 --- a/source/blender/nodes/composite/nodes/node_composite_composite.cc +++ /dev/null @@ -1,161 +0,0 @@ -/* SPDX-FileCopyrightText: 2006 Blender Authors - * - * SPDX-License-Identifier: GPL-2.0-or-later */ - -/** \file - * \ingroup cmpnodes - */ - -#include "BLI_bounds_types.hh" -#include "BLI_math_vector_types.hh" - -#include "UI_resources.hh" - -#include "GPU_shader.hh" -#include "GPU_texture.hh" - -#include "COM_node_operation.hh" -#include "COM_utilities.hh" - -#include "node_composite_util.hh" - -/* **************** COMPOSITE ******************** */ - -namespace blender::nodes::node_composite_composite_cc { - -static void cmp_node_composite_declare(NodeDeclarationBuilder &b) -{ - b.add_input("Image") - .default_value({0.0f, 0.0f, 0.0f, 1.0f}) - .structure_type(StructureType::Dynamic); -} - -using namespace blender::compositor; - -class CompositeOperation : public NodeOperation { - public: - using NodeOperation::NodeOperation; - - void execute() override - { - if (!this->context().is_valid_compositing_region()) { - return; - } - - const Result &image = this->get_input("Image"); - if (image.is_single_value()) { - this->execute_clear(); - } - else { - this->execute_copy(); - } - } - - void execute_clear() - { - const Result &image = this->get_input("Image"); - - float4 color = image.get_single_value(); - - const Domain domain = this->compute_domain(); - Result output = this->context().get_output_result(); - if (this->context().use_gpu()) { - GPU_texture_clear(output, GPU_DATA_FLOAT, color); - } - else { - parallel_for(domain.size, [&](const int2 texel) { output.store_pixel(texel, color); }); - } - } - - void execute_copy() - { - if (this->context().use_gpu()) { - this->execute_copy_gpu(); - } - else { - this->execute_copy_cpu(); - } - } - - void execute_copy_gpu() - { - const Result &image = this->get_input("Image"); - const Domain domain = this->compute_domain(); - Result output = this->context().get_output_result(); - - GPUShader *shader = this->context().get_shader("compositor_write_output", output.precision()); - GPU_shader_bind(shader); - - const Bounds bounds = this->get_output_bounds(); - GPU_shader_uniform_2iv(shader, "lower_bound", bounds.min); - GPU_shader_uniform_2iv(shader, "upper_bound", bounds.max); - - image.bind_as_texture(shader, "input_tx"); - - output.bind_as_image(shader, "output_img"); - - compute_dispatch_threads_at_least(shader, domain.size); - - image.unbind_as_texture(); - output.unbind_as_image(); - GPU_shader_unbind(); - } - - void execute_copy_cpu() - { - const Domain domain = this->compute_domain(); - const Result &image = this->get_input("Image"); - Result output = this->context().get_output_result(); - - const Bounds bounds = this->get_output_bounds(); - parallel_for(domain.size, [&](const int2 texel) { - const int2 output_texel = texel + bounds.min; - if (output_texel.x > bounds.max.x || output_texel.y > bounds.max.y) { - return; - } - output.store_pixel(texel + bounds.min, image.load_pixel(texel)); - }); - } - - /* Returns the bounds of the area of the compositing region. Only write into the compositing - * region, which might be limited to a smaller region of the output result. */ - Bounds get_output_bounds() - { - const rcti compositing_region = this->context().get_compositing_region(); - return Bounds(int2(compositing_region.xmin, compositing_region.ymin), - int2(compositing_region.xmax, compositing_region.ymax)); - } - - /* The operation domain has the same size as the compositing region without any transformations - * applied. */ - Domain compute_domain() override - { - return Domain(this->context().get_compositing_region_size()); - } -}; - -static NodeOperation *get_compositor_operation(Context &context, DNode node) -{ - return new CompositeOperation(context, node); -} - -} // namespace blender::nodes::node_composite_composite_cc - -static void register_node_type_cmp_composite() -{ - namespace file_ns = blender::nodes::node_composite_composite_cc; - - static blender::bke::bNodeType ntype; - - cmp_node_type_base(&ntype, "CompositorNodeComposite", CMP_NODE_COMPOSITE); - ntype.ui_name = "Composite"; - ntype.ui_description = "Final render output"; - ntype.enum_name_legacy = "COMPOSITE"; - ntype.nclass = NODE_CLASS_OUTPUT; - ntype.declare = file_ns::cmp_node_composite_declare; - ntype.get_compositor_operation = file_ns::get_compositor_operation; - ntype.no_muting = true; - - blender::bke::node_register_type(ntype); -} -NOD_REGISTER_NODE(register_node_type_cmp_composite) diff --git a/source/blender/nodes/composite/nodes/node_composite_group_output.cc b/source/blender/nodes/composite/nodes/node_composite_group_output.cc new file mode 100644 index 00000000000..c1df1ddf750 --- /dev/null +++ b/source/blender/nodes/composite/nodes/node_composite_group_output.cc @@ -0,0 +1,195 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BLI_bounds_types.hh" +#include "BLI_math_vector_types.hh" + +#include "BLT_translation.hh" + +#include "UI_resources.hh" + +#include "DNA_space_types.h" + +#include "GPU_shader.hh" + +#include "BKE_context.hh" + +#include "NOD_composite.hh" +#include "NOD_node_extra_info.hh" + +#include "COM_node_operation.hh" +#include "COM_utilities.hh" + +namespace blender::nodes::node_composite_group_output_cc { + +using namespace blender::compositor; + +class GroupOutputOperation : public NodeOperation { + public: + GroupOutputOperation(Context &context, DNode node) : NodeOperation(context, node) + { + for (const bNodeSocket *input : node->input_sockets()) { + if (!is_socket_available(input)) { + continue; + } + + /* The structure type of the inputs of Group Output nodes are inferred, so we need to + * manually specify this here. */ + InputDescriptor &descriptor = this->get_input_descriptor(input->identifier); + descriptor.expects_single_value = false; + } + } + + void execute() override + { + if (!this->context().is_valid_compositing_region()) { + return; + } + + /* Get the first input to be written to the output. The rest of the inputs are ignored. Only + * color sockets are supported. */ + const bNodeSocket *input_socket = this->node()->input_sockets()[0]; + if (input_socket->type != SOCK_RGBA) { + return; + } + + const Result &image = this->get_input(input_socket->identifier); + if (image.is_single_value()) { + this->execute_clear(image); + } + else { + this->execute_copy(image); + } + } + + void execute_clear(const Result &image) + { + float4 color = image.get_single_value(); + + const Domain domain = this->compute_domain(); + Result output = this->context().get_output_result(); + if (this->context().use_gpu()) { + GPU_texture_clear(output, GPU_DATA_FLOAT, color); + } + else { + parallel_for(domain.size, [&](const int2 texel) { output.store_pixel(texel, color); }); + } + } + + void execute_copy(const Result &image) + { + if (this->context().use_gpu()) { + this->execute_copy_gpu(image); + } + else { + this->execute_copy_cpu(image); + } + } + + void execute_copy_gpu(const Result &image) + { + const Domain domain = this->compute_domain(); + Result output = this->context().get_output_result(); + + GPUShader *shader = this->context().get_shader("compositor_write_output", output.precision()); + GPU_shader_bind(shader); + + const Bounds bounds = this->get_output_bounds(); + GPU_shader_uniform_2iv(shader, "lower_bound", bounds.min); + GPU_shader_uniform_2iv(shader, "upper_bound", bounds.max); + + image.bind_as_texture(shader, "input_tx"); + + output.bind_as_image(shader, "output_img"); + + compute_dispatch_threads_at_least(shader, domain.size); + + image.unbind_as_texture(); + output.unbind_as_image(); + GPU_shader_unbind(); + } + + void execute_copy_cpu(const Result &image) + { + const Domain domain = this->compute_domain(); + Result output = this->context().get_output_result(); + + const Bounds bounds = this->get_output_bounds(); + parallel_for(domain.size, [&](const int2 texel) { + const int2 output_texel = texel + bounds.min; + if (output_texel.x > bounds.max.x || output_texel.y > bounds.max.y) { + return; + } + output.store_pixel(texel + bounds.min, image.load_pixel(texel)); + }); + } + + /* Returns the bounds of the area of the compositing region. Only write into the compositing + * region, which might be limited to a smaller region of the output result. */ + Bounds get_output_bounds() + { + const rcti compositing_region = this->context().get_compositing_region(); + return Bounds(int2(compositing_region.xmin, compositing_region.ymin), + int2(compositing_region.xmax, compositing_region.ymax)); + } + + /* The operation domain has the same size as the compositing region without any transformations + * applied. */ + Domain compute_domain() override + { + return Domain(this->context().get_compositing_region_size()); + } +}; + +} // namespace blender::nodes::node_composite_group_output_cc + +namespace blender::nodes { + +compositor::NodeOperation *get_group_output_compositor_operation(compositor::Context &context, + DNode node) +{ + return new node_composite_group_output_cc::GroupOutputOperation(context, node); +} + +void get_compositor_group_output_extra_info(blender::nodes::NodeExtraInfoParams ¶meters) +{ + if (parameters.tree.type != NTREE_COMPOSIT) { + return; + } + + SpaceNode *space_node = CTX_wm_space_node(¶meters.C); + if (space_node->edittree != space_node->nodetree) { + return; + } + + Span group_outputs = parameters.node.input_sockets().drop_back(1); + if (group_outputs.is_empty()) { + blender::nodes::NodeExtraInfoRow row; + row.text = IFACE_("No Output"); + row.icon = ICON_ERROR; + row.tooltip = TIP_("Node group must have a Color output socket"); + parameters.rows.append(std::move(row)); + return; + } + + if (group_outputs[0]->type != SOCK_RGBA) { + blender::nodes::NodeExtraInfoRow row; + row.text = IFACE_("Wrong Output Type"); + row.icon = ICON_ERROR; + row.tooltip = TIP_("Node group's first output must be a color output"); + parameters.rows.append(std::move(row)); + return; + } + + if (group_outputs.size() > 1) { + blender::nodes::NodeExtraInfoRow row; + row.text = IFACE_("Ignored Outputs"); + row.icon = ICON_WARNING_LARGE; + row.tooltip = TIP_("Only the first output is considered while the rest are ignored"); + parameters.rows.append(std::move(row)); + return; + } +} + +} // namespace blender::nodes diff --git a/source/blender/nodes/composite/nodes/node_composite_viewer.cc b/source/blender/nodes/composite/nodes/node_composite_viewer.cc index b1c21f982ed..71d6e4ff874 100644 --- a/source/blender/nodes/composite/nodes/node_composite_viewer.cc +++ b/source/blender/nodes/composite/nodes/node_composite_viewer.cc @@ -53,7 +53,7 @@ class ViewerOperation : public NodeOperation { { /* Viewers are treated as composite outputs that should be in the bounds of the compositing * region, so do nothing if the compositing region is invalid. */ - if (this->context().treat_viewer_as_composite_output() && + if (this->context().treat_viewer_as_compositor_output() && !this->context().is_valid_compositing_region()) { return; @@ -143,7 +143,7 @@ class ViewerOperation : public NodeOperation { { /* Viewers are treated as composite outputs that should be in the bounds of the compositing * region. */ - if (context().treat_viewer_as_composite_output()) { + if (context().treat_viewer_as_compositor_output()) { const rcti compositing_region = context().get_compositing_region(); return Bounds(int2(compositing_region.xmin, compositing_region.ymin), int2(compositing_region.xmax, compositing_region.ymax)); @@ -157,7 +157,7 @@ class ViewerOperation : public NodeOperation { { /* Viewers are treated as composite outputs that should be in the domain of the compositing * region. */ - if (context().treat_viewer_as_composite_output()) { + if (context().treat_viewer_as_compositor_output()) { return Domain(context().get_compositing_region_size()); } diff --git a/source/blender/nodes/intern/node_common.cc b/source/blender/nodes/intern/node_common.cc index 312ae97398f..61d8e0f4f41 100644 --- a/source/blender/nodes/intern/node_common.cc +++ b/source/blender/nodes/intern/node_common.cc @@ -469,7 +469,7 @@ void node_group_declare(NodeDeclarationBuilder &b) group->ensure_interface_cache(); Map structure_type_by_socket; - if (group->type == NTREE_GEOMETRY) { + if (ELEM(group->type, NTREE_GEOMETRY, NTREE_COMPOSIT)) { structure_type_by_socket.reserve(group->interface_items().size()); const Span inputs = group->interface_inputs(); @@ -901,6 +901,7 @@ bNodeSocket *node_group_output_find_socket(bNode *node, const StringRef identifi static void node_group_output_extra_info(blender::nodes::NodeExtraInfoParams ¶ms) { + get_compositor_group_output_extra_info(params); const blender::Span group_output_nodes = params.tree.nodes_by_type( "NodeGroupOutput"); if (group_output_nodes.size() <= 1) { @@ -931,6 +932,7 @@ void register_node_type_group_output() ntype->declare = blender::nodes::group_output_declare; ntype->insert_link = blender::nodes::group_output_insert_link; ntype->get_extra_info = node_group_output_extra_info; + ntype->get_compositor_operation = blender::nodes::get_group_output_compositor_operation; ntype->no_muting = true; diff --git a/source/blender/nodes/intern/socket_usage_inference.cc b/source/blender/nodes/intern/socket_usage_inference.cc index 3f69feb8013..377181b1697 100644 --- a/source/blender/nodes/intern/socket_usage_inference.cc +++ b/source/blender/nodes/intern/socket_usage_inference.cc @@ -252,7 +252,6 @@ struct SocketUsageInferencer { case SH_NODE_OUTPUT_LINESTYLE: case SH_NODE_OUTPUT_MATERIAL: case CMP_NODE_OUTPUT_FILE: - case CMP_NODE_COMPOSITE: case TEX_NODE_OUTPUT: { this->usage_task__input__output_node(socket); break; diff --git a/source/blender/render/intern/pipeline.cc b/source/blender/render/intern/pipeline.cc index 89c0b3ec719..43b51e3860c 100644 --- a/source/blender/render/intern/pipeline.cc +++ b/source/blender/render/intern/pipeline.cc @@ -1222,26 +1222,20 @@ static bool compositor_needs_render(Scene *scene) return false; } -/** Returns true if the node tree has a composite output node. */ -static bool node_tree_has_composite_output(const bNodeTree *node_tree) +/** Returns true if the node tree has a group output node. */ +static bool node_tree_has_group_output(const bNodeTree *node_tree) { if (node_tree == nullptr) { return false; } - for (const bNode *node : node_tree->all_nodes()) { - if (node->is_muted()) { - continue; - } - if (node->type_legacy == CMP_NODE_COMPOSITE && node->flag & NODE_DO_OUTPUT) { + node_tree->ensure_topology_cache(); + for (const bNode *node : node_tree->nodes_by_type("NodeGroupOutput")) { + if (node->flag & NODE_DO_OUTPUT && !node->is_muted()) { return true; } - if (node->is_group() && node->id) { - if (node_tree_has_composite_output(reinterpret_cast(node->id))) { - return true; - } - } } + return false; } @@ -1328,9 +1322,9 @@ static void do_render_compositor(Render *re) /* Scene render process already updates animsys. */ update_newframe = true; - /* The compositor does not have an output, skip writing the render result. See R_SKIP_WRITE for - * more information. */ - if (!node_tree_has_composite_output(re->pipeline_scene_eval->compositing_node_group)) { + /* The compositor does not have a group output, skip writing the render result. See + * R_SKIP_WRITE for more information. */ + if (!node_tree_has_group_output(re->pipeline_scene_eval->compositing_node_group)) { re->flag |= R_SKIP_WRITE; } } @@ -1775,27 +1769,34 @@ static int check_valid_camera(Scene *scene, Object *camera_override, ReportList return true; } -static bool node_tree_has_any_compositor_output(const bNodeTree *ntree) +static bool node_tree_has_file_output(const bNodeTree *node_tree) { - for (const bNode *node : ntree->all_nodes()) { - if (ELEM(node->type_legacy, CMP_NODE_COMPOSITE, CMP_NODE_OUTPUT_FILE)) { + node_tree->ensure_topology_cache(); + for (const bNode *node : node_tree->nodes_by_type("CompositorNodeOutputFile")) { + if (!node->is_muted()) { return true; } - if (node->is_group()) { - if (node->id) { - if (node_tree_has_any_compositor_output((const bNodeTree *)node->id)) { - return true; - } - } + } + + for (const bNode *node : node_tree->group_nodes()) { + if (node->is_muted() || !node->id) { + continue; + } + + if (node_tree_has_file_output(reinterpret_cast(node->id))) { + return true; } } return false; } -static int check_compositor_output(Scene *scene) +static bool scene_has_compositor_output(Scene *scene) { - return node_tree_has_any_compositor_output(scene->compositing_node_group); + if (node_tree_has_group_output(scene->compositing_node_group)) { + return true; + } + return node_tree_has_file_output(scene->compositing_node_group); } /* Identify if the compositor can run on the GPU. Currently, this only checks if the compositor is @@ -1847,8 +1848,8 @@ bool RE_is_rendering_allowed(Scene *scene, } else if (scemode & R_DOCOMP && scene->compositing_node_group) { /* Compositor */ - if (!check_compositor_output(scene)) { - BKE_report(reports, RPT_ERROR, "No render output node in scene"); + if (!scene_has_compositor_output(scene)) { + BKE_report(reports, RPT_ERROR, "No Group Output or File Output nodes in scene"); return false; } diff --git a/source/blender/render/intern/render_types.h b/source/blender/render/intern/render_types.h index 44e8038e1bb..ef11e936a7a 100644 --- a/source/blender/render/intern/render_types.h +++ b/source/blender/render/intern/render_types.h @@ -261,8 +261,8 @@ struct Render : public BaseRender { /** #R.flag */ #define R_ANIMATION 1 << 0 /* Indicates that the render pipeline should not write its render result. This happens for instance - * when the render pipeline uses the compositor, but the compositor node tree does not have an - * output composite node or a render layer input, and consequently no render result. In that case, - * the output will be written from the File Output nodes, since the render pipeline will early fail - * if neither a File Output nor a Composite node exist in the scene. */ + * when the render pipeline uses the compositor, but the compositor node tree does not have a group + * output node or a render layer input, and consequently no render result. In that case, the output + * will be written from the File Output nodes, since the render pipeline will early fail if neither + * a File Output nor a Group Output node exist in the scene. */ #define R_SKIP_WRITE 1 << 1