From 458c6cd813c8d801de9e2cbe453189419b561ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sun, 15 Mar 2026 21:57:42 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20cinematic=20render=20=E2=80=94=20li?= =?UTF-8?q?near=20keyframes,=203=20segments,=20250=20frames,=20white=20bg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes per user feedback: - Keyframe interpolation: BEZIER → LINEAR (all fcurves set to LINEAR) - Removed segment 4 (closeup) — now 3 segments only - Frame count: 480 → 250 (10 seconds at 25fps) - FPS: 24 → 25 - Easing removed — pure linear interpolation between segment params - White background by default (World node Color = white) - Transparent bg still available as override Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/services/render_blender.py | 4 +- render-worker/scripts/cinematic_render.py | 76 +++++++++++------------ 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/backend/app/services/render_blender.py b/backend/app/services/render_blender.py index 08844f4..8545739 100644 --- a/backend/app/services/render_blender.py +++ b/backend/app/services/render_blender.py @@ -563,8 +563,8 @@ def render_cinematic_to_file( import time # Cinematic parameters are fixed - frame_count = 480 - fps = 24 + frame_count = 250 + fps = 25 blender_bin = find_blender() if not blender_bin: diff --git a/render-worker/scripts/cinematic_render.py b/render-worker/scripts/cinematic_render.py index 4dace36..11c2be4 100644 --- a/render-worker/scripts/cinematic_render.py +++ b/render-worker/scripts/cinematic_render.py @@ -249,14 +249,11 @@ def _apply_material_library(parts, mat_lib_path, mat_map, part_names_ordered=Non # ── Cinematic camera animation ─────────────────────────────────────────────── -TOTAL_FRAMES = 480 -SEGMENT_LENGTH = 120 # frames per segment +TOTAL_FRAMES = 250 +NUM_SEGMENTS = 3 +SEGMENT_LENGTH = TOTAL_FRAMES // NUM_SEGMENTS # ~83 frames per segment -# Segment definitions: (start_azimuth_offset, end_azimuth_offset, -# start_elevation, end_elevation, -# start_dist_factor, end_dist_factor, -# start_lens, end_lens, -# use_dof) +# 3 segments (no closeup): establishing, detail sweep, crane up SEGMENTS = [ # Segment 1: Establishing shot — orbit 45deg, push in, 50mm { @@ -282,25 +279,9 @@ SEGMENTS = [ "lens_start": 35.0, "lens_end": 35.0, "dof": False, }, - # Segment 4: Hero close — push in, settle down - { - "az_start": 120.0, "az_end": 120.0, - "el_start": 35.0, "el_end": 25.0, - "dist_start": 3.0, "dist_end": 2.2, - "lens_start": 65.0, "lens_end": 65.0, - "dof": False, - }, ] -def _ease_in_out(t: float) -> float: - """Cubic ease in-out, t in [0, 1].""" - if t < 0.5: - return 4.0 * t * t * t - else: - return 1.0 - (-2.0 * t + 2.0) ** 3 / 2.0 - - def _lerp(a: float, b: float, t: float) -> float: """Linear interpolation.""" return a + (b - a) * t @@ -322,21 +303,10 @@ def _get_segment_params(frame: int, bsphere_radius: float): Returns (azimuth_deg, elevation_deg, distance, lens_mm, use_dof). """ - # Determine which segment (0-3) and local t (0-1) + # Determine which segment and local t (0-1) — linear interpolation seg_index = min((frame - 1) // SEGMENT_LENGTH, len(SEGMENTS) - 1) local_frame = (frame - 1) - seg_index * SEGMENT_LENGTH - raw_t = local_frame / max(SEGMENT_LENGTH - 1, 1) - - # Apply easing: smooth start for segment 1, smooth stop for segment 4, - # ease-in-out for segments 2 and 3 - if seg_index == 0: - # Smooth start: ease-out (decelerate into motion) - t = _ease_in_out(raw_t) - elif seg_index == 3: - # Smooth stop: ease-in-out with emphasis on deceleration - t = _ease_in_out(raw_t) - else: - t = _ease_in_out(raw_t) + t = local_frame / max(SEGMENT_LENGTH - 1, 1) # linear, no easing seg = SEGMENTS[seg_index] @@ -409,7 +379,21 @@ def _setup_cinematic_camera(parts, bbox_center, bsphere_radius, total_frames): cam_obj.data.dof.keyframe_insert(data_path="focus_distance", frame=frame) cam_obj.data.dof.keyframe_insert(data_path="aperture_fstop", frame=frame) - print(f"[cinematic_render] camera keyframed: {total_frames} frames across 4 segments") + # Set all keyframes to LINEAR interpolation (no bezier smoothing) + if cam_obj.animation_data and cam_obj.animation_data.action: + for fcurve in cam_obj.animation_data.action.fcurves: + for kp in fcurve.keyframe_points: + kp.interpolation = 'LINEAR' + if cam_obj.data.animation_data and cam_obj.data.animation_data.action: + for fcurve in cam_obj.data.animation_data.action.fcurves: + for kp in fcurve.keyframe_points: + kp.interpolation = 'LINEAR' + if cam_obj.data.dof.animation_data and cam_obj.data.dof.animation_data.action: + for fcurve in cam_obj.data.dof.animation_data.action.fcurves: + for kp in fcurve.keyframe_points: + kp.interpolation = 'LINEAR' + + print(f"[cinematic_render] camera keyframed: {total_frames} frames across {len(SEGMENTS)} segments (LINEAR interpolation)", flush=True) return cam_obj @@ -828,13 +812,25 @@ def main(): scene.render.resolution_percentage = 100 scene.render.image_settings.file_format = 'PNG' - # ── Transparent background ─────────────────────────────────────────────── + # ── White background (default for cinematic) ──────────────────────────── + # Set world to white unless template provides its own + if not template_path: + world = bpy.data.worlds.new("CinematicWorld") + scene.world = world + world.use_nodes = True + bg_node = world.node_tree.nodes.get("Background") + if bg_node: + bg_node.inputs["Color"].default_value = (1.0, 1.0, 1.0, 1.0) + bg_node.inputs["Strength"].default_value = 1.0 + print("[cinematic_render] white background set", flush=True) + + # ── Transparent background (override if requested) ──────────────────── if bg_color or transparent_bg: scene.render.film_transparent = True if bg_color: - print(f"[cinematic_render] film_transparent=True for FFmpeg bg_color compositing ({bg_color})") + print(f"[cinematic_render] film_transparent=True for FFmpeg bg_color compositing ({bg_color})", flush=True) else: - print("[cinematic_render] transparent_bg enabled (alpha PNG frames)") + print("[cinematic_render] transparent_bg enabled (alpha PNG frames)", flush=True) # ── Persistent data (Cycles BVH caching between frames) ────────────────── scene.render.use_persistent_data = True