Browse Source

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.
pull/40/head
Adam Johnson 9 years ago
parent
commit
3f98e6ea9d
  1. 107
      korman/exporter/animation.py
  2. 1
      korman/exporter/rtlight.py
  3. 3
      korman/nodes/node_messages.py
  4. 72
      korman/properties/modifiers/anim.py
  5. 36
      korman/ui/modifiers/anim.py

107
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

1
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]

3
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:

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

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

Loading…
Cancel
Save