feat: cinematic highlight render — 20s procedural camera animation
New render type: 4-segment cinematic camera animation (480 frames @ 24fps) for professional product highlight videos. Camera sequence: 1. Establishing (5s): slow 45° orbit + push-in, 50mm lens 2. Detail sweep (5s): low-angle close arc, 85mm telephoto, shallow DOF 3. Crane up (5s): rising 30°→60°, 35mm wide reveal, pull-back 4. Hero close (5s): push-in to beauty angle, 65mm, smooth ease-out Technical: - cinematic_render.py: procedural camera from bounding sphere, cubic easing, per-frame keyframes (location, rotation, focal length, DOF) - render_cinematic_to_file(): service function (same pattern as turntable) - Pipeline routing: render_settings.cinematic flag → cinematic path - Depth of field enabled (f-stop scales with product size) - use_persistent_data for BVH caching between frames - Same material/template/USD pipeline as turntable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -300,6 +300,13 @@ def render_order_line_task(self, order_line_id: str):
|
||||
# Determine if this is an animation output type
|
||||
is_animation = bool(line.output_type and getattr(line.output_type, 'is_animation', False))
|
||||
|
||||
# Detect cinematic render type (render_settings.cinematic flag)
|
||||
is_cinematic = bool(
|
||||
line.output_type and
|
||||
line.output_type.render_settings and
|
||||
line.output_type.render_settings.get("cinematic")
|
||||
)
|
||||
|
||||
# Determine output format/extension
|
||||
out_ext = "jpg"
|
||||
if line.output_type and line.output_type.output_format:
|
||||
@@ -434,7 +441,75 @@ def render_order_line_task(self, order_line_id: str):
|
||||
|
||||
tmpl_info = f" template={template.name}" if template else ""
|
||||
|
||||
if is_animation:
|
||||
if is_cinematic:
|
||||
# ── Cinematic highlight animation path ──────────────────────
|
||||
_cine_fps = 24
|
||||
_cine_frames = 480
|
||||
emit(order_line_id, f"Starting cinematic render: {_cine_frames} frames @ {_cine_fps}fps, {render_width or 1920}x{render_height or 1080}{tmpl_info}")
|
||||
pl.step_start("blender_cinematic", {"frame_count": _cine_frames, "fps": _cine_fps})
|
||||
from app.services.render_blender import is_blender_available, render_cinematic_to_file
|
||||
if not is_blender_available():
|
||||
raise RuntimeError("Blender not available on this worker")
|
||||
|
||||
from app.services.step_processor import _get_all_settings
|
||||
_sys = _get_all_settings()
|
||||
try:
|
||||
service_data = render_cinematic_to_file(
|
||||
step_path=_Path(cad_file.stored_path),
|
||||
output_path=_Path(output_path),
|
||||
width=render_width or 1920,
|
||||
height=render_height or 1080,
|
||||
engine=render_engine or _sys.get("blender_engine", "cycles"),
|
||||
samples=render_samples or int(_sys.get(f"blender_{render_engine or _sys.get('blender_engine','cycles')}_samples", 128)),
|
||||
smooth_angle=int(_sys.get("blender_smooth_angle", 30)),
|
||||
cycles_device=cycles_device_val,
|
||||
transparent_bg=transparent_bg,
|
||||
part_colors=part_colors or None,
|
||||
template_path=template.blend_file_path if template else None,
|
||||
target_collection=template.target_collection if template else "Product",
|
||||
material_library_path=material_library if use_materials else None,
|
||||
material_map=material_map,
|
||||
part_names_ordered=part_names_ordered,
|
||||
lighting_only=bool(template.lighting_only) if template else False,
|
||||
shadow_catcher=bool(template.shadow_catcher_enabled) if template else False,
|
||||
rotation_x=rotation_x,
|
||||
rotation_y=rotation_y,
|
||||
rotation_z=rotation_z,
|
||||
usd_path=usd_render_path,
|
||||
focal_length_mm=focal_length_mm,
|
||||
sensor_width_mm=sensor_width_mm,
|
||||
material_override=override_mat,
|
||||
)
|
||||
success = True
|
||||
render_log = {
|
||||
"renderer": "blender",
|
||||
"type": "cinematic",
|
||||
"format": "mp4",
|
||||
"engine": render_engine or _sys.get("blender_engine", "cycles"),
|
||||
"engine_used": service_data.get("engine_used", "cycles"),
|
||||
"samples": render_samples,
|
||||
"cycles_device": cycles_device_val,
|
||||
"width": render_width or 1920,
|
||||
"height": render_height or 1080,
|
||||
"frame_count": service_data.get("frame_count", _cine_frames),
|
||||
"fps": _cine_fps,
|
||||
"total_duration_s": service_data.get("total_duration_s"),
|
||||
"stl_duration_s": service_data.get("stl_duration_s"),
|
||||
"render_duration_s": service_data.get("render_duration_s"),
|
||||
"ffmpeg_duration_s": service_data.get("ffmpeg_duration_s"),
|
||||
"stl_size_bytes": service_data.get("stl_size_bytes"),
|
||||
"output_size_bytes": service_data.get("output_size_bytes"),
|
||||
"log_lines": service_data.get("log_lines", []),
|
||||
}
|
||||
if template:
|
||||
render_log["template"] = template.blend_file_path
|
||||
pl.step_done("blender_cinematic")
|
||||
except Exception as exc:
|
||||
success = False
|
||||
render_log = {"renderer": "blender", "type": "cinematic", "error": str(exc)[:500]}
|
||||
pl.step_error("blender_cinematic", str(exc), exc)
|
||||
logger.error("Cinematic render failed for %s: %s", order_line_id, exc)
|
||||
elif is_animation:
|
||||
# ── Turntable animation path ────────────────────────────────
|
||||
emit(order_line_id, f"Starting turntable render: {frame_count} frames @ {fps}fps, {render_width or 1920}x{render_height or 1920}{tmpl_info}")
|
||||
pl.step_start("blender_turntable", {"frame_count": frame_count, "fps": fps})
|
||||
|
||||
Reference in New Issue
Block a user