From 3f98e6ea9da8e6eb0646beb158b8aca8da689313 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 27 Jun 2016 15:22:49 -0400 Subject: [PATCH] Add lamp color and energy animations This makes things incredibly more complex because Blender stores those animations on the ObData ID instead of the Object ID data block. Dang. So, the animation modifier's detection code had to be pretty much scrapped. The newer code is a little hacky in places. Hopefully we can address this soon-ish. --- korman/exporter/animation.py | 107 +++++++++++++++++++++++----- korman/exporter/rtlight.py | 1 + korman/nodes/node_messages.py | 3 +- korman/properties/modifiers/anim.py | 72 +++++++++++-------- korman/ui/modifiers/anim.py | 36 ++++++---- 5 files changed, 158 insertions(+), 61 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 925dc4a..5b69f16 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -31,24 +31,32 @@ class AnimationConverter: return frame_num / self._bl_fps def convert_object_animations(self, bo, so): - anim = bo.animation_data - if anim is None: - return - action = anim.action - if action is None: - return - fcurves = action.fcurves - if not fcurves: + if not self.is_animated(bo): return + def fetch_animation_data(id_data): + if id_data is not None: + if id_data.animation_data is not None: + action = id_data.animation_data.action + return action, getattr(action, "fcurves", None) + return None, None + + # TODO: At some point, we should consider supporting NLA stuff. + # But for now, this seems sufficient. + obj_action, obj_fcurves = fetch_animation_data(bo) + data_action, data_fcurves = fetch_animation_data(bo.data) + # We're basically just going to throw all the FCurves at the controller converter (read: wall) # and see what sticks. PlasmaMAX has some nice animation channel stuff that allows for some # form of separation, but Blender's NLA editor is way confusing and appears to not work with # things that aren't the typical position, rotation, scale animations. applicators = [] - applicators.append(self._convert_transform_animation(bo.name, fcurves, bo.matrix_basis)) + applicators.append(self._convert_transform_animation(bo.name, obj_fcurves, bo.matrix_basis)) if bo.plasma_modifiers.soundemit.enabled: - applicators.extend(self._convert_sound_volume_animation(bo.name, fcurves, bo.plasma_modifiers.soundemit)) + applicators.extend(self._convert_sound_volume_animation(bo.name, obj_fcurves, bo.plasma_modifiers.soundemit)) + if isinstance(bo.data, bpy.types.Lamp): + lamp = bo.data + applicators.extend(self._convert_lamp_color_animation(bo.name, data_fcurves, lamp)) # Check to make sure we have some valid animation applicators before proceeding. if not any(applicators): @@ -68,14 +76,19 @@ class AnimationConverter: # This was previously part of the Animation Modifier, however, there can be lots of animations # Therefore we move it here. - markers = action.pose_markers + def get_ranges(*args, **kwargs): + index = kwargs.get("index", 0) + for i in args: + if i is not None: + yield i.frame_range[index] atcanim.name = "(Entire Animation)" - atcanim.start = self._convert_frame_time(action.frame_range[0]) - atcanim.end = self._convert_frame_time(action.frame_range[1]) + atcanim.start = self._convert_frame_time(min(get_ranges(obj_action, data_action, index=0))) + atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, data_action, index=1))) # Marker points - for marker in markers: - atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) + if obj_action is not None: + for marker in obj_action.pose_markers: + atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) # Fixme? Not sure if we really need to expose this... atcanim.easeInMin = 1.0 @@ -85,7 +98,55 @@ class AnimationConverter: atcanim.easeOutMax = 1.0 atcanim.easeOutLength = 1.0 + def _convert_lamp_color_animation(self, name, fcurves, lamp): + if not fcurves: + return None + + energy_curve = next((i for i in fcurves if i.data_path == "energy" and i.keyframe_points), None) + color_curves = sorted((i for i in fcurves if i.data_path == "color" and i.keyframe_points), key=lambda x: x.array_index) + if energy_curve is None and color_curves is None: + return None + elif lamp.use_only_shadow: + self._exporter().report.warn("Cannot animate Lamp color because this lamp only casts shadows", indent=3) + return None + elif not lamp.use_specular and not lamp.use_diffuse: + self._exporter().report.warn("Cannot animate Lamp color because neither Diffuse nor Specular are enabled", indent=3) + return None + + # OK Specular is easy. We just toss out the color as a point3. + color_keyframes, color_bez = self._process_keyframes(color_curves, convert=lambda x: x * -1.0 if lamp.use_negative else None) + if color_keyframes and lamp.use_specular: + channel = plPointControllerChannel() + channel.controller = self._make_point3_controller(color_curves, color_keyframes, color_bez, lamp.color) + applicator = plLightSpecularApplicator() + applicator.channelName = name + applicator.channel = channel + yield applicator + + # Hey, look, it's a third way to process FCurves. YAY! + def convert_diffuse_animation(color, energy): + if lamp.use_negative: + return { key: (0.0 - value) * energy[0] for key, value in color.items() } + else: + return { key: value * energy[0] for key, value in color.items() } + diffuse_defaults = { "color": lamp.color, "energy": lamp.energy } + diffuse_fcurves = color_curves + [energy_curve,] + diffuse_keyframes = self._process_fcurves(diffuse_fcurves, convert_diffuse_animation, diffuse_defaults) + if not diffuse_keyframes: + return None + + # Whew. + channel = plPointControllerChannel() + channel.controller = self._make_point3_controller([], diffuse_keyframes, False, []) + applicator = plLightDiffuseApplicator() + applicator.channelName = name + applicator.channel = channel + yield applicator + def _convert_sound_volume_animation(self, name, fcurves, soundemit): + if not fcurves: + return None + def convert_volume(value): if value == 0.0: return 0.0 @@ -111,6 +172,9 @@ class AnimationConverter: yield applicator def _convert_transform_animation(self, name, fcurves, xform): + if not fcurves: + return None + pos = self.make_pos_controller(fcurves, xform) rot = self.make_rot_controller(fcurves, xform) scale = self.make_scale_controller(fcurves, xform) @@ -152,6 +216,17 @@ class AnimationConverter: master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo) return mod, master + def is_animated(self, bo): + if bo.animation_data is not None: + if bo.animation_data.action is not None: + return True + data = getattr(bo, "data", None) + if data is not None: + if data.animation_data is not None: + if data.animation_data.action is not None: + return True + return False + def make_matrix44_controller(self, pos_fcurves, scale_fcurves, default_pos, default_scale): pos_keyframes, pos_bez = self._process_keyframes(pos_fcurves) scale_keyframes, scale_bez = self._process_keyframes(scale_fcurves) @@ -160,7 +235,7 @@ class AnimationConverter: # Matrix keyframes cannot do bezier schtuff if pos_bez or scale_bez: - self._exporter().report.warn("This animation cannot use bezier keyframes--forcing linear", indent=3) + self._exporter().report.warn("Matrix44 controllers cannot use bezier keyframes--forcing linear", indent=3) # Let's pair up the pos and scale schtuff based on frame numbers. I realize that we're creating # a lot of temporary objects, but until I see profiling results that this is terrible, I prefer diff --git a/korman/exporter/rtlight.py b/korman/exporter/rtlight.py index 01787e2..b37eef5 100644 --- a/korman/exporter/rtlight.py +++ b/korman/exporter/rtlight.py @@ -104,6 +104,7 @@ class LightConverter: self._converter_funcs[bl_light.type](bl_light, pl_light) # Light color nonsense + # Please note that these calculations are duplicated in the AnimationConverter energy = bl_light.energy if bl_light.use_negative: diff_color = [(0.0 - i) * energy for i in bl_light.color] diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index 6ea5be7..05e28d8 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -168,8 +168,7 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): obj = bpy.data.objects.get(self.object_name, None) if obj is None: self.raise_error("invalid object: '{}'".format(self.object_name)) - anim = obj.plasma_modifiers.animation - if not anim.enabled: + if not exporter.animation.is_animated(obj): self.raise_error("invalid animation") group = obj.plasma_modifiers.animation_group if group.enabled: diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index e14dece..dd45bfe 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -24,14 +24,21 @@ def _convert_frame_time(frame_num): fps = bpy.context.scene.render.fps return frame_num / fps -def _get_blender_action(bo): - if bo.animation_data is None or bo.animation_data.action is None: - raise ExportError("Object '{}' has no Action to export".format(bo.name)) - if not bo.animation_data.action.fcurves: - raise ExportError("Object '{}' is animated but has no FCurves".format(bo.name)) - return bo.animation_data.action - -class PlasmaAnimationModifier(PlasmaModifierProperties): +class ActionModifier: + @property + def blender_action(self): + bo = self.id_data + if bo.animation_data is not None and bo.animation_data.action is not None: + return bo.animation_data.action + if bo.data is not None: + if bo.data.animation_data is not None and bo.data.animation_data.action is not None: + # we will not use this action for any animation logic. that must be stored on the Object + # datablock for simplicity's sake. + return None + raise ExportError("Object '{}' is not animated".format(bo.name)) + + +class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): pl_id = "animation" bl_category = "Animation" @@ -58,30 +65,35 @@ class PlasmaAnimationModifier(PlasmaModifierProperties): return True def export(self, exporter, bo, so): - action = _get_blender_action(bo) - markers = action.pose_markers + action = self.blender_action atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) atcanim.autoStart = self.auto_start atcanim.loop = self.loop # Simple start and loop info - initial_marker = markers.get(self.initial_marker) - if initial_marker is not None: - atcanim.initial = _convert_frame_time(initial_marker.frame) - else: - atcanim.initial = -1.0 - if self.loop: - loop_start = markers.get(self.loop_start) - if loop_start is not None: - atcanim.loopStart = _convert_frame_time(loop_start.frame) + if action is not None: + markers = action.pose_markers + initial_marker = markers.get(self.initial_marker) + if initial_marker is not None: + atcanim.initial = _convert_frame_time(initial_marker.frame) else: - atcanim.loopStart = _convert_frame_time(action.frame_range[0]) - loop_end = markers.get(self.loop_end) - if loop_end is not None: - atcanim.loopEnd = _convert_frame_time(loop_end.frame) - else: - atcanim.loopEnd = _convert_frame_time(action.frame_range[1]) + atcanim.initial = -1.0 + if self.loop: + loop_start = markers.get(self.loop_start) + if loop_start is not None: + atcanim.loopStart = _convert_frame_time(loop_start.frame) + else: + atcanim.loopStart = atcanim.start + loop_end = markers.get(self.loop_end) + if loop_end is not None: + atcanim.loopEnd = _convert_frame_time(loop_end.frame) + else: + atcanim.loopEnd = atcanim.end + else: + if self.loop: + atcanim.loopStart = atcanim.start + atcanim.loopEnd = atcanim.end def _make_physical_movable(self, so): sim = so.sim @@ -128,8 +140,8 @@ class PlasmaAnimationGroupModifier(PlasmaModifierProperties): active_child_index = IntProperty(options={"HIDDEN"}) def export(self, exporter, bo, so): - action = _get_blender_action(bo) - key_name = bo.plasma_modifiers.animation.key_name + if not exporter.animation.is_animated(bo): + raise ExportError("'{}': Object is not animated".format(bo.name)) # The message forwarder is the guy that makes sure that everybody knows WTF is going on msgfwd = exporter.mgr.find_create_object(plMsgForwarder, so=so, name=self.key_name) @@ -171,7 +183,7 @@ class LoopMarker(bpy.types.PropertyGroup): description="Marker name from whence the loop ends") -class PlasmaAnimationLoopModifier(PlasmaModifierProperties): +class PlasmaAnimationLoopModifier(ActionModifier, PlasmaModifierProperties): pl_id = "animation_loop" pl_depends = {"animation"} @@ -186,7 +198,9 @@ class PlasmaAnimationLoopModifier(PlasmaModifierProperties): active_loop_index = IntProperty(options={"HIDDEN"}) def export(self, exporter, bo, so): - action = _get_blender_action(bo) + action = self.blender_action + if action is None: + raise ExportError("'{}': No object animation data".format(bo.name)) markers = action.pose_markers atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) diff --git a/korman/ui/modifiers/anim.py b/korman/ui/modifiers/anim.py index e9b2fd5..84c9edc 100644 --- a/korman/ui/modifiers/anim.py +++ b/korman/ui/modifiers/anim.py @@ -15,16 +15,19 @@ import bpy -def _check_for_anim(layout, context): - if context.object.animation_data is None or context.object.animation_data.action is None: +def _check_for_anim(layout, modifier): + try: + action = modifier.blender_action + except: layout.label("Object has no animation data", icon="ERROR") - return False - return True + return None + else: + return action if action is not None else False def animation(modifier, layout, context): - if not _check_for_anim(layout, context): + action = _check_for_anim(layout, modifier) + if action is None: return - action = context.object.animation_data.action split = layout.split() col = split.column() @@ -32,11 +35,12 @@ def animation(modifier, layout, context): col = split.column() col.prop(modifier, "loop") - layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER") - col = layout.column() - col.enabled = modifier.loop - col.prop_search(modifier, "loop_start", action, "pose_markers", icon="PMARKER") - col.prop_search(modifier, "loop_end", action, "pose_markers", icon="PMARKER") + if action: + layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER") + col = layout.column() + col.enabled = modifier.loop + col.prop_search(modifier, "loop_start", action, "pose_markers", icon="PMARKER") + col.prop_search(modifier, "loop_end", action, "pose_markers", icon="PMARKER") class GroupListUI(bpy.types.UIList): @@ -45,7 +49,8 @@ class GroupListUI(bpy.types.UIList): def animation_group(modifier, layout, context): - if not _check_for_anim(layout, context): + action = _check_for_anim(layout, modifier) + if not action: return row = layout.row() @@ -67,7 +72,11 @@ class LoopListUI(bpy.types.UIList): def animation_loop(modifier, layout, context): - if not _check_for_anim(layout, context): + action = _check_for_anim(layout, modifier) + if action is False: + layout.label("Object must be animated, not ObData", icon="ERROR") + return + elif action is None: return row = layout.row() @@ -86,7 +95,6 @@ def animation_loop(modifier, layout, context): # Modify the loop points if modifier.loops: - action = context.object.animation_data.action loop = modifier.loops[modifier.active_loop_index] layout.prop_search(loop, "loop_start", action, "pose_markers", icon="PMARKER") layout.prop_search(loop, "loop_end", action, "pose_markers", icon="PMARKER")