← sequencer-addon

Support (animated) strip opacity when batch-rendering

3f68877842b8dec627d35977c5f94fb3346b653b · Antoine Boellinger · Mar 8, 2023 16:06

+56 −9 · 8 files changed

  • .gitignore +1 −0
  • CHANGELOG.md +4 −0
  • README.md +1 −1
  • spa_sequencer/__init__.py +1 −1
  • spa_sequencer/editorial/ops.py +0 −2
  • spa_sequencer/editorial/ui.py +0 −1
  • spa_sequencer/render/tasks.py +49 −3
  • spa_sequencer/sync/core.py +0 −1
diff --git a/.gitignore b/.gitignore
index 02ab6ed..22655f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@
 __pycache__
 *$py.classs
 .venv
+venv*/
 
 # datas or personal files
 /build
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e722690..50246a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ## [Unreleased]
 
+## [1.0.1] - 2023-3-08
+
+- Added support for strip opacity in the batch render. Now values and keyframed animation on opacity strips is transferred during batch render.
+
 ## [1.0.0] - 2023-01-09
 
 Initial release.
diff --git a/README.md b/README.md
index 2098f68..0a1e1f1 100644
--- a/README.md
+++ b/README.md
@@ -52,5 +52,5 @@ As per section 7 of the GPLv3, the license is supplemented with the following ad
 
 ## Authors
 
-The SPA Studios pipeline team (2021-2023):  
+The SPA Studios pipeline team (2021-2023):
 Antoine Boellinger ([@aboellinger](https://github.com/aboellinger)), Bryan Fordney ([@bryab](https://github.com/bryab)), Falk David ([@falkdavid](https://github.com/falkdavid)), Mickael Villain ([@micka-06](https://github.com/micka-06)), Nick Alberelli ([@NickTiny](https://github.com/NickTiny)), Paolo Fazio ([@PaoloFazio](https://github.com/PaoloFazio)), Yann Lanthony ([@yann-lty](https://github.com/yann-lty)).
diff --git a/spa_sequencer/__init__.py b/spa_sequencer/__init__.py
index d4216f9..74e7889 100644
--- a/spa_sequencer/__init__.py
+++ b/spa_sequencer/__init__.py
@@ -18,7 +18,7 @@ bl_info = {
     "author": "The SPA Studios",
     "description": "Toolset to improve the sequence workflow in Blender.",
     "blender": (3, 3, 0),
-    "version": (1, 0, 0),
+    "version": (1, 0, 1),
     "location": "",
     "warning": "",
     "category": "SPA",
diff --git a/spa_sequencer/editorial/ops.py b/spa_sequencer/editorial/ops.py
index 1529c11..192eec2 100644
--- a/spa_sequencer/editorial/ops.py
+++ b/spa_sequencer/editorial/ops.py
@@ -203,14 +203,12 @@ class SEQUENCER_OT_edit_conform_shots_from_editorial(bpy.types.Operator):
         return context.window_manager.invoke_props_dialog(self)
 
     def execute(self, context: bpy.types.Context):
-
         seq_editor = context.scene.sequence_editor
         regex = re.compile(self.shot_id_regex)
 
         shot_naming = ShotNaming()
 
         for strip in seq_editor.sequences:
-
             if self.ref_channel != 0 and strip.channel != self.ref_channel:
                 continue
 
diff --git a/spa_sequencer/editorial/ui.py b/spa_sequencer/editorial/ui.py
index 8c6fb2a..2bba0e4 100644
--- a/spa_sequencer/editorial/ui.py
+++ b/spa_sequencer/editorial/ui.py
@@ -23,7 +23,6 @@ class SEQUENCER_MT_editorial(bpy.types.Menu):
     bl_label = "Editorial"
 
     def draw(self, context):
-
         if HAS_OTIO:
             self.layout.menu("SEQUENCER_MT_edit_io")
 
diff --git a/spa_sequencer/render/tasks.py b/spa_sequencer/render/tasks.py
index 73905ab..0a7d3fb 100644
--- a/spa_sequencer/render/tasks.py
+++ b/spa_sequencer/render/tasks.py
@@ -82,7 +82,6 @@ class BaseTask:
 
 @dataclass
 class BaseRenderTask(BaseTask):
-
     is_modal: bool = True
     handlers_registered: bool = False
 
@@ -319,7 +318,6 @@ class StripRenderTask(BaseRenderTask):
                 channel=scene_strip.channel + channel_offset,
                 frame_start=scene_strip.frame_final_start,
             )
-
             start_handle = frames_handles
             if frames_handles > 0 and render_options.renderer == "INTERNAL":
                 # Internal render does not support negative frame rendering.
@@ -338,6 +336,10 @@ class StripRenderTask(BaseRenderTask):
                 (strip, scene_strip.scene.frame_start, scene_strip.scene.frame_end)
             )
 
+        # Transfer properties from source to new strip
+        props = ("blend_alpha",)
+        _transfer_strip_properties(scene_strip, strip, props)
+
         for strip, frame_start, frame_end in strips:
             # Re-assign the strip channel to force Blender to evalute overlaps
             strip.channel = strip.channel
@@ -352,6 +354,51 @@ class StripRenderTask(BaseRenderTask):
             strip[STRIP_PROP_SOURCE_FRAME_END] = frame_end
 
 
+def _transfer_strip_properties(src_strip, dst_strip, props):
+    src_scene = src_strip.id_data
+    dst_scene = dst_strip.id_data
+
+    # First cover the common and simples case: no animation at all.
+    for prop in props:
+        setattr(dst_strip, prop, getattr(src_strip, prop))
+
+    # Then copy fcurves
+    src_paths = {prop: src_strip.path_from_id(prop) for prop in props}
+    for prop, src_path in src_paths.items():
+        src_curve = _get_fcurve(src_scene, src_path)
+        if not src_curve:
+            continue
+
+        # Touch destination curve
+        dst_path = dst_strip.path_from_id(prop)
+        dst_scene.keyframe_insert(dst_path)
+        dst_curve = _get_fcurve(dst_scene, dst_path)
+
+        # Add back a copy of the points
+        src_points = src_curve.keyframe_points
+        dst_points = dst_curve.keyframe_points
+        dst_points.clear()
+        dst_points.add(len(src_points))
+        for i, point in enumerate(src_points):
+            dst_points[i].co = point.co
+
+    # TODO: Support the driver case ?
+
+
+def _get_fcurve(block, path):
+    anim = block.animation_data
+    if not anim:
+        return
+
+    action = anim.action
+    if not action:
+        return
+
+    for curve in action.fcurves:
+        if curve.data_path == path:
+            return curve
+
+
 @dataclass
 class CopySoundStripsTask(BaseTask):
     src_scene: Optional[bpy.types.Scene] = None
@@ -437,7 +484,6 @@ class CopySoundStripsTask(BaseTask):
 
 @dataclass
 class FitResolutionToContentTask(BaseTask):
-
     scene: Optional[bpy.types.Scene] = None
 
     def run(self, context: bpy.types.Context, render_options: BatchRenderOptions):
diff --git a/spa_sequencer/sync/core.py b/spa_sequencer/sync/core.py
index 5e3a712..4260214 100644
--- a/spa_sequencer/sync/core.py
+++ b/spa_sequencer/sync/core.py
@@ -456,7 +456,6 @@ def sync_system_update(context: bpy.types.Context, force: bool = False):
 
         # Strip at target time is valid but differs from current master strip:
         if new_strip != strip:
-
             # Discard changes coming from outside the strip range.
             if sync_settings.last_strip_scene_frame_out_of_range:
                 return