diff --git a/web/project-website/content/third-party-jobs/_index.md b/web/project-website/content/third-party-jobs/_index.md index 6fc4cbff..0c2824f8 100644 --- a/web/project-website/content/third-party-jobs/_index.md +++ b/web/project-website/content/third-party-jobs/_index.md @@ -4,14 +4,25 @@ weight: 30 --- This section contains third-party job types for Flamenco. These have been -submitted by the community. If you wish to contribute, consider joining the -[Blender Chat channel][flamencochannel] and chime-in there. +submitted by the community. If you wish to contribute your custom job type, +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? 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. +## 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 {{< flamenco/toc-children >}} @@ -19,3 +30,4 @@ This is described [Job Types][jobtypes]. It is recommended to use the [jobtypes]: {{< ref "usage/job-types" >}} [built-in-scripts]: https://projects.blender.org/studio/flamenco/src/branch/main/internal/manager/job_compilers/scripts [flamencochannel]: https://blender.chat/channel/flamenco +[tracker]: https://projects.blender.org/studio/flamenco/issues diff --git a/web/project-website/content/third-party-jobs/cycles-optix-gpu/cycles-optix-gpu.webp b/web/project-website/content/third-party-jobs/cycles-optix-gpu/cycles-optix-gpu.webp new file mode 100644 index 00000000..aae940fe Binary files /dev/null and b/web/project-website/content/third-party-jobs/cycles-optix-gpu/cycles-optix-gpu.webp differ diff --git a/web/project-website/content/third-party-jobs/cycles-optix-gpu/cycles_optix_gpu.js b/web/project-website/content/third-party-jobs/cycles-optix-gpu/cycles_optix_gpu.js new file mode 100644 index 00000000..35bb98cd --- /dev/null +++ b/web/project-website/content/third-party-jobs/cycles-optix-gpu/cycles_optix_gpu.js @@ -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]; + } +} diff --git a/web/project-website/content/third-party-jobs/cycles-optix-gpu/index.md b/web/project-website/content/third-party-jobs/cycles-optix-gpu/index.md new file mode 100644 index 00000000..eae3d475 --- /dev/null +++ b/web/project-website/content/third-party-jobs/cycles-optix-gpu/index.md @@ -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. + + + +{{< img name="screenshot" size="medium" >}} diff --git a/web/project-website/static/custom.css b/web/project-website/static/custom.css index 5d37395f..b7213283 100644 --- a/web/project-website/static/custom.css +++ b/web/project-website/static/custom.css @@ -224,13 +224,13 @@ table tbody td { margin: 0.6ex; } .compatibility-box dl { - flex-basis: 20%; + flex-basis: 30%; display: flex; flex-flow: row wrap; } .compatibility-box dl dt { margin: 0; - flex-basis: 66%; + flex-basis: 55%; padding: 0.2em 0.4em; text-align: right; } @@ -238,7 +238,7 @@ table tbody td { content: ":"; } .compatibility-box dl dd { - flex-basis: 33%; + flex-basis: 45%; flex-grow: 1; margin: 0; padding: 0.2em 0.4em;