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
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<bNodeSocket *>(group_output_node->inputs.first);
|
||||
BLI_assert(blender::StringRef(image_input->name) == "Image");
|
||||
copy_v4_v4(image_input->default_value_typed<bNodeSocketValueRGBA>()->value,
|
||||
old_image_input->default_value_typed<bNodeSocketValueRGBA>()->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.
|
||||
|
||||
@@ -658,6 +658,15 @@ bool all_scenes_use(Main *bmain, const blender::Span<const char *> 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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<DNode>
|
||||
}
|
||||
|
||||
/* 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<decl::Color>("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<float4>();
|
||||
|
||||
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<int2> 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<int2> 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<float4>(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<int2> get_output_bounds()
|
||||
{
|
||||
const rcti compositing_region = this->context().get_compositing_region();
|
||||
return Bounds<int2>(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)
|
||||
@@ -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<float4>();
|
||||
|
||||
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<int2> 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<int2> 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<float4>(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<int2> get_output_bounds()
|
||||
{
|
||||
const rcti compositing_region = this->context().get_compositing_region();
|
||||
return Bounds<int2>(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<const bNodeSocket *> 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
|
||||
@@ -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>(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());
|
||||
}
|
||||
|
||||
|
||||
@@ -469,7 +469,7 @@ void node_group_declare(NodeDeclarationBuilder &b)
|
||||
group->ensure_interface_cache();
|
||||
|
||||
Map<const bNodeTreeInterfaceSocket *, StructureType> 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<const bNodeTreeInterfaceSocket *> 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<const bNode *> 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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<const bNodeTree *>(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<const bNodeTree *>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user