diff --git a/source/blender/blenkernel/BKE_blender_version.h b/source/blender/blenkernel/BKE_blender_version.h index 96d4f3749a5..75e37e6bc3d 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 53 +#define BLENDER_FILE_SUBVERSION 54 /* 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/intern/node.cc b/source/blender/blenkernel/intern/node.cc index faf24d31319..2ec849ccbc9 100644 --- a/source/blender/blenkernel/intern/node.cc +++ b/source/blender/blenkernel/intern/node.cc @@ -979,18 +979,6 @@ void node_tree_blend_write(BlendWriter *writer, bNodeTree *ntree) node_blend_write_storage(writer, ntree, node); } - if (node->type_legacy == CMP_NODE_OUTPUT_FILE) { - /* Inputs have their own storage data. */ - NodeImageMultiFile *nimf = (NodeImageMultiFile *)node->storage; - BKE_image_format_blend_write(writer, &nimf->format); - - LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) { - NodeImageMultiFileSocket *sockdata = static_cast( - sock->storage); - BLO_write_struct(writer, NodeImageMultiFileSocket, sockdata); - BKE_image_format_blend_write(writer, &sockdata->format); - } - } if (ELEM(node->type_legacy, CMP_NODE_IMAGE, CMP_NODE_R_LAYERS)) { /* Write extra socket info. */ LISTBASE_FOREACH (bNodeSocket *, sock, &node->outputs) { @@ -1608,11 +1596,6 @@ static void node_blend_read_data_storage(BlendDataReader *reader, bNodeTree *ntr iuser->scene = nullptr; break; } - case CMP_NODE_OUTPUT_FILE: { - NodeImageMultiFile *nimf = static_cast(node->storage); - BKE_image_format_blend_read_data(reader, &nimf->format); - break; - } default: break; } diff --git a/source/blender/blenloader/intern/versioning_260.cc b/source/blender/blenloader/intern/versioning_260.cc index b168cd35ab1..2037d6fde81 100644 --- a/source/blender/blenloader/intern/versioning_260.cc +++ b/source/blender/blenloader/intern/versioning_260.cc @@ -56,6 +56,7 @@ #include "BKE_anim_visualization.h" #include "BKE_customdata.hh" #include "BKE_image.hh" +#include "BKE_image_format.hh" #include "BKE_main.hh" /* for Main */ #include "BKE_mesh_legacy_convert.hh" #include "BKE_modifier.hh" @@ -248,13 +249,125 @@ static void do_versions_nodetree_socket_use_flags_2_62(bNodeTree *ntree) } } +/* find unique path */ +static bool unique_path_unique_check(ListBase *lb, + bNodeSocket *sock, + const blender::StringRef name) +{ + LISTBASE_FOREACH (bNodeSocket *, sock_iter, lb) { + if (sock_iter != sock) { + NodeImageMultiFileSocket *sockdata = (NodeImageMultiFileSocket *)sock_iter->storage; + if (sockdata->path == name) { + return true; + } + } + } + return false; +} + +static void ntreeCompositOutputFileUniquePath(ListBase *list, + bNodeSocket *sock, + const char defname[], + char delim) +{ + /* See if we are given an empty string */ + if (ELEM(nullptr, sock, defname)) { + return; + } + NodeImageMultiFileSocket *sockdata = (NodeImageMultiFileSocket *)sock->storage; + BLI_uniquename_cb( + [&](const blender::StringRef check_name) { + return unique_path_unique_check(list, sock, check_name); + }, + defname, + delim, + sockdata->path, + sizeof(sockdata->path)); +} + +/* find unique EXR layer */ +static bool unique_layer_unique_check(ListBase *lb, + bNodeSocket *sock, + const blender::StringRef name) +{ + LISTBASE_FOREACH (bNodeSocket *, sock_iter, lb) { + if (sock_iter != sock) { + NodeImageMultiFileSocket *sockdata = (NodeImageMultiFileSocket *)sock_iter->storage; + if (sockdata->layer == name) { + return true; + } + } + } + return false; +} + +static void ntreeCompositOutputFileUniqueLayer(ListBase *list, + bNodeSocket *sock, + const char defname[], + char delim) +{ + /* See if we are given an empty string */ + if (ELEM(nullptr, sock, defname)) { + return; + } + NodeImageMultiFileSocket *sockdata = (NodeImageMultiFileSocket *)sock->storage; + BLI_uniquename_cb( + [&](const blender::StringRef check_name) { + return unique_layer_unique_check(list, sock, check_name); + }, + defname, + delim, + sockdata->layer, + sizeof(sockdata->layer)); +} + +static bNodeSocket *ntreeCompositOutputFileAddSocket(bNodeTree *ntree, + bNode *node, + const char *name, + const ImageFormatData *im_format) +{ + NodeCompositorFileOutput *nimf = (NodeCompositorFileOutput *)node->storage; + bNodeSocket *sock = blender::bke::node_add_static_socket( + *ntree, *node, SOCK_IN, SOCK_RGBA, PROP_NONE, "", name); + + /* create format data for the input socket */ + NodeImageMultiFileSocket *sockdata = MEM_callocN(__func__); + sock->storage = sockdata; + + STRNCPY_UTF8(sockdata->path, name); + ntreeCompositOutputFileUniquePath(&node->inputs, sock, name, '_'); + STRNCPY_UTF8(sockdata->layer, name); + ntreeCompositOutputFileUniqueLayer(&node->inputs, sock, name, '_'); + + if (im_format) { + BKE_image_format_copy(&sockdata->format, im_format); + sockdata->format.color_management = R_IMF_COLOR_MANAGEMENT_FOLLOW_SCENE; + if (BKE_imtype_is_movie(sockdata->format.imtype)) { + sockdata->format.imtype = R_IMF_IMTYPE_OPENEXR; + } + } + else { + BKE_image_format_init(&sockdata->format, false); + } + BKE_image_format_update_color_space_for_type(&sockdata->format); + + /* use node data format by default */ + sockdata->use_node_format = true; + sockdata->save_as_render = true; + + nimf->active_item_index = BLI_findindex(&node->inputs, sock); + + return sock; +} + static void do_versions_nodetree_multi_file_output_format_2_62_1(Scene *sce, bNodeTree *ntree) { LISTBASE_FOREACH (bNode *, node, &ntree->nodes) { if (node->type_legacy == CMP_NODE_OUTPUT_FILE) { /* previous CMP_NODE_OUTPUT_FILE nodes get converted to multi-file outputs */ NodeImageFile *old_data = static_cast(node->storage); - NodeImageMultiFile *nimf = MEM_callocN("node image multi file"); + NodeCompositorFileOutput *nimf = MEM_callocN( + "node image multi file"); bNodeSocket *old_image = static_cast(BLI_findlink(&node->inputs, 0)); bNodeSocket *old_z = static_cast(BLI_findlink(&node->inputs, 1)); @@ -276,7 +389,7 @@ static void do_versions_nodetree_multi_file_output_format_2_62_1(Scene *sce, bNo BLI_path_split_dir_file( old_data->name, basepath, sizeof(basepath), filename, sizeof(filename)); - STRNCPY(nimf->base_path, basepath); + STRNCPY(nimf->directory, basepath); nimf->format = old_data->im_format; } else { @@ -326,7 +439,7 @@ static void do_versions_nodetree_multi_file_output_format_2_62_1(Scene *sce, bNo } } else if (node->type_legacy == CMP_NODE_OUTPUT_MULTI_FILE__DEPRECATED) { - NodeImageMultiFile *nimf = static_cast(node->storage); + NodeCompositorFileOutput *nimf = static_cast(node->storage); /* CMP_NODE_OUTPUT_MULTI_FILE has been re-declared as CMP_NODE_OUTPUT_FILE */ node->type_legacy = CMP_NODE_OUTPUT_FILE; diff --git a/source/blender/blenloader/intern/versioning_300.cc b/source/blender/blenloader/intern/versioning_300.cc index 894cde6ee39..368b89c204a 100644 --- a/source/blender/blenloader/intern/versioning_300.cc +++ b/source/blender/blenloader/intern/versioning_300.cc @@ -3713,7 +3713,7 @@ void blo_do_versions_300(FileData *fd, Library * /*lib*/, Main *bmain) } if (node->storage) { - NodeImageMultiFile *nimf = (NodeImageMultiFile *)node->storage; + NodeCompositorFileOutput *nimf = (NodeCompositorFileOutput *)node->storage; version_fix_image_format_copy(bmain, &nimf->format); } } diff --git a/source/blender/blenloader/intern/versioning_430.cc b/source/blender/blenloader/intern/versioning_430.cc index 5f055f5a738..e4caa2dee0d 100644 --- a/source/blender/blenloader/intern/versioning_430.cc +++ b/source/blender/blenloader/intern/versioning_430.cc @@ -308,7 +308,7 @@ void blo_do_versions_430(FileData * /*fd*/, Library * /*lib*/, Main *bmain) } /* Initialize node format color space if it is not set. */ - NodeImageMultiFile *storage = static_cast(node->storage); + NodeCompositorFileOutput *storage = static_cast(node->storage); if (storage->format.linear_colorspace_settings.name[0] == '\0') { BKE_image_format_update_color_space_for_type(&storage->format); } diff --git a/source/blender/blenloader/intern/versioning_450.cc b/source/blender/blenloader/intern/versioning_450.cc index d0448ad1254..bb2ea31fb75 100644 --- a/source/blender/blenloader/intern/versioning_450.cc +++ b/source/blender/blenloader/intern/versioning_450.cc @@ -3201,8 +3201,8 @@ static void version_escape_curly_braces_in_compositor_file_output_nodes(bNodeTre continue; } - NodeImageMultiFile *node_data = static_cast(node->storage); - version_escape_curly_braces(node_data->base_path, FILE_MAX); + NodeCompositorFileOutput *node_data = static_cast(node->storage); + version_escape_curly_braces(node_data->directory, FILE_MAX); LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) { NodeImageMultiFileSocket *socket_data = static_cast( diff --git a/source/blender/blenloader/intern/versioning_500.cc b/source/blender/blenloader/intern/versioning_500.cc index 27ebd74ebfd..c639747cda7 100644 --- a/source/blender/blenloader/intern/versioning_500.cc +++ b/source/blender/blenloader/intern/versioning_500.cc @@ -1390,6 +1390,59 @@ static void do_version_composite_node_in_scene_tree(bNodeTree &node_tree, bNode version_node_remove(node_tree, node); } +/* The file output node started using item accessors, so we need to free socket storage and copy + * them to the new items members. Additionally, the base path was split into a directory and a file + * name, so we need to split it. */ +static void do_version_file_output_node(bNode &node) +{ + if (node.storage == nullptr) { + return; + } + + NodeCompositorFileOutput *data = static_cast(node.storage); + + /* The directory previously stored both the directory and the file name. */ + char directory[FILE_MAX] = ""; + char file_name[FILE_MAX] = ""; + BLI_path_split_dir_file(data->directory, directory, FILE_MAX, file_name, FILE_MAX); + BLI_strncpy(data->directory, directory, FILE_MAX); + data->file_name = BLI_strdup_null(file_name); + + data->items_count = BLI_listbase_count(&node.inputs); + data->items = MEM_calloc_arrayN(data->items_count, __func__); + int i = 0; + LISTBASE_FOREACH_INDEX (bNodeSocket *, input, &node.inputs, i) { + NodeImageMultiFileSocket *old_item_data = static_cast( + input->storage); + NodeCompositorFileOutputItem *item_data = &data->items[i]; + + item_data->identifier = i; + BKE_image_format_copy(&item_data->format, &old_item_data->format); + item_data->save_as_render = old_item_data->save_as_render; + item_data->override_node_format = !bool(old_item_data->use_node_format); + + item_data->socket_type = input->type; + if (item_data->socket_type == SOCK_VECTOR) { + item_data->vector_socket_dimensions = + input->default_value_typed()->dimensions; + } + + if (data->format.imtype == R_IMF_IMTYPE_MULTILAYER) { + item_data->name = BLI_strdup(old_item_data->layer); + } + else { + item_data->name = BLI_strdup(old_item_data->path); + } + + const std::string identifier = "Item_" + std::to_string(item_data->identifier); + STRNCPY(input->identifier, identifier.c_str()); + + BKE_image_format_free(&old_item_data->format); + MEM_freeN(old_item_data); + input->storage = nullptr; + } +} + /* Updates the media type of the given format to match its imtype. */ static void update_format_media_type(ImageFormatData *format) { @@ -1915,7 +1968,7 @@ void blo_do_versions_500(FileData * /*fd*/, Library * /*lib*/, Main *bmain) continue; } - NodeImageMultiFile *storage = static_cast(node->storage); + NodeCompositorFileOutput *storage = static_cast(node->storage); update_format_media_type(&storage->format); LISTBASE_FOREACH (bNodeSocket *, input, &node->inputs) { @@ -2107,6 +2160,20 @@ void blo_do_versions_500(FileData * /*fd*/, Library * /*lib*/, Main *bmain) } } + if (!MAIN_VERSION_FILE_ATLEAST(bmain, 500, 54)) { + FOREACH_NODETREE_BEGIN (bmain, node_tree, id) { + if (node_tree->type != NTREE_COMPOSIT) { + continue; + } + LISTBASE_FOREACH (bNode *, node, &node_tree->nodes) { + if (node->type_legacy == CMP_NODE_OUTPUT_FILE) { + do_version_file_output_node(*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/editors/interface/interface_layout.cc b/source/blender/editors/interface/interface_layout.cc index eeb27783b83..55cf08499be 100644 --- a/source/blender/editors/interface/interface_layout.cc +++ b/source/blender/editors/interface/interface_layout.cc @@ -1137,7 +1137,7 @@ static uiBut *ui_item_with_label(uiLayout *layout, /* We include PROP_NONE here because some plain string properties are used * as parts of paths. For example, the sub-paths in the compositor's File * Output node. */ - if (ELEM(subtype, PROP_FILEPATH, PROP_DIRPATH, PROP_NONE)) { + if (ELEM(subtype, PROP_FILEPATH, PROP_DIRPATH, PROP_FILENAME, PROP_NONE)) { if ((RNA_property_flag(prop) & PROP_PATH_SUPPORTS_TEMPLATES) != 0) { const std::string path = RNA_property_string_get(ptr, prop); if (BKE_path_contains_template_syntax(path)) { diff --git a/source/blender/editors/interface/regions/interface_region_tooltip.cc b/source/blender/editors/interface/regions/interface_region_tooltip.cc index d753ae26a4e..648ead534ba 100644 --- a/source/blender/editors/interface/regions/interface_region_tooltip.cc +++ b/source/blender/editors/interface/regions/interface_region_tooltip.cc @@ -1186,7 +1186,7 @@ static std::unique_ptr ui_tooltip_data_from_button_or_extra_icon( /* We include PROP_NONE here because some plain string properties are used * as parts of paths. For example, the sub-paths in the compositor's File * Output node. */ - if (ELEM(subtype, PROP_FILEPATH, PROP_DIRPATH, PROP_NONE)) { + if (ELEM(subtype, PROP_FILEPATH, PROP_DIRPATH, PROP_FILENAME, PROP_NONE)) { /* Template parse errors, for paths that support it. */ if ((RNA_property_flag(rnaprop) & PROP_PATH_SUPPORTS_TEMPLATES) != 0) { const std::string path = RNA_property_string_get(&but->rnapoin, rnaprop); diff --git a/source/blender/editors/space_image/image_buttons.cc b/source/blender/editors/space_image/image_buttons.cc index 1664d165aac..55bb34abdb0 100644 --- a/source/blender/editors/space_image/image_buttons.cc +++ b/source/blender/editors/space_image/image_buttons.cc @@ -995,7 +995,11 @@ void uiTemplateImageSettings(uiLayout *layout, col->use_property_split_set(true); col->use_property_decorate_set(false); - col->prop(imfptr, "media_type", UI_ITEM_NONE, std::nullopt, ICON_NONE); + /* The file output node draws the media type itself. */ + const bool is_file_output = (id && GS(id->name) == ID_NT); + if (!is_file_output) { + col->prop(imfptr, "media_type", UI_ITEM_NONE, std::nullopt, ICON_NONE); + } /* Multi layer images and video media types only have a single supported format, * so we needn't draw the format enum. */ diff --git a/source/blender/editors/space_node/drawnode.cc b/source/blender/editors/space_node/drawnode.cc index 25bd406c6b5..adcc9fb7464 100644 --- a/source/blender/editors/space_node/drawnode.cc +++ b/source/blender/editors/space_node/drawnode.cc @@ -1008,51 +1008,6 @@ static const SocketColorFn std_node_socket_color_funcs[] = { std_node_socket_color_fn, }; -/* draw function for file output node sockets, - * displays only sub-path and format, no value button */ -static void node_file_output_socket_draw(bContext *C, - uiLayout *layout, - PointerRNA *ptr, - PointerRNA *node_ptr) -{ - bNodeTree *ntree = (bNodeTree *)ptr->owner_id; - bNodeSocket *sock = (bNodeSocket *)ptr->data; - uiLayout *row; - PointerRNA inputptr; - - row = &layout->row(false); - - PointerRNA imfptr = RNA_pointer_get(node_ptr, "format"); - int imtype = RNA_enum_get(&imfptr, "file_format"); - - if (imtype == R_IMF_IMTYPE_MULTILAYER) { - NodeImageMultiFileSocket *input = (NodeImageMultiFileSocket *)sock->storage; - inputptr = RNA_pointer_create_discrete(&ntree->id, &RNA_NodeOutputFileSlotLayer, input); - - row->label(input->layer, ICON_NONE); - } - else { - NodeImageMultiFileSocket *input = (NodeImageMultiFileSocket *)sock->storage; - uiBlock *block; - inputptr = RNA_pointer_create_discrete(&ntree->id, &RNA_NodeOutputFileSlotFile, input); - - row->label(input->path, ICON_NONE); - - if (!RNA_boolean_get(&inputptr, "use_node_format")) { - imfptr = RNA_pointer_get(&inputptr, "format"); - } - - const char *imtype_name; - PropertyRNA *imtype_prop = RNA_struct_find_property(&imfptr, "file_format"); - RNA_property_enum_name( - C, &imfptr, imtype_prop, RNA_property_enum_get(&imfptr, imtype_prop), &imtype_name); - block = row->block(); - UI_block_emboss_set(block, ui::EmbossType::Pulldown); - row->label(imtype_name, ICON_NONE); - UI_block_emboss_set(block, ui::EmbossType::None); - } -} - static bool socket_needs_attribute_search(bNode &node, bNodeSocket &socket) { const nodes::NodeDeclaration *node_decl = node.declaration(); @@ -1151,12 +1106,6 @@ static void std_node_socket_draw( layout->active_set(false); } - /* XXX not nice, eventually give this node its own socket type ... */ - if (node->type_legacy == CMP_NODE_OUTPUT_FILE) { - node_file_output_socket_draw(C, layout, ptr, node_ptr); - return; - } - const bool has_gizmo = tree->runtime->gizmo_propagation ? tree->runtime->gizmo_propagation->gizmo_endpoint_sockets.contains( sock) : diff --git a/source/blender/editors/space_node/node_edit.cc b/source/blender/editors/space_node/node_edit.cc index c55adf20944..55d0d146594 100644 --- a/source/blender/editors/space_node/node_edit.cc +++ b/source/blender/editors/space_node/node_edit.cc @@ -2102,194 +2102,6 @@ void NODE_OT_delete_reconnect(wmOperatorType *ot) /** \} */ -/* -------------------------------------------------------------------- */ -/** \name Node File Output Add Socket Operator - * \{ */ - -static wmOperatorStatus node_output_file_add_socket_exec(bContext *C, wmOperator *op) -{ - Scene *scene = CTX_data_scene(C); - SpaceNode *snode = CTX_wm_space_node(C); - PointerRNA ptr = CTX_data_pointer_get(C, "node"); - bNodeTree *ntree = nullptr; - bNode *node = nullptr; - char file_path[MAX_NAME]; - - if (ptr.data) { - node = (bNode *)ptr.data; - ntree = (bNodeTree *)ptr.owner_id; - } - else if (snode && snode->edittree) { - ntree = snode->edittree; - node = bke::node_get_active(*snode->edittree); - } - - if (!node || node->type_legacy != CMP_NODE_OUTPUT_FILE) { - return OPERATOR_CANCELLED; - } - - RNA_string_get(op->ptr, "file_path", file_path); - - if (file_path[0] != '\0') { - ntreeCompositOutputFileAddSocket(ntree, node, file_path, &scene->r.im_format); - } - else { - ntreeCompositOutputFileAddSocket(ntree, node, DATA_("Image"), &scene->r.im_format); - } - - BKE_main_ensure_invariants(*CTX_data_main(C), snode->edittree->id); - - return OPERATOR_FINISHED; -} - -void NODE_OT_output_file_add_socket(wmOperatorType *ot) -{ - /* identifiers */ - ot->name = "Add File Node Socket"; - ot->description = "Add a new input to a file output node"; - ot->idname = "NODE_OT_output_file_add_socket"; - - /* callbacks */ - ot->exec = node_output_file_add_socket_exec; - ot->poll = composite_node_editable; - - /* flags */ - ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; - - RNA_def_string( - ot->srna, "file_path", nullptr, MAX_NAME, "File Path", "Subpath of the output file"); -} - -/** \} */ - -/* -------------------------------------------------------------------- */ -/** \name Node Multi File Output Remove Socket Operator - * \{ */ - -static wmOperatorStatus node_output_file_remove_active_socket_exec(bContext *C, - wmOperator * /*op*/) -{ - SpaceNode *snode = CTX_wm_space_node(C); - PointerRNA ptr = CTX_data_pointer_get(C, "node"); - bNodeTree *ntree = nullptr; - bNode *node = nullptr; - - if (ptr.data) { - node = (bNode *)ptr.data; - ntree = (bNodeTree *)ptr.owner_id; - } - else if (snode && snode->edittree) { - ntree = snode->edittree; - node = bke::node_get_active(*snode->edittree); - } - - if (!node || node->type_legacy != CMP_NODE_OUTPUT_FILE) { - return OPERATOR_CANCELLED; - } - - if (!ntreeCompositOutputFileRemoveActiveSocket(ntree, node)) { - return OPERATOR_CANCELLED; - } - - BKE_main_ensure_invariants(*CTX_data_main(C), ntree->id); - - return OPERATOR_FINISHED; -} - -void NODE_OT_output_file_remove_active_socket(wmOperatorType *ot) -{ - /* identifiers */ - ot->name = "Remove File Node Socket"; - ot->description = "Remove the active input from a file output node"; - ot->idname = "NODE_OT_output_file_remove_active_socket"; - - /* callbacks */ - ot->exec = node_output_file_remove_active_socket_exec; - ot->poll = composite_node_editable; - - /* flags */ - ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; -} - -/** \} */ - -/* -------------------------------------------------------------------- */ -/** \name Node Multi File Output Move Socket Node - * \{ */ - -static wmOperatorStatus node_output_file_move_active_socket_exec(bContext *C, wmOperator *op) -{ - SpaceNode *snode = CTX_wm_space_node(C); - PointerRNA ptr = CTX_data_pointer_get(C, "node"); - bNode *node = nullptr; - - if (ptr.data) { - node = (bNode *)ptr.data; - } - else if (snode && snode->edittree) { - node = bke::node_get_active(*snode->edittree); - } - - if (!node || node->type_legacy != CMP_NODE_OUTPUT_FILE) { - return OPERATOR_CANCELLED; - } - - NodeImageMultiFile *nimf = (NodeImageMultiFile *)node->storage; - - bNodeSocket *sock = (bNodeSocket *)BLI_findlink(&node->inputs, nimf->active_input); - if (!sock) { - return OPERATOR_CANCELLED; - } - - int direction = RNA_enum_get(op->ptr, "direction"); - - if (direction == 1) { - bNodeSocket *before = sock->prev; - if (!before) { - return OPERATOR_CANCELLED; - } - BLI_remlink(&node->inputs, sock); - BLI_insertlinkbefore(&node->inputs, before, sock); - nimf->active_input--; - } - else { - bNodeSocket *after = sock->next; - if (!after) { - return OPERATOR_CANCELLED; - } - BLI_remlink(&node->inputs, sock); - BLI_insertlinkafter(&node->inputs, after, sock); - nimf->active_input++; - } - - BKE_ntree_update_tag_node_property(snode->edittree, node); - BKE_main_ensure_invariants(*CTX_data_main(C), snode->edittree->id); - - return OPERATOR_FINISHED; -} - -void NODE_OT_output_file_move_active_socket(wmOperatorType *ot) -{ - static const EnumPropertyItem direction_items[] = { - {1, "UP", 0, "Up", ""}, {2, "DOWN", 0, "Down", ""}, {0, nullptr, 0, nullptr, nullptr}}; - - /* identifiers */ - ot->name = "Move File Node Socket"; - ot->description = "Move the active input of a file output node up or down the list"; - ot->idname = "NODE_OT_output_file_move_active_socket"; - - /* callbacks */ - ot->exec = node_output_file_move_active_socket_exec; - ot->poll = composite_node_editable; - - /* flags */ - ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; - - RNA_def_enum(ot->srna, "direction", direction_items, 2, "Direction", ""); -} - -/** \} */ - /* -------------------------------------------------------------------- */ /** \name Node Copy Node Color Operator * \{ */ diff --git a/source/blender/editors/space_node/node_intern.hh b/source/blender/editors/space_node/node_intern.hh index ee5c9ce66df..4b0ad59bbb2 100644 --- a/source/blender/editors/space_node/node_intern.hh +++ b/source/blender/editors/space_node/node_intern.hh @@ -389,10 +389,6 @@ void NODE_OT_activate_viewer(wmOperatorType *ot); void NODE_OT_read_viewlayers(wmOperatorType *ot); void NODE_OT_render_changed(wmOperatorType *ot); -void NODE_OT_output_file_add_socket(wmOperatorType *ot); -void NODE_OT_output_file_remove_active_socket(wmOperatorType *ot); -void NODE_OT_output_file_move_active_socket(wmOperatorType *ot); - /** * \note clipboard_cut is a simple macro of copy + delete. */ diff --git a/source/blender/editors/space_node/node_ops.cc b/source/blender/editors/space_node/node_ops.cc index cb4ef60a0b2..35c51a774b0 100644 --- a/source/blender/editors/space_node/node_ops.cc +++ b/source/blender/editors/space_node/node_ops.cc @@ -92,10 +92,6 @@ void node_operatortypes() WM_operatortype_append(NODE_OT_new_node_tree); WM_operatortype_append(NODE_OT_new_compositing_node_group); - WM_operatortype_append(NODE_OT_output_file_add_socket); - WM_operatortype_append(NODE_OT_output_file_remove_active_socket); - WM_operatortype_append(NODE_OT_output_file_move_active_socket); - WM_operatortype_append(NODE_OT_parent_set); WM_operatortype_append(NODE_OT_join); WM_operatortype_append(NODE_OT_attach); diff --git a/source/blender/makesdna/DNA_node_types.h b/source/blender/makesdna/DNA_node_types.h index 669eb4bc6c1..03a873a984d 100644 --- a/source/blender/makesdna/DNA_node_types.h +++ b/source/blender/makesdna/DNA_node_types.h @@ -1234,32 +1234,61 @@ typedef struct NodeImageFile { int sfra, efra; } NodeImageFile; -/** - * XXX: first struct fields should match #NodeImageFile to ensure forward compatibility. - */ -typedef struct NodeImageMultiFile { - char base_path[/*FILE_MAX*/ 1024]; - ImageFormatData format; - /** XXX old frame rand values from NodeImageFile for forward compatibility. */ - int sfra DNA_DEPRECATED, efra DNA_DEPRECATED; - /** Selected input in details view list. */ - int active_input; +typedef struct NodeCompositorFileOutputItem { + /* The unique identifier of the item used to construct the socket identifier. */ + int identifier; + /* The type of socket for the item, which is limited to the types listed in the + * FileOutputItemsAccessor::supports_socket_type. */ + int16_t socket_type; + /* The number of dimensions in the vector socket if the socket type is vector, otherwise, it is + * unused, */ + char vector_socket_dimensions; + /* If true and the node is saving individual files, the format an save_as_render members of this + * struct will be used, otherwise, the members of the NodeCompositorFileOutput struct will be + * used for all items. */ + char override_node_format; + /* Apply the render part of the display transform when saving non-linear images. Unused if + * override_node_format is false or the node is saving multi-layer images. */ char save_as_render; - char _pad[3]; -} NodeImageMultiFile; + char _pad[7]; + /* The unique name of the item. It is used as the file name when saving individual files and used + * as the layer name when saving multi-layer images. */ + char *name; + /* The image format to use when saving individual images and override_node_format is true. */ + ImageFormatData format; +} NodeCompositorFileOutputItem; + +typedef struct NodeCompositorFileOutput { + char directory[/*FILE_MAX*/ 1024]; + /* The base name of the file. Can be nullptr. */ + char *file_name; + /* The image format to use when saving the images. */ + ImageFormatData format; + /* The file output images. They can represent individual images or layers depending on whether + * multi-layer images are being saved. */ + NodeCompositorFileOutputItem *items; + /* The number of file output items. */ + int items_count; + /* The currently active file output item. */ + int active_item_index; + /* Apply the render part of the display transform when saving non-linear images. */ + char save_as_render; + char _pad[7]; +} NodeCompositorFileOutput; + typedef struct NodeImageMultiFileSocket { /* single layer file output */ short use_render_format DNA_DEPRECATED; /** Use overall node image format. */ - short use_node_format; - char save_as_render; + short use_node_format DNA_DEPRECATED; + char save_as_render DNA_DEPRECATED; char _pad1[3]; - char path[/*FILE_MAX*/ 1024]; - ImageFormatData format; + char path[/*FILE_MAX*/ 1024] DNA_DEPRECATED; + ImageFormatData format DNA_DEPRECATED; /* Multi-layer output. */ /** Subtract 2 because '.' and channel char are appended. */ - char layer[/*EXR_TOT_MAXNAME - 2*/ 62]; + char layer[/*EXR_TOT_MAXNAME - 2*/ 62] DNA_DEPRECATED; char _pad2[2]; } NodeImageMultiFileSocket; diff --git a/source/blender/makesdna/intern/dna_rename_defs.h b/source/blender/makesdna/intern/dna_rename_defs.h index d109d0533ac..e9baad9a826 100644 --- a/source/blender/makesdna/intern/dna_rename_defs.h +++ b/source/blender/makesdna/intern/dna_rename_defs.h @@ -44,6 +44,7 @@ DNA_STRUCT_RENAME(ActionChannelBag, ActionChannelbag) DNA_STRUCT_RENAME(Lamp, Light) +DNA_STRUCT_RENAME(NodeImageMultiFile, NodeCompositorFileOutput) DNA_STRUCT_RENAME(SeqConnection, StripConnection) DNA_STRUCT_RENAME(SeqRetimingHandle, SeqRetimingKey) DNA_STRUCT_RENAME(Sequence, Strip) @@ -170,6 +171,8 @@ DNA_STRUCT_RENAME_MEMBER(MovieTrackingTrack, search_max, search_max_legacy) DNA_STRUCT_RENAME_MEMBER(MovieTrackingTrack, search_min, search_min_legacy) DNA_STRUCT_RENAME_MEMBER(NlaStrip, action_slot_name, last_slot_identifier) DNA_STRUCT_RENAME_MEMBER(NodeCryptomatte, num_inputs, inputs_num) +DNA_STRUCT_RENAME_MEMBER(NodeCompositorFileOutput, base_path, directory) +DNA_STRUCT_RENAME_MEMBER(NodeCompositorFileOutput, active_input, active_item_index) DNA_STRUCT_RENAME_MEMBER(NodeGeometryAttributeCapture, data_type, data_type_legacy) DNA_STRUCT_RENAME_MEMBER(NodesModifierData, simulation_bake_directory, bake_directory) DNA_STRUCT_RENAME_MEMBER(Object, col, color) diff --git a/source/blender/makesrna/intern/CMakeLists.txt b/source/blender/makesrna/intern/CMakeLists.txt index 6b0f88ddad0..f93960f68ef 100644 --- a/source/blender/makesrna/intern/CMakeLists.txt +++ b/source/blender/makesrna/intern/CMakeLists.txt @@ -253,6 +253,7 @@ set(INC ../../io/usd ../../modifiers ../../nodes + ../../nodes/composite/include ../../nodes/function/include ../../nodes/geometry/include ../../sequencer diff --git a/source/blender/makesrna/intern/rna_nodetree.cc b/source/blender/makesrna/intern/rna_nodetree.cc index f8f44e4284f..6b96e4cb92c 100644 --- a/source/blender/makesrna/intern/rna_nodetree.cc +++ b/source/blender/makesrna/intern/rna_nodetree.cc @@ -656,6 +656,7 @@ static const EnumPropertyItem node_cryptomatte_layer_name_items[] = { # include "NOD_common.hh" # include "NOD_composite.hh" +# include "NOD_compositor_file_output.hh" # include "NOD_fn_format_string.hh" # include "NOD_geo_bake.hh" # include "NOD_geo_bundle.hh" @@ -692,6 +693,7 @@ using blender::nodes::ClosureOutputItemsAccessor; using blender::nodes::CombineBundleItemsAccessor; using blender::nodes::EvaluateClosureInputItemsAccessor; using blender::nodes::EvaluateClosureOutputItemsAccessor; +using blender::nodes::FileOutputItemsAccessor; using blender::nodes::ForeachGeometryElementGenerationItemsAccessor; using blender::nodes::ForeachGeometryElementInputItemsAccessor; using blender::nodes::ForeachGeometryElementMainItemsAccessor; @@ -3553,20 +3555,6 @@ static void rna_Image_Node_update_id(Main *bmain, Scene *scene, PointerRNA *ptr) rna_Node_update_relations(bmain, scene, ptr); } -static void rna_NodeOutputFile_slots_begin(CollectionPropertyIterator *iter, PointerRNA *ptr) -{ - bNode *node = ptr->data_as(); - rna_iterator_listbase_begin(iter, ptr, &node->inputs, nullptr); -} - -static PointerRNA rna_NodeOutputFile_slot_file_get(CollectionPropertyIterator *iter) -{ - bNodeSocket *sock = static_cast(rna_iterator_listbase_get(iter)); - PointerRNA ptr = RNA_pointer_create_with_parent( - iter->parent, &RNA_NodeOutputFileSlotFile, sock->storage); - return ptr; -} - /* -------------------------------------------------------------------- * White Balance Node. */ @@ -3999,81 +3987,6 @@ static const EnumPropertyItem *rna_NodeGeometryCaptureAttributeItem_data_type_it /* ******** Node Socket Types ******** */ -static PointerRNA rna_NodeOutputFile_slot_layer_get(CollectionPropertyIterator *iter) -{ - bNodeSocket *sock = static_cast(rna_iterator_listbase_get(iter)); - PointerRNA ptr = RNA_pointer_create_with_parent( - iter->parent, &RNA_NodeOutputFileSlotLayer, sock->storage); - return ptr; -} - -static int rna_NodeOutputFileSocket_find_node(bNodeTree *ntree, - NodeImageMultiFileSocket *data, - bNode **nodep, - bNodeSocket **sockp) -{ - bNode *node; - bNodeSocket *sock; - - for (node = static_cast(ntree->nodes.first); node; node = node->next) { - for (sock = static_cast(node->inputs.first); sock; sock = sock->next) { - NodeImageMultiFileSocket *sockdata = static_cast(sock->storage); - if (sockdata == data) { - *nodep = node; - *sockp = sock; - return 1; - } - } - } - - *nodep = nullptr; - *sockp = nullptr; - return 0; -} - -static void rna_NodeOutputFileSlotFile_path_set(PointerRNA *ptr, const char *value) -{ - bNodeTree *ntree = reinterpret_cast(ptr->owner_id); - NodeImageMultiFileSocket *sockdata = static_cast(ptr->data); - bNode *node; - bNodeSocket *sock; - - if (rna_NodeOutputFileSocket_find_node(ntree, sockdata, &node, &sock)) { - ntreeCompositOutputFileSetPath(node, sock, value); - } -} - -static void rna_NodeOutputFileSlotLayer_name_set(PointerRNA *ptr, const char *value) -{ - bNodeTree *ntree = reinterpret_cast(ptr->owner_id); - NodeImageMultiFileSocket *sockdata = static_cast(ptr->data); - bNode *node; - bNodeSocket *sock; - - if (rna_NodeOutputFileSocket_find_node(ntree, sockdata, &node, &sock)) { - ntreeCompositOutputFileSetLayer(node, sock, value); - } -} - -static bNodeSocket *rna_NodeOutputFile_slots_new( - ID *id, bNode *node, bContext *C, ReportList * /*reports*/, const char *name) -{ - bNodeTree *ntree = reinterpret_cast(id); - Scene *scene = CTX_data_scene(C); - ImageFormatData *im_format = nullptr; - bNodeSocket *sock; - if (scene) { - im_format = &scene->r.im_format; - } - - sock = ntreeCompositOutputFileAddSocket(ntree, node, name, im_format); - - BKE_main_ensure_invariants(*CTX_data_main(C), ntree->id); - WM_main_add_notifier(NC_NODE | NA_EDITED, ntree); - - return sock; -} - static void rna_FrameNode_label_size_update(Main *bmain, Scene *scene, PointerRNA *ptr) { BLF_cache_clear(); @@ -4593,6 +4506,127 @@ static const EnumPropertyItem node_scatter_phase_items[] = { {0, nullptr, 0, nullptr, nullptr}, }; +static void rna_def_node_item_array_socket_item_common( + StructRNA *srna, + const char *accessor, + const bool add_socket_type, + const bool add_vector_socket_dimensions = false) +{ + static blender::LinearAllocator<> allocator; + PropertyRNA *prop; + + char name_set_func[128]; + SNPRINTF(name_set_func, "rna_Node_ItemArray_item_name_set<%s>", accessor); + + char item_update_func[128]; + SNPRINTF(item_update_func, "rna_Node_ItemArray_item_update<%s>", accessor); + const char *item_update_func_ptr = allocator.copy_string(item_update_func).c_str(); + + char socket_type_itemf[128]; + SNPRINTF(socket_type_itemf, "rna_Node_ItemArray_socket_type_itemf<%s>", accessor); + + char color_get_func[128]; + SNPRINTF(color_get_func, "rna_Node_ItemArray_item_color_get<%s>", accessor); + + prop = RNA_def_property(srna, "name", PROP_STRING, PROP_NONE); + RNA_def_property_string_funcs( + prop, nullptr, nullptr, allocator.copy_string(name_set_func).c_str()); + RNA_def_property_ui_text(prop, "Name", ""); + RNA_def_struct_name_property(srna, prop); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, item_update_func_ptr); + + if (add_socket_type) { + prop = RNA_def_property(srna, "socket_type", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_items(prop, rna_enum_node_socket_data_type_items); + RNA_def_property_enum_funcs( + prop, nullptr, nullptr, allocator.copy_string(socket_type_itemf).c_str()); + RNA_def_property_ui_text(prop, "Socket Type", ""); + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, item_update_func_ptr); + + if (add_vector_socket_dimensions) { + prop = RNA_def_property(srna, "vector_socket_dimensions", PROP_INT, PROP_NONE); + RNA_def_property_int_sdna(prop, nullptr, "vector_socket_dimensions"); + RNA_def_property_range(prop, 2, 4); + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + RNA_def_property_ui_text(prop, "Dimensions", "Dimensions of the vector socket"); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, item_update_func_ptr); + } + } + + prop = RNA_def_property(srna, "color", PROP_FLOAT, PROP_COLOR_GAMMA); + RNA_def_property_array(prop, 4); + RNA_def_property_float_funcs( + prop, allocator.copy_string(color_get_func).c_str(), nullptr, nullptr); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + RNA_def_property_ui_text( + prop, "Color", "Color of the corresponding socket type in the node editor"); +} + +static void rna_def_node_item_array_common_functions(StructRNA *srna, + const char *item_name, + const char *accessor_name) +{ + static blender::LinearAllocator<> allocator; + PropertyRNA *parm; + FunctionRNA *func; + + char remove_call[128]; + SNPRINTF(remove_call, "rna_Node_ItemArray_remove<%s>", accessor_name); + char clear_call[128]; + SNPRINTF(clear_call, "rna_Node_ItemArray_clear<%s>", accessor_name); + char move_call[128]; + SNPRINTF(move_call, "rna_Node_ItemArray_move<%s>", accessor_name); + + func = RNA_def_function(srna, "remove", allocator.copy_string(remove_call).c_str()); + RNA_def_function_ui_description(func, "Remove an item"); + RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN | FUNC_USE_REPORTS); + parm = RNA_def_pointer(func, "item", item_name, "Item", "The item to remove"); + RNA_def_parameter_flags(parm, PROP_NEVER_NULL, PARM_REQUIRED); + + func = RNA_def_function(srna, "clear", allocator.copy_string(clear_call).c_str()); + RNA_def_function_ui_description(func, "Remove all items"); + RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN); + + func = RNA_def_function(srna, "move", allocator.copy_string(move_call).c_str()); + RNA_def_function_ui_description(func, "Move an item to another position"); + RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN); + parm = RNA_def_int( + func, "from_index", -1, 0, INT_MAX, "From Index", "Index of the item to move", 0, 10000); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + parm = RNA_def_int( + func, "to_index", -1, 0, INT_MAX, "To Index", "Target index for the item", 0, 10000); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); +} + +static void rna_def_node_item_array_new_with_socket_and_name(StructRNA *srna, + const char *item_name, + const char *accessor_name) +{ + static blender::LinearAllocator<> allocator; + PropertyRNA *parm; + FunctionRNA *func; + + char name[128]; + SNPRINTF(name, "rna_Node_ItemArray_new_with_socket_and_name<%s>", accessor_name); + + func = RNA_def_function(srna, "new", allocator.copy_string(name).c_str()); + RNA_def_function_ui_description(func, "Add an item at the end"); + RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN | FUNC_USE_REPORTS); + parm = RNA_def_enum(func, + "socket_type", + rna_enum_node_socket_data_type_items, + SOCK_GEOMETRY, + "Socket Type", + "Socket type of the item"); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + parm = RNA_def_string(func, "name", nullptr, MAX_NAME, "Name", ""); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + /* return value */ + parm = RNA_def_pointer(func, "item", item_name, "Item", "New item"); + RNA_def_function_return(func, parm); +} + /* -- Common nodes ---------------------------------------------------------- */ static void def_group_input(BlenderRNA * /*brna*/, StructRNA * /*srna*/) {} @@ -6487,19 +6521,18 @@ static void def_cmp_render_layers(BlenderRNA * /*brna*/, StructRNA *srna) RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_view_layer_update"); } -static void rna_def_cmp_output_file_slot_file(BlenderRNA *brna) +static void rna_def_cmp_file_output_item(BlenderRNA *brna) { - StructRNA *srna; - PropertyRNA *prop; + StructRNA *srna = RNA_def_struct(brna, "NodeCompositorFileOutputItem", nullptr); + RNA_def_struct_ui_text(srna, "File Output Item", ""); - srna = RNA_def_struct(brna, "NodeOutputFileSlotFile", nullptr); - RNA_def_struct_sdna(srna, "NodeImageMultiFileSocket"); - RNA_def_struct_ui_text( - srna, "Output File Slot", "Single layer file slot of the file output node"); + rna_def_node_item_array_socket_item_common(srna, "FileOutputItemsAccessor", true, true); - prop = RNA_def_property(srna, "use_node_format", PROP_BOOLEAN, PROP_NONE); - RNA_def_property_boolean_sdna(prop, nullptr, "use_node_format", 1); - RNA_def_property_ui_text(prop, "Node Format", ""); + PropertyRNA *prop = RNA_def_property(srna, "override_node_format", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "override_node_format", 1); + RNA_def_property_ui_text(prop, + "Override Node Format", + "Use a different format instead of the node format for this file"); RNA_def_property_update(prop, NC_NODE | NA_EDITED, nullptr); prop = RNA_def_property(srna, "save_as_render", PROP_BOOLEAN, PROP_NONE); @@ -6510,99 +6543,58 @@ static void rna_def_cmp_output_file_slot_file(BlenderRNA *brna) prop = RNA_def_property(srna, "format", PROP_POINTER, PROP_NONE); RNA_def_property_struct_type(prop, "ImageFormatSettings"); - - prop = RNA_def_property(srna, "path", PROP_STRING, PROP_NONE); - RNA_def_property_string_sdna(prop, nullptr, "path"); - RNA_def_property_string_funcs(prop, nullptr, nullptr, "rna_NodeOutputFileSlotFile_path_set"); - RNA_def_struct_name_property(srna, prop); - RNA_def_property_ui_text(prop, "Path", "Subpath used for this slot"); - RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_EDITOR_FILEBROWSER); - RNA_def_property_flag(prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_TEMPLATES); - RNA_def_property_path_template_type(prop, PROP_VARIABLES_RENDER_OUTPUT); - RNA_def_property_update(prop, NC_NODE | NA_EDITED, nullptr); } -static void rna_def_cmp_output_file_slot_layer(BlenderRNA *brna) + +static void rna_def_cmp_file_output_items(BlenderRNA *brna) { - StructRNA *srna; - PropertyRNA *prop; - - srna = RNA_def_struct(brna, "NodeOutputFileSlotLayer", nullptr); - RNA_def_struct_sdna(srna, "NodeImageMultiFileSocket"); - RNA_def_struct_ui_text( - srna, "Output File Layer Slot", "Multilayer slot of the file output node"); - - prop = RNA_def_property(srna, "name", PROP_STRING, PROP_NONE); - RNA_def_property_string_sdna(prop, nullptr, "layer"); - RNA_def_property_string_funcs(prop, nullptr, nullptr, "rna_NodeOutputFileSlotLayer_name_set"); - RNA_def_struct_name_property(srna, prop); - RNA_def_property_ui_text(prop, "Name", "OpenEXR layer name used for this slot"); - RNA_def_property_update(prop, NC_NODE | NA_EDITED, nullptr); -} -static void rna_def_cmp_output_file_slots_api(BlenderRNA *brna, - PropertyRNA *cprop, - const char *struct_name) -{ - StructRNA *srna; - PropertyRNA *parm; - FunctionRNA *func; - - RNA_def_property_srna(cprop, struct_name); - srna = RNA_def_struct(brna, struct_name, nullptr); + StructRNA *srna = RNA_def_struct(brna, "NodeCompositorFileOutputItems", nullptr); RNA_def_struct_sdna(srna, "bNode"); - RNA_def_struct_ui_text(srna, "File Output Slots", "Collection of File Output node slots"); + RNA_def_struct_ui_text(srna, "Items", "Collection of file output items"); - func = RNA_def_function(srna, "new", "rna_NodeOutputFile_slots_new"); - RNA_def_function_ui_description(func, "Add a file slot to this node"); - RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_REPORTS | FUNC_USE_CONTEXT); - parm = RNA_def_string(func, "name", nullptr, MAX_NAME, "Name", ""); - RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); - /* return value */ - parm = RNA_def_pointer(func, "socket", "NodeSocket", "", "New socket"); - RNA_def_function_return(func, parm); - - /* NOTE: methods below can use the standard node socket API functions, - * included here for completeness. */ - - func = RNA_def_function(srna, "remove", "rna_Node_socket_remove"); - RNA_def_function_ui_description(func, "Remove a file slot from this node"); - RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN | FUNC_USE_REPORTS); - parm = RNA_def_pointer(func, "socket", "NodeSocket", "", "The socket to remove"); - RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); - - func = RNA_def_function(srna, "clear", "rna_Node_inputs_clear"); - RNA_def_function_ui_description(func, "Remove all file slots from this node"); - RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN | FUNC_USE_REPORTS); - - func = RNA_def_function(srna, "move", "rna_Node_inputs_move"); - RNA_def_function_ui_description(func, "Move a file slot to another position"); - RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN | FUNC_USE_REPORTS); - parm = RNA_def_int( - func, "from_index", -1, 0, INT_MAX, "From Index", "Index of the socket to move", 0, 10000); - RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); - parm = RNA_def_int( - func, "to_index", -1, 0, INT_MAX, "To Index", "Target index for the socket", 0, 10000); - RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + rna_def_node_item_array_new_with_socket_and_name( + srna, "NodeCompositorFileOutputItem", "FileOutputItemsAccessor"); + rna_def_node_item_array_common_functions( + srna, "NodeCompositorFileOutputItem", "FileOutputItemsAccessor"); } -static void def_cmp_output_file(BlenderRNA *brna, StructRNA *srna) + +static void def_cmp_file_output(BlenderRNA *brna, StructRNA *srna) { PropertyRNA *prop; - rna_def_cmp_output_file_slot_file(brna); - rna_def_cmp_output_file_slot_layer(brna); + rna_def_cmp_file_output_item(brna); + rna_def_cmp_file_output_items(brna); - RNA_def_struct_sdna_from(srna, "NodeImageMultiFile", "storage"); + RNA_def_struct_sdna_from(srna, "NodeCompositorFileOutput", "storage"); - prop = RNA_def_property(srna, "base_path", PROP_STRING, PROP_FILEPATH); - RNA_def_property_string_sdna(prop, nullptr, "base_path"); - RNA_def_property_ui_text(prop, "Base Path", "Base output path for the image"); + prop = RNA_def_property(srna, "file_output_items", PROP_COLLECTION, PROP_NONE); + RNA_def_property_collection_sdna(prop, nullptr, "items", "items_count"); + RNA_def_property_struct_type(prop, "NodeCompositorFileOutputItem"); + RNA_def_property_ui_text(prop, "Items", ""); + RNA_def_property_srna(prop, "NodeCompositorFileOutputItems"); + + prop = RNA_def_property(srna, "active_item_index", PROP_INT, PROP_UNSIGNED); + RNA_def_property_int_sdna(prop, nullptr, "active_item_index"); + RNA_def_property_ui_text(prop, "Active Item Index", "Index of the active item"); + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + RNA_def_property_flag(prop, PROP_NO_DEG_UPDATE); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, nullptr); + + prop = RNA_def_property(srna, "directory", PROP_STRING, PROP_DIRPATH); + RNA_def_property_string_sdna(prop, nullptr, "directory"); + RNA_def_property_ui_text(prop, "Directory", "The directory where the image will be written"); RNA_def_property_flag( prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_BLEND_RELATIVE | PROP_PATH_SUPPORTS_TEMPLATES); RNA_def_property_path_template_type(prop, PROP_VARIABLES_RENDER_OUTPUT); RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update"); - prop = RNA_def_property(srna, "active_input_index", PROP_INT, PROP_NONE); - RNA_def_property_int_sdna(prop, nullptr, "active_input"); - RNA_def_property_ui_text(prop, "Active Input Index", "Active input index in details view list"); + prop = RNA_def_property(srna, "file_name", PROP_STRING, PROP_FILENAME); + RNA_def_property_string_sdna(prop, nullptr, "file_name"); + RNA_def_property_ui_text(prop, + "File Name", + "The base name of the file. Other information might be included in the " + "final file name depending on the node options"); + RNA_def_property_flag(prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_TEMPLATES); + RNA_def_property_path_template_type(prop, PROP_VARIABLES_RENDER_OUTPUT); RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update"); prop = RNA_def_property(srna, "format", PROP_POINTER, PROP_NONE); @@ -6613,38 +6605,6 @@ static void def_cmp_output_file(BlenderRNA *brna, StructRNA *srna) RNA_def_property_ui_text( prop, "Save as Render", "Apply render part of display transform when saving byte image"); RNA_def_property_update(prop, NC_NODE | NA_EDITED, nullptr); - - /* XXX using two different collections here for the same basic DNA list! - * Details of the output slots depend on whether the node is in Multilayer EXR mode. - */ - - prop = RNA_def_property(srna, "file_slots", PROP_COLLECTION, PROP_NONE); - RNA_def_property_collection_funcs(prop, - "rna_NodeOutputFile_slots_begin", - "rna_iterator_listbase_next", - "rna_iterator_listbase_end", - "rna_NodeOutputFile_slot_file_get", - nullptr, - nullptr, - nullptr, - nullptr); - RNA_def_property_struct_type(prop, "NodeOutputFileSlotFile"); - RNA_def_property_ui_text(prop, "File Slots", ""); - rna_def_cmp_output_file_slots_api(brna, prop, "CompositorNodeOutputFileFileSlots"); - - prop = RNA_def_property(srna, "layer_slots", PROP_COLLECTION, PROP_NONE); - RNA_def_property_collection_funcs(prop, - "rna_NodeOutputFile_slots_begin", - "rna_iterator_listbase_next", - "rna_iterator_listbase_end", - "rna_NodeOutputFile_slot_layer_get", - nullptr, - nullptr, - nullptr, - nullptr); - RNA_def_property_struct_type(prop, "NodeOutputFileSlotLayer"); - RNA_def_property_ui_text(prop, "EXR Layer Slots", ""); - rna_def_cmp_output_file_slots_api(brna, prop, "CompositorNodeOutputFileLayerSlots"); } static void def_cmp_dilate_erode(BlenderRNA * /*brna*/, StructRNA *srna) @@ -8051,116 +8011,6 @@ static void def_closure_input(BlenderRNA *brna, StructRNA *srna) def_common_zone_input(brna, srna); } -static void rna_def_node_item_array_socket_item_common(StructRNA *srna, - const char *accessor, - const bool add_socket_type) -{ - static blender::LinearAllocator<> allocator; - PropertyRNA *prop; - - char name_set_func[128]; - SNPRINTF(name_set_func, "rna_Node_ItemArray_item_name_set<%s>", accessor); - - char item_update_func[128]; - SNPRINTF(item_update_func, "rna_Node_ItemArray_item_update<%s>", accessor); - const char *item_update_func_ptr = allocator.copy_string(item_update_func).c_str(); - - char socket_type_itemf[128]; - SNPRINTF(socket_type_itemf, "rna_Node_ItemArray_socket_type_itemf<%s>", accessor); - - char color_get_func[128]; - SNPRINTF(color_get_func, "rna_Node_ItemArray_item_color_get<%s>", accessor); - - prop = RNA_def_property(srna, "name", PROP_STRING, PROP_NONE); - RNA_def_property_string_funcs( - prop, nullptr, nullptr, allocator.copy_string(name_set_func).c_str()); - RNA_def_property_ui_text(prop, "Name", ""); - RNA_def_struct_name_property(srna, prop); - RNA_def_property_update(prop, NC_NODE | NA_EDITED, item_update_func_ptr); - - if (add_socket_type) { - prop = RNA_def_property(srna, "socket_type", PROP_ENUM, PROP_NONE); - RNA_def_property_enum_items(prop, rna_enum_node_socket_data_type_items); - RNA_def_property_enum_funcs( - prop, nullptr, nullptr, allocator.copy_string(socket_type_itemf).c_str()); - RNA_def_property_ui_text(prop, "Socket Type", ""); - RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); - RNA_def_property_update(prop, NC_NODE | NA_EDITED, item_update_func_ptr); - } - - prop = RNA_def_property(srna, "color", PROP_FLOAT, PROP_COLOR_GAMMA); - RNA_def_property_array(prop, 4); - RNA_def_property_float_funcs( - prop, allocator.copy_string(color_get_func).c_str(), nullptr, nullptr); - RNA_def_property_clear_flag(prop, PROP_EDITABLE); - RNA_def_property_ui_text( - prop, "Color", "Color of the corresponding socket type in the node editor"); -} - -static void rna_def_node_item_array_common_functions(StructRNA *srna, - const char *item_name, - const char *accessor_name) -{ - static blender::LinearAllocator<> allocator; - PropertyRNA *parm; - FunctionRNA *func; - - char remove_call[128]; - SNPRINTF(remove_call, "rna_Node_ItemArray_remove<%s>", accessor_name); - char clear_call[128]; - SNPRINTF(clear_call, "rna_Node_ItemArray_clear<%s>", accessor_name); - char move_call[128]; - SNPRINTF(move_call, "rna_Node_ItemArray_move<%s>", accessor_name); - - func = RNA_def_function(srna, "remove", allocator.copy_string(remove_call).c_str()); - RNA_def_function_ui_description(func, "Remove an item"); - RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN | FUNC_USE_REPORTS); - parm = RNA_def_pointer(func, "item", item_name, "Item", "The item to remove"); - RNA_def_parameter_flags(parm, PROP_NEVER_NULL, PARM_REQUIRED); - - func = RNA_def_function(srna, "clear", allocator.copy_string(clear_call).c_str()); - RNA_def_function_ui_description(func, "Remove all items"); - RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN); - - func = RNA_def_function(srna, "move", allocator.copy_string(move_call).c_str()); - RNA_def_function_ui_description(func, "Move an item to another position"); - RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN); - parm = RNA_def_int( - func, "from_index", -1, 0, INT_MAX, "From Index", "Index of the item to move", 0, 10000); - RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); - parm = RNA_def_int( - func, "to_index", -1, 0, INT_MAX, "To Index", "Target index for the item", 0, 10000); - RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); -} - -static void rna_def_node_item_array_new_with_socket_and_name(StructRNA *srna, - const char *item_name, - const char *accessor_name) -{ - static blender::LinearAllocator<> allocator; - PropertyRNA *parm; - FunctionRNA *func; - - char name[128]; - SNPRINTF(name, "rna_Node_ItemArray_new_with_socket_and_name<%s>", accessor_name); - - func = RNA_def_function(srna, "new", allocator.copy_string(name).c_str()); - RNA_def_function_ui_description(func, "Add an item at the end"); - RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN | FUNC_USE_REPORTS); - parm = RNA_def_enum(func, - "socket_type", - rna_enum_node_socket_data_type_items, - SOCK_GEOMETRY, - "Socket Type", - "Socket type of the item"); - RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); - parm = RNA_def_string(func, "name", nullptr, MAX_NAME, "Name", ""); - RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); - /* return value */ - parm = RNA_def_pointer(func, "item", item_name, "Item", "New item"); - RNA_def_function_return(func, parm); -} - static void rna_def_geo_simulation_state_item(BlenderRNA *brna) { PropertyRNA *prop; @@ -10722,7 +10572,7 @@ static void rna_def_nodes(BlenderRNA *brna) define("CompositorNode", "CompositorNodeMovieDistortion", def_cmp_moviedistortion); define("CompositorNode", "CompositorNodeNormal"); define("CompositorNode", "CompositorNodeNormalize"); - define("CompositorNode", "CompositorNodeOutputFile", def_cmp_output_file); + define("CompositorNode", "CompositorNodeOutputFile", def_cmp_file_output); define("CompositorNode", "CompositorNodePixelate"); define("CompositorNode", "CompositorNodePlaneTrackDeform", def_cmp_planetrackdeform); define("CompositorNode", "CompositorNodePosterize"); diff --git a/source/blender/makesrna/intern/rna_scene.cc b/source/blender/makesrna/intern/rna_scene.cc index 1033ba2b9cc..81e57822eee 100644 --- a/source/blender/makesrna/intern/rna_scene.cc +++ b/source/blender/makesrna/intern/rna_scene.cc @@ -733,6 +733,7 @@ static const EnumPropertyItem eevee_resolution_scale_items[] = { # include +# include "BLI_index_range.hh" # include "BLI_string_utils.hh" # include "DNA_anim_types.h" @@ -780,6 +781,7 @@ static const EnumPropertyItem eevee_resolution_scale_items[] = { # include "BKE_unit.hh" # include "NOD_composite.hh" +# include "NOD_compositor_file_output.hh" # include "ED_grease_pencil.hh" # include "ED_image.hh" @@ -813,6 +815,7 @@ static const EnumPropertyItem eevee_resolution_scale_items[] = { # include "ANIM_keyingsets.hh" using blender::Vector; +using blender::nodes::FileOutputItemsAccessor; static int rna_ToolSettings_snap_mode_get(PointerRNA *ptr) { @@ -1335,24 +1338,26 @@ static std::optional rna_ImageFormatSettings_path( for (bNode *node : ntree->all_nodes()) { if (node->type_legacy == CMP_NODE_OUTPUT_FILE) { - if (match(&((NodeImageMultiFile *)node->storage)->format)) { + NodeCompositorFileOutput &storage = *static_cast( + node->storage); + if (match(&storage.format)) { char node_name_esc[sizeof(node->name) * 2]; BLI_str_escape(node_name_esc, node->name, sizeof(node_name_esc)); return fmt::format("nodes[\"{}\"].format", node_name_esc); } else { - LISTBASE_FOREACH (bNodeSocket *, socket, &node->inputs) { - NodeImageMultiFileSocket *sockdata = static_cast( - socket->storage); - if (match(&sockdata->format)) { + for (const int i : blender::IndexRange(storage.items_count)) { + NodeCompositorFileOutputItem &item = storage.items[i]; + if (match(&item.format)) { char node_name_esc[sizeof(node->name) * 2]; BLI_str_escape(node_name_esc, node->name, sizeof(node_name_esc)); - char socketdata_path_esc[sizeof(sockdata->path) * 2]; - BLI_str_escape(socketdata_path_esc, sockdata->path, sizeof(socketdata_path_esc)); - - return fmt::format( - "nodes[\"{}\"].file_slots[\"{}\"].format", node_name_esc, socketdata_path_esc); + const std::string identifier = FileOutputItemsAccessor::socket_identifier_for_item( + item); + const std::string escaped_identifier = BLI_str_escape(identifier.c_str()); + return fmt::format("nodes[\"{}\"].file_output_items[\"{}\"].format", + node_name_esc, + escaped_identifier.c_str()); } } } diff --git a/source/blender/nodes/NOD_composite.hh b/source/blender/nodes/NOD_composite.hh index 4fb4ea2e3fb..c99a25c106d 100644 --- a/source/blender/nodes/NOD_composite.hh +++ b/source/blender/nodes/NOD_composite.hh @@ -63,24 +63,6 @@ void ntreeCompositUpdateRLayers(bNodeTree *ntree); void ntreeCompositClearTags(bNodeTree *ntree); -bNodeSocket *ntreeCompositOutputFileAddSocket(bNodeTree *ntree, - bNode *node, - const char *name, - const ImageFormatData *im_format); - -int ntreeCompositOutputFileRemoveActiveSocket(bNodeTree *ntree, bNode *node); -void ntreeCompositOutputFileSetPath(bNode *node, bNodeSocket *sock, const char *name); -void ntreeCompositOutputFileSetLayer(bNode *node, bNodeSocket *sock, const char *name); -/* needed in do_versions */ -void ntreeCompositOutputFileUniquePath(ListBase *list, - bNodeSocket *sock, - const char defname[], - char delim); -void ntreeCompositOutputFileUniqueLayer(ListBase *list, - bNodeSocket *sock, - const char defname[], - char delim); - void ntreeCompositCryptomatteSyncFromAdd(bNode *node); void ntreeCompositCryptomatteSyncFromRemove(bNode *node); bNodeSocket *ntreeCompositCryptomatteAddSocket(bNodeTree *ntree, bNode *node); diff --git a/source/blender/nodes/NOD_socket_items.hh b/source/blender/nodes/NOD_socket_items.hh index 668bf7dc327..8dc1f1799d7 100644 --- a/source/blender/nodes/NOD_socket_items.hh +++ b/source/blender/nodes/NOD_socket_items.hh @@ -17,6 +17,8 @@ * #RepeatItemsAccessor and to implement the same methods. */ +#include + #include "BLI_string.h" #include "BLI_string_utils.hh" @@ -34,6 +36,7 @@ struct SocketItemsAccessorDefaults { static constexpr bool has_single_identifier_str = true; static constexpr bool has_name_validation = false; static constexpr bool has_custom_initial_name = false; + static constexpr bool has_vector_dimensions = false; static constexpr char unique_name_separator = '.'; }; @@ -189,17 +192,30 @@ template inline typename Accessor::ItemT &add_item_to_array(b } // namespace detail /** - * Add a new item at the end with the given socket type and name. + * Add a new item at the end with the given socket type and name. The optional dimensions argument + * can be provided for types that support multiple possible dimensions like Vector. It is expected + * to be in the range [2, 4] and if not provided, 3 should be assumed. */ template inline typename Accessor::ItemT *add_item_with_socket_type_and_name( - bNodeTree &ntree, bNode &node, const eNodeSocketDatatype socket_type, const char *name) + bNodeTree &ntree, + bNode &node, + const eNodeSocketDatatype socket_type, + const char *name, + std::optional dimensions = std::nullopt) { using ItemT = typename Accessor::ItemT; BLI_assert(Accessor::supports_socket_type(socket_type, ntree.type)); + BLI_assert(!(dimensions.has_value() && socket_type != SOCK_VECTOR)); + BLI_assert(ELEM(dimensions.value_or(3), 2, 3, 4)); UNUSED_VARS_NDEBUG(ntree); ItemT &new_item = detail::add_item_to_array(node); - Accessor::init_with_socket_type_and_name(node, new_item, socket_type, name); + if constexpr (Accessor::has_vector_dimensions) { + Accessor::init_with_socket_type_and_name(node, new_item, socket_type, name, dimensions); + } + else { + Accessor::init_with_socket_type_and_name(node, new_item, socket_type, name); + } return &new_item; } @@ -275,8 +291,12 @@ template if constexpr (Accessor::has_custom_initial_name) { name = Accessor::custom_initial_name(storage_node, name); } + std::optional dimensions = std::nullopt; + if (socket_type == SOCK_VECTOR) { + dimensions = src_socket->default_value_typed()->dimensions; + } item = add_item_with_socket_type_and_name( - ntree, storage_node, socket_type, name.c_str()); + ntree, storage_node, socket_type, name.c_str(), dimensions); } else if constexpr (Accessor::has_name && !Accessor::has_type) { item = add_item_with_name(storage_node, src_socket->name); diff --git a/source/blender/nodes/composite/CMakeLists.txt b/source/blender/nodes/composite/CMakeLists.txt index 276742e390c..ec59734988d 100644 --- a/source/blender/nodes/composite/CMakeLists.txt +++ b/source/blender/nodes/composite/CMakeLists.txt @@ -5,6 +5,7 @@ set(INC . .. + include ../intern ../../editors/include ../../compositor @@ -108,6 +109,7 @@ set(SRC node_composite_tree.cc node_composite_util.cc + include/NOD_compositor_file_output.hh node_composite_util.hh ) @@ -115,6 +117,7 @@ set(LIB PRIVATE bf::blenkernel PRIVATE bf::blenlib PRIVATE bf::blentranslation + PRIVATE bf::blenloader PRIVATE bf::depsgraph PRIVATE bf::dna PRIVATE bf::functions diff --git a/source/blender/nodes/composite/include/NOD_compositor_file_output.hh b/source/blender/nodes/composite/include/NOD_compositor_file_output.hh new file mode 100644 index 00000000000..76a1e8347fc --- /dev/null +++ b/source/blender/nodes/composite/include/NOD_compositor_file_output.hh @@ -0,0 +1,123 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include "MEM_guardedalloc.h" + +#include "BLI_math_base.hh" +#include "BLI_string.h" +#include "BLI_string_ref.hh" +#include "BLI_utildefines.h" + +#include "DNA_node_types.h" + +#include "BKE_image_format.hh" + +#include "NOD_socket_items.hh" + +namespace blender::nodes { + +struct FileOutputItemsAccessor : public socket_items::SocketItemsAccessorDefaults { + using ItemT = NodeCompositorFileOutputItem; + static StructRNA *item_srna; + static constexpr StringRefNull node_idname = "CompositorNodeOutputFile"; + static constexpr bool has_type = true; + static constexpr bool has_name = true; + static constexpr bool has_name_validation = true; + static constexpr bool has_vector_dimensions = true; + static constexpr char unique_name_separator = '_'; + struct operator_idnames { + static constexpr StringRefNull add_item = "NODE_OT_file_output_item_add"; + static constexpr StringRefNull remove_item = "NODE_OT_file_output_item_remove"; + static constexpr StringRefNull move_item = "NODE_OT_file_output_item_move"; + }; + struct ui_idnames { + static constexpr StringRefNull list = "DATA_UL_file_output_items"; + }; + struct rna_names { + static constexpr StringRefNull items = "file_output_items"; + static constexpr StringRefNull active_index = "active_item_index"; + }; + + static socket_items::SocketItemsRef get_items_from_node( + bNode &node) + { + auto *storage = static_cast(node.storage); + return {&storage->items, &storage->items_count, &storage->active_item_index}; + } + + static void copy_item(const NodeCompositorFileOutputItem &source, + NodeCompositorFileOutputItem &destination) + { + destination = source; + destination.name = BLI_strdup_null(destination.name); + BKE_image_format_copy(&destination.format, &source.format); + } + + static void destruct_item(NodeCompositorFileOutputItem *item) + { + MEM_SAFE_FREE(item->name); + BKE_image_format_free(&item->format); + } + + static void blend_write_item(BlendWriter *writer, const ItemT &item); + static void blend_read_data_item(BlendDataReader *reader, ItemT &item); + + static eNodeSocketDatatype get_socket_type(const NodeCompositorFileOutputItem &item) + { + return eNodeSocketDatatype(item.socket_type); + } + + static char **get_name(NodeCompositorFileOutputItem &item) + { + return &item.name; + } + + static bool supports_socket_type(const eNodeSocketDatatype socket_type, const int /*ntree_type*/) + { + return ELEM(socket_type, SOCK_FLOAT, SOCK_VECTOR, SOCK_RGBA); + } + + static int find_available_identifier(const NodeCompositorFileOutputItem *items, + const int items_count) + { + if (items_count == 0) { + return 0; + } + int max_identifier = items[0].identifier; + for (int i = 0; i < items_count; i++) { + max_identifier = math::max(items[i].identifier, max_identifier); + } + return max_identifier + 1; + } + + static void init_with_socket_type_and_name(bNode &node, + NodeCompositorFileOutputItem &item, + const eNodeSocketDatatype socket_type, + const char *name, + std::optional dimensions = std::nullopt) + { + auto *storage = static_cast(node.storage); + item.identifier = FileOutputItemsAccessor::find_available_identifier(storage->items, + storage->items_count); + + item.socket_type = socket_type; + item.vector_socket_dimensions = dimensions.value_or(3); + socket_items::set_item_name_and_make_unique(node, item, name); + + item.save_as_render = true; + BKE_image_format_init(&item.format, false); + BKE_image_format_update_color_space_for_type(&item.format); + } + + static std::string validate_name(const StringRef name); + + static std::string socket_identifier_for_item(const NodeCompositorFileOutputItem &item) + { + return "Item_" + std::to_string(item.identifier); + } +}; + +} // namespace blender::nodes diff --git a/source/blender/nodes/composite/nodes/node_composite_file_output.cc b/source/blender/nodes/composite/nodes/node_composite_file_output.cc index bbb09a98d38..46bca080350 100644 --- a/source/blender/nodes/composite/nodes/node_composite_file_output.cc +++ b/source/blender/nodes/composite/nodes/node_composite_file_output.cc @@ -13,13 +13,8 @@ #include "BLI_generic_pointer.hh" #include "BLI_index_range.hh" #include "BLI_listbase.h" -#include "BLI_math_vector.h" #include "BLI_path_utils.hh" #include "BLI_string.h" -#include "BLI_string_utf8.h" -#include "BLI_string_utils.hh" -#include "BLI_task.hh" -#include "BLI_utildefines.h" #include "BLT_translation.hh" @@ -28,12 +23,13 @@ #include "DNA_node_types.h" #include "DNA_scene_types.h" +#include "BLO_read_write.hh" + #include "BKE_context.hh" #include "BKE_cryptomatte.hh" #include "BKE_image.hh" #include "BKE_image_format.hh" #include "BKE_main.hh" -#include "BKE_node_tree_update.hh" #include "BKE_scene.hh" #include "RNA_access.hh" @@ -52,463 +48,338 @@ #include "COM_node_operation.hh" #include "COM_utilities.hh" +#include "NOD_compositor_file_output.hh" +#include "NOD_socket_items_blend.hh" +#include "NOD_socket_items_ops.hh" +#include "NOD_socket_items_ui.hh" #include "NOD_socket_search_link.hh" #include "node_composite_util.hh" namespace path_templates = blender::bke::path_templates; -/* **************** OUTPUT FILE ******************** */ - -/* find unique path */ -static bool unique_path_unique_check(ListBase *lb, - bNodeSocket *sock, - const blender::StringRef name) -{ - LISTBASE_FOREACH (bNodeSocket *, sock_iter, lb) { - if (sock_iter != sock) { - NodeImageMultiFileSocket *sockdata = (NodeImageMultiFileSocket *)sock_iter->storage; - if (sockdata->path == name) { - return true; - } - } - } - return false; -} -void ntreeCompositOutputFileUniquePath(ListBase *list, - bNodeSocket *sock, - const char defname[], - char delim) -{ - /* See if we are given an empty string */ - if (ELEM(nullptr, sock, defname)) { - return; - } - NodeImageMultiFileSocket *sockdata = (NodeImageMultiFileSocket *)sock->storage; - BLI_uniquename_cb( - [&](const blender::StringRef check_name) { - return unique_path_unique_check(list, sock, check_name); - }, - defname, - delim, - sockdata->path, - sizeof(sockdata->path)); -} - -/* find unique EXR layer */ -static bool unique_layer_unique_check(ListBase *lb, - bNodeSocket *sock, - const blender::StringRef name) -{ - LISTBASE_FOREACH (bNodeSocket *, sock_iter, lb) { - if (sock_iter != sock) { - NodeImageMultiFileSocket *sockdata = (NodeImageMultiFileSocket *)sock_iter->storage; - if (sockdata->layer == name) { - return true; - } - } - } - return false; -} -void ntreeCompositOutputFileUniqueLayer(ListBase *list, - bNodeSocket *sock, - const char defname[], - char delim) -{ - /* See if we are given an empty string */ - if (ELEM(nullptr, sock, defname)) { - return; - } - NodeImageMultiFileSocket *sockdata = (NodeImageMultiFileSocket *)sock->storage; - BLI_uniquename_cb( - [&](const blender::StringRef check_name) { - return unique_layer_unique_check(list, sock, check_name); - }, - defname, - delim, - sockdata->layer, - sizeof(sockdata->layer)); -} - -bNodeSocket *ntreeCompositOutputFileAddSocket(bNodeTree *ntree, - bNode *node, - const char *name, - const ImageFormatData *im_format) -{ - NodeImageMultiFile *nimf = (NodeImageMultiFile *)node->storage; - bNodeSocket *sock = blender::bke::node_add_static_socket( - *ntree, *node, SOCK_IN, SOCK_RGBA, PROP_NONE, "", name); - - /* create format data for the input socket */ - NodeImageMultiFileSocket *sockdata = MEM_callocN(__func__); - sock->storage = sockdata; - - STRNCPY_UTF8(sockdata->path, name); - ntreeCompositOutputFileUniquePath(&node->inputs, sock, name, '_'); - STRNCPY_UTF8(sockdata->layer, name); - ntreeCompositOutputFileUniqueLayer(&node->inputs, sock, name, '_'); - - if (im_format) { - BKE_image_format_copy(&sockdata->format, im_format); - sockdata->format.color_management = R_IMF_COLOR_MANAGEMENT_FOLLOW_SCENE; - if (BKE_imtype_is_movie(sockdata->format.imtype)) { - sockdata->format.imtype = R_IMF_IMTYPE_OPENEXR; - } - } - else { - BKE_image_format_init(&sockdata->format, false); - } - BKE_image_format_update_color_space_for_type(&sockdata->format); - - /* use node data format by default */ - sockdata->use_node_format = true; - sockdata->save_as_render = true; - - nimf->active_input = BLI_findindex(&node->inputs, sock); - - return sock; -} - -int ntreeCompositOutputFileRemoveActiveSocket(bNodeTree *ntree, bNode *node) -{ - NodeImageMultiFile *nimf = (NodeImageMultiFile *)node->storage; - bNodeSocket *sock = (bNodeSocket *)BLI_findlink(&node->inputs, nimf->active_input); - int totinputs = BLI_listbase_count(&node->inputs); - - if (!sock) { - return 0; - } - - if (nimf->active_input == totinputs - 1) { - --nimf->active_input; - } - - /* free format data */ - MEM_freeN(reinterpret_cast(sock->storage)); - - blender::bke::node_remove_socket(*ntree, *node, *sock); - return 1; -} - -void ntreeCompositOutputFileSetPath(bNode *node, bNodeSocket *sock, const char *name) -{ - NodeImageMultiFileSocket *sockdata = (NodeImageMultiFileSocket *)sock->storage; - STRNCPY_UTF8(sockdata->path, name); - ntreeCompositOutputFileUniquePath(&node->inputs, sock, name, '_'); -} - -void ntreeCompositOutputFileSetLayer(bNode *node, bNodeSocket *sock, const char *name) -{ - NodeImageMultiFileSocket *sockdata = (NodeImageMultiFileSocket *)sock->storage; - STRNCPY_UTF8(sockdata->layer, name); - ntreeCompositOutputFileUniqueLayer(&node->inputs, sock, name, '_'); -} - namespace blender::nodes::node_composite_file_output_cc { -NODE_STORAGE_FUNCS(NodeImageMultiFile) +NODE_STORAGE_FUNCS(NodeCompositorFileOutput) -/* XXX uses initfunc_api callback, regular initfunc does not support context yet */ -static void init_output_file(const bContext *C, PointerRNA *ptr) +static void node_declare(NodeDeclarationBuilder &b) { - Scene *scene = CTX_data_scene(C); - bNodeTree *ntree = (bNodeTree *)ptr->owner_id; - bNode *node = (bNode *)ptr->data; - NodeImageMultiFile *nimf = MEM_callocN(__func__); - nimf->save_as_render = true; - ImageFormatData *format = nullptr; - node->storage = nimf; + b.use_custom_socket_order(); + b.allow_any_socket_order(); + b.add_default_layout(); + + const bNodeTree *node_tree = b.tree_or_null(); + const bNode *node = b.node_or_null(); + if (!node_tree || !node) { + return; + } + + const NodeCompositorFileOutput &storage = node_storage(*node); + + /* Inputs for multi-layer files need to be the same size, while they can be different for + * individual file outputs. */ + const bool is_multi_layer = storage.format.imtype == R_IMF_IMTYPE_MULTILAYER; + const CompositorInputRealizationMode realization_mode = + is_multi_layer ? CompositorInputRealizationMode::OperationDomain : + CompositorInputRealizationMode::Transforms; + + for (const int i : IndexRange(storage.items_count)) { + const NodeCompositorFileOutputItem &item = storage.items[i]; + const eNodeSocketDatatype socket_type = eNodeSocketDatatype(item.socket_type); + const std::string identifier = FileOutputItemsAccessor::socket_identifier_for_item(item); + BaseSocketDeclarationBuilder *declaration = nullptr; + if (socket_type == SOCK_VECTOR) { + declaration = &b.add_input(item.name, identifier) + .dimensions(item.vector_socket_dimensions); + } + else { + declaration = &b.add_input(socket_type, item.name, identifier); + } + declaration->structure_type(StructureType::Dynamic) + .compositor_realization_mode(realization_mode) + .socket_name_ptr(&node_tree->id, FileOutputItemsAccessor::item_srna, &item, "name"); + } + + b.add_input("", "__extend__"); +} + +static void node_init(const bContext *C, PointerRNA *node_pointer) +{ + bNode *node = node_pointer->data_as(); + NodeCompositorFileOutput *data = MEM_callocN(__func__); + node->storage = data; + data->save_as_render = true; + data->file_name = BLI_strdup("file_name"); + + BKE_image_format_init(&data->format, false); + BKE_image_format_media_type_set( + &data->format, node_pointer->owner_id, MEDIA_TYPE_MULTI_LAYER_IMAGE); + BKE_image_format_update_color_space_for_type(&data->format); + + Scene *scene = CTX_data_scene(C); if (scene) { - RenderData *rd = &scene->r; - - STRNCPY(nimf->base_path, rd->pic); - BKE_image_format_copy(&nimf->format, &rd->im_format); - nimf->format.color_management = R_IMF_COLOR_MANAGEMENT_FOLLOW_SCENE; - if (BKE_imtype_is_movie(nimf->format.imtype)) { - nimf->format.imtype = R_IMF_IMTYPE_OPENEXR; - } - - format = &nimf->format; - } - else { - BKE_image_format_init(&nimf->format, false); - } - BKE_image_format_update_color_space_for_type(&nimf->format); - - /* add one socket by default */ - ntreeCompositOutputFileAddSocket(ntree, node, DATA_("Image"), format); -} - -static void free_output_file(bNode *node) -{ - /* free storage data in sockets */ - LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) { - NodeImageMultiFileSocket *sockdata = (NodeImageMultiFileSocket *)sock->storage; - BKE_image_format_free(&sockdata->format); - MEM_freeN(sockdata); - } - - NodeImageMultiFile *nimf = (NodeImageMultiFile *)node->storage; - BKE_image_format_free(&nimf->format); - MEM_freeN(nimf); -} - -static void copy_output_file(bNodeTree * /*dst_ntree*/, bNode *dest_node, const bNode *src_node) -{ - bNodeSocket *src_sock, *dest_sock; - - dest_node->storage = MEM_dupallocN(src_node->storage); - NodeImageMultiFile *dest_nimf = (NodeImageMultiFile *)dest_node->storage; - NodeImageMultiFile *src_nimf = (NodeImageMultiFile *)src_node->storage; - BKE_image_format_copy(&dest_nimf->format, &src_nimf->format); - - /* duplicate storage data in sockets */ - for (src_sock = (bNodeSocket *)src_node->inputs.first, - dest_sock = (bNodeSocket *)dest_node->inputs.first; - src_sock && dest_sock; - src_sock = src_sock->next, dest_sock = (bNodeSocket *)dest_sock->next) - { - dest_sock->storage = MEM_dupallocN(src_sock->storage); - NodeImageMultiFileSocket *dest_sockdata = (NodeImageMultiFileSocket *)dest_sock->storage; - NodeImageMultiFileSocket *src_sockdata = (NodeImageMultiFileSocket *)src_sock->storage; - BKE_image_format_copy(&dest_sockdata->format, &src_sockdata->format); + const RenderData *render_data = &scene->r; + BLI_strncpy(data->directory, render_data->pic, FILE_MAX); } } -static void update_output_file(bNodeTree *ntree, bNode *node) +static void node_free_storage(bNode *node) { - /* XXX fix for #36706: remove invalid sockets added with bpy API. - * This is not ideal, but prevents crashes from missing storage. - * FileOutput node needs a redesign to support this properly. - */ - LISTBASE_FOREACH_MUTABLE (bNodeSocket *, sock, &node->inputs) { - if (sock->storage == nullptr) { - blender::bke::node_remove_socket(*ntree, *node, *sock); - } - } - LISTBASE_FOREACH_MUTABLE (bNodeSocket *, sock, &node->outputs) { - blender::bke::node_remove_socket(*ntree, *node, *sock); - } - - cmp_node_update_default(ntree, node); - - /* automatically update the socket type based on linked input */ - ntree->ensure_topology_cache(); - LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) { - if (sock->is_logically_linked()) { - const bNodeSocket *from_socket = sock->logically_linked_sockets()[0]; - if (sock->type != from_socket->type) { - blender::bke::node_modify_socket_type_static(ntree, node, sock, from_socket->type, 0); - BKE_ntree_update_tag_socket_property(ntree, sock); - } - } - } + socket_items::destruct_array(*node); + NodeCompositorFileOutput &data = node_storage(*node); + BKE_image_format_free(&data.format); + MEM_SAFE_FREE(data.file_name); + MEM_freeN(&data); } -static void node_composit_buts_file_output(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr) +static void node_copy_storage(bNodeTree * /*destination_node_tree*/, + bNode *destination_node, + const bNode *source_node) { - layout->prop(ptr, "base_path", UI_ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE); + const NodeCompositorFileOutput &source_storage = node_storage(*source_node); + NodeCompositorFileOutput *destination_storage = MEM_dupallocN( + __func__, source_storage); + destination_storage->file_name = BLI_strdup_null(source_storage.file_name); + BKE_image_format_copy(&destination_storage->format, &source_storage.format); + destination_node->storage = destination_storage; + socket_items::copy_array(*source_node, *destination_node); } -static void node_composit_buts_file_output_ex(uiLayout *layout, bContext *C, PointerRNA *ptr) +static bool node_insert_link(bke::NodeInsertLinkParams ¶ms) { - Scene *scene = CTX_data_scene(C); - PointerRNA imfptr = RNA_pointer_get(ptr, "format"); - PointerRNA active_input_ptr, op_ptr; - uiLayout *row, *col; - const bool multilayer = RNA_enum_get(&imfptr, "file_format") == R_IMF_IMTYPE_MULTILAYER; - const bool is_multiview = (scene->r.scemode & R_MULTIVIEW) != 0; + return socket_items::try_add_item_via_any_extend_socket( + params.ntree, params.node, params.node, params.link); +} - node_composit_buts_file_output(layout, C, ptr); +static void node_operators() +{ + socket_items::ops::make_common_operators(); +} - { +/* Computes the path of the image to be saved based on the given parameters. The given file name + * suffix, if not empty, will be added to the file name. If the given view is not empty, its file + * suffix will be appended to the name. The frame number, scene, and node are provides for variable + * substitution in the path. If there are any errors processing the path, they will be returned. */ +static Vector compute_image_path(const std::string directory, + const std::string file_name, + const std::string file_name_suffix, + const char *view, + const int frame_number, + const ImageFormatData &format, + const Scene &scene, + const bNode &node, + char *r_image_path) +{ + char base_path[FILE_MAX] = ""; + BLI_strncpy(base_path, directory.c_str(), FILE_MAX); + const std::string full_file_name = file_name + file_name_suffix; + BLI_path_append(base_path, FILE_MAX, full_file_name.c_str()); + + path_templates::VariableMap template_variables; + BKE_add_template_variables_general(template_variables, &node.owner_tree().id); + BKE_add_template_variables_for_render_path(template_variables, scene); + BKE_add_template_variables_for_node(template_variables, node); + + return BKE_image_path_from_imformat(r_image_path, + base_path, + BKE_main_blendfile_path_from_global(), + &template_variables, + frame_number, + &format, + scene.r.scemode & R_EXTENSION, + true, + BKE_scene_multiview_view_suffix_get(&scene.r, view)); +} + +static void node_layout(uiLayout *layout, bContext * /*context*/, PointerRNA *node_pointer) +{ + layout->prop(node_pointer, "directory", UI_ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE); + layout->prop(node_pointer, "file_name", UI_ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE); +} + +static void format_layout(uiLayout *layout, + bContext *context, + PointerRNA *format_pointer, + PointerRNA *node_or_item_pointer) +{ + uiLayout *column = &layout->column(true); + column->use_property_split_set(true); + column->use_property_decorate_set(false); + column->prop( + node_or_item_pointer, "save_as_render", UI_ITEM_R_SPLIT_EMPTY_NAME, std::nullopt, ICON_NONE); + const bool save_as_render = RNA_boolean_get(node_or_item_pointer, "save_as_render"); + uiTemplateImageSettings(layout, context, format_pointer, save_as_render); + + if (!save_as_render) { uiLayout *column = &layout->column(true); column->use_property_split_set(true); column->use_property_decorate_set(false); - column->prop(ptr, "save_as_render", UI_ITEM_R_SPLIT_EMPTY_NAME, std::nullopt, ICON_NONE); - } - const bool save_as_render = RNA_boolean_get(ptr, "save_as_render"); - uiTemplateImageSettings(layout, C, &imfptr, save_as_render); - - if (!save_as_render) { - uiLayout *col = &layout->column(true); - col->use_property_split_set(true); - col->use_property_decorate_set(false); - - PointerRNA linear_settings_ptr = RNA_pointer_get(&imfptr, "linear_colorspace_settings"); - col->prop(&linear_settings_ptr, "name", UI_ITEM_NONE, IFACE_("Color Space"), ICON_NONE); + PointerRNA linear_settings_ptr = RNA_pointer_get(format_pointer, "linear_colorspace_settings"); + column->prop(&linear_settings_ptr, "name", UI_ITEM_NONE, IFACE_("Color Space"), ICON_NONE); } - /* disable stereo output for multilayer, too much work for something that no one will use */ - /* if someone asks for that we can implement it */ + Scene *scene = CTX_data_scene(context); + const bool is_multiview = scene->r.scemode & R_MULTIVIEW; if (is_multiview) { - uiTemplateImageFormatViews(layout, &imfptr, nullptr); + uiTemplateImageFormatViews(layout, format_pointer, nullptr); } +} - layout->separator(); +static void output_path_layout(uiLayout *layout, + const std::string directory, + const std::string file_name, + const std::string file_name_suffix, + const char *view, + const ImageFormatData &format, + const Scene &scene, + const bNode &node) +{ - uiLayout *header = &layout->row(false); - row = &layout->row(false); - col = &row->column(true); + char image_path[FILE_MAX]; + const Vector path_errors = compute_image_path( + directory, file_name, file_name_suffix, view, scene.r.cfra, format, scene, node, image_path); - const int active_index = RNA_int_get(ptr, "active_input_index"); - PropertyRNA *slots_prop = nullptr; - /* using different collection properties if multilayer format is enabled */ - if (multilayer) { - header->label(IFACE_("Layers"), ICON_NONE); - uiTemplateList(col, - C, - "UI_UL_list", - "file_output_node", - ptr, - "layer_slots", - ptr, - "active_input_index", - nullptr, - 0, - 0, - 0, - 0, - UI_TEMPLATE_LIST_FLAG_NONE); - RNA_property_collection_lookup_int( - ptr, RNA_struct_find_property(ptr, "layer_slots"), active_index, &active_input_ptr); - slots_prop = RNA_struct_find_property(ptr, "layer_slots"); + if (path_errors.is_empty()) { + layout->label(image_path, ICON_FILE_IMAGE); } else { - header->label(IFACE_("File Subpaths"), ICON_NONE); - uiTemplateList(col, - C, - "UI_UL_list", - "file_output_node", - ptr, - "file_slots", - ptr, - "active_input_index", - nullptr, - 0, - 0, - 0, - 0, - UI_TEMPLATE_LIST_FLAG_NONE); - RNA_property_collection_lookup_int( - ptr, RNA_struct_find_property(ptr, "file_slots"), active_index, &active_input_ptr); - slots_prop = RNA_struct_find_property(ptr, "file_slots"); - } - - col = &row->column(true); - - col->op("NODE_OT_output_file_add_socket", - "", - ICON_ADD, - wm::OpCallContext::ExecDefault, - UI_ITEM_NONE); - col->op("NODE_OT_output_file_remove_active_socket", - "", - ICON_REMOVE, - wm::OpCallContext::ExecDefault, - UI_ITEM_NONE); - col->separator(); - - /* XXX collection lookup does not return the ID part of the pointer, - * setting this manually here */ - active_input_ptr.owner_id = ptr->owner_id; - - int slots_len = RNA_property_collection_length(ptr, slots_prop); - if (slots_len > 0) { - wmOperatorType *ot = WM_operatortype_find("NODE_OT_output_file_move_active_socket", false); - - uiLayout *sub = &col->column(true); - if (slots_len < 2) { - sub->active_set(false); + for (const path_templates::Error &error : path_errors) { + layout->label(BKE_path_template_error_to_string(error, image_path).c_str(), ICON_ERROR); } + } +} - op_ptr = sub->op(ot, "", ICON_TRIA_UP, wm::OpCallContext::InvokeDefault, UI_ITEM_NONE); - RNA_enum_set(&op_ptr, "direction", 1); +static void output_paths_layout(uiLayout *layout, + bContext *context, + const std::string file_name_suffix, + const bNode &node, + const ImageFormatData &format) +{ + const NodeCompositorFileOutput &storage = node_storage(node); + const std::string directory = storage.directory; + const std::string file_name = storage.file_name ? storage.file_name : ""; + const Scene &scene = *CTX_data_scene(context); - op_ptr = sub->op(ot, "", ICON_TRIA_DOWN, wm::OpCallContext::InvokeDefault, UI_ITEM_NONE); - RNA_enum_set(&op_ptr, "direction", 2); + if (bool(scene.r.scemode & R_MULTIVIEW) && format.views_format == R_IMF_VIEWS_MULTIVIEW) { + LISTBASE_FOREACH (SceneRenderView *, view, &scene.r.views) { + if (!BKE_scene_multiview_is_render_view_active(&scene.r, view)) { + continue; + } + + output_path_layout( + layout, directory, file_name, file_name_suffix, view->name, format, scene, node); + } + } + else { + output_path_layout(layout, directory, file_name, file_name_suffix, "", format, scene, node); + } +} + +static void item_layout(uiLayout *layout, + bContext *context, + PointerRNA *node_pointer, + PointerRNA *item_pointer, + const bool is_multi_layer) +{ + layout->use_property_split_set(true); + layout->use_property_decorate_set(false); + layout->prop(item_pointer, "socket_type", UI_ITEM_NONE, std::nullopt, ICON_NONE); + if (RNA_enum_get(item_pointer, "socket_type") == SOCK_VECTOR) { + layout->prop(item_pointer, "vector_socket_dimensions", UI_ITEM_NONE, std::nullopt, ICON_NONE); } - if (active_input_ptr.data) { - if (!multilayer) { - /* format details for individual files */ - imfptr = RNA_pointer_get(&active_input_ptr, "format"); + if (is_multi_layer) { + return; + } - col = &layout->column(true); - col->prop(&active_input_ptr, - "use_node_format", - UI_ITEM_R_SPLIT_EMPTY_NAME, - std::nullopt, - ICON_NONE); + layout->prop( + item_pointer, "override_node_format", UI_ITEM_R_SPLIT_EMPTY_NAME, std::nullopt, ICON_NONE); + const bool override_node_format = RNA_boolean_get(item_pointer, "override_node_format"); - const bool use_node_format = RNA_boolean_get(&active_input_ptr, "use_node_format"); + PointerRNA node_format_pointer = RNA_pointer_get(node_pointer, "format"); + PointerRNA item_format_pointer = RNA_pointer_get(item_pointer, "format"); + PointerRNA *format_pointer = override_node_format ? &item_format_pointer : &node_format_pointer; - if (!use_node_format) { - { - uiLayout *column = &layout->column(true); - column->use_property_split_set(true); - column->use_property_decorate_set(false); - column->prop(&active_input_ptr, - "save_as_render", - UI_ITEM_R_SPLIT_EMPTY_NAME, - std::nullopt, - ICON_NONE); - } + if (override_node_format) { + if (uiLayout *panel = layout->panel(context, "item_format", false, IFACE_("Item Format"))) { + format_layout(panel, context, format_pointer, item_pointer); + } + } +} - const bool use_color_management = RNA_boolean_get(&active_input_ptr, "save_as_render"); +static void node_layout_ex(uiLayout *layout, bContext *context, PointerRNA *node_pointer) +{ + node_layout(layout, context, node_pointer); - col = &layout->column(false); - uiTemplateImageSettings( - col, C, &imfptr, use_color_management, "node_settings_color_management"); + PointerRNA format_pointer = RNA_pointer_get(node_pointer, "format"); + const bool is_multi_layer = RNA_enum_get(&format_pointer, "file_format") == + R_IMF_IMTYPE_MULTILAYER; + layout->prop(&format_pointer, "media_type", UI_ITEM_R_EXPAND, std::nullopt, ICON_NONE); + if (uiLayout *panel = layout->panel(context, "node_format", false, IFACE_("Node Format"))) { + format_layout(panel, context, &format_pointer, node_pointer); + } - if (!use_color_management) { - uiLayout *col = &layout->column(true); - col->use_property_split_set(true); - col->use_property_decorate_set(false); + const char *panel_name = is_multi_layer ? IFACE_("Layers") : IFACE_("Images"); + if (uiLayout *panel = layout->panel(context, "file_output_items", false, panel_name)) { + bNodeTree &tree = *reinterpret_cast(node_pointer->owner_id); + bNode &node = *node_pointer->data_as(); + socket_items::ui::draw_items_list_with_operators( + context, panel, tree, node); + socket_items::ui::draw_active_item_props( + tree, node, [&](PointerRNA *item_pointer) { + item_layout(panel, context, node_pointer, item_pointer, is_multi_layer); + }); + } - PointerRNA linear_settings_ptr = RNA_pointer_get(&imfptr, "linear_colorspace_settings"); - col->prop(&linear_settings_ptr, "name", UI_ITEM_NONE, IFACE_("Color Space"), ICON_NONE); - } + if (uiLayout *panel = layout->panel(context, "output_paths", true, IFACE_("Output Paths"))) { + const bNode &node = *node_pointer->data_as(); + const ImageFormatData &node_format = *format_pointer.data_as(); - if (is_multiview) { - col = &layout->column(false); - uiTemplateImageFormatViews(col, &imfptr, nullptr); - } + if (is_multi_layer) { + output_paths_layout(panel, context, "", node, node_format); + } + else { + const NodeCompositorFileOutput &storage = node_storage(node); + for (const int i : IndexRange(storage.items_count)) { + const NodeCompositorFileOutputItem &item = storage.items[i]; + const auto &format = item.override_node_format ? item.format : storage.format; + output_paths_layout(panel, context, item.name, node, format); } } } } +static void node_blend_write(const bNodeTree & /*tree*/, const bNode &node, BlendWriter &writer) +{ + const NodeCompositorFileOutput &data = node_storage(node); + BLO_write_string(&writer, data.file_name); + BKE_image_format_blend_write(&writer, const_cast(&data.format)); + socket_items::blend_write(&writer, node); +} + +static void node_blend_read(bNodeTree & /*tree*/, bNode &node, BlendDataReader &reader) +{ + NodeCompositorFileOutput &data = node_storage(node); + BLO_read_string(&reader, &data.file_name); + BKE_image_format_blend_read_data(&reader, &data.format); + socket_items::blend_read_data(&reader, node); +} + using namespace blender::compositor; class FileOutputOperation : public NodeOperation { public: - FileOutputOperation(Context &context, DNode node) : NodeOperation(context, node) - { - for (const bNodeSocket *input : node->input_sockets()) { - if (!is_socket_available(input)) { - continue; - } - - InputDescriptor &descriptor = this->get_input_descriptor(input->identifier); - /* Inputs for multi-layer files need to be the same size, while they can be different for - * individual file outputs. */ - descriptor.realization_mode = this->is_multi_layer() ? - InputRealizationMode::OperationDomain : - InputRealizationMode::Transforms; - descriptor.skip_type_conversion = true; - } - } + using NodeOperation::NodeOperation; void execute() override { - if (is_multi_layer()) { - execute_multi_layer(); + if (this->is_multi_layer()) { + this->execute_multi_layer(); } else { - execute_single_layer(); + this->execute_single_layer(); } } @@ -518,53 +389,48 @@ class FileOutputOperation : public NodeOperation { void execute_single_layer() { - for (const bNodeSocket *input : this->node()->input_sockets()) { - if (!is_socket_available(input)) { - continue; - } - - const Result &result = get_input(input->identifier); + const NodeCompositorFileOutput &storage = node_storage(this->bnode()); + for (const int i : IndexRange(storage.items_count)) { + const NodeCompositorFileOutputItem &item = storage.items[i]; + const std::string identifier = FileOutputItemsAccessor::socket_identifier_for_item(item); + const Result &result = this->get_input(identifier); /* We only write images, not single values. */ if (result.is_single_value()) { continue; } - char base_path[FILE_MAX]; - const auto &socket = *static_cast(input->storage); - - if (!get_single_layer_image_base_path(socket.path, base_path)) { - /* TODO: propagate this error to the render pipeline and UI. */ - BKE_report(nullptr, - RPT_ERROR, - "Invalid path template in File Output node. Skipping writing file."); - continue; - } - /* The image saving code expects EXR images to have a different structure than standard * images. In particular, in EXR images, the buffers need to be stored in passes that are, in * turn, stored in a render layer. On the other hand, in non-EXR images, the buffers need to * be stored in views. An exception to this is stereo images, which needs to have the same * structure as non-EXR images. */ - const auto &format = socket.use_node_format ? node_storage(bnode()).format : socket.format; - const bool save_as_render = socket.use_node_format ? node_storage(bnode()).save_as_render : - socket.save_as_render; + const auto &format = item.override_node_format ? item.format : + node_storage(this->bnode()).format; + const bool save_as_render = item.override_node_format ? + item.save_as_render : + node_storage(this->bnode()).save_as_render; const bool is_exr = format.imtype == R_IMF_IMTYPE_OPENEXR; - const int views_count = BKE_scene_multiview_num_views_get(&context().get_render_data()); + const int views_count = BKE_scene_multiview_num_views_get( + &this->context().get_render_data()); if (is_exr && !(format.views_format == R_IMF_VIEWS_STEREO_3D && views_count == 2)) { - execute_single_layer_multi_view_exr(result, format, base_path, socket.layer); + this->execute_single_layer_multi_view_exr(result, format, item.name); continue; } char image_path[FILE_MAX]; - get_single_layer_image_path(base_path, format, image_path); + Vector path_errors = this->get_image_path( + format, item.name, "", image_path); + if (!path_errors.is_empty()) { + continue; + } const int2 size = result.domain().size; - FileOutput &file_output = context().render_context()->get_file_output( + FileOutput &file_output = this->context().render_context()->get_file_output( image_path, format, size, save_as_render); - add_view_for_result(file_output, result, context().get_view_name().data()); + this->add_view_for_result(file_output, result, context().get_view_name().data()); - add_meta_data_for_result(file_output, result, socket.layer); + this->add_meta_data_for_result(file_output, result, item.name); } } @@ -574,32 +440,32 @@ class FileOutputOperation : public NodeOperation { void execute_single_layer_multi_view_exr(const Result &result, const ImageFormatData &format, - const char *base_path, const char *layer_name) { const bool has_views = format.views_format != R_IMF_VIEWS_INDIVIDUAL; /* The EXR stores all views in the same file, so we supply an empty view to make sure the file * name does not contain a view suffix. */ - char image_path[FILE_MAX]; - const char *path_view = has_views ? "" : context().get_view_name().data(); + const char *path_view = has_views ? "" : this->context().get_view_name().data(); - if (!get_multi_layer_exr_image_path(base_path, path_view, false, image_path)) { - BLI_assert_unreachable(); + char image_path[FILE_MAX]; + Vector path_errors = this->get_image_path( + format, layer_name, path_view, image_path); + if (!path_errors.is_empty()) { return; } const int2 size = result.domain().size; - FileOutput &file_output = context().render_context()->get_file_output( + FileOutput &file_output = this->context().render_context()->get_file_output( image_path, format, size, true); /* The EXR stores all views in the same file, so we add the actual render view. Otherwise, we * add a default unnamed view. */ - const char *view_name = has_views ? context().get_view_name().data() : ""; + const char *view_name = has_views ? this->context().get_view_name().data() : ""; file_output.add_view(view_name); - add_pass_for_result(file_output, result, "", view_name); + this->add_pass_for_result(file_output, result, "", view_name); - add_meta_data_for_result(file_output, result, layer_name); + this->add_meta_data_for_result(file_output, result, layer_name); } /* ----------------------- @@ -614,22 +480,21 @@ class FileOutputOperation : public NodeOperation { return; } - const bool store_views_in_single_file = is_multi_view_exr(); - const char *view = context().get_view_name().data(); + const ImageFormatData format = node_storage(this->bnode()).format; + const bool store_views_in_single_file = this->is_multi_view_exr(); + const char *view = this->context().get_view_name().data(); /* If we are saving all views in a single multi-layer file, we supply an empty view to make * sure the file name does not contain a view suffix. */ char image_path[FILE_MAX]; const char *write_view = store_views_in_single_file ? "" : view; - if (!get_multi_layer_exr_image_path(get_base_path(), write_view, true, image_path)) { - /* TODO: propagate this error to the render pipeline and UI. */ - BKE_report( - nullptr, RPT_ERROR, "Invalid path template in File Output node. Skipping writing file."); + Vector path_errors = this->get_image_path( + format, "", write_view, image_path); + if (!path_errors.is_empty()) { return; } - const ImageFormatData format = node_storage(bnode()).format; - FileOutput &file_output = context().render_context()->get_file_output( + FileOutput &file_output = this->context().render_context()->get_file_output( image_path, format, size, true); /* If we are saving views in separate files, we needn't store the view in the channel names, so @@ -637,16 +502,14 @@ class FileOutputOperation : public NodeOperation { const char *pass_view = store_views_in_single_file ? view : ""; file_output.add_view(pass_view); - for (const bNodeSocket *input : this->node()->input_sockets()) { - if (!is_socket_available(input)) { - continue; - } + const NodeCompositorFileOutput &storage = node_storage(bnode()); + for (const int i : IndexRange(storage.items_count)) { + const NodeCompositorFileOutputItem &item = storage.items[i]; + const std::string identifier = FileOutputItemsAccessor::socket_identifier_for_item(item); + const Result &input_result = this->get_input(identifier); + this->add_pass_for_result(file_output, input_result, item.name, pass_view); - const Result &input_result = get_input(input->identifier); - const char *pass_name = (static_cast(input->storage))->layer; - add_pass_for_result(file_output, input_result, pass_name, pass_view); - - add_meta_data_for_result(file_output, input_result, pass_name); + this->add_meta_data_for_result(file_output, input_result, item.name); } } @@ -669,7 +532,7 @@ class FileOutputOperation : public NodeOperation { buffer = this->inflate_result(result, size); } else { - if (context().use_gpu()) { + if (this->context().use_gpu()) { GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE); buffer = static_cast(GPU_texture_read(result, GPU_DATA_FLOAT, 0)); } @@ -777,7 +640,7 @@ class FileOutputOperation : public NodeOperation { /* The image buffer in the file output will take ownership of this buffer and freeing it will * be its responsibility. */ float *buffer = nullptr; - if (context().use_gpu()) { + if (this->context().use_gpu()) { GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE); buffer = static_cast(GPU_texture_read(result, GPU_DATA_FLOAT, 0)); } @@ -867,161 +730,59 @@ class FileOutputOperation : public NodeOperation { } } - /** - * Get the base path of the image to be saved, based on the base path of the - * node. The base name is an optional initial name of the image, which will - * later be concatenated with other information like the frame number, view, - * and extension. If the base name is empty, then the base path represents a - * directory, so a trailing slash is ensured. - * - * Note: this takes care of path template expansion as well. - * - * If there are any errors processing the path, `bath_base` will be set to an - * empty string. - * - * \return True on success, false if there were any errors processing the - * path. - */ - bool get_single_layer_image_base_path(const char *base_name, char *r_base_path) + Vector get_image_path(const ImageFormatData &format, + const char *file_name_suffix, + const char *view, + char *r_image_path) { - path_templates::VariableMap template_variables; - BKE_add_template_variables_general(template_variables, &this->bnode().owner_tree().id); - BKE_add_template_variables_for_render_path(template_variables, context().get_scene()); - BKE_add_template_variables_for_node(template_variables, this->bnode()); + const Vector path_errors = compute_image_path( + this->get_directory(), + this->get_file_name(), + file_name_suffix, + view, + this->context().get_frame_number(), + format, + this->context().get_scene(), + this->bnode(), + r_image_path); - /* Do template expansion on the node's base path. */ - char node_base_path[FILE_MAX] = ""; - STRNCPY(node_base_path, get_base_path()); - { - blender::Vector errors = BKE_path_apply_template( - node_base_path, FILE_MAX, template_variables); - if (!errors.is_empty()) { - r_base_path[0] = '\0'; - return false; - } + if (!path_errors.is_empty()) { + BKE_report( + nullptr, RPT_ERROR, "Invalid path template in File Output node. Skipping writing file."); } - if (base_name[0]) { - /* Do template expansion on the socket's sub path ("base name"). */ - char sub_path[FILE_MAX] = ""; - STRNCPY(sub_path, base_name); - { - blender::Vector errors = BKE_path_apply_template( - sub_path, FILE_MAX, template_variables); - if (!errors.is_empty()) { - r_base_path[0] = '\0'; - return false; - } - } - - /* Combine the base path and sub path. */ - BLI_path_join(r_base_path, FILE_MAX, node_base_path, sub_path); - } - else { - /* Just use the base path, as a directory. */ - BLI_strncpy(r_base_path, node_base_path, FILE_MAX); - BLI_path_slash_ensure(r_base_path, FILE_MAX); - } - - return true; - } - - /* Get the path of the image to be saved based on the given format. */ - void get_single_layer_image_path(const char *base_path, - const ImageFormatData &format, - char *r_image_path) - { - BKE_image_path_from_imformat(r_image_path, - base_path, - BKE_main_blendfile_path_from_global(), - /* No variables, because path templating is - * already done by - * `get_single_layer_image_base_path()` before - * this is called. */ - nullptr, - context().get_frame_number(), - &format, - use_file_extension(), - true, - nullptr); - } - - /** - * Get the path of the EXR image to be saved. If the given view is not empty, - * its corresponding file suffix will be appended to the name. - * - * If there are any errors processing the path, the resulting path will be - * empty. - * - * \param apply_template: Whether to run templating on the path or not. This is - * needed because this function is called from more than one place, some of - * which have already applied templating to the path and some of which - * haven't. Double-applying templating can give incorrect results. - * - * \return True on success, false if there were any errors processing the - * path. - */ - bool get_multi_layer_exr_image_path(const char *base_path, - const char *view, - const bool apply_template, - char *r_image_path) - { - const Scene *scene = &context().get_scene(); - const RenderData &render_data = context().get_render_data(); - path_templates::VariableMap template_variables; - BKE_add_template_variables_general(template_variables, &this->bnode().owner_tree().id); - BKE_add_template_variables_for_render_path(template_variables, *scene); - BKE_add_template_variables_for_node(template_variables, this->bnode()); - - const char *suffix = BKE_scene_multiview_view_suffix_get(&render_data, view); - const char *relbase = BKE_main_blendfile_path_from_global(); - blender::Vector errors = BKE_image_path_from_imtype( - r_image_path, - base_path, - relbase, - apply_template ? &template_variables : nullptr, - context().get_frame_number(), - R_IMF_IMTYPE_MULTILAYER, - use_file_extension(), - true, - suffix); - - if (!errors.is_empty()) { - r_image_path[0] = '\0'; - } - - return errors.is_empty(); + return path_errors; } bool is_multi_layer() { - return node_storage(bnode()).format.imtype == R_IMF_IMTYPE_MULTILAYER; + return node_storage(this->bnode()).format.imtype == R_IMF_IMTYPE_MULTILAYER; } - const char *get_base_path() + std::string get_file_name() { - return node_storage(bnode()).base_path; + const char *file_name = node_storage(this->bnode()).file_name; + return file_name ? file_name : ""; } - /* Add the file format extensions to the rendered file name. */ - bool use_file_extension() + std::string get_directory() { - return context().get_render_data().scemode & R_EXTENSION; + return node_storage(this->bnode()).directory; } /* If true, save views in a multi-view EXR file, otherwise, save each view in its own file. */ bool is_multi_view_exr() { - if (!is_multi_view_scene()) { + if (!this->is_multi_view_scene()) { return false; } - return node_storage(bnode()).format.views_format == R_IMF_VIEWS_MULTIVIEW; + return node_storage(this->bnode()).format.views_format == R_IMF_VIEWS_MULTIVIEW; } bool is_multi_view_scene() { - return context().get_render_data().scemode & R_MULTIVIEW; + return this->context().get_render_data().scemode & R_MULTIVIEW; } Domain compute_domain() override @@ -1043,12 +804,8 @@ static NodeOperation *get_compositor_operation(Context &context, DNode node) return new FileOutputOperation(context, node); } -} // namespace blender::nodes::node_composite_file_output_cc - -static void register_node_type_cmp_output_file() +static void node_register() { - namespace file_ns = blender::nodes::node_composite_file_output_cc; - static blender::bke::bNodeType ntype; cmp_node_type_base(&ntype, "CompositorNodeOutputFile", CMP_NODE_OUTPUT_FILE); @@ -1056,14 +813,46 @@ static void register_node_type_cmp_output_file() ntype.ui_description = "Write image file to disk"; ntype.enum_name_legacy = "OUTPUT_FILE"; ntype.nclass = NODE_CLASS_OUTPUT; - ntype.draw_buttons = file_ns::node_composit_buts_file_output; - ntype.draw_buttons_ex = file_ns::node_composit_buts_file_output_ex; - ntype.initfunc_api = file_ns::init_output_file; + ntype.declare = node_declare; + ntype.draw_buttons = node_layout; + ntype.draw_buttons_ex = node_layout_ex; + ntype.insert_link = node_insert_link; + ntype.register_operators = node_operators; + ntype.initfunc_api = node_init; blender::bke::node_type_storage( - ntype, "NodeImageMultiFile", file_ns::free_output_file, file_ns::copy_output_file); - ntype.updatefunc = file_ns::update_output_file; - ntype.get_compositor_operation = file_ns::get_compositor_operation; + ntype, "NodeCompositorFileOutput", node_free_storage, node_copy_storage); + ntype.blend_write_storage_content = node_blend_write; + ntype.blend_data_read_storage_content = node_blend_read; + ntype.get_compositor_operation = get_compositor_operation; blender::bke::node_register_type(ntype); } -NOD_REGISTER_NODE(register_node_type_cmp_output_file) +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_composite_file_output_cc + +namespace blender::nodes { + +StructRNA *FileOutputItemsAccessor::item_srna = &RNA_NodeCompositorFileOutputItem; + +void FileOutputItemsAccessor::blend_write_item(BlendWriter *writer, const ItemT &item) +{ + BLO_write_string(writer, item.name); + BKE_image_format_blend_write(writer, const_cast(&item.format)); +} + +void FileOutputItemsAccessor::blend_read_data_item(BlendDataReader *reader, ItemT &item) +{ + BLO_read_string(reader, &item.name); + BKE_image_format_blend_read_data(reader, &item.format); +} + +std::string FileOutputItemsAccessor::validate_name(const StringRef name) +{ + char file_name[FILE_MAX] = ""; + BLI_strncpy(file_name, name.data(), FILE_MAX); + BLI_path_make_safe_filename(file_name); + return file_name; +} + +} // namespace blender::nodes diff --git a/tests/files/compositor/file_output/exr_passes/Vector0001.exr b/tests/files/compositor/file_output/exr_passes/Vector0001.exr index a86620fb92d..378a2df78ec 100644 --- a/tests/files/compositor/file_output/exr_passes/Vector0001.exr +++ b/tests/files/compositor/file_output/exr_passes/Vector0001.exr @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f85842dbcee222ac84a16d2469abe9ba5ac96fd4c1b9e50f0bd46cca4048b00 -size 633 +oid sha256:a3f3b334ddcb4344bea4c60a3c81fa30353bf427a650875f98318c2d5396b7e1 +size 741 diff --git a/tests/files/compositor/file_output/exr_png_group_multilayer_passes/Image_0010001.png b/tests/files/compositor/file_output/exr_png_group_multilayer_passes/Image_0010001.png index 08c429ec1c3..d53b082a3b0 100644 --- a/tests/files/compositor/file_output/exr_png_group_multilayer_passes/Image_0010001.png +++ b/tests/files/compositor/file_output/exr_png_group_multilayer_passes/Image_0010001.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90887b48e2ce2de207cdfb974e0fd00fcfbc11f7d0eefdc8ac204937f04529d6 -size 502 +oid sha256:cd7e8ab76368604c5976fb5e57c07ffaacedaddc272b62a658d07871ca4a417a +size 608 diff --git a/tests/files/compositor/file_output/exr_png_group_multilayer_passes/Image_0020001.png b/tests/files/compositor/file_output/exr_png_group_multilayer_passes/Image_0020001.png index 08c429ec1c3..590faa245c2 100644 --- a/tests/files/compositor/file_output/exr_png_group_multilayer_passes/Image_0020001.png +++ b/tests/files/compositor/file_output/exr_png_group_multilayer_passes/Image_0020001.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90887b48e2ce2de207cdfb974e0fd00fcfbc11f7d0eefdc8ac204937f04529d6 -size 502 +oid sha256:bb7f6f8242c005bb20fc94a28a40528a8a0856f27a70e9f81d706d8677ad3baf +size 609 diff --git a/tests/python/compositor_file_output_tests.py b/tests/python/compositor_file_output_tests.py index 3b67fc32b62..c676883c7c4 100644 --- a/tests/python/compositor_file_output_tests.py +++ b/tests/python/compositor_file_output_tests.py @@ -197,16 +197,17 @@ class FileOutputTest(unittest.TestCase): self.assertTrue(ok) def run_test_script(self, blendfile, curr_out_dir): - def set_basepath(node_tree, base_path): + def set_directory(node_tree, base_path): for node in node_tree.nodes: if node.type == 'OUTPUT_FILE': - node.base_path = f'{curr_out_dir}/' + node.directory = f'{curr_out_dir}/' + node.file_name = "" elif node.type == 'GROUP' and node.node_tree: - set_basepath(node.node_tree, base_path) + set_directory(node.node_tree, base_path) bpy.ops.wm.open_mainfile(filepath=blendfile) # Set output directory for all existing file output nodes. - set_basepath(bpy.data.scenes[0].compositing_node_group, f'{curr_out_dir}/') + set_directory(bpy.data.scenes[0].compositing_node_group, f'{curr_out_dir}/') bpy.data.scenes[0].render.compositor_device = f'{self.execution_device}' bpy.ops.render.render()