Website: add Cycles/OPTIX/GPU job type

Add the latest version of the Blender Studio custom job type to the
website, as it does a few things others may find interesting.
This commit is contained in:
Sybren A. Stüvel 2024-05-16 12:28:50 +02:00
parent 44ffb09d7d
commit 80f7ae99b9
5 changed files with 370 additions and 5 deletions

View File

@ -4,14 +4,25 @@ weight: 30
--- ---
This section contains third-party job types for Flamenco. These have been This section contains third-party job types for Flamenco. These have been
submitted by the community. If you wish to contribute, consider joining the submitted by the community. If you wish to contribute your custom job type,
[Blender Chat channel][flamencochannel] and chime-in there. either
- join the [#flamenco Blender Chat channel][flamencochannel] and poke `@dr.sybren`, or
- write an [issue in the tracker][tracker] with your proposal.
## How can I create my own Job Type? ## How can I create my own Job Type?
This is described [Job Types][jobtypes]. It is recommended to use the This is described [Job Types][jobtypes]. It is recommended to use the
[built-in scripts][built-in-scripts] as examples and adjust them from there. [built-in scripts][built-in-scripts] as examples and adjust them from there.
## Installation
Each job type consists of a `.js` file. After downloading, place those in the
`scripts` directory next to the Flamenco Manager executable. Create the
directory if necessary. Then restart Flamenco Manager and in Blender press the
"Refresh from Manager" button.
## Third-Party Job Types ## Third-Party Job Types
{{< flamenco/toc-children >}} {{< flamenco/toc-children >}}
@ -19,3 +30,4 @@ This is described [Job Types][jobtypes]. It is recommended to use the
[jobtypes]: {{< ref "usage/job-types" >}} [jobtypes]: {{< ref "usage/job-types" >}}
[built-in-scripts]: https://projects.blender.org/studio/flamenco/src/branch/main/internal/manager/job_compilers/scripts [built-in-scripts]: https://projects.blender.org/studio/flamenco/src/branch/main/internal/manager/job_compilers/scripts
[flamencochannel]: https://blender.chat/channel/flamenco [flamencochannel]: https://blender.chat/channel/flamenco
[tracker]: https://projects.blender.org/studio/flamenco/issues

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,317 @@
// SPDX-License-Identifier: GPL-3.0-or-later
const JOB_TYPE = {
label: 'Cycles OPTIX GPU',
description:
'OPTIX GPU rendering + extra checkboxes for some experimental features + extra CLI args for Blender',
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',
},
{
key: 'experimental_gp3',
label: 'Experimental: GPv3',
description: 'Experimental Flag: Grease Pencil 3',
type: 'bool',
required: false,
},
{
key: 'experimental_new_anim',
label: 'Experimental: Baklava',
description: 'Experimental Flag: New Animation Data-block',
type: 'bool',
required: false,
},
// Extra CLI arguments for Blender, for debugging purposes.
{
key: 'blender_args_before',
label: 'Blender CLI args: Before',
description: 'CLI arguments for Blender, placed before the .blend filename',
type: 'string',
required: false,
},
{
key: 'blender_args_after',
label: 'After',
description: 'CLI arguments for Blender, placed after the .blend filename',
type: 'string',
required: false,
},
// 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);
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);
}
cleanupJobSettings(job.settings);
}
// 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;
}
});
}
const enable_all_optix = `
import bpy
cycles_prefs = bpy.context.preferences.addons['cycles'].preferences
cycles_prefs.compute_device_type = 'OPTIX'
for dev in cycles_prefs.get_devices_for_type('OPTIX'):
dev.use = (dev.type != 'CPU')
`;
const enable_experimental_common = `
import bpy
exp_prefs = bpy.context.preferences.experimental
`;
function authorRenderTasks(settings, renderDir, renderOutput) {
print('authorRenderTasks(', renderDir, renderOutput, ')');
// Extra arguments for Blender.
const blender_args_before = shellSplit(settings.blender_args_before);
const blender_args_after = shellSplit(settings.blender_args_after);
// More arguments for Blender, which will be the same for each task.
const task_invariant_args = [
'--python-expr',
enable_all_optix,
'--python-expr',
"import bpy; bpy.context.scene.cycles.device = 'GPU'",
'--render-output',
path.join(renderDir, path.basename(renderOutput)),
'--render-format',
settings.format,
].concat(blender_args_after);
// Add any experimental flags.
{
let py_code_to_join = [enable_experimental_common];
if (settings.experimental_gp3) {
py_code_to_join.push('exp_prefs.use_grease_pencil_version3 = True');
}
if (settings.experimental_new_anim) {
py_code_to_join.push('exp_prefs.use_animation_baklava = True');
}
// If it's not just the common code, at least one flag was enabled.
if (py_code_to_join.length > 1) {
task_invariant_args.push('--python-expr');
task_invariant_args.push(py_code_to_join.join('\n'));
}
}
// Construct a task for each chunk.
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: blender_args_before,
blendfile: settings.blendfile,
args: task_invariant_args.concat([
'--render-frame',
chunk.replaceAll('-', '..'), // Convert to Blender frame range notation.
]),
});
task.addCommand(command);
renderTasks.push(task);
}
return renderTasks;
}
function authorCreateVideoTask(settings, renderDir) {
const needsPreviews = ffmpegIncompatibleImageFormats.has(settings.format);
if (needsPreviews && !settings.has_previews) {
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 = needsPreviews ? '.jpg' : settings.image_file_extension;
const task = author.Task('preview-video', 'ffmpeg');
const command = author.Command('frames-to-video', {
exe: 'ffmpeg',
fps: settings.fps,
inputGlob: path.join(renderDir, `*${outfileExt}`),
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);
print(`Creating output video for ${settings.format}`);
return task;
}
// Clean up empty job settings so that they're no longer shown in the web UI.
function cleanupJobSettings(settings) {
const settings_to_check = [
'blender_args_before',
'blender_args_after',
'experimental_gp3',
'experimental_new_anim',
];
for (let setting_name of settings_to_check) {
if (!settings[setting_name]) delete settings[setting_name];
}
}

View File

@ -0,0 +1,36 @@
---
title: Cycles/OPTIX + Experimental
weight: 20
resources:
- name: screenshot
src: cycles-optix-gpu.webp
title: Screenshot of the Flamenco job submission panel in Blender
---
{{< flamenco/thirdPartyCompatibility blender="v4.2-alpha+" flamenco="v3.5+" >}}
Documented and maintained by [Sybren Stüvel][author].
Please report any issues at [Flamenco's tracker][tracker].
[author]: https://projects.blender.org/dr.sybren
[tracker]: https://projects.blender.org/studio/flamenco/issues
{{< /flamenco/thirdPartyCompatibility >}}
This job type is the most-used one at [Blender Studio](https://studio.blender.org/). It includes a few features:
- Always enable GPU rendering with OPTIX.
- Checkboxes to enable specific experimental flags.
- Extra input fields for arbitrary commandline arguments for Blender.
To use, download [cycles_optix_gpu.js](cycles_optix_gpu.js) and place it in the
`scripts` directory next to the Flamenco Manager executable. Create the
directory if necessary. Then restart Flamenco Manager and in Blender press the
"Refresh from Manager" button.
<style>
figure {
width: 30em;
}
</style>
{{< img name="screenshot" size="medium" >}}

View File

@ -224,13 +224,13 @@ table tbody td {
margin: 0.6ex; margin: 0.6ex;
} }
.compatibility-box dl { .compatibility-box dl {
flex-basis: 20%; flex-basis: 30%;
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
} }
.compatibility-box dl dt { .compatibility-box dl dt {
margin: 0; margin: 0;
flex-basis: 66%; flex-basis: 55%;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
text-align: right; text-align: right;
} }
@ -238,7 +238,7 @@ table tbody td {
content: ":"; content: ":";
} }
.compatibility-box dl dd { .compatibility-box dl dd {
flex-basis: 33%; flex-basis: 45%;
flex-grow: 1; flex-grow: 1;
margin: 0; margin: 0;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;