feat: initial commit
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
// Schaeffler Turntable Animation job type for Flamenco 3.x
|
||||
// Pipeline: STEP -> STL (cadquery) -> Blender scene setup -> Blender -a render -> FFmpeg video
|
||||
//
|
||||
// Task flow:
|
||||
// 1. convert-step : STEP → STL via cadquery
|
||||
// 2. setup-scene : turntable_setup.py imports STL, applies materials/camera/animation,
|
||||
// saves a ready-to-render .blend to output_dir/scene.blend
|
||||
// 3. render-frames : blender --background scene.blend --python turntable_gpu_setup.py -a
|
||||
// Blender's native -a keeps GPU scene (BVH, textures) loaded for ALL
|
||||
// frames — no per-frame re-upload overhead.
|
||||
// 4. compose-video : FFmpeg encodes frame PNGs → MP4
|
||||
|
||||
const JOB_TYPE = {
|
||||
label: "Schaeffler Turntable",
|
||||
settings: [
|
||||
{ key: "step_path", type: "string", required: true,
|
||||
description: "Absolute path to STEP file" },
|
||||
{ key: "output_dir", type: "string", required: true,
|
||||
description: "Directory for rendered frames and final video" },
|
||||
{ key: "output_name", type: "string", required: true, default: "turntable",
|
||||
description: "Base name for output files" },
|
||||
{ key: "frame_count", type: "int32", default: 120,
|
||||
description: "Number of frames to render" },
|
||||
{ key: "fps", type: "int32", default: 30,
|
||||
description: "Frames per second for output video" },
|
||||
{ key: "turntable_degrees", type: "int32", default: 360,
|
||||
description: "Total rotation in degrees" },
|
||||
{ key: "width", type: "int32", default: 1920,
|
||||
description: "Output width in pixels" },
|
||||
{ key: "height", type: "int32", default: 1080,
|
||||
description: "Output height in pixels" },
|
||||
{ key: "engine", type: "string", default: "cycles",
|
||||
description: "Blender render engine: cycles or eevee" },
|
||||
{ key: "samples", type: "int32", default: 128,
|
||||
description: "Render samples" },
|
||||
{ key: "stl_quality", type: "string", default: "low",
|
||||
description: "STL mesh quality: low or high" },
|
||||
{ key: "part_colors_json", type: "string", default: "{}",
|
||||
description: "JSON dict mapping part names to hex colors" },
|
||||
{ key: "template_path", type: "string", default: "",
|
||||
description: "Path to .blend template file (empty = factory settings)" },
|
||||
{ key: "target_collection", type: "string", default: "Product",
|
||||
description: "Blender collection name to import geometry into" },
|
||||
{ key: "material_library_path", type: "string", default: "",
|
||||
description: "Path to material library .blend file" },
|
||||
{ key: "material_map_json", type: "string", default: "{}",
|
||||
description: "JSON dict mapping part names to material names" },
|
||||
{ key: "part_names_ordered_json", type: "string", default: "[]",
|
||||
description: "JSON array of STEP part names in solid order (for index-based matching)" },
|
||||
{ key: "lighting_only", type: "bool", default: false,
|
||||
description: "Use template only for World/HDRI lighting; always auto-frame with computed camera" },
|
||||
{ key: "cycles_device", type: "string", default: "auto",
|
||||
description: "Cycles compute device: auto (try GPU, fall back to CPU), gpu (force GPU), cpu (force CPU)" },
|
||||
{ key: "shadow_catcher", type: "bool", default: false,
|
||||
description: "Enable Shadowcatcher collection from template and position plane under product (Cycles only)" },
|
||||
{ key: "rotation_x", type: "float", default: 0.0,
|
||||
description: "Product rotation around X axis in degrees (render position)" },
|
||||
{ key: "rotation_y", type: "float", default: 0.0,
|
||||
description: "Product rotation around Y axis in degrees (render position)" },
|
||||
{ key: "rotation_z", type: "float", default: 0.0,
|
||||
description: "Product rotation around Z axis in degrees (render position)" },
|
||||
{ key: "turntable_axis", type: "string", default: "world_z",
|
||||
description: "Turntable rotation axis: world_z (default), world_x, or world_y" },
|
||||
{ key: "bg_color", type: "string", default: "",
|
||||
description: "Solid background hex color for compositing (e.g. #1a1a2e); empty = HDR visible as background" },
|
||||
{ key: "camera_orbit", type: "bool", default: true,
|
||||
description: "Rotate camera around product instead of rotating product (true = better GPU performance, BVH cached)" },
|
||||
{ key: "noise_threshold", type: "string", default: "",
|
||||
description: "Adaptive sampling noise threshold (empty = Blender default 0.01)" },
|
||||
{ key: "denoiser", type: "string", default: "",
|
||||
description: "Cycles denoiser: OPTIX, OPENIMAGEDENOISE, or empty for auto" },
|
||||
{ key: "denoising_input_passes", type: "string", default: "",
|
||||
description: "Denoising input passes: RGB, RGB_ALBEDO, RGB_ALBEDO_NORMAL, or empty for default" },
|
||||
{ key: "denoising_prefilter", type: "string", default: "",
|
||||
description: "Denoising prefilter: NONE, FAST, ACCURATE, or empty for default" },
|
||||
{ key: "denoising_quality", type: "string", default: "",
|
||||
description: "Denoising quality: HIGH, BALANCED, FAST, or empty for default (Blender 4.2+)" },
|
||||
{ key: "denoising_use_gpu", type: "string", default: "",
|
||||
description: "Route OIDN denoising through GPU: 1, 0, or empty for auto" },
|
||||
],
|
||||
};
|
||||
|
||||
function compileJob(job) {
|
||||
const settings = job.settings;
|
||||
// Cache STL next to STEP file: {step_dir}/{step_stem}_{quality}.stl
|
||||
const stepDir = settings.step_path.replace(/\/[^/]+$/, "");
|
||||
const stepBasename = settings.step_path.replace(/.*\//, "");
|
||||
const stepStem = stepBasename.replace(/\.[^.]+$/, "");
|
||||
const stlPath = stepDir + "/" + stepStem + "_" + settings.stl_quality + ".stl";
|
||||
const framesDir = settings.output_dir + "/frames";
|
||||
const scenePath = settings.output_dir + "/scene.blend";
|
||||
const videoPath = settings.output_dir + "/" + settings.output_name + ".mp4";
|
||||
|
||||
// Task 1: Convert STEP to STL
|
||||
const convertTask = author.Task("convert-step", "misc");
|
||||
convertTask.addCommand(author.Command("exec", {
|
||||
exe: "{python}",
|
||||
args: [
|
||||
"/opt/flamenco/scripts/convert_step.py",
|
||||
settings.step_path,
|
||||
stlPath,
|
||||
settings.stl_quality,
|
||||
],
|
||||
}));
|
||||
job.addTask(convertTask);
|
||||
|
||||
// Task 2: Setup Blender scene and save to scene.blend
|
||||
// turntable_setup.py imports the STL, assigns materials, sets up the
|
||||
// camera rig and pivot animation, configures the compositor (bg_color),
|
||||
// and saves the complete scene — ready for native -a rendering.
|
||||
const setupTask = author.Task("setup-scene", "blender");
|
||||
setupTask.addCommand(author.Command("exec", {
|
||||
exe: "{blender}",
|
||||
args: [
|
||||
"--background", "--python",
|
||||
"/opt/flamenco/scripts/turntable_setup.py",
|
||||
"--",
|
||||
stlPath,
|
||||
framesDir,
|
||||
String(settings.frame_count),
|
||||
String(settings.turntable_degrees),
|
||||
String(settings.width),
|
||||
String(settings.height),
|
||||
settings.engine,
|
||||
String(settings.samples),
|
||||
settings.part_colors_json,
|
||||
settings.template_path || "",
|
||||
settings.target_collection || "Product",
|
||||
settings.material_library_path || "",
|
||||
settings.material_map_json || "{}",
|
||||
settings.part_names_ordered_json || "[]",
|
||||
settings.lighting_only ? "1" : "0",
|
||||
settings.cycles_device || "gpu",
|
||||
settings.shadow_catcher ? "1" : "0",
|
||||
String(settings.rotation_x || 0),
|
||||
String(settings.rotation_y || 0),
|
||||
String(settings.rotation_z || 0),
|
||||
settings.turntable_axis || "world_z",
|
||||
settings.bg_color || "",
|
||||
settings.transparent_bg ? "1" : "0",
|
||||
scenePath,
|
||||
settings.camera_orbit !== false ? "1" : "0",
|
||||
settings.noise_threshold || "",
|
||||
settings.denoiser || "",
|
||||
settings.denoising_input_passes || "",
|
||||
settings.denoising_prefilter || "",
|
||||
settings.denoising_quality || "",
|
||||
settings.denoising_use_gpu || "",
|
||||
],
|
||||
}));
|
||||
setupTask.addDependency(convertTask);
|
||||
job.addTask(setupTask);
|
||||
|
||||
// Task 3: Render all frames using Blender's native -a (--render-anim)
|
||||
// turntable_gpu_setup.py re-applies GPU preferences (user-level, not stored
|
||||
// in .blend), then -a renders all frames in one process — GPU scene stays
|
||||
// loaded between frames, no per-frame BVH re-upload.
|
||||
const renderTask = author.Task("render-frames", "blender");
|
||||
renderTask.addCommand(author.Command("exec", {
|
||||
exe: "{blender}",
|
||||
args: [
|
||||
"--background",
|
||||
scenePath,
|
||||
"--python",
|
||||
"/opt/flamenco/scripts/turntable_gpu_setup.py",
|
||||
"-a",
|
||||
],
|
||||
}));
|
||||
renderTask.addDependency(setupTask);
|
||||
job.addTask(renderTask);
|
||||
|
||||
// Task 4: Compose video with FFmpeg
|
||||
// Blender writes transparent PNG frames (film_transparent=True) when bg_color is set.
|
||||
// FFmpeg composites them over a solid colour background using the lavfi color source.
|
||||
// Without bg_color, frames are opaque and encoded directly.
|
||||
const composeTask = author.Task("compose-video", "misc");
|
||||
const bgHex = (settings.bg_color || "").replace(/^#/, "");
|
||||
const ffmpegArgs = bgHex
|
||||
? [
|
||||
"-y",
|
||||
// Background: solid colour at video resolution and frame rate
|
||||
"-f", "lavfi",
|
||||
"-i", "color=c=0x" + bgHex + ":size=" + String(settings.width) + "x" + String(settings.height) + ":rate=" + String(settings.fps),
|
||||
// Foreground: transparent PNG frame sequence
|
||||
"-framerate", String(settings.fps),
|
||||
"-i", framesDir + "/frame_%04d.png",
|
||||
// Composite foreground over background
|
||||
"-filter_complex", "[0:v][1:v]overlay=0:0:shortest=1",
|
||||
"-c:v", "libx264",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-preset", "medium",
|
||||
"-crf", "18",
|
||||
videoPath,
|
||||
]
|
||||
: [
|
||||
"-y",
|
||||
"-framerate", String(settings.fps),
|
||||
"-i", framesDir + "/frame_%04d.png",
|
||||
"-c:v", "libx264",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-preset", "medium",
|
||||
"-crf", "18",
|
||||
videoPath,
|
||||
];
|
||||
composeTask.addCommand(author.Command("exec", {
|
||||
exe: "ffmpeg",
|
||||
args: ffmpegArgs,
|
||||
}));
|
||||
composeTask.addDependency(renderTask);
|
||||
job.addTask(composeTask);
|
||||
}
|
||||
Reference in New Issue
Block a user