refactor: cinematic render — linear keyframes, 3 segments, 250 frames, white bg
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) <noreply@anthropic.com>
This commit is contained in:
@@ -563,8 +563,8 @@ def render_cinematic_to_file(
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
# Cinematic parameters are fixed
|
# Cinematic parameters are fixed
|
||||||
frame_count = 480
|
frame_count = 250
|
||||||
fps = 24
|
fps = 25
|
||||||
|
|
||||||
blender_bin = find_blender()
|
blender_bin = find_blender()
|
||||||
if not blender_bin:
|
if not blender_bin:
|
||||||
|
|||||||
@@ -249,14 +249,11 @@ def _apply_material_library(parts, mat_lib_path, mat_map, part_names_ordered=Non
|
|||||||
|
|
||||||
# ── Cinematic camera animation ───────────────────────────────────────────────
|
# ── Cinematic camera animation ───────────────────────────────────────────────
|
||||||
|
|
||||||
TOTAL_FRAMES = 480
|
TOTAL_FRAMES = 250
|
||||||
SEGMENT_LENGTH = 120 # frames per segment
|
NUM_SEGMENTS = 3
|
||||||
|
SEGMENT_LENGTH = TOTAL_FRAMES // NUM_SEGMENTS # ~83 frames per segment
|
||||||
|
|
||||||
# Segment definitions: (start_azimuth_offset, end_azimuth_offset,
|
# 3 segments (no closeup): establishing, detail sweep, crane up
|
||||||
# start_elevation, end_elevation,
|
|
||||||
# start_dist_factor, end_dist_factor,
|
|
||||||
# start_lens, end_lens,
|
|
||||||
# use_dof)
|
|
||||||
SEGMENTS = [
|
SEGMENTS = [
|
||||||
# Segment 1: Establishing shot — orbit 45deg, push in, 50mm
|
# Segment 1: Establishing shot — orbit 45deg, push in, 50mm
|
||||||
{
|
{
|
||||||
@@ -282,25 +279,9 @@ SEGMENTS = [
|
|||||||
"lens_start": 35.0, "lens_end": 35.0,
|
"lens_start": 35.0, "lens_end": 35.0,
|
||||||
"dof": False,
|
"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:
|
def _lerp(a: float, b: float, t: float) -> float:
|
||||||
"""Linear interpolation."""
|
"""Linear interpolation."""
|
||||||
return a + (b - a) * t
|
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).
|
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)
|
seg_index = min((frame - 1) // SEGMENT_LENGTH, len(SEGMENTS) - 1)
|
||||||
local_frame = (frame - 1) - seg_index * SEGMENT_LENGTH
|
local_frame = (frame - 1) - seg_index * SEGMENT_LENGTH
|
||||||
raw_t = local_frame / max(SEGMENT_LENGTH - 1, 1)
|
t = local_frame / max(SEGMENT_LENGTH - 1, 1) # linear, no easing
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
seg = SEGMENTS[seg_index]
|
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="focus_distance", frame=frame)
|
||||||
cam_obj.data.dof.keyframe_insert(data_path="aperture_fstop", 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
|
return cam_obj
|
||||||
|
|
||||||
|
|
||||||
@@ -828,13 +812,25 @@ def main():
|
|||||||
scene.render.resolution_percentage = 100
|
scene.render.resolution_percentage = 100
|
||||||
scene.render.image_settings.file_format = 'PNG'
|
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:
|
if bg_color or transparent_bg:
|
||||||
scene.render.film_transparent = True
|
scene.render.film_transparent = True
|
||||||
if bg_color:
|
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:
|
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) ──────────────────
|
# ── Persistent data (Cycles BVH caching between frames) ──────────────────
|
||||||
scene.render.use_persistent_data = True
|
scene.render.use_persistent_data = True
|
||||||
|
|||||||
Reference in New Issue
Block a user