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" },
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user