From f5fb56a42ba123ed919da21b76d5844d4acf25ae Mon Sep 17 00:00:00 2001 From: "k8ieone@Sodium" Date: Mon, 27 Feb 2023 20:54:45 +0100 Subject: [PATCH 01/29] Initial version of the single-frame job type --- .../scripts/single_frame_blender_render.js | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 internal/manager/job_compilers/scripts/single_frame_blender_render.js diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js new file mode 100644 index 00000000..fbc7b9b7 --- /dev/null +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +const JOB_TYPE = { + label: "Single-frame Blender Render", + settings: [ + // Settings for artists to determine: + { key: "frame", type: "int32", required: true, eval: "C.scene.frame_current", + description: "Frame to render"}, + { key: "chunk_size", type: "int32", default: 128, description: "Number of samples to render in one Blender render task", + visible: "submission" }, + { key: "denoising", type: "bool", required: true, default: false, + description: "Toggles OpenImageDenoise" }, + + + // render_output_root + add_path_components determine the value of render_output_path. + { key: "render_output_root", type: "string", subtype: "dir_path", required: true, visible: "submission", + description: "Base directory of where render output is stored. Will have some job-specific parts appended to it"}, + { key: "add_path_components", type: "int32", required: true, default: 0, propargs: {min: 0, max: 32}, visible: "submission", + description: "Number of path components of the current blend file to use in the render output path"}, + { key: "render_output_path", type: "string", subtype: "file_path", editable: false, + eval: "str(Path(abspath(settings.render_output_root), last_n_dir_parts(settings.add_path_components), jobname, '{timestamp}'))", + description: "Final file path of where render output will be saved"}, + + // Automatically evaluated settings: + { key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: "web" }, + { key: "samples", type: "string", required: true, eval: "f'0-{C.scene.cycles.samples}'", visible: "web", + description: "Total number of samples in the job" }, + ] +}; + +function compileJob(job) { + print("Single-frame Render job submitted"); + print("job: ", job); + + const settings = job.settings; + const renderOutput = renderOutputPath(job); + + // Make sure that when the job is investigated later, it shows the + // actually-used render output: + settings.render_output_path = renderOutput; + + const renderDir = path.dirname(renderOutput); + const renderTasks = authorRenderTasks(settings, renderDir, renderOutput); + const mergeTask = authorCreateMergeTask(settings, renderOutput); + const denoiseTask = authorCreateDenoiseTask(settings, renderOutput); + + for (const rt of renderTasks) { + job.addTask(rt); + } + // All render tasks are a dependency of the merge task + for (const rt of renderTasks) { + mergeTask.addDependency(rt); + } + job.addTask(mergeTask); + // Add a denoise task if denoising is enabled and + // set the merge task as its dependency + if (denoiseTask) { + denoiseTask.addDependency(mergeTask); + job.addTask(denoiseTask); + } +} + +// Do field replacement on the render output path. +function renderOutputPath(job) { + let path = job.settings.render_output_path; + if (!path) { + throw "no render_output_path setting!"; + } + return path.replace(/{([^}]+)}/g, (match, group0) => { + switch (group0) { + case "timestamp": + return formatTimestampLocal(job.created); + default: + return match; + } + }); +} + +function authorRenderTasks(settings, renderDir, renderOutput) { + print("authorRenderTasks(", renderDir, renderOutput, ")"); + let renderTasks = []; + let chunks = frameChunker(settings.samples, settings.chunk_size); + for (let chunk of chunks) { + const task = author.Task(`render-${chunk}`, "blender"); + let chunk_size = chunk.split("-", 2)[1] - chunk.split("-", 2)[0] + let pythonExpression = ` +import bpy +bpy.context.scene.render.engine = 'CYCLES' +bpy.context.scene.render.use_compositing = False +bpy.context.scene.cycles.use_denoising = False +bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' +bpy.context.scene.cycles.samples = ${chunk_size} +bpy.context.scene.cycles.sample_offset = ${chunk.split("-", 1)}`; + if (settings.denoising) { + pythonExpression += ` +for layer in bpy.context.scene.view_layers: + layer['cycles']['denoising_store_passes'] = 1 + layer.use_pass_vector = True`; + } + print(pythonExpression); + const command = author.Command("blender-render", { + exe: "{blender}", + exeArgs: "{blenderArgs}", + argsBefore: [], + blendfile: settings.blendfile, + args: [ + "--python-expr", pythonExpression, + "--render-output", path.join(renderDir, path.basename(renderOutput), chunk), + "--render-frame", settings.frame + ] + }); + task.addCommand(command); + renderTasks.push(task); + } + return renderTasks; +} + +function authorCreateMergeTask(settings, renderOutput) { + const task = author.Task(`merge`, "blender"); + let pythonExpression = ` +import os +import pathlib +import bpy + +basepath = "${renderOutput}/" +tmp = basepath + 'merge_tmp/' +filenames = [f for f in os.listdir(basepath) if os.path.isfile(basepath + f)] +if 'MERGED.exr' in filenames: + filenames.remove('MERGED.exr') +filenames.sort() +if len(filenames) <= 1: + print('This job only has one file, merging not required.') + print('Renaming ' + basepath + filenames[0] + ' to ' + basepath + 'MERGED.exr') + pathlib.Path(basepath + filenames[0]).rename(basepath + 'MERGED.exr') + exit() +if not os.path.exists(tmp): + os.makedirs(tmp) + +index = 0 +while len(filenames) > 0: + if index == 0: + print('Merging ' + basepath + filenames[0] + ' and ' + basepath + filenames[1]) + bpy.ops.cycles.merge_images(input_filepath1=basepath+filenames[0], input_filepath2=basepath + filenames[1], output_filepath=tmp+str(index)+'.exr') + del filenames[0] + del filenames[0] + else: + print('Merging ' + tmp + str(index - 1) + '.exr' + ' and ' + basepath + filenames[0]) + bpy.ops.cycles.merge_images(input_filepath1=tmp + str(index - 1) + '.exr', input_filepath2=basepath + filenames[0], output_filepath=tmp+str(index)+'.exr') + del filenames[0] + index += 1 + +print('Moving ' + tmp + str(index-1) + '.exr' + ' to ' + basepath + 'MERGED.exr') +pathlib.Path(tmp + str(index-1)+'.exr').rename(basepath + 'MERGED.exr')`; + const command = author.Command("blender-render", { + exe: "{blender}", + exeArgs: "{blenderArgs}", + argsBefore: [], + blendfile: settings.blendfile, + args: [ + "--python-expr", pythonExpression + ] + }); + task.addCommand(command); + return task; +} + +function authorCreateDenoiseTask(settings, renderOutput) { + if (! settings.denoising) { + return; + } + const task = author.Task(`denoise`, "blender"); + let pythonExpression = ` +import bpy +print("Running the denoiser") +bpy.ops.cycles.denoise_animation(input_filepath="${renderOutput}/MERGED.exr", output_filepath="${renderOutput}/DENOISED.exr")`; + const command = author.Command("blender-render", { + exe: "{blender}", + exeArgs: "{blenderArgs}", + argsBefore: [], + blendfile: settings.blendfile, + args: [ + "--python-expr", pythonExpression + ] + }); + task.addCommand(command); + return task; +} -- 2.30.2 From 99324ca6821199d49a4f68496515b6e21f9b51b9 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Mon, 27 Feb 2023 21:12:47 +0100 Subject: [PATCH 02/29] Ignore the DENOISED file if it exists --- .../job_compilers/scripts/single_frame_blender_render.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index fbc7b9b7..ff722560 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -127,6 +127,8 @@ tmp = basepath + 'merge_tmp/' filenames = [f for f in os.listdir(basepath) if os.path.isfile(basepath + f)] if 'MERGED.exr' in filenames: filenames.remove('MERGED.exr') +if 'DENOISED.exr' in filenames: + filenames.remove('DENOISED.exr') filenames.sort() if len(filenames) <= 1: print('This job only has one file, merging not required.') -- 2.30.2 From b1595985004c597037be4f12def0540145c7bb42 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Tue, 28 Feb 2023 12:40:22 +0100 Subject: [PATCH 03/29] Fix chunking --- .../scripts/single_frame_blender_render.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index ff722560..c9549879 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -23,7 +23,7 @@ const JOB_TYPE = { // Automatically evaluated settings: { key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: "web" }, - { key: "samples", type: "string", required: true, eval: "f'0-{C.scene.cycles.samples}'", visible: "web", + { key: "samples", type: "string", required: true, eval: "f'1-{C.scene.cycles.samples}'", visible: "web", description: "Total number of samples in the job" }, ] }; @@ -82,7 +82,18 @@ function authorRenderTasks(settings, renderDir, renderOutput) { let chunks = frameChunker(settings.samples, settings.chunk_size); for (let chunk of chunks) { const task = author.Task(`render-${chunk}`, "blender"); - let chunk_size = chunk.split("-", 2)[1] - chunk.split("-", 2)[0] + let chunk_arr = chunk.split("-", 2) + let chunk_end + let chunk_start + if (chunk_arr.length < 2) { + chunk_end = chunk_arr[0] + chunk_start = chunk_arr[0] - 1 + } + else { + chunk_end = chunk_arr[1] + chunk_start = chunk_arr[0] - 1 + } + let chunk_size = chunk_end - chunk_start let pythonExpression = ` import bpy bpy.context.scene.render.engine = 'CYCLES' @@ -90,7 +101,7 @@ bpy.context.scene.render.use_compositing = False bpy.context.scene.cycles.use_denoising = False bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' bpy.context.scene.cycles.samples = ${chunk_size} -bpy.context.scene.cycles.sample_offset = ${chunk.split("-", 1)}`; +bpy.context.scene.cycles.sample_offset = ${chunk_start}`; if (settings.denoising) { pythonExpression += ` for layer in bpy.context.scene.view_layers: -- 2.30.2 From cea6940a701fbf9c7021200cc4b7380e0ebf8d67 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Tue, 28 Feb 2023 13:04:42 +0100 Subject: [PATCH 04/29] Add frame number to the render output path --- .../job_compilers/scripts/single_frame_blender_render.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index c9549879..0d8c692b 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -18,7 +18,7 @@ const JOB_TYPE = { { key: "add_path_components", type: "int32", required: true, default: 0, propargs: {min: 0, max: 32}, visible: "submission", description: "Number of path components of the current blend file to use in the render output path"}, { key: "render_output_path", type: "string", subtype: "file_path", editable: false, - eval: "str(Path(abspath(settings.render_output_root), last_n_dir_parts(settings.add_path_components), jobname, '{timestamp}'))", + eval: "str(Path(abspath(settings.render_output_root), last_n_dir_parts(settings.add_path_components), jobname, 'frame_' + str(settings.frame), '{timestamp}'))", description: "Final file path of where render output will be saved"}, // Automatically evaluated settings: -- 2.30.2 From 53d535ad5d59af04bc6dc9e814b409e1900ce29d Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Tue, 28 Feb 2023 18:51:59 +0100 Subject: [PATCH 05/29] Support for compositing --- .../scripts/single_frame_blender_render.js | 121 ++++++++++++++++-- 1 file changed, 112 insertions(+), 9 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 0d8c692b..17a515de 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -6,7 +6,7 @@ const JOB_TYPE = { // Settings for artists to determine: { key: "frame", type: "int32", required: true, eval: "C.scene.frame_current", description: "Frame to render"}, - { key: "chunk_size", type: "int32", default: 128, description: "Number of samples to render in one Blender render task", + { key: "chunk_size", type: "int32", default: 128, propargs: {min: 1}, description: "Number of samples to render in one Blender render task", visible: "submission" }, { key: "denoising", type: "bool", required: true, default: false, description: "Toggles OpenImageDenoise" }, @@ -23,6 +23,10 @@ const JOB_TYPE = { // Automatically evaluated settings: { key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: "web" }, + { key: "format", type: "string", required: true, eval: "C.scene.render.image_settings.file_format", visible: "web" }, + { key: "uses_compositing", type: "bool", required: true, eval: "C.scene.use_nodes and C.scene.render.use_compositing", visible: "web" }, + { key: "image_file_extension", type: "string", required: true, eval: "C.scene.render.file_extension", visible: "hidden", + description: "File extension used for the final export" }, { key: "samples", type: "string", required: true, eval: "f'1-{C.scene.cycles.samples}'", visible: "web", description: "Total number of samples in the job" }, ] @@ -43,6 +47,7 @@ function compileJob(job) { const renderTasks = authorRenderTasks(settings, renderDir, renderOutput); const mergeTask = authorCreateMergeTask(settings, renderOutput); const denoiseTask = authorCreateDenoiseTask(settings, renderOutput); + const compositeTask = authorCreateCompositeTask(settings, renderOutput); for (const rt of renderTasks) { job.addTask(rt); @@ -57,7 +62,12 @@ function compileJob(job) { if (denoiseTask) { denoiseTask.addDependency(mergeTask); job.addTask(denoiseTask); + compositeTask.addDependency(denoiseTask); } + else { + compositeTask.addDependency(mergeTask); + } + job.addTask(compositeTask); } // Do field replacement on the render output path. @@ -82,18 +92,18 @@ function authorRenderTasks(settings, renderDir, renderOutput) { let chunks = frameChunker(settings.samples, settings.chunk_size); for (let chunk of chunks) { const task = author.Task(`render-${chunk}`, "blender"); - let chunk_arr = chunk.split("-", 2) - let chunk_end - let chunk_start + let chunk_arr = chunk.split("-", 2); + let chunk_end; + let chunk_start; if (chunk_arr.length < 2) { - chunk_end = chunk_arr[0] - chunk_start = chunk_arr[0] - 1 + chunk_end = chunk_arr[0]; + chunk_start = chunk_arr[0] - 1; } else { - chunk_end = chunk_arr[1] - chunk_start = chunk_arr[0] - 1 + chunk_end = chunk_arr[1]; + chunk_start = chunk_arr[0] - 1; } - let chunk_size = chunk_end - chunk_start + let chunk_size = chunk_end - chunk_start; let pythonExpression = ` import bpy bpy.context.scene.render.engine = 'CYCLES' @@ -115,6 +125,7 @@ for layer in bpy.context.scene.view_layers: argsBefore: [], blendfile: settings.blendfile, args: [ + "--python-exit-code", 1, "--python-expr", pythonExpression, "--render-output", path.join(renderDir, path.basename(renderOutput), chunk), "--render-frame", settings.frame @@ -170,6 +181,7 @@ pathlib.Path(tmp + str(index-1)+'.exr').rename(basepath + 'MERGED.exr')`; argsBefore: [], blendfile: settings.blendfile, args: [ + "--python-exit-code", 1, "--python-expr", pythonExpression ] }); @@ -192,6 +204,97 @@ bpy.ops.cycles.denoise_animation(input_filepath="${renderOutput}/MERGED.exr", ou argsBefore: [], blendfile: settings.blendfile, args: [ + "--python-exit-code", 1, + "--python-expr", pythonExpression + ] + }); + task.addCommand(command); + return task; +} + +function authorCreateCompositeTask(settings, renderOutput) { + let filename; + if (settings.denoising) { + filename = "DENOISED.exr"; + } + else { + filename = "MERGED.exr"; + } + let pythonExpression = ` +import pathlib +import bpy +C = bpy.context +basepath = "${renderOutput}/" +filename = "${filename}"`; + if (settings.uses_compositing) { + // Do the full composite+export pipeline + // uses snippets from + // https://github.com/state-of-the-art/BlendNet/blob/master/BlendNet/script-compose.py#L94 + pythonExpression += ` +bpy.ops.image.open(filepath=basepath + filename, use_sequence_detection=False) +image = bpy.data.images[bpy.path.basename(filename)] +image_node = C.scene.node_tree.nodes.new(type='CompositorNodeImage') +image_node.image = image +nodes_to_remove = [] +links_to_create = [] +for node in C.scene.node_tree.nodes: + print('DEBUG: Checking node %s' % (node,)) + if not isinstance(node, bpy.types.CompositorNodeRLayers) or node.scene != C.scene: + continue + nodes_to_remove.append(node) + print('INFO: Reconnecting %s links to render image' % (node,)) + for link in C.scene.node_tree.links: + print('DEBUG: Checking link %s - %s' % (link.from_node, link.to_node)) + if link.from_node != node: + continue + print('DEBUG: Found link %s - %s' % (link.from_socket, link.to_socket)) + link_name = "Combined" + for output in image_node.outputs: + print('DEBUG: Checking output:', output.name, link_name) + if output.name != link_name: + continue + links_to_create.append((output, link)) + break + +for output, link in links_to_create: + print('INFO: Connecting "%s" output to %s.%s input' % ( + output, link.to_node, link.to_socket + )) + C.scene.node_tree.links.new(output, link.to_socket) + +print("Removing the nodes could potentially break the pipeline") +for node in nodes_to_remove: + print('INFO: Removing %s' % (node,)) + C.scene.node_tree.nodes.remove(node) +C.scene.render.filepath = basepath + "FINAL" +bpy.ops.render.render(write_still=True)`; + } + else { + pythonExpression += ` +print("Compositing is disabled")`; + if (settings.format == "OPEN_EXR_MULTILAYER") { + // Only rename + pythonExpression += ` +print('Renaming ' + basepath + filename + ' to ' + basepath + 'FINAL.exr') +pathlib.Path(basepath + filename).rename(basepath + 'FINAL.exr')`; + } + else { + // Only export + pythonExpression += ` +bpy.ops.image.open(filepath=basepath + filename, use_sequence_detection=False) +image = bpy.data.images[bpy.path.basename(filename)] +print("Saving final image to " + basepath + "FINAL" + "${settings.image_file_extension}") +image.save_render(basepath + "FINAL" + "${settings.image_file_extension}")`; + } + } + const task = author.Task(`composite`, "blender"); + const command = author.Command("blender-render", { + exe: "{blender}", + exeArgs: "{blenderArgs}", + argsBefore: [], + blendfile: settings.blendfile, + args: [ + "--python-exit-code", 1, "--python-expr", pythonExpression ] }); -- 2.30.2 From 57c2ed5b6146553dd74136dc25bb839623a4885c Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Tue, 28 Feb 2023 19:02:45 +0100 Subject: [PATCH 06/29] Put the render outputs in a separate directory --- .../scripts/single_frame_blender_render.js | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 17a515de..d405eddc 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -127,7 +127,7 @@ for layer in bpy.context.scene.view_layers: args: [ "--python-exit-code", 1, "--python-expr", pythonExpression, - "--render-output", path.join(renderDir, path.basename(renderOutput), chunk), + "--render-output", path.join(renderDir, path.basename(renderOutput), "samples", chunk), "--render-frame", settings.frame ] }); @@ -145,17 +145,14 @@ import pathlib import bpy basepath = "${renderOutput}/" +renders = basepath + "samples/" tmp = basepath + 'merge_tmp/' -filenames = [f for f in os.listdir(basepath) if os.path.isfile(basepath + f)] -if 'MERGED.exr' in filenames: - filenames.remove('MERGED.exr') -if 'DENOISED.exr' in filenames: - filenames.remove('DENOISED.exr') +filenames = [f for f in os.listdir(renders) if os.path.isfile(renders + f)] filenames.sort() if len(filenames) <= 1: print('This job only has one file, merging not required.') - print('Renaming ' + basepath + filenames[0] + ' to ' + basepath + 'MERGED.exr') - pathlib.Path(basepath + filenames[0]).rename(basepath + 'MERGED.exr') + print('Moving ' + renders + filenames[0] + ' to ' + basepath + 'MERGED.exr') + pathlib.Path(renders + filenames[0]).rename(basepath + 'MERGED.exr') exit() if not os.path.exists(tmp): os.makedirs(tmp) @@ -163,13 +160,13 @@ if not os.path.exists(tmp): index = 0 while len(filenames) > 0: if index == 0: - print('Merging ' + basepath + filenames[0] + ' and ' + basepath + filenames[1]) - bpy.ops.cycles.merge_images(input_filepath1=basepath+filenames[0], input_filepath2=basepath + filenames[1], output_filepath=tmp+str(index)+'.exr') + print('Merging ' + renders + filenames[0] + ' and ' + renders + filenames[1]) + bpy.ops.cycles.merge_images(input_filepath1=renders + filenames[0], input_filepath2=renders + filenames[1], output_filepath=tmp + str(index)+'.exr') del filenames[0] del filenames[0] else: - print('Merging ' + tmp + str(index - 1) + '.exr' + ' and ' + basepath + filenames[0]) - bpy.ops.cycles.merge_images(input_filepath1=tmp + str(index - 1) + '.exr', input_filepath2=basepath + filenames[0], output_filepath=tmp+str(index)+'.exr') + print('Merging ' + tmp + str(index - 1) + '.exr' + ' and ' + renders + filenames[0]) + bpy.ops.cycles.merge_images(input_filepath1=tmp + str(index - 1) + '.exr', input_filepath2=renders + filenames[0], output_filepath=tmp + str(index) + '.exr') del filenames[0] index += 1 -- 2.30.2 From b9aca93b54bdfb15c72d42c2d5604dc5f4afb6bf Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Fri, 3 Mar 2023 14:27:17 +0100 Subject: [PATCH 07/29] Rename parameters and automatically evaluate denoising --- .../job_compilers/scripts/single_frame_blender_render.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index d405eddc..b02c4599 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -8,8 +8,6 @@ const JOB_TYPE = { description: "Frame to render"}, { key: "chunk_size", type: "int32", default: 128, propargs: {min: 1}, description: "Number of samples to render in one Blender render task", visible: "submission" }, - { key: "denoising", type: "bool", required: true, default: false, - description: "Toggles OpenImageDenoise" }, // render_output_root + add_path_components determine the value of render_output_path. @@ -24,7 +22,9 @@ const JOB_TYPE = { // Automatically evaluated settings: { key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: "web" }, { key: "format", type: "string", required: true, eval: "C.scene.render.image_settings.file_format", visible: "web" }, - { key: "uses_compositing", type: "bool", required: true, eval: "C.scene.use_nodes and C.scene.render.use_compositing", visible: "web" }, + { key: "compositing", type: "bool", required: true, eval: "C.scene.use_nodes and C.scene.render.use_compositing", visible: "web" }, + { key: "denoising", type: "bool", required: true, eval: "C.scene.cycles.use_denoising", visible: "web", + description: "Toggles OpenImageDenoise" }, { key: "image_file_extension", type: "string", required: true, eval: "C.scene.render.file_extension", visible: "hidden", description: "File extension used for the final export" }, { key: "samples", type: "string", required: true, eval: "f'1-{C.scene.cycles.samples}'", visible: "web", @@ -223,7 +223,7 @@ import bpy C = bpy.context basepath = "${renderOutput}/" filename = "${filename}"`; - if (settings.uses_compositing) { + if (settings.compositing) { // Do the full composite+export pipeline // uses snippets from // https://github.com/state-of-the-art/BlendNet/blob/master/BlendNet/script-compose.py#L94 -- 2.30.2 From 7c684cca0c320fd267e9b24ec7a02e797df30db0 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Mon, 6 Mar 2023 14:55:56 +0100 Subject: [PATCH 08/29] Use imperative property names --- .../scripts/single_frame_blender_render.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index b02c4599..2270bd8c 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -22,8 +22,8 @@ const JOB_TYPE = { // Automatically evaluated settings: { key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: "web" }, { key: "format", type: "string", required: true, eval: "C.scene.render.image_settings.file_format", visible: "web" }, - { key: "compositing", type: "bool", required: true, eval: "C.scene.use_nodes and C.scene.render.use_compositing", visible: "web" }, - { key: "denoising", type: "bool", required: true, eval: "C.scene.cycles.use_denoising", visible: "web", + { key: "use_compositing", type: "bool", required: true, eval: "C.scene.use_nodes and C.scene.render.use_compositing", visible: "web" }, + { key: "use_denoising", type: "bool", required: true, eval: "C.scene.cycles.use_denoising", visible: "web", description: "Toggles OpenImageDenoise" }, { key: "image_file_extension", type: "string", required: true, eval: "C.scene.render.file_extension", visible: "hidden", description: "File extension used for the final export" }, @@ -112,7 +112,7 @@ bpy.context.scene.cycles.use_denoising = False bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' bpy.context.scene.cycles.samples = ${chunk_size} bpy.context.scene.cycles.sample_offset = ${chunk_start}`; - if (settings.denoising) { + if (settings.use_denoising) { pythonExpression += ` for layer in bpy.context.scene.view_layers: layer['cycles']['denoising_store_passes'] = 1 @@ -187,7 +187,7 @@ pathlib.Path(tmp + str(index-1)+'.exr').rename(basepath + 'MERGED.exr')`; } function authorCreateDenoiseTask(settings, renderOutput) { - if (! settings.denoising) { + if (! settings.use_denoising) { return; } const task = author.Task(`denoise`, "blender"); @@ -211,7 +211,7 @@ bpy.ops.cycles.denoise_animation(input_filepath="${renderOutput}/MERGED.exr", ou function authorCreateCompositeTask(settings, renderOutput) { let filename; - if (settings.denoising) { + if (settings.use_denoising) { filename = "DENOISED.exr"; } else { @@ -223,7 +223,7 @@ import bpy C = bpy.context basepath = "${renderOutput}/" filename = "${filename}"`; - if (settings.compositing) { + if (settings.use_compositing) { // Do the full composite+export pipeline // uses snippets from // https://github.com/state-of-the-art/BlendNet/blob/master/BlendNet/script-compose.py#L94 -- 2.30.2 From e6ef57d144e8d2025eae3f68a44425f0b02912bc Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Mon, 13 Mar 2023 19:58:57 +0100 Subject: [PATCH 09/29] Render in tiles --- .../scripts/single_frame_blender_render.js | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 2270bd8c..b4bccd76 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -6,7 +6,7 @@ const JOB_TYPE = { // Settings for artists to determine: { key: "frame", type: "int32", required: true, eval: "C.scene.frame_current", description: "Frame to render"}, - { key: "chunk_size", type: "int32", default: 128, propargs: {min: 1}, description: "Number of samples to render in one Blender render task", + { key: "tile_size", type: "int32", default: 5, propargs: {min: 1, max: 100}, description: "Tile size for each Task (sizes are in % of the full image)", visible: "submission" }, @@ -86,39 +86,42 @@ function renderOutputPath(job) { }); } +function tileChunker(tile_size) { + let tiles = []; + const rows = Math.floor(100 / tile_size); + const columns = Math.floor(100 / tile_size); + for (let row = 1; row <= rows; row++) { + for (let column = 1; column <= columns; column++) { + tiles.push({"row": row, "column": column}); + } + } + return tiles; +} + function authorRenderTasks(settings, renderDir, renderOutput) { print("authorRenderTasks(", renderDir, renderOutput, ")"); let renderTasks = []; - let chunks = frameChunker(settings.samples, settings.chunk_size); - for (let chunk of chunks) { - const task = author.Task(`render-${chunk}`, "blender"); - let chunk_arr = chunk.split("-", 2); - let chunk_end; - let chunk_start; - if (chunk_arr.length < 2) { - chunk_end = chunk_arr[0]; - chunk_start = chunk_arr[0] - 1; - } - else { - chunk_end = chunk_arr[1]; - chunk_start = chunk_arr[0] - 1; - } - let chunk_size = chunk_end - chunk_start; + let tiles = tileChunker(settings.tile_size); + print(tiles); + for (let tile of tiles) { + const task = author.Task(`render-r${tile.row}c${tile.column}`, "blender"); let pythonExpression = ` import bpy bpy.context.scene.render.engine = 'CYCLES' bpy.context.scene.render.use_compositing = False bpy.context.scene.cycles.use_denoising = False bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' -bpy.context.scene.cycles.samples = ${chunk_size} -bpy.context.scene.cycles.sample_offset = ${chunk_start}`; +bpy.context.scene.render.use_crop_to_border = False +bpy.context.scene.render.border_min_x = ${tile.column} * ${settings.tile_size} / 100 +bpy.context.scene.render.border_max_x = ${settings.tile_size} / 100 + ${tile.column} * ${settings.tile_size} / 100 +bpy.context.scene.render.border_min_y = ${tile.row} * ${settings.tile_size} / 100 +bpy.context.scene.render.border_max_y = ${settings.tile_size} / 100 + ${tile.row} * ${settings.tile_size} / 100`; if (settings.use_denoising) { pythonExpression += ` for layer in bpy.context.scene.view_layers: layer['cycles']['denoising_store_passes'] = 1 layer.use_pass_vector = True`; } - print(pythonExpression); const command = author.Command("blender-render", { exe: "{blender}", exeArgs: "{blenderArgs}", @@ -127,7 +130,7 @@ for layer in bpy.context.scene.view_layers: args: [ "--python-exit-code", 1, "--python-expr", pythonExpression, - "--render-output", path.join(renderDir, path.basename(renderOutput), "samples", chunk), + "--render-output", path.join(renderDir, path.basename(renderOutput), "tiles", "r" + tile.row + "c" + tile.column), "--render-frame", settings.frame ] }); @@ -145,7 +148,7 @@ import pathlib import bpy basepath = "${renderOutput}/" -renders = basepath + "samples/" +renders = basepath + "tiles/" tmp = basepath + 'merge_tmp/' filenames = [f for f in os.listdir(renders) if os.path.isfile(renders + f)] filenames.sort() -- 2.30.2 From 46a9db0bb3e5fabd290a92ae0556e8368ff3f000 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Mon, 13 Mar 2023 21:33:53 +0100 Subject: [PATCH 10/29] Implement basic merging using the compositor --- .../scripts/single_frame_blender_render.js | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index b4bccd76..5f6c9a16 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -149,7 +149,6 @@ import bpy basepath = "${renderOutput}/" renders = basepath + "tiles/" -tmp = basepath + 'merge_tmp/' filenames = [f for f in os.listdir(renders) if os.path.isfile(renders + f)] filenames.sort() if len(filenames) <= 1: @@ -157,24 +156,33 @@ if len(filenames) <= 1: print('Moving ' + renders + filenames[0] + ' to ' + basepath + 'MERGED.exr') pathlib.Path(renders + filenames[0]).rename(basepath + 'MERGED.exr') exit() -if not os.path.exists(tmp): - os.makedirs(tmp) -index = 0 -while len(filenames) > 0: +image_nodes = [] + +for index, image in enumerate(filenames): + bpy.ops.image.open(filepath=renders + image, use_sequence_detection=False) + image = bpy.data.images[bpy.path.basename(image)] + image_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeImage')) + image_nodes[index].image = image + +alpha_over_nodes = [] + +for index, node in enumerate(image_nodes): if index == 0: - print('Merging ' + renders + filenames[0] + ' and ' + renders + filenames[1]) - bpy.ops.cycles.merge_images(input_filepath1=renders + filenames[0], input_filepath2=renders + filenames[1], output_filepath=tmp + str(index)+'.exr') - del filenames[0] - del filenames[0] + # Take the first two image nodes and combine them + alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) + bpy.context.scene.node_tree.links.new(image_nodes[0].outputs['Combined'], alpha_over_nodes[0].inputs[1]) + bpy.context.scene.node_tree.links.new(image_nodes[1].outputs['Combined'], alpha_over_nodes[0].inputs[2]) else: - print('Merging ' + tmp + str(index - 1) + '.exr' + ' and ' + renders + filenames[0]) - bpy.ops.cycles.merge_images(input_filepath1=tmp + str(index - 1) + '.exr', input_filepath2=renders + filenames[0], output_filepath=tmp + str(index) + '.exr') - del filenames[0] - index += 1 - -print('Moving ' + tmp + str(index-1) + '.exr' + ' to ' + basepath + 'MERGED.exr') -pathlib.Path(tmp + str(index-1)+'.exr').rename(basepath + 'MERGED.exr')`; + # Take one image node and the previous alpha over node + alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) + bpy.context.scene.node_tree.links.new(alpha_over_nodes[index-1].outputs['Image'], alpha_over_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(image_nodes[index+1].outputs['Combined'], alpha_over_nodes[index].inputs[2]) + if index + 1 == len(image_nodes) - 1: + # Link the last image node and feed the output into the composite node + bpy.context.scene.node_tree.links.new(alpha_over_nodes[index].outputs['Image'], bpy.context.scene.node_tree.nodes['Composite'].inputs['Image']) + break +`; const command = author.Command("blender-render", { exe: "{blender}", exeArgs: "{blenderArgs}", -- 2.30.2 From 56a91bb7a2ca15cf33c5528fc2369f95ce7bd9b8 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Tue, 14 Mar 2023 00:03:01 +0100 Subject: [PATCH 11/29] Finish up the tiling implementation --- .../scripts/single_frame_blender_render.js | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 5f6c9a16..9e2130ea 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -6,10 +6,9 @@ const JOB_TYPE = { // Settings for artists to determine: { key: "frame", type: "int32", required: true, eval: "C.scene.frame_current", description: "Frame to render"}, - { key: "tile_size", type: "int32", default: 5, propargs: {min: 1, max: 100}, description: "Tile size for each Task (sizes are in % of the full image)", + { key: "tile_size", type: "int32", default: 5, propargs: {min: 1, max: 100}, description: "Tile size for each Task (sizes are in % of each dimension)", visible: "submission" }, - // render_output_root + add_path_components determine the value of render_output_path. { key: "render_output_root", type: "string", subtype: "dir_path", required: true, visible: "submission", description: "Base directory of where render output is stored. Will have some job-specific parts appended to it"}, @@ -27,8 +26,9 @@ const JOB_TYPE = { description: "Toggles OpenImageDenoise" }, { key: "image_file_extension", type: "string", required: true, eval: "C.scene.render.file_extension", visible: "hidden", description: "File extension used for the final export" }, - { key: "samples", type: "string", required: true, eval: "f'1-{C.scene.cycles.samples}'", visible: "web", - description: "Total number of samples in the job" }, + { key: "resolution_x", type: "int32", required: true, eval: "C.scene.render.resolution_x", visible: "hidden"}, + { key: "resolution_y", type: "int32", required: true, eval: "C.scene.render.resolution_y", visible: "hidden"}, + { key: "resolution_percentage", type: "int32", required: true, eval: "C.scene.render.resolution_percentage", visible: "hidden"} ] }; @@ -90,8 +90,8 @@ function tileChunker(tile_size) { let tiles = []; const rows = Math.floor(100 / tile_size); const columns = Math.floor(100 / tile_size); - for (let row = 1; row <= rows; row++) { - for (let column = 1; column <= columns; column++) { + for (let row = 0; row < rows; row++) { + for (let column = 0; column < columns; column++) { tiles.push({"row": row, "column": column}); } } @@ -157,8 +157,16 @@ if len(filenames) <= 1: pathlib.Path(renders + filenames[0]).rename(basepath + 'MERGED.exr') exit() -image_nodes = [] +bpy.context.scene.render.resolution_x = ${settings.resolution_x} +bpy.context.scene.render.resolution_y = ${settings.resolution_y} +bpy.context.scene.render.resolution_percentage = ${settings.resolution_percentage} +bpy.context.scene.render.use_compositing = True +bpy.context.scene.use_nodes = True +bpy.context.scene.view_layers[0].use = False +bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' +bpy.context.scene.render.filepath = basepath + "MERGED" +image_nodes = [] for index, image in enumerate(filenames): bpy.ops.image.open(filepath=renders + image, use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(image)] @@ -166,7 +174,6 @@ for index, image in enumerate(filenames): image_nodes[index].image = image alpha_over_nodes = [] - for index, node in enumerate(image_nodes): if index == 0: # Take the first two image nodes and combine them @@ -182,7 +189,7 @@ for index, node in enumerate(image_nodes): # Link the last image node and feed the output into the composite node bpy.context.scene.node_tree.links.new(alpha_over_nodes[index].outputs['Image'], bpy.context.scene.node_tree.nodes['Composite'].inputs['Image']) break -`; +bpy.ops.render.render(write_still=True)`; const command = author.Command("blender-render", { exe: "{blender}", exeArgs: "{blenderArgs}", -- 2.30.2 From e51a2aedff5b61472ae083f792aa27f4fde16ac2 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Wed, 15 Mar 2023 17:45:34 +0100 Subject: [PATCH 12/29] Fix tiling when using sizes over 50% --- .../job_compilers/scripts/single_frame_blender_render.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 9e2130ea..560d88a7 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -88,8 +88,8 @@ function renderOutputPath(job) { function tileChunker(tile_size) { let tiles = []; - const rows = Math.floor(100 / tile_size); - const columns = Math.floor(100 / tile_size); + const rows = Math.ceil(100 / tile_size); + const columns = Math.ceil(100 / tile_size); for (let row = 0; row < rows; row++) { for (let column = 0; column < columns; column++) { tiles.push({"row": row, "column": column}); @@ -163,6 +163,8 @@ bpy.context.scene.render.resolution_percentage = ${settings.resolution_percentag bpy.context.scene.render.use_compositing = True bpy.context.scene.use_nodes = True bpy.context.scene.view_layers[0].use = False +if 'Render Layers' in bpy.context.scene.node_tree.nodes: + bpy.context.scene.node_tree.nodes.remove(bpy.context.scene.node_tree.nodes['Render Layers']) bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' bpy.context.scene.render.filepath = basepath + "MERGED" -- 2.30.2 From c87a5887e76d0e6deab65514d0d9ec71c832e363 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Thu, 16 Mar 2023 13:35:27 +0100 Subject: [PATCH 13/29] Fix the merge creating an empty layer in the merged image --- .../scripts/single_frame_blender_render.js | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 560d88a7..73e84fbc 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -162,11 +162,8 @@ bpy.context.scene.render.resolution_y = ${settings.resolution_y} bpy.context.scene.render.resolution_percentage = ${settings.resolution_percentage} bpy.context.scene.render.use_compositing = True bpy.context.scene.use_nodes = True -bpy.context.scene.view_layers[0].use = False -if 'Render Layers' in bpy.context.scene.node_tree.nodes: - bpy.context.scene.node_tree.nodes.remove(bpy.context.scene.node_tree.nodes['Render Layers']) +bpy.context.scene.node_tree.nodes.clear() bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' -bpy.context.scene.render.filepath = basepath + "MERGED" image_nodes = [] for index, image in enumerate(filenames): @@ -189,9 +186,11 @@ for index, node in enumerate(image_nodes): bpy.context.scene.node_tree.links.new(image_nodes[index+1].outputs['Combined'], alpha_over_nodes[index].inputs[2]) if index + 1 == len(image_nodes) - 1: # Link the last image node and feed the output into the composite node - bpy.context.scene.node_tree.links.new(alpha_over_nodes[index].outputs['Image'], bpy.context.scene.node_tree.nodes['Composite'].inputs['Image']) + output_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeOutputFile') + output_node.base_path = basepath + "MERGED" + bpy.context.scene.node_tree.links.new(alpha_over_nodes[index].outputs['Image'], output_node.inputs['Image']) break -bpy.ops.render.render(write_still=True)`; +bpy.ops.render.render()`; const command = author.Command("blender-render", { exe: "{blender}", exeArgs: "{blenderArgs}", @@ -231,18 +230,18 @@ bpy.ops.cycles.denoise_animation(input_filepath="${renderOutput}/MERGED.exr", ou function authorCreateCompositeTask(settings, renderOutput) { let filename; - if (settings.use_denoising) { - filename = "DENOISED.exr"; - } - else { - filename = "MERGED.exr"; - } let pythonExpression = ` import pathlib +import os import bpy C = bpy.context basepath = "${renderOutput}/" -filename = "${filename}"`; +filenames = [f for f in os.listdir(basepath) if os.path.isfile(basepath + f)] +if "DENOISED.exr" in filenames: + filename = "DENOISED.exr" +else: + filename = next((s for s in filenames if "MERGED" in s), None) +`; if (settings.use_compositing) { // Do the full composite+export pipeline // uses snippets from @@ -265,7 +264,7 @@ for node in C.scene.node_tree.nodes: if link.from_node != node: continue print('DEBUG: Found link %s - %s' % (link.from_socket, link.to_socket)) - link_name = "Combined" + link_name = "Image" for output in image_node.outputs: print('DEBUG: Checking output:', output.name, link_name) if output.name != link_name: @@ -287,12 +286,9 @@ C.scene.render.filepath = basepath + "FINAL" bpy.ops.render.render(write_still=True)`; } else { - pythonExpression += ` -print("Compositing is disabled")`; if (settings.format == "OPEN_EXR_MULTILAYER") { // Only rename pythonExpression += ` -print('Renaming ' + basepath + filename + ' to ' + basepath + 'FINAL.exr') pathlib.Path(basepath + filename).rename(basepath + 'FINAL.exr')`; } else { @@ -300,7 +296,6 @@ pathlib.Path(basepath + filename).rename(basepath + 'FINAL.exr')`; pythonExpression += ` bpy.ops.image.open(filepath=basepath + filename, use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(filename)] -print("Saving final image to " + basepath + "FINAL" + "${settings.image_file_extension}") image.save_render(basepath + "FINAL" + "${settings.image_file_extension}")`; } } -- 2.30.2 From a2b191f12791c2f9b51763cb8ec6065dcd81cd43 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Thu, 16 Mar 2023 16:15:02 +0100 Subject: [PATCH 14/29] Add support for denoising --- .../scripts/single_frame_blender_render.js | 87 +++++++++---------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 73e84fbc..7eaec2c4 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -46,7 +46,6 @@ function compileJob(job) { const renderDir = path.dirname(renderOutput); const renderTasks = authorRenderTasks(settings, renderDir, renderOutput); const mergeTask = authorCreateMergeTask(settings, renderOutput); - const denoiseTask = authorCreateDenoiseTask(settings, renderOutput); const compositeTask = authorCreateCompositeTask(settings, renderOutput); for (const rt of renderTasks) { @@ -57,17 +56,8 @@ function compileJob(job) { mergeTask.addDependency(rt); } job.addTask(mergeTask); - // Add a denoise task if denoising is enabled and - // set the merge task as its dependency - if (denoiseTask) { - denoiseTask.addDependency(mergeTask); - job.addTask(denoiseTask); - compositeTask.addDependency(denoiseTask); - } - else { - compositeTask.addDependency(mergeTask); - } - job.addTask(compositeTask); + compositeTask.addDependency(mergeTask); + job.addTask(compositeTask); } // Do field replacement on the render output path. @@ -164,6 +154,7 @@ bpy.context.scene.render.use_compositing = True bpy.context.scene.use_nodes = True bpy.context.scene.node_tree.nodes.clear() bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' +bpy.context.scene.render.filepath = basepath + "MERGED" image_nodes = [] for index, image in enumerate(filenames): @@ -172,25 +163,53 @@ for index, image in enumerate(filenames): image_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeImage')) image_nodes[index].image = image +output_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeComposite') +denoising = "${settings.use_denoising}" == "true" alpha_over_nodes = [] +albedo_mix_nodes = [] +normal_mix_nodes = [] + for index, node in enumerate(image_nodes): if index == 0: # Take the first two image nodes and combine them alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) - bpy.context.scene.node_tree.links.new(image_nodes[0].outputs['Combined'], alpha_over_nodes[0].inputs[1]) - bpy.context.scene.node_tree.links.new(image_nodes[1].outputs['Combined'], alpha_over_nodes[0].inputs[2]) + bpy.context.scene.node_tree.links.new(image_nodes[0].outputs['Combined'], alpha_over_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(image_nodes[1].outputs['Combined'], alpha_over_nodes[index].inputs[2]) + if denoising: + albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) + albedo_mix_nodes[index].blend_type = 'ADD' + bpy.context.scene.node_tree.links.new(image_nodes[0].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(image_nodes[1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) + normal_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) + normal_mix_nodes[index].blend_type = 'ADD' + bpy.context.scene.node_tree.links.new(image_nodes[0].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(image_nodes[1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) else: # Take one image node and the previous alpha over node alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) bpy.context.scene.node_tree.links.new(alpha_over_nodes[index-1].outputs['Image'], alpha_over_nodes[index].inputs[1]) bpy.context.scene.node_tree.links.new(image_nodes[index+1].outputs['Combined'], alpha_over_nodes[index].inputs[2]) + if denoising: + albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) + albedo_mix_nodes[index].blend_type = 'ADD' + bpy.context.scene.node_tree.links.new(albedo_mix_nodes[index-1].outputs['Image'], albedo_mix_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(image_nodes[index+1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) + normal_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) + normal_mix_nodes[index].blend_type = 'ADD' + bpy.context.scene.node_tree.links.new(normal_mix_nodes[index-1].outputs['Image'], normal_mix_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(image_nodes[index+1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) if index + 1 == len(image_nodes) - 1: - # Link the last image node and feed the output into the composite node - output_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeOutputFile') - output_node.base_path = basepath + "MERGED" - bpy.context.scene.node_tree.links.new(alpha_over_nodes[index].outputs['Image'], output_node.inputs['Image']) + if denoising: + denoise_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeDenoise') + bpy.context.scene.node_tree.links.new(alpha_over_nodes[index].outputs['Image'], denoise_node.inputs['Image']) + bpy.context.scene.node_tree.links.new(albedo_mix_nodes[index].outputs['Image'], denoise_node.inputs['Albedo']) + bpy.context.scene.node_tree.links.new(normal_mix_nodes[index].outputs['Image'], denoise_node.inputs['Normal']) + bpy.context.scene.node_tree.links.new(denoise_node.outputs['Image'], output_node.inputs['Image']) + else: + # Link the last image node and feed the output into the composite node + bpy.context.scene.node_tree.links.new(alpha_over_nodes[index].outputs['Image'], output_node.inputs['Image']) break -bpy.ops.render.render()`; +bpy.ops.render.render(write_still=True)`; const command = author.Command("blender-render", { exe: "{blender}", exeArgs: "{blenderArgs}", @@ -205,42 +224,14 @@ bpy.ops.render.render()`; return task; } -function authorCreateDenoiseTask(settings, renderOutput) { - if (! settings.use_denoising) { - return; - } - const task = author.Task(`denoise`, "blender"); - let pythonExpression = ` -import bpy -print("Running the denoiser") -bpy.ops.cycles.denoise_animation(input_filepath="${renderOutput}/MERGED.exr", output_filepath="${renderOutput}/DENOISED.exr")`; - const command = author.Command("blender-render", { - exe: "{blender}", - exeArgs: "{blenderArgs}", - argsBefore: [], - blendfile: settings.blendfile, - args: [ - "--python-exit-code", 1, - "--python-expr", pythonExpression - ] - }); - task.addCommand(command); - return task; -} - function authorCreateCompositeTask(settings, renderOutput) { let filename; let pythonExpression = ` import pathlib -import os import bpy C = bpy.context basepath = "${renderOutput}/" -filenames = [f for f in os.listdir(basepath) if os.path.isfile(basepath + f)] -if "DENOISED.exr" in filenames: - filename = "DENOISED.exr" -else: - filename = next((s for s in filenames if "MERGED" in s), None) +filename = "MERGED.exr" `; if (settings.use_compositing) { // Do the full composite+export pipeline -- 2.30.2 From 77013fb991892e35da5ea0c7379378e237d4549f Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Thu, 16 Mar 2023 16:54:49 +0100 Subject: [PATCH 15/29] Fix using the wrong socket name --- .../job_compilers/scripts/single_frame_blender_render.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 7eaec2c4..a7809ed4 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -255,7 +255,7 @@ for node in C.scene.node_tree.nodes: if link.from_node != node: continue print('DEBUG: Found link %s - %s' % (link.from_socket, link.to_socket)) - link_name = "Image" + link_name = "Combined" for output in image_node.outputs: print('DEBUG: Checking output:', output.name, link_name) if output.name != link_name: -- 2.30.2 From e349abb9c6f0338e1e0ab387ed93077ab796c950 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Wed, 29 Mar 2023 18:32:11 +0200 Subject: [PATCH 16/29] Initial fix for the memory usage --- .../scripts/single_frame_blender_render.js | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index a7809ed4..3b755f15 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -97,15 +97,16 @@ function authorRenderTasks(settings, renderDir, renderOutput) { const task = author.Task(`render-r${tile.row}c${tile.column}`, "blender"); let pythonExpression = ` import bpy -bpy.context.scene.render.engine = 'CYCLES' -bpy.context.scene.render.use_compositing = False +render = bpy.context.scene.render +render.engine = 'CYCLES' +render.use_compositing = False bpy.context.scene.cycles.use_denoising = False -bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' -bpy.context.scene.render.use_crop_to_border = False -bpy.context.scene.render.border_min_x = ${tile.column} * ${settings.tile_size} / 100 -bpy.context.scene.render.border_max_x = ${settings.tile_size} / 100 + ${tile.column} * ${settings.tile_size} / 100 -bpy.context.scene.render.border_min_y = ${tile.row} * ${settings.tile_size} / 100 -bpy.context.scene.render.border_max_y = ${settings.tile_size} / 100 + ${tile.row} * ${settings.tile_size} / 100`; +render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' +render.use_crop_to_border = True +render.border_min_x = ${tile.column} * ${settings.tile_size} / 100 +render.border_max_x = ${settings.tile_size} / 100 + ${tile.column} * ${settings.tile_size} / 100 +render.border_min_y = ${tile.row} * ${settings.tile_size} / 100 +render.border_max_y = ${settings.tile_size} / 100 + ${tile.row} * ${settings.tile_size} / 100`; if (settings.use_denoising) { pythonExpression += ` for layer in bpy.context.scene.view_layers: @@ -120,7 +121,7 @@ for layer in bpy.context.scene.view_layers: args: [ "--python-exit-code", 1, "--python-expr", pythonExpression, - "--render-output", path.join(renderDir, path.basename(renderOutput), "tiles", "r" + tile.row + "c" + tile.column), + "--render-output", path.join(renderDir, path.basename(renderOutput), "tiles", "r" + tile.row + "c" + tile.column + "_"), "--render-frame", settings.frame ] }); @@ -136,17 +137,23 @@ function authorCreateMergeTask(settings, renderOutput) { import os import pathlib import bpy +import math + +def normalize(number): + return round(-0.5 + (number - 0) * (0.5 - -0.5) / 1 - 0, 10) basepath = "${renderOutput}/" renders = basepath + "tiles/" filenames = [f for f in os.listdir(renders) if os.path.isfile(renders + f)] -filenames.sort() + if len(filenames) <= 1: print('This job only has one file, merging not required.') print('Moving ' + renders + filenames[0] + ' to ' + basepath + 'MERGED.exr') pathlib.Path(renders + filenames[0]).rename(basepath + 'MERGED.exr') exit() +row_max = math.ceil(100 / ${settings.tile_size}) - 1 + bpy.context.scene.render.resolution_x = ${settings.resolution_x} bpy.context.scene.render.resolution_y = ${settings.resolution_y} bpy.context.scene.render.resolution_percentage = ${settings.resolution_percentage} @@ -158,10 +165,21 @@ bpy.context.scene.render.filepath = basepath + "MERGED" image_nodes = [] for index, image in enumerate(filenames): + dimensions = {'X': int(image.split('c')[-1].split('_')[0]), 'Y': int(image.split('c')[0].replace('r', ''))} bpy.ops.image.open(filepath=renders + image, use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(image)] - image_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeImage')) - image_nodes[index].image = image + image_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeImage') + image_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeTranslate')) + image_node.image = image + image_nodes[index].use_relative = True + bpy.context.scene.node_tree.links.new(image_node.outputs['Combined'], image_nodes[index].inputs['Image']) + for dimension in dimensions: + if dimensions[dimension] == 0: + image_nodes[index].inputs[dimension].default_value = normalize(${settings.tile_size} / 100 / 2) + elif dimensions[dimension] == row_max: + image_nodes[index].inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + ((100 % ${settings.tile_size}) / 100) / 2) + else: + image_nodes[index].inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + (${settings.tile_size} / 100) / 2) output_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeComposite') denoising = "${settings.use_denoising}" == "true" @@ -173,8 +191,8 @@ for index, node in enumerate(image_nodes): if index == 0: # Take the first two image nodes and combine them alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) - bpy.context.scene.node_tree.links.new(image_nodes[0].outputs['Combined'], alpha_over_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_nodes[1].outputs['Combined'], alpha_over_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(image_nodes[0].outputs['Image'], alpha_over_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(image_nodes[1].outputs['Image'], alpha_over_nodes[index].inputs[2]) if denoising: albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) albedo_mix_nodes[index].blend_type = 'ADD' @@ -188,7 +206,7 @@ for index, node in enumerate(image_nodes): # Take one image node and the previous alpha over node alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) bpy.context.scene.node_tree.links.new(alpha_over_nodes[index-1].outputs['Image'], alpha_over_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_nodes[index+1].outputs['Combined'], alpha_over_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(image_nodes[index+1].outputs['Image'], alpha_over_nodes[index].inputs[2]) if denoising: albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) albedo_mix_nodes[index].blend_type = 'ADD' -- 2.30.2 From 306e4a1fdf0bc92c68a7c04f62a156040f1532d9 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Wed, 29 Mar 2023 21:01:32 +0200 Subject: [PATCH 17/29] Fix merging for percentages without remainders --- .../job_compilers/scripts/single_frame_blender_render.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 3b755f15..3ef36736 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -176,7 +176,7 @@ for index, image in enumerate(filenames): for dimension in dimensions: if dimensions[dimension] == 0: image_nodes[index].inputs[dimension].default_value = normalize(${settings.tile_size} / 100 / 2) - elif dimensions[dimension] == row_max: + elif dimensions[dimension] == row_max and 100 % ${settings.tile_size} != 0: image_nodes[index].inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + ((100 % ${settings.tile_size}) / 100) / 2) else: image_nodes[index].inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + (${settings.tile_size} / 100) / 2) -- 2.30.2 From 7e245730c5d37bab24197b31e7a6363653f9c65c Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Wed, 29 Mar 2023 21:09:58 +0200 Subject: [PATCH 18/29] Show the tile size in the web UI --- .../scripts/single_frame_blender_render.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 3ef36736..9b3499a5 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -5,18 +5,17 @@ const JOB_TYPE = { settings: [ // Settings for artists to determine: { key: "frame", type: "int32", required: true, eval: "C.scene.frame_current", - description: "Frame to render"}, - { key: "tile_size", type: "int32", default: 5, propargs: {min: 1, max: 100}, description: "Tile size for each Task (sizes are in % of each dimension)", - visible: "submission" }, + description: "Frame to render" }, + { key: "tile_size", type: "int32", default: 5, propargs: {min: 1, max: 100}, description: "Tile size for each Task (sizes are in % of each dimension)" }, // render_output_root + add_path_components determine the value of render_output_path. { key: "render_output_root", type: "string", subtype: "dir_path", required: true, visible: "submission", - description: "Base directory of where render output is stored. Will have some job-specific parts appended to it"}, + description: "Base directory of where render output is stored. Will have some job-specific parts appended to it" }, { key: "add_path_components", type: "int32", required: true, default: 0, propargs: {min: 0, max: 32}, visible: "submission", - description: "Number of path components of the current blend file to use in the render output path"}, + description: "Number of path components of the current blend file to use in the render output path" }, { key: "render_output_path", type: "string", subtype: "file_path", editable: false, eval: "str(Path(abspath(settings.render_output_root), last_n_dir_parts(settings.add_path_components), jobname, 'frame_' + str(settings.frame), '{timestamp}'))", - description: "Final file path of where render output will be saved"}, + description: "Final file path of where render output will be saved" }, // Automatically evaluated settings: { key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: "web" }, @@ -26,9 +25,9 @@ const JOB_TYPE = { description: "Toggles OpenImageDenoise" }, { key: "image_file_extension", type: "string", required: true, eval: "C.scene.render.file_extension", visible: "hidden", description: "File extension used for the final export" }, - { key: "resolution_x", type: "int32", required: true, eval: "C.scene.render.resolution_x", visible: "hidden"}, - { key: "resolution_y", type: "int32", required: true, eval: "C.scene.render.resolution_y", visible: "hidden"}, - { key: "resolution_percentage", type: "int32", required: true, eval: "C.scene.render.resolution_percentage", visible: "hidden"} + { key: "resolution_x", type: "int32", required: true, eval: "C.scene.render.resolution_x", visible: "hidden" }, + { key: "resolution_y", type: "int32", required: true, eval: "C.scene.render.resolution_y", visible: "hidden" }, + { key: "resolution_percentage", type: "int32", required: true, eval: "C.scene.render.resolution_percentage", visible: "hidden" } ] }; -- 2.30.2 From fd3c1ddf98adc3f540d8bfb4421d4eb6b10512ac Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Sat, 1 Apr 2023 15:16:04 +0200 Subject: [PATCH 19/29] Rename variable for more readability --- .../scripts/single_frame_blender_render.js | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 9b3499a5..e647d652 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -162,23 +162,23 @@ bpy.context.scene.node_tree.nodes.clear() bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' bpy.context.scene.render.filepath = basepath + "MERGED" -image_nodes = [] +image_node_translates = [] for index, image in enumerate(filenames): dimensions = {'X': int(image.split('c')[-1].split('_')[0]), 'Y': int(image.split('c')[0].replace('r', ''))} bpy.ops.image.open(filepath=renders + image, use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(image)] image_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeImage') - image_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeTranslate')) + image_node_translates.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeTranslate')) image_node.image = image - image_nodes[index].use_relative = True - bpy.context.scene.node_tree.links.new(image_node.outputs['Combined'], image_nodes[index].inputs['Image']) + image_node_translates[index].use_relative = True + bpy.context.scene.node_tree.links.new(image_node.outputs['Combined'], image_node_translates[index].inputs['Image']) for dimension in dimensions: if dimensions[dimension] == 0: - image_nodes[index].inputs[dimension].default_value = normalize(${settings.tile_size} / 100 / 2) + image_node_translates[index].inputs[dimension].default_value = normalize(${settings.tile_size} / 100 / 2) elif dimensions[dimension] == row_max and 100 % ${settings.tile_size} != 0: - image_nodes[index].inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + ((100 % ${settings.tile_size}) / 100) / 2) + image_node_translates[index].inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + ((100 % ${settings.tile_size}) / 100) / 2) else: - image_nodes[index].inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + (${settings.tile_size} / 100) / 2) + image_node_translates[index].inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + (${settings.tile_size} / 100) / 2) output_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeComposite') denoising = "${settings.use_denoising}" == "true" @@ -186,36 +186,36 @@ alpha_over_nodes = [] albedo_mix_nodes = [] normal_mix_nodes = [] -for index, node in enumerate(image_nodes): +for index, node in enumerate(image_node_translates): if index == 0: # Take the first two image nodes and combine them alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) - bpy.context.scene.node_tree.links.new(image_nodes[0].outputs['Image'], alpha_over_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_nodes[1].outputs['Image'], alpha_over_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(image_node_translates[0].outputs['Image'], alpha_over_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(image_node_translates[1].outputs['Image'], alpha_over_nodes[index].inputs[2]) if denoising: albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) albedo_mix_nodes[index].blend_type = 'ADD' - bpy.context.scene.node_tree.links.new(image_nodes[0].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_nodes[1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(image_node_translates[0].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(image_node_translates[1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) normal_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) normal_mix_nodes[index].blend_type = 'ADD' - bpy.context.scene.node_tree.links.new(image_nodes[0].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_nodes[1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(image_node_translates[0].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(image_node_translates[1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) else: # Take one image node and the previous alpha over node alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) bpy.context.scene.node_tree.links.new(alpha_over_nodes[index-1].outputs['Image'], alpha_over_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_nodes[index+1].outputs['Image'], alpha_over_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(image_node_translates[index+1].outputs['Image'], alpha_over_nodes[index].inputs[2]) if denoising: albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) albedo_mix_nodes[index].blend_type = 'ADD' bpy.context.scene.node_tree.links.new(albedo_mix_nodes[index-1].outputs['Image'], albedo_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_nodes[index+1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(image_node_translates[index+1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) normal_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) normal_mix_nodes[index].blend_type = 'ADD' bpy.context.scene.node_tree.links.new(normal_mix_nodes[index-1].outputs['Image'], normal_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_nodes[index+1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) - if index + 1 == len(image_nodes) - 1: + bpy.context.scene.node_tree.links.new(image_node_translates[index+1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) + if index + 1 == len(image_node_translates) - 1: if denoising: denoise_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeDenoise') bpy.context.scene.node_tree.links.new(alpha_over_nodes[index].outputs['Image'], denoise_node.inputs['Image']) -- 2.30.2 From c7d5d8fe9a4f0050b58fbf40e282480eaf23cfe9 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Sat, 1 Apr 2023 15:42:15 +0200 Subject: [PATCH 20/29] Make the tile placement logic a function --- .../scripts/single_frame_blender_render.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index e647d652..f4d072f3 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -162,23 +162,28 @@ bpy.context.scene.node_tree.nodes.clear() bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' bpy.context.scene.render.filepath = basepath + "MERGED" +def create_translate(index, dimensions): + translate_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeTranslate') + translate_node.use_relative = True + for dimension in dimensions: + if dimensions[dimension] == 0: + translate_node.inputs[dimension].default_value = normalize(${settings.tile_size} / 100 / 2) + elif dimensions[dimension] == row_max and 100 % ${settings.tile_size} != 0: + translate_node.inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + ((100 % ${settings.tile_size}) / 100) / 2) + else: + translate_node.inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + (${settings.tile_size} / 100) / 2) + return translate_node + image_node_translates = [] + for index, image in enumerate(filenames): dimensions = {'X': int(image.split('c')[-1].split('_')[0]), 'Y': int(image.split('c')[0].replace('r', ''))} bpy.ops.image.open(filepath=renders + image, use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(image)] image_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeImage') - image_node_translates.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeTranslate')) + image_node_translates.append(create_translate(index, dimensions)) image_node.image = image - image_node_translates[index].use_relative = True bpy.context.scene.node_tree.links.new(image_node.outputs['Combined'], image_node_translates[index].inputs['Image']) - for dimension in dimensions: - if dimensions[dimension] == 0: - image_node_translates[index].inputs[dimension].default_value = normalize(${settings.tile_size} / 100 / 2) - elif dimensions[dimension] == row_max and 100 % ${settings.tile_size} != 0: - image_node_translates[index].inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + ((100 % ${settings.tile_size}) / 100) / 2) - else: - image_node_translates[index].inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + (${settings.tile_size} / 100) / 2) output_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeComposite') denoising = "${settings.use_denoising}" == "true" -- 2.30.2 From adaa1ad0a05ae06a64dc2d86c2fab1981380e301 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Sat, 1 Apr 2023 16:38:57 +0200 Subject: [PATCH 21/29] Some more restructuring before implementing denoising --- .../scripts/single_frame_blender_render.js | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index f4d072f3..3d5460d6 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -161,8 +161,10 @@ bpy.context.scene.use_nodes = True bpy.context.scene.node_tree.nodes.clear() bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' bpy.context.scene.render.filepath = basepath + "MERGED" +denoising = "${settings.use_denoising}" == "true" -def create_translate(index, dimensions): +# Takes the column and row number and creates a translate node with the right coordinates to align a tile +def create_translate(dimensions): translate_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeTranslate') translate_node.use_relative = True for dimension in dimensions: @@ -174,53 +176,64 @@ def create_translate(index, dimensions): translate_node.inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + (${settings.tile_size} / 100) / 2) return translate_node -image_node_translates = [] +def align_tiles(input): + translated_list = [] + for index, image_node in enumerate(image_nodes): + file_name = image_node.image.name_full + dimensions = {'X': int(file_name.split('c')[-1].split('_')[0]), 'Y': int(file_name.split('c')[0].replace('r', ''))} + translated_list.append(create_translate(dimensions)) + bpy.context.scene.node_tree.links.new(image_node.outputs[input], translated_list[index].inputs['Image']) + return translated_list +# Create a list of image nodes +image_nodes = [] for index, image in enumerate(filenames): - dimensions = {'X': int(image.split('c')[-1].split('_')[0]), 'Y': int(image.split('c')[0].replace('r', ''))} bpy.ops.image.open(filepath=renders + image, use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(image)] - image_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeImage') - image_node_translates.append(create_translate(index, dimensions)) - image_node.image = image - bpy.context.scene.node_tree.links.new(image_node.outputs['Combined'], image_node_translates[index].inputs['Image']) + image_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeImage')) + image_nodes[index].image = image + +# Create translates for Combined, Albedo and Normal and put them in a list +combined_translates = align_tiles("Combined") +if denoising: + albedo_translates = align_tiles("Denoising Albedo") + normal_translates = align_tiles("Denoising Normal") output_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeComposite') -denoising = "${settings.use_denoising}" == "true" alpha_over_nodes = [] albedo_mix_nodes = [] normal_mix_nodes = [] -for index, node in enumerate(image_node_translates): +for index, node in enumerate(combined_translates): if index == 0: # Take the first two image nodes and combine them alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) - bpy.context.scene.node_tree.links.new(image_node_translates[0].outputs['Image'], alpha_over_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_node_translates[1].outputs['Image'], alpha_over_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(combined_translates[0].outputs['Image'], alpha_over_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(combined_translates[1].outputs['Image'], alpha_over_nodes[index].inputs[2]) if denoising: albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) albedo_mix_nodes[index].blend_type = 'ADD' - bpy.context.scene.node_tree.links.new(image_node_translates[0].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_node_translates[1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(combined_translates[0].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(combined_translates[1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) normal_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) normal_mix_nodes[index].blend_type = 'ADD' - bpy.context.scene.node_tree.links.new(image_node_translates[0].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_node_translates[1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(combined_translates[0].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(combined_translates[1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) else: # Take one image node and the previous alpha over node alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) bpy.context.scene.node_tree.links.new(alpha_over_nodes[index-1].outputs['Image'], alpha_over_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_node_translates[index+1].outputs['Image'], alpha_over_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(combined_translates[index+1].outputs['Image'], alpha_over_nodes[index].inputs[2]) if denoising: albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) albedo_mix_nodes[index].blend_type = 'ADD' bpy.context.scene.node_tree.links.new(albedo_mix_nodes[index-1].outputs['Image'], albedo_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_node_translates[index+1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(combined_translates[index+1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) normal_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) normal_mix_nodes[index].blend_type = 'ADD' bpy.context.scene.node_tree.links.new(normal_mix_nodes[index-1].outputs['Image'], normal_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(image_node_translates[index+1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) - if index + 1 == len(image_node_translates) - 1: + bpy.context.scene.node_tree.links.new(combined_translates[index+1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) + if index + 1 == len(combined_translates) - 1: if denoising: denoise_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeDenoise') bpy.context.scene.node_tree.links.new(alpha_over_nodes[index].outputs['Image'], denoise_node.inputs['Image']) -- 2.30.2 From f6e18d53af9c9c300f3d0aa3fb67afb17004b999 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Sat, 1 Apr 2023 17:21:37 +0200 Subject: [PATCH 22/29] Final tweaks for denoising --- .../scripts/single_frame_blender_render.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 3d5460d6..b56c7f8e 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -213,12 +213,12 @@ for index, node in enumerate(combined_translates): if denoising: albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) albedo_mix_nodes[index].blend_type = 'ADD' - bpy.context.scene.node_tree.links.new(combined_translates[0].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(combined_translates[1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(albedo_translates[0].outputs['Image'], albedo_mix_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(albedo_translates[1].outputs['Image'], albedo_mix_nodes[index].inputs[2]) normal_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) normal_mix_nodes[index].blend_type = 'ADD' - bpy.context.scene.node_tree.links.new(combined_translates[0].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(combined_translates[1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(normal_translates[0].outputs['Image'], normal_mix_nodes[index].inputs[1]) + bpy.context.scene.node_tree.links.new(normal_translates[1].outputs['Image'], normal_mix_nodes[index].inputs[2]) else: # Take one image node and the previous alpha over node alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) @@ -228,11 +228,11 @@ for index, node in enumerate(combined_translates): albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) albedo_mix_nodes[index].blend_type = 'ADD' bpy.context.scene.node_tree.links.new(albedo_mix_nodes[index-1].outputs['Image'], albedo_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(combined_translates[index+1].outputs['Denoising Albedo'], albedo_mix_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(albedo_translates[index+1].outputs['Image'], albedo_mix_nodes[index].inputs[2]) normal_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) normal_mix_nodes[index].blend_type = 'ADD' bpy.context.scene.node_tree.links.new(normal_mix_nodes[index-1].outputs['Image'], normal_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(combined_translates[index+1].outputs['Denoising Normal'], normal_mix_nodes[index].inputs[2]) + bpy.context.scene.node_tree.links.new(normal_translates[index+1].outputs['Image'], normal_mix_nodes[index].inputs[2]) if index + 1 == len(combined_translates) - 1: if denoising: denoise_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeDenoise') -- 2.30.2 From 87581ebdb432b45a6535a7df2ea068420bde73cb Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Tue, 4 Apr 2023 15:55:04 +0200 Subject: [PATCH 23/29] Replace os with pathlib --- .../scripts/single_frame_blender_render.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index b56c7f8e..684231f1 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -97,7 +97,6 @@ function authorRenderTasks(settings, renderDir, renderOutput) { let pythonExpression = ` import bpy render = bpy.context.scene.render -render.engine = 'CYCLES' render.use_compositing = False bpy.context.scene.cycles.use_denoising = False render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' @@ -133,7 +132,6 @@ for layer in bpy.context.scene.view_layers: function authorCreateMergeTask(settings, renderOutput) { const task = author.Task(`merge`, "blender"); let pythonExpression = ` -import os import pathlib import bpy import math @@ -143,12 +141,12 @@ def normalize(number): basepath = "${renderOutput}/" renders = basepath + "tiles/" -filenames = [f for f in os.listdir(renders) if os.path.isfile(renders + f)] +filenames = list(pathlib.Path(renders).iterdir()) if len(filenames) <= 1: print('This job only has one file, merging not required.') - print('Moving ' + renders + filenames[0] + ' to ' + basepath + 'MERGED.exr') - pathlib.Path(renders + filenames[0]).rename(basepath + 'MERGED.exr') + print('Moving ' + renders + filenames[0].name + ' to ' + basepath + 'MERGED.exr') + pathlib.Path(renders + filenames[0].name).rename(basepath + 'MERGED.exr') exit() row_max = math.ceil(100 / ${settings.tile_size}) - 1 @@ -187,9 +185,9 @@ def align_tiles(input): # Create a list of image nodes image_nodes = [] -for index, image in enumerate(filenames): - bpy.ops.image.open(filepath=renders + image, use_sequence_detection=False) - image = bpy.data.images[bpy.path.basename(image)] +for index, file in enumerate(filenames): + bpy.ops.image.open(filepath=renders + file.name, use_sequence_detection=False) + image = bpy.data.images[bpy.path.basename(file.name)] image_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeImage')) image_nodes[index].image = image -- 2.30.2 From 22aedb6c3b28058952452ffc18a50d6940e21ba8 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Tue, 4 Apr 2023 16:06:35 +0200 Subject: [PATCH 24/29] Use helper variables for easier readability --- .../scripts/single_frame_blender_render.js | 87 ++++++++++--------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 684231f1..fdb21e32 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -101,10 +101,13 @@ render.use_compositing = False bpy.context.scene.cycles.use_denoising = False render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' render.use_crop_to_border = True -render.border_min_x = ${tile.column} * ${settings.tile_size} / 100 -render.border_max_x = ${settings.tile_size} / 100 + ${tile.column} * ${settings.tile_size} / 100 -render.border_min_y = ${tile.row} * ${settings.tile_size} / 100 -render.border_max_y = ${settings.tile_size} / 100 + ${tile.row} * ${settings.tile_size} / 100`; + +tile_size_normalized = ${settings.tile_size} / 100 + +render.border_min_x = ${tile.column} * tile_size_normalized +render.border_max_x = tile_size_normalized + ${tile.column} * tile_size_normalized +render.border_min_y = ${tile.row} * tile_size_normalized +render.border_max_y = tile_size_normalized + ${tile.row} * tile_size_normalized`; if (settings.use_denoising) { pythonExpression += ` for layer in bpy.context.scene.view_layers: @@ -151,19 +154,23 @@ if len(filenames) <= 1: row_max = math.ceil(100 / ${settings.tile_size}) - 1 -bpy.context.scene.render.resolution_x = ${settings.resolution_x} -bpy.context.scene.render.resolution_y = ${settings.resolution_y} -bpy.context.scene.render.resolution_percentage = ${settings.resolution_percentage} -bpy.context.scene.render.use_compositing = True -bpy.context.scene.use_nodes = True -bpy.context.scene.node_tree.nodes.clear() -bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' -bpy.context.scene.render.filepath = basepath + "MERGED" +bpy_render = bpy_render +bpy_scene = bpy.context.scene +node_tree = bpy_scene.node_tree + +bpy_render.resolution_x = ${settings.resolution_x} +bpy_render.resolution_y = ${settings.resolution_y} +bpy_render.resolution_percentage = ${settings.resolution_percentage} +bpy_render.use_compositing = True +bpy_scene.use_nodes = True +node_tree.nodes.clear() +bpy_render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' +bpy_render.filepath = basepath + "MERGED" denoising = "${settings.use_denoising}" == "true" # Takes the column and row number and creates a translate node with the right coordinates to align a tile def create_translate(dimensions): - translate_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeTranslate') + translate_node = node_tree.nodes.new(type='CompositorNodeTranslate') translate_node.use_relative = True for dimension in dimensions: if dimensions[dimension] == 0: @@ -180,7 +187,7 @@ def align_tiles(input): file_name = image_node.image.name_full dimensions = {'X': int(file_name.split('c')[-1].split('_')[0]), 'Y': int(file_name.split('c')[0].replace('r', ''))} translated_list.append(create_translate(dimensions)) - bpy.context.scene.node_tree.links.new(image_node.outputs[input], translated_list[index].inputs['Image']) + node_tree.links.new(image_node.outputs[input], translated_list[index].inputs['Image']) return translated_list # Create a list of image nodes @@ -188,7 +195,7 @@ image_nodes = [] for index, file in enumerate(filenames): bpy.ops.image.open(filepath=renders + file.name, use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(file.name)] - image_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeImage')) + image_nodes.append(node_tree.nodes.new(type='CompositorNodeImage')) image_nodes[index].image = image # Create translates for Combined, Albedo and Normal and put them in a list @@ -197,7 +204,7 @@ if denoising: albedo_translates = align_tiles("Denoising Albedo") normal_translates = align_tiles("Denoising Normal") -output_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeComposite') +output_node = node_tree.nodes.new(type='CompositorNodeComposite') alpha_over_nodes = [] albedo_mix_nodes = [] normal_mix_nodes = [] @@ -205,42 +212,42 @@ normal_mix_nodes = [] for index, node in enumerate(combined_translates): if index == 0: # Take the first two image nodes and combine them - alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) - bpy.context.scene.node_tree.links.new(combined_translates[0].outputs['Image'], alpha_over_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(combined_translates[1].outputs['Image'], alpha_over_nodes[index].inputs[2]) + alpha_over_nodes.append(node_tree.nodes.new(type='CompositorNodeAlphaOver')) + node_tree.links.new(combined_translates[0].outputs['Image'], alpha_over_nodes[index].inputs[1]) + node_tree.links.new(combined_translates[1].outputs['Image'], alpha_over_nodes[index].inputs[2]) if denoising: - albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) + albedo_mix_nodes.append(node_tree.nodes.new(type='CompositorNodeMixRGB')) albedo_mix_nodes[index].blend_type = 'ADD' - bpy.context.scene.node_tree.links.new(albedo_translates[0].outputs['Image'], albedo_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(albedo_translates[1].outputs['Image'], albedo_mix_nodes[index].inputs[2]) - normal_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) + node_tree.links.new(albedo_translates[0].outputs['Image'], albedo_mix_nodes[index].inputs[1]) + node_tree.links.new(albedo_translates[1].outputs['Image'], albedo_mix_nodes[index].inputs[2]) + normal_mix_nodes.append(node_tree.nodes.new(type='CompositorNodeMixRGB')) normal_mix_nodes[index].blend_type = 'ADD' - bpy.context.scene.node_tree.links.new(normal_translates[0].outputs['Image'], normal_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(normal_translates[1].outputs['Image'], normal_mix_nodes[index].inputs[2]) + node_tree.links.new(normal_translates[0].outputs['Image'], normal_mix_nodes[index].inputs[1]) + node_tree.links.new(normal_translates[1].outputs['Image'], normal_mix_nodes[index].inputs[2]) else: # Take one image node and the previous alpha over node - alpha_over_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeAlphaOver')) - bpy.context.scene.node_tree.links.new(alpha_over_nodes[index-1].outputs['Image'], alpha_over_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(combined_translates[index+1].outputs['Image'], alpha_over_nodes[index].inputs[2]) + alpha_over_nodes.append(node_tree.nodes.new(type='CompositorNodeAlphaOver')) + node_tree.links.new(alpha_over_nodes[index-1].outputs['Image'], alpha_over_nodes[index].inputs[1]) + node_tree.links.new(combined_translates[index+1].outputs['Image'], alpha_over_nodes[index].inputs[2]) if denoising: - albedo_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) + albedo_mix_nodes.append(node_tree.nodes.new(type='CompositorNodeMixRGB')) albedo_mix_nodes[index].blend_type = 'ADD' - bpy.context.scene.node_tree.links.new(albedo_mix_nodes[index-1].outputs['Image'], albedo_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(albedo_translates[index+1].outputs['Image'], albedo_mix_nodes[index].inputs[2]) - normal_mix_nodes.append(bpy.context.scene.node_tree.nodes.new(type='CompositorNodeMixRGB')) + node_tree.links.new(albedo_mix_nodes[index-1].outputs['Image'], albedo_mix_nodes[index].inputs[1]) + node_tree.links.new(albedo_translates[index+1].outputs['Image'], albedo_mix_nodes[index].inputs[2]) + normal_mix_nodes.append(node_tree.nodes.new(type='CompositorNodeMixRGB')) normal_mix_nodes[index].blend_type = 'ADD' - bpy.context.scene.node_tree.links.new(normal_mix_nodes[index-1].outputs['Image'], normal_mix_nodes[index].inputs[1]) - bpy.context.scene.node_tree.links.new(normal_translates[index+1].outputs['Image'], normal_mix_nodes[index].inputs[2]) + node_tree.links.new(normal_mix_nodes[index-1].outputs['Image'], normal_mix_nodes[index].inputs[1]) + node_tree.links.new(normal_translates[index+1].outputs['Image'], normal_mix_nodes[index].inputs[2]) if index + 1 == len(combined_translates) - 1: if denoising: - denoise_node = bpy.context.scene.node_tree.nodes.new(type='CompositorNodeDenoise') - bpy.context.scene.node_tree.links.new(alpha_over_nodes[index].outputs['Image'], denoise_node.inputs['Image']) - bpy.context.scene.node_tree.links.new(albedo_mix_nodes[index].outputs['Image'], denoise_node.inputs['Albedo']) - bpy.context.scene.node_tree.links.new(normal_mix_nodes[index].outputs['Image'], denoise_node.inputs['Normal']) - bpy.context.scene.node_tree.links.new(denoise_node.outputs['Image'], output_node.inputs['Image']) + denoise_node = node_tree.nodes.new(type='CompositorNodeDenoise') + node_tree.links.new(alpha_over_nodes[index].outputs['Image'], denoise_node.inputs['Image']) + node_tree.links.new(albedo_mix_nodes[index].outputs['Image'], denoise_node.inputs['Albedo']) + node_tree.links.new(normal_mix_nodes[index].outputs['Image'], denoise_node.inputs['Normal']) + node_tree.links.new(denoise_node.outputs['Image'], output_node.inputs['Image']) else: # Link the last image node and feed the output into the composite node - bpy.context.scene.node_tree.links.new(alpha_over_nodes[index].outputs['Image'], output_node.inputs['Image']) + node_tree.links.new(alpha_over_nodes[index].outputs['Image'], output_node.inputs['Image']) break bpy.ops.render.render(write_still=True)`; const command = author.Command("blender-render", { -- 2.30.2 From 89935c5865486f433bd5b95b129219d1841b215e Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Tue, 4 Apr 2023 16:25:36 +0200 Subject: [PATCH 25/29] More readability --- .../scripts/single_frame_blender_render.js | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index fdb21e32..3ccca8b2 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -102,12 +102,12 @@ bpy.context.scene.cycles.use_denoising = False render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' render.use_crop_to_border = True -tile_size_normalized = ${settings.tile_size} / 100 +tile_size_decimal = ${settings.tile_size} / 100 -render.border_min_x = ${tile.column} * tile_size_normalized -render.border_max_x = tile_size_normalized + ${tile.column} * tile_size_normalized -render.border_min_y = ${tile.row} * tile_size_normalized -render.border_max_y = tile_size_normalized + ${tile.row} * tile_size_normalized`; +render.border_min_x = ${tile.column} * tile_size_decimal +render.border_max_x = tile_size_decimal + ${tile.column} * tile_size_decimal +render.border_min_y = ${tile.row} * tile_size_decimal +render.border_max_y = tile_size_decimal + ${tile.row} * tile_size_decimal`; if (settings.use_denoising) { pythonExpression += ` for layer in bpy.context.scene.view_layers: @@ -168,17 +168,21 @@ bpy_render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' bpy_render.filepath = basepath + "MERGED" denoising = "${settings.use_denoising}" == "true" +tile_size_decimal = ${settings.tile_size} / 100 + # Takes the column and row number and creates a translate node with the right coordinates to align a tile def create_translate(dimensions): translate_node = node_tree.nodes.new(type='CompositorNodeTranslate') translate_node.use_relative = True for dimension in dimensions: if dimensions[dimension] == 0: - translate_node.inputs[dimension].default_value = normalize(${settings.tile_size} / 100 / 2) - elif dimensions[dimension] == row_max and 100 % ${settings.tile_size} != 0: - translate_node.inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + ((100 % ${settings.tile_size}) / 100) / 2) + translate_node.inputs[dimension].default_value = normalize(tile_size_decimal / 2) else: - translate_node.inputs[dimension].default_value = normalize((${settings.tile_size} / 100 + (dimensions[dimension] - 1) * ${settings.tile_size} / 100) + (${settings.tile_size} / 100) / 2) + if dimensions[dimension] == row_max and 100 % ${settings.tile_size} != 0: + half_this_tile = ((100 % ${settings.tile_size}) / 100) / 2 + else: + half_this_tile = (tile_size_decimal) / 2 + translate_node.inputs[dimension].default_value = normalize((tile_size_decimal + (dimensions[dimension] - 1) * tile_size_decimal) + half_this_tile) return translate_node def align_tiles(input): -- 2.30.2 From 201ad62b8329331487d015523bacf876a9427b4e Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Tue, 4 Apr 2023 16:30:26 +0200 Subject: [PATCH 26/29] Fix wrong variable definition --- .../job_compilers/scripts/single_frame_blender_render.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 3ccca8b2..d8106489 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -154,8 +154,8 @@ if len(filenames) <= 1: row_max = math.ceil(100 / ${settings.tile_size}) - 1 -bpy_render = bpy_render bpy_scene = bpy.context.scene +bpy_render = bpy_scene.render node_tree = bpy_scene.node_tree bpy_render.resolution_x = ${settings.resolution_x} -- 2.30.2 From cce0038efeee5b78ca51647520b4a71b1296bbff Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Sat, 8 Jul 2023 19:42:30 +0200 Subject: [PATCH 27/29] Use Path objects for file paths in the composite task --- .../scripts/single_frame_blender_render.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index d8106489..16958709 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -7,7 +7,7 @@ const JOB_TYPE = { { key: "frame", type: "int32", required: true, eval: "C.scene.frame_current", description: "Frame to render" }, { key: "tile_size", type: "int32", default: 5, propargs: {min: 1, max: 100}, description: "Tile size for each Task (sizes are in % of each dimension)" }, - + // render_output_root + add_path_components determine the value of render_output_path. { key: "render_output_root", type: "string", subtype: "dir_path", required: true, visible: "submission", description: "Base directory of where render output is stored. Will have some job-specific parts appended to it" }, @@ -274,7 +274,7 @@ function authorCreateCompositeTask(settings, renderOutput) { import pathlib import bpy C = bpy.context -basepath = "${renderOutput}/" +basepath = pathlib.Path("${renderOutput}") filename = "MERGED.exr" `; if (settings.use_compositing) { @@ -282,7 +282,7 @@ filename = "MERGED.exr" // uses snippets from // https://github.com/state-of-the-art/BlendNet/blob/master/BlendNet/script-compose.py#L94 pythonExpression += ` -bpy.ops.image.open(filepath=basepath + filename, use_sequence_detection=False) +bpy.ops.image.open(filepath=str(basepath / filename), use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(filename)] image_node = C.scene.node_tree.nodes.new(type='CompositorNodeImage') image_node.image = image @@ -317,21 +317,21 @@ print("Removing the nodes could potentially break the pipeline") for node in nodes_to_remove: print('INFO: Removing %s' % (node,)) C.scene.node_tree.nodes.remove(node) -C.scene.render.filepath = basepath + "FINAL" +C.scene.render.filepath = str(basepath / "FINAL") bpy.ops.render.render(write_still=True)`; } else { if (settings.format == "OPEN_EXR_MULTILAYER") { // Only rename pythonExpression += ` -pathlib.Path(basepath + filename).rename(basepath + 'FINAL.exr')`; +(basepath / filename).rename(basepath / 'FINAL.exr')`; } else { // Only export pythonExpression += ` -bpy.ops.image.open(filepath=basepath + filename, use_sequence_detection=False) +bpy.ops.image.open(filepath=str(basepath / filename), use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(filename)] -image.save_render(basepath + "FINAL" + "${settings.image_file_extension}")`; +image.save_render(str(basepath / "FINAL") + "${settings.image_file_extension}")`; } } const task = author.Task(`composite`, "blender"); -- 2.30.2 From 654d8aba8b948fbf0bd56dfc862b97b11edacdb5 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Sat, 8 Jul 2023 19:47:05 +0200 Subject: [PATCH 28/29] Fix using combined indent style --- .../scripts/single_frame_blender_render.js | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 16958709..7a1d4b0f 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -55,8 +55,8 @@ function compileJob(job) { mergeTask.addDependency(rt); } job.addTask(mergeTask); - compositeTask.addDependency(mergeTask); - job.addTask(compositeTask); + compositeTask.addDependency(mergeTask); + job.addTask(compositeTask); } // Do field replacement on the render output path. @@ -76,15 +76,15 @@ function renderOutputPath(job) { } function tileChunker(tile_size) { - let tiles = []; - const rows = Math.ceil(100 / tile_size); - const columns = Math.ceil(100 / tile_size); - for (let row = 0; row < rows; row++) { - for (let column = 0; column < columns; column++) { - tiles.push({"row": row, "column": column}); - } - } - return tiles; + let tiles = []; + const rows = Math.ceil(100 / tile_size); + const columns = Math.ceil(100 / tile_size); + for (let row = 0; row < rows; row++) { + for (let column = 0; column < columns; column++) { + tiles.push({"row": row, "column": column}); + } + } + return tiles; } function authorRenderTasks(settings, renderDir, renderOutput) { @@ -111,8 +111,8 @@ render.border_max_y = tile_size_decimal + ${tile.row} * tile_size_decimal`; if (settings.use_denoising) { pythonExpression += ` for layer in bpy.context.scene.view_layers: - layer['cycles']['denoising_store_passes'] = 1 - layer.use_pass_vector = True`; + layer['cycles']['denoising_store_passes'] = 1 + layer.use_pass_vector = True`; } const command = author.Command("blender-render", { exe: "{blender}", @@ -120,7 +120,7 @@ for layer in bpy.context.scene.view_layers: argsBefore: [], blendfile: settings.blendfile, args: [ - "--python-exit-code", 1, + "--python-exit-code", 1, "--python-expr", pythonExpression, "--render-output", path.join(renderDir, path.basename(renderOutput), "tiles", "r" + tile.row + "c" + tile.column + "_"), "--render-frame", settings.frame @@ -269,19 +269,19 @@ bpy.ops.render.render(write_still=True)`; } function authorCreateCompositeTask(settings, renderOutput) { - let filename; - let pythonExpression = ` + let filename; + let pythonExpression = ` import pathlib import bpy C = bpy.context basepath = pathlib.Path("${renderOutput}") filename = "MERGED.exr" `; - if (settings.use_compositing) { - // Do the full composite+export pipeline - // uses snippets from - // https://github.com/state-of-the-art/BlendNet/blob/master/BlendNet/script-compose.py#L94 - pythonExpression += ` + if (settings.use_compositing) { + // Do the full composite+export pipeline + // uses snippets from + // https://github.com/state-of-the-art/BlendNet/blob/master/BlendNet/script-compose.py#L94 + pythonExpression += ` bpy.ops.image.open(filepath=str(basepath / filename), use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(filename)] image_node = C.scene.node_tree.nodes.new(type='CompositorNodeImage') @@ -319,21 +319,21 @@ for node in nodes_to_remove: C.scene.node_tree.nodes.remove(node) C.scene.render.filepath = str(basepath / "FINAL") bpy.ops.render.render(write_still=True)`; - } - else { - if (settings.format == "OPEN_EXR_MULTILAYER") { - // Only rename - pythonExpression += ` + } + else { + if (settings.format == "OPEN_EXR_MULTILAYER") { + // Only rename + pythonExpression += ` (basepath / filename).rename(basepath / 'FINAL.exr')`; - } - else { - // Only export - pythonExpression += ` + } + else { + // Only export + pythonExpression += ` bpy.ops.image.open(filepath=str(basepath / filename), use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(filename)] image.save_render(str(basepath / "FINAL") + "${settings.image_file_extension}")`; - } - } + } + } const task = author.Task(`composite`, "blender"); const command = author.Command("blender-render", { exe: "{blender}", -- 2.30.2 From 75f8068eaef84fb9ed1051952bc9e23fb97f9978 Mon Sep 17 00:00:00 2001 From: "a13xie@Sodium" Date: Sun, 9 Jul 2023 13:23:54 +0200 Subject: [PATCH 29/29] Use Path objects for paths in the merge job --- .../scripts/single_frame_blender_render.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/manager/job_compilers/scripts/single_frame_blender_render.js b/internal/manager/job_compilers/scripts/single_frame_blender_render.js index 7a1d4b0f..8662b455 100644 --- a/internal/manager/job_compilers/scripts/single_frame_blender_render.js +++ b/internal/manager/job_compilers/scripts/single_frame_blender_render.js @@ -142,14 +142,14 @@ import math def normalize(number): return round(-0.5 + (number - 0) * (0.5 - -0.5) / 1 - 0, 10) -basepath = "${renderOutput}/" -renders = basepath + "tiles/" -filenames = list(pathlib.Path(renders).iterdir()) +basepath = pathlib.Path("${renderOutput}") +renders = basepath / "tiles" +filenames = list(renders.iterdir()) if len(filenames) <= 1: print('This job only has one file, merging not required.') - print('Moving ' + renders + filenames[0].name + ' to ' + basepath + 'MERGED.exr') - pathlib.Path(renders + filenames[0].name).rename(basepath + 'MERGED.exr') + print('Moving ' + str(renders / filenames[0].name) + ' to ' + str(basepath / 'MERGED.exr')) + (renders / filenames[0].name).rename(basepath / 'MERGED.exr') exit() row_max = math.ceil(100 / ${settings.tile_size}) - 1 @@ -165,7 +165,7 @@ bpy_render.use_compositing = True bpy_scene.use_nodes = True node_tree.nodes.clear() bpy_render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' -bpy_render.filepath = basepath + "MERGED" +bpy_render.filepath = str(basepath / "MERGED") denoising = "${settings.use_denoising}" == "true" tile_size_decimal = ${settings.tile_size} / 100 @@ -197,7 +197,7 @@ def align_tiles(input): # Create a list of image nodes image_nodes = [] for index, file in enumerate(filenames): - bpy.ops.image.open(filepath=renders + file.name, use_sequence_detection=False) + bpy.ops.image.open(filepath=str(renders / file.name), use_sequence_detection=False) image = bpy.data.images[bpy.path.basename(file.name)] image_nodes.append(node_tree.nodes.new(type='CompositorNodeImage')) image_nodes[index].image = image @@ -260,7 +260,7 @@ bpy.ops.render.render(write_still=True)`; argsBefore: [], blendfile: settings.blendfile, args: [ - "--python-exit-code", 1, + "--python-exit-code", 1, "--python-expr", pythonExpression ] }); @@ -341,7 +341,7 @@ image.save_render(str(basepath / "FINAL") + "${settings.image_file_extension}")` argsBefore: [], blendfile: settings.blendfile, args: [ - "--python-exit-code", 1, + "--python-exit-code", 1, "--python-expr", pythonExpression ] }); -- 2.30.2