WIP: Single-frame job compiler #104194

Draft
k8ie wants to merge 30 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 5d11ce3219 - Show all commits

View File

@ -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;
}