Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
34722df
feat: speedup light effects by using shared material of drones
vasarhelyi Sep 2, 2025
69d63cb
fix: fix find_all_f_curves_for_data_path() to return curves sorted by…
vasarhelyi Sep 3, 2025
1d4c33c
feat: add migration operator to convert old blender files with one ma…
vasarhelyi Sep 3, 2025
a92784e
fix: set both solid and wireframe color to object color in all views …
vasarhelyi Sep 3, 2025
b512593
fixup! fix: set both solid and wireframe color to object color in all…
vasarhelyi Sep 3, 2025
827a44f
fix: add logging to migration operator
vasarhelyi Sep 3, 2025
5a1bf1a
fix: configure logging explicitly as in Blender 4.5 default log level…
vasarhelyi Sep 3, 2025
f435f3b
feat: add MigrationOperator for automatic user-confirmed migration op…
vasarhelyi Sep 3, 2025
addb96f
feat: add internal version number for automating migration handling
vasarhelyi Sep 5, 2025
67e43cf
fix: do not mess around with logging as a default init call
vasarhelyi Sep 6, 2025
a6ed36f
fix: update viewport object and wireframe colors properly on all acti…
vasarhelyi Sep 6, 2025
3e22659
feat: add warnings to panels on version, formation size or shading co…
vasarhelyi Sep 7, 2025
9e5e14e
Merge branch 'main' into feat/speedup-light-effects
vasarhelyi Sep 10, 2025
63f56d9
Merge branch 'main' into feat/speedup-light-effects
vasarhelyi Sep 29, 2025
a391b29
fix: do not use __annotations__ when getting latest version
vasarhelyi Sep 29, 2025
77a6a85
fix: several small fixes based on review from @ntamas
vasarhelyi Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/addons/ui_skybrush_studio.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
UpdateFrameRangeFromStoryboardOperator,
UpdateTimeMarkersFromStoryboardOperator,
UseSelectedVertexGroupForFormationOperator,
UseSharedMaterialForAllDronesMigrationOperator,
ValidateTrajectoriesOperator,
VVIZExportOperator,
)
Expand Down Expand Up @@ -254,6 +255,7 @@
AddMarkersFromQRCodeOperator,
RefreshFileFormatsOperator,
RunFullProximityCheckOperator,
UseSharedMaterialForAllDronesMigrationOperator,
)

#: List widgets in this addon.
Expand Down
11 changes: 5 additions & 6 deletions src/modules/sbstudio/plugin/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,7 @@ def find_f_curve_for_data_path_and_index(
return None


def find_all_f_curves_for_data_path(
object_or_action, data_path: str
) -> Optional[FCurve]:
def find_all_f_curves_for_data_path(object_or_action, data_path: str) -> list[FCurve]:
"""Finds all F-curves in the F-curves of the action whose data path
matches the given property, sorted by the array index of the curves.

Expand All @@ -166,9 +164,10 @@ def find_all_f_curves_for_data_path(
else:
action = object_or_action

# TODO(ntamas): sort by array index!
result = [curve for curve in action.fcurves if curve.data_path == data_path]
return result
return sorted(
[curve for curve in action.fcurves if curve.data_path == data_path],
key=lambda c: c.array_index,
)


def cleanup_actions_for_object(object):
Expand Down
71 changes: 71 additions & 0 deletions src/modules/sbstudio/plugin/colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import bpy

from sbstudio.model.types import RGBAColor, RGBAColorLike
from sbstudio.plugin.actions import ensure_action_exists_for_object
from sbstudio.plugin.keyframes import set_keyframes

__all__ = (
"create_keyframe_for_color_of_drone",
"get_color_of_drone",
"set_color_of_drone",
)

is_blender_4 = bpy.app.version >= (4, 0, 0)


def create_keyframe_for_color_of_drone(
drone,
color: tuple[float, float, float] | tuple[float, float, float, float] | RGBAColor,
*,
frame: int | None = None,
step: bool = False,
):
"""Creates color keyframes for the given drone to set
in the given frame.

Parameters:
drone: the drone object to modify
color: the RGB color to use for the color of the drone
frame: the frame to apply the color on; `None` means the
current frame
step: whether to insert an additional keyframe in the preceding frame to
ensure an abrupt transition
"""
if frame is None:
frame = bpy.context.scene.frame_current

ensure_action_exists_for_object(drone)

if hasattr(color, "r"):
color_as_rgba = color.r, color.g, color.b, 1.0
else:
color_as_rgba = color[0], color[1], color[2], 1.0

keyframes = [(frame, color_as_rgba)]
if step and frame > bpy.context.scene.frame_start:
keyframes.insert(0, (frame - 1, None))

set_keyframes(drone, "color", keyframes, interpolation="LINEAR")


def get_color_of_drone(drone) -> RGBAColor:
"""Returns the color of the LED light on the given drone.

Parameters:
drone: the drone to query
color: the color to apply to the LED light of the drone
"""
if drone.color is not None:
return drone.color

return (0.0, 0.0, 0.0, 0.0)


def set_color_of_drone(drone, color: RGBAColorLike):
"""Sets the color of the LED light on the given drone.

Parameters:
drone: the drone to update
color: the color to apply to the LED light of the drone
"""
drone.color = color
3 changes: 3 additions & 0 deletions src/modules/sbstudio/plugin/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
DEFAULT_OUTDOOR_DRONE_RADIUS = 0.5
"""Default outdoor drone radius"""

LATEST_SKYBRUSH_PLUGIN_VERSION = 2
"""The latest (current) plugin version."""

NUM_PYRO_CHANNELS = 6
"""The number of pyro channels that we support."""

Expand Down
35 changes: 34 additions & 1 deletion src/modules/sbstudio/plugin/keyframes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Functions related to the handling of keyframes in animation actions."""

from bpy.types import Action, FCurve
from collections import defaultdict
from typing import Callable, Optional, Sequence, Tuple, Union

from .actions import (
Expand All @@ -9,7 +10,7 @@
get_action_for_object,
)

__all__ = ("clear_keyframes", "set_keyframes")
__all__ = ("clear_keyframes", "get_keyframes", "set_keyframes")


def clear_keyframes(
Expand Down Expand Up @@ -67,6 +68,38 @@ def clear_keyframes(
points.remove(points[index])


def get_keyframes(
object,
data_path: str,
) -> list[tuple[float, float | list[float]]]:
"""Gets the values of all keyframes of an object at the given data path.

Parameters:
object: the object on which the keyframes are to be retrieved
data_path: the data path to use

Returns:
the keyframes of the object at the given data path
"""
source, sep, prop = data_path.rpartition(".")
source = object.path_resolve(source) if sep else object

fcurves = find_all_f_curves_for_data_path(object, data_path)

match len(fcurves):
case 0:
return []
case 1:
return [(p.co.x, p.co.y) for p in fcurves[0].keyframe_points]
case _:
frames_dict = defaultdict(lambda: [0.0] * len(fcurves))
for curve in fcurves:
if curve.data_path == data_path:
for point in curve.keyframe_points:
frames_dict[int(point.co.x)][curve.array_index] = point.co.y
return sorted(frames_dict.items())


def set_keyframes(
object,
data_path: str,
Expand Down
20 changes: 14 additions & 6 deletions src/modules/sbstudio/plugin/materials.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
__all__ = (
"create_colored_material",
"create_glowing_material",
"create_keyframe_for_diffuse_color_of_material",
"get_material_for_led_light_color",
"get_material_for_pyro",
"set_led_light_color",
"get_led_light_color_from_material",
"set_emission_strength_of_material",
"set_led_light_color_to_material",
)

is_blender_4 = bpy.app.version >= (4, 0, 0)
Expand Down Expand Up @@ -96,11 +99,17 @@ def create_glowing_material(
links.clear()
nodes.clear()

object_info_node = nodes.new("ShaderNodeObjectInfo")
object_info_node.location = (-300, 0)

emission_node = nodes.new("ShaderNodeEmission")
emission_node.inputs["Strength"].default_value = strength
emission_node.location = (0, 0)

output_node = nodes.new("ShaderNodeOutputMaterial")
output_node.location = (300, 0)

links.new(object_info_node.outputs["Color"], emission_node.inputs["Color"])
links.new(emission_node.outputs["Emission"], output_node.inputs["Surface"])

_set_diffuse_color_of_material(mat, color)
Expand Down Expand Up @@ -140,13 +149,12 @@ def get_material_for_pyro(drone) -> Optional[Material]:


def _get_diffuse_color_of_material(material) -> RGBAColor:
"""Returns the diffuse color of the given material to the given value.
"""Returns the diffuse color of the given material.

The material must use a principled BSDF or an emission shader.

Parameters:
material: the Blender material to update
color: the color to apply to the material
material: the Blender material to get the color from
"""
if material.use_nodes:
# Material is using shader nodes so we need to adjust the diffuse
Expand Down Expand Up @@ -238,7 +246,7 @@ def create_keyframe_for_diffuse_color_of_material(
set_keyframes(node_tree, data_path, keyframes, interpolation="LINEAR")


def get_led_light_color(drone) -> RGBAColor:
def get_led_light_color_from_material(drone) -> RGBAColor:
"""Returns the color of the LED light on the given drone.

Parameters:
Expand All @@ -252,7 +260,7 @@ def get_led_light_color(drone) -> RGBAColor:
return (0.0, 0.0, 0.0, 0.0)


def set_led_light_color(drone, color: RGBAColorLike):
def set_led_light_color_to_material(drone, color: RGBAColorLike):
"""Sets the color of the LED light on the given drone.

Parameters:
Expand Down
15 changes: 14 additions & 1 deletion src/modules/sbstudio/plugin/model/show.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from bpy.props import PointerProperty
from bpy.props import IntProperty, PointerProperty
from bpy.types import PropertyGroup

from sbstudio.plugin.constants import LATEST_SKYBRUSH_PLUGIN_VERSION

from .formations_panel import FormationsPanelProperties
from .led_control import LEDControlPanelProperties
from .light_effects import LightEffectCollection
Expand Down Expand Up @@ -35,3 +37,14 @@ class DroneShowAddonProperties(PropertyGroup):
type=DroneShowAddonFileSpecificSettings
)
storyboard: Storyboard = PointerProperty(type=Storyboard)
version: IntProperty = IntProperty(
name="Version",
description=(
"Current version of the show content stored in Blender. "
"Version 1 is the initial version (plugin version <= 3.13.2). "
"Version 2 uses a shared material for all drones to speed up light effects."
),
min=1,
max=LATEST_SKYBRUSH_PLUGIN_VERSION,
default=1,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm I wonder whether this will be okay this way. On one hand, we need to make sure that any already-saved Blender file without this property starts from version 1 because they do not have a shared material so we need to upgrade them. On the other hand, we also need to make sure that newly created Blender files start from version 2.

Can you check whether this is how it works at the moment? I'm afraid that newly created files will start from version 1 and won't go through an upgrade cycle until they are saved first and loaded back again.

Copy link
Contributor Author

@vasarhelyi vasarhelyi Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double checked this, it is OK I think. I deliberately start with version 1 on empty as well to be able to go though all migrations properly on init. And confirmed that when I open a new Blender file then bpy.context.scene.skybrush.version will be 2 after init. See perform_migrations() in InitializationTask. The needs_migration() property of UseSharedMaterialForAllDronesMigrationOperator returns False if there are no drones yet (empty Blender file), and thus the MigrationOperators execute() will simply return {"FINISHED"} without calling execute_migration().

)
4 changes: 4 additions & 0 deletions src/modules/sbstudio/plugin/operators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
from .get_formation_stats import GetFormationStatisticsOperator
from .import_light_effects import ImportLightEffectsOperator
from .land import LandOperator
from .migrations.use_common_material_for_all_drones import (
UseSharedMaterialForAllDronesMigrationOperator,
)
from .move_light_effect import (
MoveLightEffectDownOperator,
MoveLightEffectUpOperator,
Expand Down Expand Up @@ -128,6 +131,7 @@
"UpdateFrameRangeFromStoryboardOperator",
"UpdateTimeMarkersFromStoryboardOperator",
"UseSelectedVertexGroupForFormationOperator",
"UseSharedMaterialForAllDronesMigrationOperator",
"ValidateTrajectoriesOperator",
"VVIZExportOperator",
)
14 changes: 3 additions & 11 deletions src/modules/sbstudio/plugin/operators/apply_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
from bpy.types import Operator
from random import shuffle

from sbstudio.plugin.materials import (
create_keyframe_for_diffuse_color_of_material,
get_material_for_led_light_color,
)
from sbstudio.plugin.colors import create_keyframe_for_color_of_drone
from sbstudio.plugin.props import ColorProperty
from sbstudio.plugin.selection import get_selected_drones
from sbstudio.plugin.utils.evaluator import create_position_evaluator
Expand Down Expand Up @@ -158,11 +155,6 @@ def _apply_color_to_single_drone(self, drone, frame: int, index: int, ratio: flo
# Gradient
color = self.primary_color * (1 - ratio) + self.secondary_color * ratio

material = get_material_for_led_light_color(drone)
if not material:
# Drone does not have an LED light
return

create_keyframe_for_diffuse_color_of_material(
material, color, frame=frame, step=not self.fade
create_keyframe_for_color_of_drone(
drone, color, frame=frame, step=not self.fade
)
61 changes: 60 additions & 1 deletion src/modules/sbstudio/plugin/operators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from numpy.typing import NDArray
from typing import Any, Optional

from bpy.props import BoolProperty, EnumProperty
from bpy.props import BoolProperty, EnumProperty, IntProperty
from bpy.types import Collection, Context, Object, Operator
from bpy_extras.io_utils import ExportHelper

Expand Down Expand Up @@ -496,3 +496,62 @@ def _propose_marker_count(self, context: Context) -> int:
else:
num_existing_markers = 0
return max(0, num_drones - num_existing_markers)


class MigrationOperator(Operator):
"""Operator mixin for migrations/upgrades for files created in earlier
versions of the Skybrush Studio for Blender plugin."""

version_from = IntProperty(name="Input format version", options={"HIDDEN"})
version_to = IntProperty(name="Output format version", options={"HIDDEN"})

@classmethod
def poll(cls, context: Context):
return context.scene.skybrush

def execute(self, context: Context):
if context.scene.skybrush.version < self.version_from:
raise RuntimeError(
f"Input format version should be {self.version_from}, "
f"not {context.scene.skybrush.version}"
)
elif context.scene.skybrush.version == self.version_from:
retval = (
self.execute_migration(context)
if self.needs_migration()
else {"FINISHED"}
)
if retval == {"FINISHED"}:
context.scene.skybrush.version = self.version_to

return retval

return {"FINISHED"}

def invoke(self, context: Context, event):
self.initialize_migration()

if context.scene.skybrush.version >= self.version_to:
return {"CANCELLED"}

if self.needs_migration():
return context.window_manager.invoke_confirm(
self, event, title=self.bl_label, message=self.bl_description
)
else:
return self.execute(context)

def execute_migration(self, context: Context):
"""Executes the migration/upgrade on the current Blender content."""
raise NotImplementedError

def initialize_migration(self) -> None:
"""Initializes the operator by setting up the from/to versions."""
raise NotImplementedError

def needs_migration(self) -> bool:
"""Returns whether the current Blender content needs migration.

Note that return value is checked based on actual content,
irrespective of the current plugin version."""
raise NotImplementedError
Loading