(sneak preview BCon23 special)
a bit of knowledge helps...
1 Job | Highest level assignment |
N Tasks | Unit of work, assigned to one worker |
1…N Commands | High-ish level CLI invocation |
C:\Flamenco\Software
C:\Flamenco\Project
flamenco-manager
(in a terminal for non-Windows)
C:\Flamenco\Project
|
|
Approach | Simple | Efficient | Isolated |
---|---|---|---|
Work directly on the shared storage | ✅ | ✅ | ❌ |
Create a copy for each render job | ✅ | ❌ | ✅ |
Shaman Storage System | ❌ | ✅ | ✅ |
Linux & macOS | ✅ | Shaman enabled (except for the BCON23 build) |
Windows | ❌ | Shaman disabled |
Mixed OS Farm | Determined by OS of Manager |
flamenco-bcon23-demo.blend
to your desktopC:\Flamenco\Project
scripts/echo_sleep_test.js
JavaScript with a hint of Python
const JOB_TYPE = {
label: "Echo Sleep Test",
settings: [
{ key: "message", type: "string", required: true },
{ key: "sleep_duration_seconds", type: "int32", default: 1 },
{ key: "sleep_repeats", type: "int32", default: 1 },
]
};
function compileJob(job) {
const settings = job.settings;
const echoTask = author.Task("echo", "misc");
echoTask.addCommand(author.Command("echo", {message: settings.message}));
job.addTask(echoTask);
for (let repeat=0; repeat < settings.sleep_repeats; repeat++) {
const sleepTask = author.Task("sleep", "misc")
sleepTask.addCommand(author.Command("sleep", {duration_in_seconds: settings.sleep_duration_seconds}))
sleepTask.addDependency(echoTask); // Ensure sleeping happens after echo, and not at the same time.
job.addTask(sleepTask);
}
}
scripts/echo_sleep_test.js
const JOB_TYPE = {
label: "Simple Blender Render",
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"},
{ 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"},
{ 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, visible: "web",
description: "Path of the Blend file to render, set by the Blender add-on" },
{ key: "fps", type: "float", visible: "hidden",
eval: "C.scene.render.fps / C.scene.render.fps_base" },
{ key: "format", type: "string", required: true, visible: "web",
eval: "C.scene.render.image_settings.file_format" },
{ key: "image_file_extension", type: "string", required: true, visible: "hidden",
eval: "C.scene.render.file_extension", description: "File extension used when rendering images" },
{ key: "has_previews", type: "bool", required: false, visible: "hidden",
eval: "C.scene.render.image_settings.use_preview",
description: "Whether Blender will render EXR preview images"},
]
};
// 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);
const videoTask = authorVideoTask(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);
}
}
// 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.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;
}
function authorVideoTask(settings, renderDir) {
if (!settings.fps) {
print("Not authoring video task, no FPS known:", settings);
return;
}
const stem = path.stem(settings.blendfile);
const outfile = path.join(renderDir, `${stem}-${settings.frames}.mp4`);
const task = author.Task("preview-video", "ffmpeg");
const command = author.Command("frames-to-video", {
exe: "ffmpeg",
fps: settings.fps,
inputGlob: path.join(renderDir, `*${settings.image_file_extension}`),
outputFile: outfile,
args: [
"-c:v", "h264",
"-crf", "20",
"-g", "18",
"-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
"-pix_fmt", "yuv420p",
"-r", settings.fps,
"-y", // Be sure to always pass either "-n" or "-y".
],
});
task.addCommand(command);
return task;
}
simple_blender_render.js
scripts/simple_blender_render.js
to
scripts/my_render.js
label
in the JOB_TYPE
render_output_path
settinglike ${blender}
and ${blenderArgs}
Variable | OS | Value |
---|---|---|
blender | Linux | /shared/software/blender-4.0/blender |
Windows |
C:\Program Files\Blender\4.0\blender.exe
|
|
macOS |
/Applications/Blender.app/Contents/MacOS/Blender
|
|
blenderArgs |
-b -y
--python-expr "import bpy;
bpy.context.preferences.system.use_gpu_subdivision=False"
|
Variable | OS | Value |
---|---|---|
my_storage | Linux | /media/shared/flamenco |
Windows | S:\flamenco |
|
macOS | /Volumes/shared/flamenco |
macOS | Artist submits |
/Volumes/shared/flamenco/shot/file.blend
|
Manager stores |
{my_storage}/shot/file.blend
|
|
Windows | Worker gets |
S:\flamenco\shot\file.blend
|
Job Perspective
Worker Perspective
|
Get Involved: flamenco.blender.org |