WIP: Initial version of a single-frame job compiler #104189
@ -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" },
|
||||||
k8ie marked this conversation as resolved
Outdated
|
|||||||
|
|
||||||
|
|
||||||
|
// 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" },
|
||||||
k8ie marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
The properties are typically named as 'imperative' ("do X", "use Y") and not 'descriptive' ("does X", "uses Y"). So this could be The properties are typically named as 'imperative' ("do X", "use Y") and not 'descriptive' ("does X", "uses Y"). So this could be `use_compositing`.
k8ie
commented
Should I change Should I change `denoising` to be `use_denoising` or `use_compositing` to be just `compositing`? Just to keep things consistent.
Sybren A. Stüvel
commented
Use Use `use_denoising` and `use_compositing`.
|
|||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user
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.