CC-BY, Blender Studio (2022)
Blender Conference 2022
Sintel, Blender Studio (2010)

Flamenco v3.3

a Hands-on Class with dr. Sybren Stüvel

Overview

  • Getting Things Running
  • Storage Options
  • Custom Job Types
  • Variables & Cross-Platform Support
  • Conclusion / Q&A

Download Flamenco + blend file

Just download, instructions will follow

(sneak preview BCon23 special)

https://flamenco.blender.org/bcon23

Getting Things Running

a bit of knowledge helps...

Job Structure

1 Job Highest level assignment
N Tasks Unit of work, assigned to one worker
1…N Commands High-ish level CLI invocation

Install & First Run

  1. Download & Extract Flamenco to C:\Flamenco\Software
  2. Ensure Blender 3.6+ is installed
  3. Copy the demo blend file to C:\Flamenco\Project
  4. Run flamenco-manager (in a terminal for non-Windows)
  5. Set the Shared Storage to C:\Flamenco\Project
  6. Flamenco Manager should find Blender by itself

https://flamenco.blender.org/bcon23

Blender add-on

  1. Start Blender
  2. Edit > Preferences
  3. Install & enable Flamenco add-on
  4. Open up the add-on panel
  5. Leave the URL as-is
  6. Click the refresh button
  7. Close Preferences window

https://beeldbellen.vc4all.nl/flamenco

Submit your Job

  1. Open the demo Blend file
  2. Properties Editor  »  Scene  »  Flamenco 3
  3. Fetch Job Types & choose Simple Blender Render
  4. Render Output Root: Some directory on your computer
  5. Submit to Flamenco
  6. Start your Worker
    ./flamenco-worker
    (in a Terminal for non-Windows)
  7. Check the Manager web interface

Storage

  • Simple
  • Efficient
  • Isolated

Pick two

Storage

Approach Simple Efficient Isolated
Work directly on the shared storage
Create a copy for each render job
Shaman Storage System

Shaman

  • Identify files by their SHA256 checksum
  • Only upload new files
  • Creates directories, symlinks files

Storage Defaults

Linux & macOS Shaman enabled (except for the BCON23 build)
Windows Shaman disabled
Mixed OS Farm Determined by OS of Manager

Storage Experiment

  1. Copy flamenco-bcon23-demo.blend to your desktop
    (so not on the shared storage)
  2. Submit the file to Flamenco
  3. Inspect C:\Flamenco\Project

Job Types

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

Create your Own

  1. Copy scripts/simple_blender_render.js to scripts/my_render.js
  2. Open the file in a text editor
  3. Change the label in the JOB_TYPE
  4. Change the render_output_path setting
  5. Restart Flamenco Manager (Ctrl+C to stop it cleanly)
  6. Refresh the job types in Blender
  7. Submit a file & see your new code at work!

Variables

like ${blender} and ${blenderArgs}

Variables

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"

Two-Way Variables

for mixed-platform farms

Two-Way Variables

Variable OS Value
my_storage Linux /media/shared/flamenco
Windows S:\flamenco
macOS /Volumes/shared/flamenco

Two-Way Variables

macOS Artist submits /Volumes/shared/flamenco/shot/file.blend
Manager stores {my_storage}/shot/file.blend
Windows Worker gets S:\flamenco\shot\file.blend

Worker Tags

Job Perspective

  • A job can have one tag, or no tag.
  • Tagged → only for workers with that tag.
  • Untagged → any worker.

Worker Perspective

  • A worker can have any number of tags.
  • Tagged ← only jobs with one of those tags, and tagless jobs.
  • Untagged ← only tagless jobs.

Not Presented Here

  • Worker Sleep Schedule
  • Blocklisting & auto-requeueing for failing workers
  • Project Finders
  • Systemd integration on Linux

Thank You!

Any Questions?

Get Involved: flamenco.blender.org