WIP: Initial version of a single-frame job compiler #104189

Closed
k8ie wants to merge 26 commits from k8ie/flamenco:single-frame into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
Showing only changes of commit 53d535ad5d - Show all commits

View File

@ -6,7 +6,7 @@ const JOB_TYPE = {
// Settings for artists to determine: // Settings for artists to determine:
{ key: "frame", type: "int32", required: true, eval: "C.scene.frame_current", { key: "frame", type: "int32", required: true, eval: "C.scene.frame_current",
description: "Frame to render"}, 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" }, visible: "submission" },
{ key: "denoising", type: "bool", required: true, default: false, { key: "denoising", type: "bool", required: true, default: false,
description: "Toggles OpenImageDenoise" }, description: "Toggles OpenImageDenoise" },
k8ie marked this conversation as resolved Outdated

I think this can be moved into the hidden settings, with expr: "bpy.context.scene.cycles.use_denoising". That way the denoising is managed via the normal setting, and not yet again by Flamenco.

I think this can be moved into the hidden settings, with `expr: "bpy.context.scene.cycles.use_denoising"`. That way the denoising is managed via the normal setting, and not yet again by Flamenco.
@ -23,6 +23,10 @@ const JOB_TYPE = {
// Automatically evaluated settings: // Automatically evaluated settings:
{ key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: "web" }, { 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" },
k8ie marked this conversation as resolved Outdated

The properties are typically named as 'imperative' ("do X", "use Y") and not 'descriptive' ("does X", "uses Y"). So this could be use_compositing.

The properties are typically named as 'imperative' ("do X", "use Y") and not 'descriptive' ("does X", "uses Y"). So this could be `use_compositing`.
Outdated
Review

Should I change denoising to be use_denoising or use_compositing to be just compositing? Just to keep things consistent.

Should I change `denoising` to be `use_denoising` or `use_compositing` to be just `compositing`? Just to keep things consistent.

Use use_denoising and use_compositing.

Use `use_denoising` and `use_compositing`.
{ 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", { key: "samples", type: "string", required: true, eval: "f'1-{C.scene.cycles.samples}'", visible: "web",
description: "Total number of samples in the job" }, description: "Total number of samples in the job" },
] ]
@ -43,6 +47,7 @@ function compileJob(job) {
const renderTasks = authorRenderTasks(settings, renderDir, renderOutput); const renderTasks = authorRenderTasks(settings, renderDir, renderOutput);
const mergeTask = authorCreateMergeTask(settings, renderOutput); const mergeTask = authorCreateMergeTask(settings, renderOutput);
const denoiseTask = authorCreateDenoiseTask(settings, renderOutput); const denoiseTask = authorCreateDenoiseTask(settings, renderOutput);
const compositeTask = authorCreateCompositeTask(settings, renderOutput);
for (const rt of renderTasks) { for (const rt of renderTasks) {
job.addTask(rt); job.addTask(rt);
@ -57,7 +62,12 @@ function compileJob(job) {
if (denoiseTask) { if (denoiseTask) {
denoiseTask.addDependency(mergeTask); denoiseTask.addDependency(mergeTask);
job.addTask(denoiseTask); job.addTask(denoiseTask);
compositeTask.addDependency(denoiseTask);
} }
else {
compositeTask.addDependency(mergeTask);
}
job.addTask(compositeTask);
} }
// Do field replacement on the render output path. // 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); let chunks = frameChunker(settings.samples, settings.chunk_size);
for (let chunk of chunks) { for (let chunk of chunks) {
const task = author.Task(`render-${chunk}`, "blender"); const task = author.Task(`render-${chunk}`, "blender");
let chunk_arr = chunk.split("-", 2) let chunk_arr = chunk.split("-", 2);
let chunk_end let chunk_end;
let chunk_start let chunk_start;
if (chunk_arr.length < 2) { if (chunk_arr.length < 2) {
chunk_end = chunk_arr[0] chunk_end = chunk_arr[0];
chunk_start = chunk_arr[0] - 1 chunk_start = chunk_arr[0] - 1;
} }
else { else {
chunk_end = chunk_arr[1] chunk_end = chunk_arr[1];
chunk_start = chunk_arr[0] - 1 chunk_start = chunk_arr[0] - 1;
} }
let chunk_size = chunk_end - chunk_start let chunk_size = chunk_end - chunk_start;
let pythonExpression = ` let pythonExpression = `
import bpy import bpy
bpy.context.scene.render.engine = 'CYCLES' bpy.context.scene.render.engine = 'CYCLES'
@ -115,6 +125,7 @@ for layer in bpy.context.scene.view_layers:
argsBefore: [], argsBefore: [],
blendfile: settings.blendfile, blendfile: settings.blendfile,
args: [ args: [
"--python-exit-code", 1,
"--python-expr", pythonExpression, "--python-expr", pythonExpression,
"--render-output", path.join(renderDir, path.basename(renderOutput), chunk), "--render-output", path.join(renderDir, path.basename(renderOutput), chunk),
"--render-frame", settings.frame "--render-frame", settings.frame
@ -170,6 +181,7 @@ pathlib.Path(tmp + str(index-1)+'.exr').rename(basepath + 'MERGED.exr')`;
argsBefore: [], argsBefore: [],
blendfile: settings.blendfile, blendfile: settings.blendfile,
args: [ args: [
"--python-exit-code", 1,
"--python-expr", pythonExpression "--python-expr", pythonExpression
] ]
}); });
@ -192,6 +204,97 @@ bpy.ops.cycles.denoise_animation(input_filepath="${renderOutput}/MERGED.exr", ou
argsBefore: [], argsBefore: [],
blendfile: settings.blendfile, blendfile: settings.blendfile,
args: [ 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 "--python-expr", pythonExpression
] ]
}); });