From b10d767c4e374b73c64c215a6b8f0c825a264403 Mon Sep 17 00:00:00 2001 From: Omar Emara Date: Fri, 12 Sep 2025 09:01:31 +0200 Subject: [PATCH] Fix #134920: File Output writes frame for single render The File Output node always appends the frame number even if the render is not an animation. This patch makes it such that the frame number is only written if the render is an animation. The user can use a frame variable to manually append the frame number if needed. The command line rendering interface already uses animation rendering in all cases, so it should not be affected. However, rendering using the render.render() operator with animation=False will see the behavior change, however, setting animation=False and start_frame and end_frame to the same frame number should be sufficient to restore the old behavior. Pull Request: https://projects.blender.org/blender/blender/pulls/141209 --- .../blender/compositor/COM_render_context.hh | 4 ++++ .../nodes/node_composite_file_output.cc | 24 ++++++++++++++++--- source/blender/render/intern/pipeline.cc | 1 + tests/python/compositor_file_output_tests.py | 2 +- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/source/blender/compositor/COM_render_context.hh b/source/blender/compositor/COM_render_context.hh index 62fe09acc19..281ef858eb7 100644 --- a/source/blender/compositor/COM_render_context.hh +++ b/source/blender/compositor/COM_render_context.hh @@ -90,6 +90,10 @@ class FileOutput { * data from each of the evaluations of each view, for instance, to save all views in a single file * for the File Output node, see the file_outputs_ member for more information. */ class RenderContext { + public: + /* True if the render context represents an animation render. */ + bool is_animation_render = false; + private: /* A mapping between file outputs and their image file paths. Those are constructed in the * get_file_output method and saved in the save_file_outputs method. See those methods for more 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 c05246cfad7..7b66251a4ca 100644 --- a/source/blender/nodes/composite/nodes/node_composite_file_output.cc +++ b/source/blender/nodes/composite/nodes/node_composite_file_output.cc @@ -169,6 +169,7 @@ static Vector compute_image_path(const StringRefNull dire const ImageFormatData &format, const Scene &scene, const bNode &node, + const bool is_animation_render, char *r_image_path) { char base_path[FILE_MAX] = ""; @@ -188,7 +189,7 @@ static Vector compute_image_path(const StringRefNull dire frame_number, &format, scene.r.scemode & R_EXTENSION, - true, + is_animation_render, BKE_scene_multiview_view_suffix_get(&scene.r, view)); } @@ -238,8 +239,16 @@ static void output_path_layout(uiLayout *layout, { 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 Vector path_errors = compute_image_path(directory, + file_name, + file_name_suffix, + view, + scene.r.cfra, + format, + scene, + node, + false, + image_path); if (path_errors.is_empty()) { layout->label(image_path, ICON_FILE_IMAGE); @@ -729,6 +738,7 @@ class FileOutputOperation : public NodeOperation { format, this->context().get_scene(), this->bnode(), + this->is_animation_render(), r_image_path); if (!path_errors.is_empty()) { @@ -782,6 +792,14 @@ class FileOutputOperation : public NodeOperation { domain.transformation.location() = float2(0.0f); return domain; } + + bool is_animation_render() + { + if (!this->context().render_context()) { + return false; + } + return this->context().render_context()->is_animation_render; + } }; static NodeOperation *get_compositor_operation(Context &context, DNode node) diff --git a/source/blender/render/intern/pipeline.cc b/source/blender/render/intern/pipeline.cc index 31e22f3a5fb..30c7c9e654e 100644 --- a/source/blender/render/intern/pipeline.cc +++ b/source/blender/render/intern/pipeline.cc @@ -1366,6 +1366,7 @@ static void do_render_compositor(Render *re) CLOG_STR_INFO(&LOG, "Executing compositor"); blender::compositor::RenderContext compositor_render_context; + compositor_render_context.is_animation_render = re->flag & R_ANIMATION; LISTBASE_FOREACH (RenderView *, rv, &re->result->views) { COM_execute(re, &re->r, diff --git a/tests/python/compositor_file_output_tests.py b/tests/python/compositor_file_output_tests.py index c676883c7c4..d2017ea39df 100644 --- a/tests/python/compositor_file_output_tests.py +++ b/tests/python/compositor_file_output_tests.py @@ -209,7 +209,7 @@ class FileOutputTest(unittest.TestCase): # Set output directory for all existing file output nodes. 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() + bpy.ops.render.render(animation=True, start_frame=1, end_frame=1) if __name__ == "__main__":