flamenco/internal/manager/job_compilers/scripts/simple_blender_render.js
Sybren A. Stüvel 7bfde1df0b Manager: determine final render output path in job compiler
This might not be the best way to do things, but it is very flexible and
allows TDs to determine the behaviour in their own job compiler script.
It doesn't allow a preview of "this is what the final render path will be"
in the Blender GUI though.
2022-03-15 13:17:55 +01:00

168 lines
6.5 KiB
JavaScript

// SPDX-License-Identifier: GPL-3.0-or-later
const JOB_TYPE = {
label: "Simple Blender Render",
settings: [
// Settings for artists to determine:
{ key: "chunk_size", type: "int32", default: 1, description: "Number of frames to render in one Blender render task" },
{ key: "frames", type: "string", required: true, eval: "f'{C.scene.frame_start}-{C.scene.frame_end}'",
description: "Frame range to render. Examples: '47', '1-30', '3, 5-10, 47-327'" },
{ key: "render_output_root", type: "string", subtype: "dir_path", required: true,
description: "Base directory of where render output is stored. Will have some job-specific parts appended to it"},
// Automatically evaluated settings:
{ key: "blender_cmd", type: "string", default: "{blender}", visible: false },
{ key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: false },
{ key: "render_output_path", type: "string", subtype: "file_path", visible: false,
description: "Final file path of where render output is stored, set by the job compiler"},
{ key: "fps", type: "float", eval: "C.scene.render.fps / C.scene.render.fps_base", visible: false },
{
key: "images_or_video",
type: "string",
required: true,
choices: ["images", "video"],
visible: false,
eval: "'video' if C.scene.render.image_settings.file_format in {'FFMPEG', 'AVI_RAW', 'AVI_JPEG'} else 'images'"
},
{ key: "format", type: "string", required: true, eval: "C.scene.render.image_settings.file_format", visible: false },
{ key: "image_file_extension", type: "string", required: true, eval: "C.scene.render.file_extension", visible: false,
description: "File extension used when rendering images; ignored when images_or_video='video'" },
{ key: "video_container_format", type: "string", required: true, eval: "C.scene.render.ffmpeg.format", visible: false,
description: "Container format used when rendering video; ignored when images_or_video='images'" },
]
};
// Set of scene.render.image_settings.file_format values that produce
// files which FFmpeg is known not to handle as input.
const ffmpegIncompatibleImageFormats = new Set([
"EXR",
"MULTILAYER", // Old CLI-style format indicators
"OPEN_EXR",
"OPEN_EXR_MULTILAYER", // DNA values for these formats.
]);
// Mapping from video container (scene.render.ffmpeg.format) to the file name
// extension typically used to store those videos.
const videoContainerToExtension = {
"QUICKTIME": ".mov",
"MPEG1": ".mpg",
"MPEG2": ".dvd",
"MPEG4": ".mp4",
"OGG": ".ogv",
"FLASH": ".flv",
};
function compileJob(job) {
print("Blender Render job submitted");
const renderOutput = renderOutputPath(job);
job.settings.render_output_path = renderOutput;
print("job: ", job);
const finalDir = path.dirname(renderOutput);
const renderDir = intermediatePath(job, finalDir);
const settings = job.settings;
const renderTasks = authorRenderTasks(settings, renderDir, renderOutput);
const videoTask = authorCreateVideoTask(settings, renderDir);
for (const rt of renderTasks) {
job.addTask(rt);
}
if (videoTask) {
// If there is a video task, all other tasks have to be done first.
for (const rt of renderTasks) {
videoTask.addDependency(rt);
}
job.addTask(videoTask);
}
}
// Return the intended render output path.
function renderOutputPath(job) {
// {DIR}/{job name}/{date and time of job submission}/######.{extension}
const pathSafeJobName = job.name.replace(/[/\\:?*]/, "-");
const extension = guessOutputFileExtension(job.settings);
return path.join(
job.settings.render_output_root,
pathSafeJobName,
formatTimestampLocal(job.created),
`######${extension}`
);
}
// Determine the intermediate render output path.
function intermediatePath(job, finalDir) {
const basename = path.basename(finalDir);
const name = `${basename}__intermediate-${formatTimestampLocal(job.created)}`;
return path.join(path.dirname(finalDir), name);
}
function authorRenderTasks(settings, renderDir, renderOutput) {
print("authorRenderTasks(", renderDir, renderOutput, ")");
let renderTasks = [];
let chunks = frameChunker(settings.frames, settings.chunk_size);
for (let chunk of chunks) {
const task = author.Task(`render-${chunk}`, "blender");
const command = author.Command("blender-render", {
exe: settings.blender_cmd,
argsBefore: [],
blendfile: settings.blendfile,
args: [
"--render-output", path.join(renderDir, path.basename(renderOutput)),
"--render-format", settings.format,
"--render-frame", chunk,
]
});
task.addCommand(command);
renderTasks.push(task);
}
return renderTasks;
}
function authorCreateVideoTask(settings, renderDir) {
if (settings.images_or_video == "video") {
print("Not authoring video task, render output is already a video");
}
if (ffmpegIncompatibleImageFormats.has(settings.format)) {
print("Not authoring video task, FFmpeg-incompatible render output")
return;
}
if (!settings.fps) {
print("Not authoring video task, no FPS known:", settings);
return;
}
const stem = path.stem(settings.blendfile).replace('.flamenco', '');
const outfile = path.join(renderDir, `${stem}-${settings.frames}.mp4`);
const outfileExt = guessOutputFileExtension(settings);
const task = author.Task('create-video', 'ffmpeg');
const command = author.Command("create-video", {
input_files: path.join(renderDir, `*${outfileExt}`),
output_file: outfile,
fps: settings.fps,
});
task.addCommand(command);
print(`Creating output video for ${settings.format}`);
return task;
}
// Return file name extension, including period, like '.png' or '.mkv'.
function guessOutputFileExtension(settings) {
switch (settings.images_or_video) {
case "images":
return settings.image_file_extension;
case "video":
const container = settings.video_container_format;
if (container in videoContainerToExtension) {
return videoContainerToExtension[container];
}
return "." + container.lower();
default:
throw `invalid setting images_or_video: "${settings.images_or_video}"`
}
}