Skip to content

Instantly share code, notes, and snippets.

@whoisryosuke
Created November 9, 2023 09:43
Show Gist options
  • Save whoisryosuke/a55912549c00a8c51c2137f691416426 to your computer and use it in GitHub Desktop.
Save whoisryosuke/a55912549c00a8c51c2137f691416426 to your computer and use it in GitHub Desktop.
Blender Flamenco - Render Frames and Sleep - Select a series of frames, sleep duration, and batch size -- and it'll render all frames then in batches while sleeping between. Good for chunking renders and letting PC rest between segments in a single job (instead of queuing chunks manually)
// SPDX-License-Identifier: GPL-3.0-or-later
const JOB_TYPE = {
label: "Render All and Sleep",
settings: [
// Settings for artists to determine:
{
key: "frames",
type: "string",
required: true,
eval: "f'{C.scene.frame_start}-{C.scene.frame_end}'",
evalInfo: {
showLinkButton: true,
description: "Scene frame range",
},
description:
"Frame range to render. Examples: '47', '1-30', '3, 5-10, 47-327'",
},
{
key: "chunk_size",
type: "int32",
default: 1,
description: "Number of frames to render in one Blender render task",
visible: "submission",
},
// 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",
},
// Sleep settings
{
key: "sleep_duration_seconds",
type: "int32",
default: 30,
description: "Number of seconds to sleep between frame batches",
},
{
key: "batch_size",
type: "int32",
default: 25,
description: "Number of frames to render before going to sleep",
visible: "submission",
},
// Automatically evaluated settings:
{
key: "blendfile",
type: "string",
required: true,
description: "Path of the Blend file to render",
visible: "web",
},
{
key: "fps",
type: "float",
eval: "C.scene.render.fps / C.scene.render.fps_base",
visible: "hidden",
},
{
key: "format",
type: "string",
required: true,
eval: "C.scene.render.image_settings.file_format",
visible: "web",
},
{
key: "image_file_extension",
type: "string",
required: true,
eval: "C.scene.render.file_extension",
visible: "hidden",
description: "File extension used when rendering images",
},
{
key: "has_previews",
type: "bool",
required: false,
eval: "C.scene.render.image_settings.use_preview",
visible: "hidden",
description: "Whether Blender will render preview 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.
]);
// File formats that would cause rendering to video.
// This is not supported by this job type.
const videoFormats = ["FFMPEG", "AVI_RAW", "AVI_JPEG"];
function compileJob(job) {
print("Blender Render job submitted");
print("job: ", job);
const settings = job.settings;
if (videoFormats.indexOf(settings.format) >= 0) {
throw `This job type only renders images, and not "${settings.format}"`;
}
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);
function newSleepTask() {
const sleepTask = author.Task("sleep", "misc");
sleepTask.addCommand(
author.Command("sleep", {
duration_in_seconds: settings.sleep_duration_seconds,
})
);
return sleepTask;
}
let sleepTask = newSleepTask();
let loopCount = 0;
for (const rt of renderTasks) {
job.addTask(rt);
// We add a sleep task in between a certain number of render tasks (aka rendering 1 frame usually)
if (sleepTask) {
// If there is a video task, all other tasks have to be done first.
sleepTask.addDependency(rt);
loopCount = loopCount + 1;
if (loopCount == settings.batch_size) {
loopCount = 0;
job.addTask(sleepTask);
sleepTask = newSleepTask();
}
}
}
}
// 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, ")");
print("frames", settings.frames);
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: "{blender}",
exeArgs: "{blenderArgs}",
argsBefore: [],
blendfile: settings.blendfile,
args: [
"--render-output",
path.join(renderDir, path.basename(renderOutput)),
"--render-format",
settings.format,
"--render-frame",
chunk.replaceAll("-", ".."), // Convert to Blender frame range notation.
],
});
task.addCommand(command);
renderTasks.push(task);
}
return renderTasks;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment