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")