Browse Source

Merge pull request #40 from Hoikas/animation

Moar Animations
pull/42/head
Adam Johnson 9 years ago committed by GitHub
parent
commit
6c9fb61715
  1. 517
      korman/exporter/animation.py
  2. 3
      korman/exporter/convert.py
  3. 7
      korman/exporter/material.py
  4. 29
      korman/exporter/rtlight.py
  5. 7
      korman/nodes/node_messages.py
  6. 139
      korman/properties/modifiers/anim.py
  7. 26
      korman/properties/modifiers/physics.py
  8. 19
      korman/properties/modifiers/sound.py
  9. 33
      korman/ui/modifiers/anim.py

517
korman/exporter/animation.py

@ -27,55 +27,331 @@ class AnimationConverter:
self._exporter = weakref.ref(exporter) self._exporter = weakref.ref(exporter)
self._bl_fps = bpy.context.scene.render.fps self._bl_fps = bpy.context.scene.render.fps
def convert_action2tm(self, action, default_xform): def _convert_frame_time(self, frame_num):
"""Converts a Blender Action to a plCompoundController.""" return frame_num / self._bl_fps
fcurves = action.fcurves
def convert_object_animations(self, bo, so):
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, obj_fcurves, bo.matrix_basis))
if bo.plasma_modifiers.soundemit.enabled:
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))
if isinstance(lamp, bpy.types.SpotLamp):
applicators.extend(self._convert_spot_lamp_animation(bo.name, data_fcurves, lamp))
if isinstance(lamp, bpy.types.PointLamp):
applicators.extend(self._convert_omni_lamp_animation(bo.name, data_fcurves, lamp))
# Check to make sure we have some valid animation applicators before proceeding.
if not any(applicators):
return
# There is a race condition in the client with animation loading. It expects for modifiers
# to be listed on the SceneObject in a specific order. D'OH! So, always use these funcs.
agmod, agmaster = self.get_anigraph_objects(bo, so)
atcanim = self._mgr.find_create_object(plATCAnim, so=so)
# Add the animation data to the ATC
for i in applicators:
if i is not None:
atcanim.addApplicator(i)
agmod.channelName = bo.name
agmaster.addPrivateAnim(atcanim.key)
# This was previously part of the Animation Modifier, however, there can be lots of animations
# Therefore we move it here.
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(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
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
atcanim.easeInMax = 1.0
atcanim.easeInLength = 1.0
atcanim.easeOutMin = 1.0
atcanim.easeOutMax = 1.0
atcanim.easeOutLength = 1.0
def _convert_lamp_color_animation(self, name, fcurves, lamp):
if not fcurves: if not fcurves:
return None return None
# NOTE: plCompoundController is from Myst 5 and was backported to MOUL. energy_curve = next((i for i in fcurves if i.data_path == "energy" and i.keyframe_points), None)
# Worry not however... libHSPlasma will do the conversion for us. 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_omni_lamp_animation(self, name, fcurves, lamp):
energy_fcurve = next((i for i in fcurves if i.data_path == "energy"), None)
distance_fcurve = next((i for i in fcurves if i.data_path == "distance"), None)
if energy_fcurve is None and distance_fcurve is None:
return None
light_converter = self._exporter().light
intensity, atten_end = light_converter.convert_attenuation(lamp)
# All types allow animating cutoff
if distance_fcurve is not None:
channel = plScalarControllerChannel()
channel.controller = self.make_scalar_leaf_controller(distance_fcurve,
lambda x: x * 2 if lamp.use_sphere else x)
applicator = plOmniCutoffApplicator()
applicator.channelName = name
applicator.channel = channel
yield applicator
falloff = lamp.falloff_type
if falloff == "CONSTANT":
if energy_fcurve is not None:
self._exporter().report.warn("Constant attenuation cannot be animated in Plasma", ident=3)
elif falloff == "INVERSE_LINEAR":
def convert_linear_atten(distance, energy):
intens = abs(energy[0])
atten_end = distance[0] * 2 if lamp.use_sphere else distance[0]
return light_converter.convert_attenuation_linear(intens, atten_end)
keyframes = self._process_fcurves([distance_fcurve, energy_fcurve], convert_linear_atten,
{"distance": lamp.distance, "energy": lamp.energy})
if keyframes:
channel = plScalarControllerChannel()
channel.controller = self._make_scalar_leaf_controller(keyframes, False)
applicator = plOmniApplicator()
applicator.channelName = name
applicator.channel = channel
yield applicator
elif falloff == "INVERSE_SQUARE":
def convert_quadratic_atten(distance, energy):
intens = abs(energy[0])
atten_end = distance[0] * 2 if lamp.use_sphere else distance[0]
return light_converter.convert_attenuation_quadratic(intens, atten_end)
keyframes = self._process_fcurves([distance_fcurve, energy_fcurve], convert_quadratic_atten,
{"distance": lamp.distance, "energy": lamp.energy})
if keyframes:
channel = plScalarControllerChannel()
channel.controller = self._make_scalar_leaf_controller(keyframes, False)
applicator = plOmniSqApplicator()
applicator.channelName = name
applicator.channel = channel
yield applicator
else:
self._exporter().report.warn("Lamp Falloff '{}' animations are not supported".format(falloff), ident=3)
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
else:
return math.log10(value) * 20.0
for sound in soundemit.sounds:
path = "{}.volume".format(sound.path_from_id())
fcurve = next((i for i in fcurves if i.data_path == path and i.keyframe_points), None)
if fcurve is None:
continue
for i in soundemit.get_sound_indices(sound=sound):
applicator = plSoundVolumeApplicator()
applicator.channelName = name
applicator.index = i
# libHSPlasma assumes a channel is not shared among applicators...
# so yes, we must convert the same animation data again and again.
channel = plScalarControllerChannel()
channel.controller = self.make_scalar_leaf_controller(fcurve, convert=convert_volume)
applicator.channel = channel
yield applicator
def _convert_spot_lamp_animation(self, name, fcurves, lamp):
blend_fcurve = next((i for i in fcurves if i.data_path == "spot_blend"), None)
size_fcurve = next((i for i in fcurves if i.data_path == "spot_size"), None)
if blend_fcurve is None and size_fcurve is None:
return None
# Spot Outer is just the size keyframes...
if size_fcurve is not None:
channel = plScalarControllerChannel()
channel.controller = self.make_scalar_leaf_controller(size_fcurve, lambda x: math.degrees(x))
applicator = plSpotOuterApplicator()
applicator.channelName = name
applicator.channel = channel
yield applicator
# Spot inner must be calculated...
def convert_spot_inner(spot_blend, spot_size):
blend = min(0.001, spot_blend[0])
size = spot_size[0]
value = size - (blend * size)
return math.degrees(value)
defaults = { "spot_blend": lamp.spot_blend, "spot_size": lamp.spot_size }
keyframes = self._process_fcurves([blend_fcurve, size_fcurve], convert_spot_inner, defaults)
if keyframes:
channel = plScalarControllerChannel()
channel.controller = self._make_scalar_leaf_controller(keyframes, False)
applicator = plSpotInnerApplicator()
applicator.channelName = name
applicator.channel = channel
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)
if pos is None and rot is None and scale is None:
return None
tm = plCompoundController() tm = plCompoundController()
tm.X = self.make_pos_controller(fcurves, default_xform) tm.X = pos
tm.Y = self.make_rot_controller(fcurves, default_xform) tm.Y = rot
tm.Z = self.make_scale_controller(fcurves, default_xform) tm.Z = scale
return tm
applicator = plMatrixChannelApplicator()
def make_matrix44_controller(self, pos_fcurves, scale_fcurves, default_pos, default_scale): applicator.enabled = True
pos_keyframes, pos_bez = self._process_keyframes(pos_fcurves) applicator.channelName = name
scale_keyframes, scale_bez = self._process_keyframes(scale_fcurves) channel = plMatrixControllerChannel()
if not pos_keyframes and not scale_keyframes: channel.controller = tm
applicator.channel = channel
# Decompose the matrix into the 90s-era 3ds max affine parts sillyness
# All that's missing now is something like "(c) 1998 HeadSpin" oh wait...
affine = hsAffineParts()
affine.T = hsVector3(*xform.to_translation())
affine.K = hsVector3(*xform.to_scale())
affine.F = -1.0 if xform.determinant() < 0.0 else 1.0
rot = xform.to_quaternion()
affine.Q = utils.quaternion(rot)
rot.normalize()
affine.U = utils.quaternion(rot)
channel.affine = affine
return applicator
def get_anigraph_keys(self, bo=None, so=None):
mod = self._mgr.find_create_key(plAGModifier, so=so, bl=bo)
master = self._mgr.find_create_key(plAGMasterMod, so=so, bl=bo)
return mod, master
def get_anigraph_objects(self, bo=None, so=None):
mod = self._mgr.find_create_object(plAGModifier, so=so, bl=bo)
master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo)
return mod, master
def has_transform_animation(self, bo):
if bo.animation_data is not None:
if bo.animation_data.action is not None:
data_paths = frozenset((i.data_path for i in bo.animation_data.action.fcurves))
return {"location", "rotation_euler", "scale"} & data_paths
return False
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, fcurves, pos_path, scale_path, pos_default, scale_default):
def convert_matrix_keyframe(**kwargs):
pos = kwargs.get(pos_path)
scale = kwargs.get(scale_path)
matrix = hsMatrix44()
# Note: scale and pos are dicts, so we can't unpack
matrix.setTranslate(hsVector3(pos[0], pos[1], pos[2]))
matrix.setScale(hsVector3(scale[0], scale[1], scale[2]))
return matrix
fcurves = [i for i in fcurves if i.data_path == pos_path or i.data_path == scale_path]
if not fcurves:
return None return None
# Matrix keyframes cannot do bezier schtuff default_values = { pos_path: pos_default, scale_path: scale_default }
if pos_bez or scale_bez: keyframes = self._process_fcurves(fcurves, convert_matrix_keyframe, default_values)
self._exporter().report.warn("This animation cannot use bezier keyframes--forcing linear", indent=3) if not keyframes:
return None
# 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
# to have code that makes sense.
keyframes = []
for pos, scale in itertools.zip_longest(pos_keyframes, scale_keyframes, fillvalue=None):
if pos is None:
keyframes.append((None, scale))
elif scale is None:
keyframes.append((pos, scale))
elif pos.frame_num == scale.frame_num:
keyframes.append((pos, scale))
elif pos.frame_num < scale.frame_num:
keyframes.append((pos, None))
keyframes.append((None, scale))
elif pos.frame_num > scale.frame_num:
keyframes.append((None, scale))
keyframes.append((pos, None))
# Now we make the controller # Now we make the controller
ctrl = self._make_matrix44_controller(pos_fcurves, scale_fcurves, keyframes, default_pos, default_scale) return self._make_matrix44_controller(keyframes)
return ctrl
def make_pos_controller(self, fcurves, default_xform): def make_pos_controller(self, fcurves, default_xform, convert=None):
pos_curves = [i for i in fcurves if i.data_path == "location" and i.keyframe_points] pos_curves = [i for i in fcurves if i.data_path == "location" and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(pos_curves) keyframes, bez_chans = self._process_keyframes(pos_curves, convert)
if not keyframes: if not keyframes:
return None return None
@ -84,10 +360,10 @@ class AnimationConverter:
ctrl = self._make_point3_controller(pos_curves, keyframes, bez_chans, default_xform.to_translation()) ctrl = self._make_point3_controller(pos_curves, keyframes, bez_chans, default_xform.to_translation())
return ctrl return ctrl
def make_rot_controller(self, fcurves, default_xform): def make_rot_controller(self, fcurves, default_xform, convert=None):
# TODO: support rotation_quaternion # TODO: support rotation_quaternion
rot_curves = [i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points] rot_curves = [i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(rot_curves) keyframes, bez_chans = self._process_keyframes(rot_curves, convert=None)
if not keyframes: if not keyframes:
return None return None
@ -99,9 +375,9 @@ class AnimationConverter:
ctrl = self._make_quat_controller(rot_curves, keyframes, default_xform.to_euler()) ctrl = self._make_quat_controller(rot_curves, keyframes, default_xform.to_euler())
return ctrl return ctrl
def make_scale_controller(self, fcurves, default_xform): def make_scale_controller(self, fcurves, default_xform, convert=None):
scale_curves = [i for i in fcurves if i.data_path == "scale" and i.keyframe_points] scale_curves = [i for i in fcurves if i.data_path == "scale" and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(scale_curves) keyframes, bez_chans = self._process_keyframes(scale_curves, convert)
if not keyframes: if not keyframes:
return None return None
@ -109,49 +385,25 @@ class AnimationConverter:
ctrl = self._make_scale_value_controller(scale_curves, keyframes, bez_chans, default_xform) ctrl = self._make_scale_value_controller(scale_curves, keyframes, bez_chans, default_xform)
return ctrl return ctrl
def make_scalar_leaf_controller(self, fcurve): def make_scalar_leaf_controller(self, fcurve, convert=None):
keyframes, bezier = self._process_fcurve(fcurve) keyframes, bezier = self._process_fcurve(fcurve, convert)
if not keyframes: if not keyframes:
return None return None
ctrl = self._make_scalar_leaf_controller(keyframes, bezier) ctrl = self._make_scalar_leaf_controller(keyframes, bezier)
return ctrl return ctrl
def _make_matrix44_controller(self, pos_fcurves, scale_fcurves, keyframes, default_pos, default_scale): def _make_matrix44_controller(self, keyframes):
ctrl = plLeafController() ctrl = plLeafController()
keyframe_type = hsKeyFrame.kMatrix44KeyFrame keyframe_type = hsKeyFrame.kMatrix44KeyFrame
exported_frames = [] exported_frames = []
pcurves = { i.array_index: i for i in pos_fcurves }
scurves = { i.array_index: i for i in scale_fcurves }
def eval_fcurve(fcurves, keyframe, i, default_xform):
try:
return fcurves[i].evaluate(keyframe.frame_num_blender)
except KeyError:
return default_xform[i]
for pos_key, scale_key in keyframes: for keyframe in keyframes:
valid_key = pos_key if pos_key is not None else scale_key
exported = hsMatrix44Key() exported = hsMatrix44Key()
exported.frame = valid_key.frame_num exported.frame = keyframe.frame_num
exported.frameTime = valid_key.frame_time exported.frameTime = keyframe.frame_time
exported.type = keyframe_type exported.type = keyframe_type
exported.value = keyframe.value
if pos_key is not None:
pos_value = [pos_key.values[i] if i in pos_key.values else eval_fcurve(pcurves, pos_key, i, default_pos) for i in range(3)]
else:
pos_value = [eval_fcurve(pcurves, valid_key, i, default_pos) for i in range(3)]
if scale_key is not None:
scale_value = [scale_key.values[i] if i in scale_key.values else eval_fcurve(scurves, scale_key, i, default_scale) for i in range(3)]
else:
scale_value = [eval_fcurve(scurves, valid_key, i, default_scale) for i in range(3)]
pos_value = hsVector3(*pos_value)
scale_value = hsVector3(*scale_value)
value = hsMatrix44()
value.setTranslate(pos_value)
value.setScale(scale_value)
exported.value = value
exported_frames.append(exported) exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type) ctrl.keys = (exported_frames, keyframe_type)
return ctrl return ctrl
@ -315,7 +567,7 @@ class AnimationConverter:
ctrl.keys = (exported_frames, keyframe_type) ctrl.keys = (exported_frames, keyframe_type)
return ctrl return ctrl
def _process_fcurve(self, fcurve): def _process_fcurve(self, fcurve, convert=None):
"""Like _process_keyframes, but for one fcurve""" """Like _process_keyframes, but for one fcurve"""
keyframe_data = type("KeyFrameData", (), {}) keyframe_data = type("KeyFrameData", (), {})
fps = self._bl_fps fps = self._bl_fps
@ -339,12 +591,121 @@ class AnimationConverter:
else: else:
keyframe.in_tan = 0.0 keyframe.in_tan = 0.0
keyframe.out_tan = 0.0 keyframe.out_tan = 0.0
keyframe.value = value keyframe.value = value if convert is None else convert(value)
keyframes[frame_num] = keyframe keyframes[frame_num] = keyframe
final_keyframes = [keyframes[i] for i in sorted(keyframes)] final_keyframes = [keyframes[i] for i in sorted(keyframes)]
return (final_keyframes, bezier) return (final_keyframes, bezier)
def _process_keyframes(self, fcurves): def _process_fcurves(self, fcurves, convert, defaults=None):
"""Processes FCurves of different data sets and converts them into a single list of keyframes.
This should be used when multiple Blender fields map to a single Plasma option."""
class KeyFrameData:
def __init__(self):
self.values = {}
fps = self._bl_fps
pi = math.pi
# It is assumed therefore that any multichannel FCurves will have all channels represented.
# This seems fairly safe with my experiments with Lamp colors...
grouped_fcurves = {}
for fcurve in fcurves:
if fcurve is None:
continue
fcurve.update()
if fcurve.data_path in grouped_fcurves:
grouped_fcurves[fcurve.data_path][fcurve.array_index] = fcurve
else:
grouped_fcurves[fcurve.data_path] = { fcurve.array_index: fcurve }
# Default values for channels that are not animated
for key, value in defaults.items():
if key not in grouped_fcurves:
if hasattr(value, "__len__"):
grouped_fcurves[key] = value
else:
grouped_fcurves[key] = [value,]
# Assemble a dict { PlasmaFrameNum: { FCurveDataPath: KeyFrame } }
keyframe_points = {}
for fcurve in fcurves:
if fcurve is None:
continue
for keyframe in fcurve.keyframe_points:
frame_num_blender, value = keyframe.co
frame_num = int(frame_num_blender * (30.0 / fps))
# This is a temporary keyframe, so we're not going to worry about converting everything
# Only the frame number to Plasma so we can go ahead and merge any rounded dupes
entry, data = keyframe_points.get(frame_num), None
if entry is None:
entry = {}
keyframe_points[frame_num] = entry
else:
data = entry.get(fcurve.data_path)
if data is None:
data = KeyFrameData()
data.frame_num = frame_num
data.frame_num_blender = frame_num_blender
entry[fcurve.data_path] = data
data.values[fcurve.array_index] = value
# Now, we loop through our assembled keyframes and interpolate any missing data using the FCurves
fcurve_chans = { key: len(value) for key, value in grouped_fcurves.items() }
expected_values = sum(fcurve_chans.values())
all_chans = frozenset(grouped_fcurves.keys())
# We will also do the final convert here as well...
final_keyframes = []
for frame_num in sorted(keyframe_points.copy().keys()):
keyframes = keyframe_points[frame_num]
frame_num_blender = next(iter(keyframes.values())).frame_num_blender
# If any data_paths are missing, init a dummy
missing_channels = all_chans - frozenset(keyframes.keys())
for chan in missing_channels:
dummy = KeyFrameData()
dummy.frame_num = frame_num
dummy.frame_num_blender = frame_num_blender
keyframes[chan] = dummy
# Ensure all values are filled out.
num_values = sum(map(len, (i.values for i in keyframes.values())))
if num_values != expected_values:
for chan, sorted_fcurves in grouped_fcurves.items():
chan_keyframes = keyframes[chan]
chan_values = fcurve_chans[chan]
if len(chan_keyframes.values) == chan_values:
continue
for i in range(chan_values):
if i not in chan_keyframes.values:
fcurve = grouped_fcurves[chan][i]
if isinstance(fcurve, bpy.types.FCurve):
chan_keyframes.values[i] = fcurve.evaluate(chan_keyframes.frame_num_blender)
else:
# it's actually a default value!
chan_keyframes.values[i] = fcurve
# All values are calculated! Now we convert the disparate key data into a single keyframe.
kwargs = { data_path: keyframe.values for data_path, keyframe in keyframes.items() }
final_keyframe = KeyFrameData()
final_keyframe.frame_num = frame_num
final_keyframe.frame_num_blender = frame_num_blender
final_keyframe.frame_time = frame_num / fps
value = convert(**kwargs)
if hasattr(value, "__len__"):
final_keyframe.in_tans = [0.0] * len(value)
final_keyframe.out_tans = [0.0] * len(value)
final_keyframe.values = value
else:
final_keyframe.in_tan = 0.0
final_keyframe.out_tan = 0.0
final_keyframe.value = value
final_keyframes.append(final_keyframe)
return final_keyframes
def _process_keyframes(self, fcurves, convert=None):
"""Groups all FCurves for the same frame together""" """Groups all FCurves for the same frame together"""
keyframe_data = type("KeyFrameData", (), {}) keyframe_data = type("KeyFrameData", (), {})
fps = self._bl_fps fps = self._bl_fps
@ -371,7 +732,7 @@ class AnimationConverter:
keyframe.values = {} keyframe.values = {}
keyframes[frame_num] = keyframe keyframes[frame_num] = keyframe
idx = fcurve.array_index idx = fcurve.array_index
keyframe.values[idx] = value keyframe.values[idx] = value if convert is None else convert(value)
# Calculate the bezier interpolation nonsense # Calculate the bezier interpolation nonsense
if fkey.interpolation == "BEZIER": if fkey.interpolation == "BEZIER":

3
korman/exporter/convert.py

@ -207,6 +207,7 @@ class Exporter:
sceneobject = self.mgr.find_create_object(plSceneObject, bl=bl_obj) sceneobject = self.mgr.find_create_object(plSceneObject, bl=bl_obj)
self._export_actor(sceneobject, bl_obj) self._export_actor(sceneobject, bl_obj)
export_fn(sceneobject, bl_obj) export_fn(sceneobject, bl_obj)
self.animation.convert_object_animations(bl_obj, sceneobject)
# And now we puke out the modifiers... # And now we puke out the modifiers...
for mod in bl_obj.plasma_modifiers.modifiers: for mod in bl_obj.plasma_modifiers.modifiers:
@ -258,6 +259,8 @@ class Exporter:
return True return True
if bo.name in self.actors: if bo.name in self.actors:
return True return True
if self.animation.has_transform_animation(bo):
return True
for mod in bo.plasma_modifiers.modifiers: for mod in bo.plasma_modifiers.modifiers:
if mod.enabled: if mod.enabled:

7
korman/exporter/material.py

@ -295,11 +295,12 @@ class MaterialConverter:
return None return None
def _export_layer_transform_animation(self, tex_slot, base_layer, fcurves): def _export_layer_transform_animation(self, tex_slot, base_layer, fcurves):
pos_fcurves = [i for i in fcurves if i.data_path.find("offset") != -1] path = tex_slot.path_from_id()
scale_fcurves = [i for i in fcurves if i.data_path.find("scale") != -1] pos_path = "{}.offset".format(path)
scale_path = "{}.scale".format(path)
# Plasma uses the controller to generate a matrix44... so we have to produce a leaf controller # Plasma uses the controller to generate a matrix44... so we have to produce a leaf controller
ctrl = self._exporter().animation.make_matrix44_controller(pos_fcurves, scale_fcurves, tex_slot.offset, tex_slot.scale) ctrl = self._exporter().animation.make_matrix44_controller(fcurves, pos_path, scale_path, tex_slot.offset, tex_slot.scale)
return ctrl return ctrl
def _export_texture_type_environment_map(self, bo, layer, slot): def _export_texture_type_environment_map(self, bo, layer, slot):

29
korman/exporter/rtlight.py

@ -40,11 +40,8 @@ class LightConverter:
} }
def _convert_attenuation(self, bl, pl): def _convert_attenuation(self, bl, pl):
intens = bl.energy # If you change these calculations, be sure to update the AnimationConverter!
if intens < 0: intens, attenEnd = self.convert_attenuation(bl)
intens = -intens
attenEnd = bl.distance * 2 if bl.use_sphere else bl.distance
if bl.falloff_type == "CONSTANT": if bl.falloff_type == "CONSTANT":
print(" Attenuation: No Falloff") print(" Attenuation: No Falloff")
pl.attenConst = intens pl.attenConst = intens
@ -54,18 +51,29 @@ class LightConverter:
elif bl.falloff_type == "INVERSE_LINEAR": elif bl.falloff_type == "INVERSE_LINEAR":
print(" Attenuation: Inverse Linear") print(" Attenuation: Inverse Linear")
pl.attenConst = 1.0 pl.attenConst = 1.0
pl.attenLinear = max(0.0, (intens * _FAR_POWER - 1.0) / attenEnd) pl.attenLinear = self.convert_attenuation_linear(intens, attenEnd)
pl.attenQuadratic = 0.0 pl.attenQuadratic = 0.0
pl.attenCutoff = attenEnd pl.attenCutoff = attenEnd
elif bl.falloff_type == "INVERSE_SQUARE": elif bl.falloff_type == "INVERSE_SQUARE":
print(" Attenuation: Inverse Square") print(" Attenuation: Inverse Square")
pl.attenConst = 1.0 pl.attenConst = 1.0
pl.attenLinear = 0.0 pl.attenLinear = 0.0
pl.attenQuadratic = max(0.0, (intens * _FAR_POWER - 1.0) / (attenEnd * attenEnd)) pl.attenQuadratic = self.convert_attenuation_quadratic(intens, attenEnd)
pl.attenCutoff = attenEnd pl.attenCutoff = attenEnd
else: else:
raise BlenderOptionNotSupportedError(bl.falloff_type) raise BlenderOptionNotSupportedError(bl.falloff_type)
def convert_attenuation(self, lamp):
intens = abs(lamp.energy)
attenEnd = lamp.distance * 2 if lamp.use_sphere else lamp.distance
return (intens, attenEnd)
def convert_attenuation_linear(self, intensity, end):
return max(0.0, (intensity * _FAR_POWER - 1.0) / end)
def convert_attenuation_quadratic(self, intensity, end):
return max(0.0, (intensity * _FAR_POWER - 1.0) / pow(end, 2))
def _convert_area_lamp(self, bl, pl): def _convert_area_lamp(self, bl, pl):
print(" [LimitedDirLightInfo '{}']".format(bl.name)) print(" [LimitedDirLightInfo '{}']".format(bl.name))
@ -104,6 +112,7 @@ class LightConverter:
self._converter_funcs[bl_light.type](bl_light, pl_light) self._converter_funcs[bl_light.type](bl_light, pl_light)
# Light color nonsense # Light color nonsense
# Please note that these calculations are duplicated in the AnimationConverter
energy = bl_light.energy energy = bl_light.energy
if bl_light.use_negative: if bl_light.use_negative:
diff_color = [(0.0 - i) * energy for i in bl_light.color] diff_color = [(0.0 - i) * energy for i in bl_light.color]
@ -175,6 +184,11 @@ class LightConverter:
if projectors: if projectors:
self._export_rt_projector(bo, pl_light, projectors) self._export_rt_projector(bo, pl_light, projectors)
# If the lamp has any sort of animation attached, then it needs to be marked movable.
# Otherwise, Plasma may not use it for lighting.
if projectors or self._exporter().animation.is_animated(bo):
pl_light.setProperty(plLightInfo.kLPMovable, True)
# *Sigh* # *Sigh*
pl_light.sceneNode = self.mgr.get_scene_node(location=so.key.location) pl_light.sceneNode = self.mgr.get_scene_node(location=so.key.location)
@ -202,7 +216,6 @@ class LightConverter:
else: else:
state.miscFlags |= hsGMatState.kMiscOrthoProjection state.miscFlags |= hsGMatState.kMiscOrthoProjection
state.ZFlags |= hsGMatState.kZNoZWrite state.ZFlags |= hsGMatState.kZNoZWrite
pl_light.setProperty(plLightInfo.kLPMovable, True)
pl_light.setProperty(plLightInfo.kLPCastShadows, False) pl_light.setProperty(plLightInfo.kLPCastShadows, False)
if slot.blend_type == "ADD": if slot.blend_type == "ADD":

7
korman/nodes/node_messages.py

@ -168,8 +168,7 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node):
obj = bpy.data.objects.get(self.object_name, None) obj = bpy.data.objects.get(self.object_name, None)
if obj is None: if obj is None:
self.raise_error("invalid object: '{}'".format(self.object_name)) self.raise_error("invalid object: '{}'".format(self.object_name))
anim = obj.plasma_modifiers.animation if not exporter.animation.is_animated(obj):
if not anim.enabled:
self.raise_error("invalid animation") self.raise_error("invalid animation")
group = obj.plasma_modifiers.animation_group group = obj.plasma_modifiers.animation_group
if group.enabled: if group.enabled:
@ -178,9 +177,7 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node):
# (but obviously this is not wrong...) # (but obviously this is not wrong...)
target = exporter.mgr.find_create_key(plMsgForwarder, bl=obj, name=group.key_name) target = exporter.mgr.find_create_key(plMsgForwarder, bl=obj, name=group.key_name)
else: else:
# remember, the AGModifier MUST exist first... so just in case... _agmod_trash, target = exporter.animation.get_anigraph_keys(obj)
exporter.mgr.find_create_key(plAGModifier, bl=obj, name=anim.key_name)
target = exporter.mgr.find_create_key(plAGMasterMod, bl=obj, name=anim.key_name)
else: else:
material = bpy.data.materials.get(self.material_name, None) material = bpy.data.materials.get(self.material_name, None)
if material is None: if material is None:

139
korman/properties/modifiers/anim.py

@ -24,14 +24,21 @@ def _convert_frame_time(frame_num):
fps = bpy.context.scene.render.fps fps = bpy.context.scene.render.fps
return frame_num / fps return frame_num / fps
def _get_blender_action(bo): class ActionModifier:
if bo.animation_data is None or bo.animation_data.action is None: @property
raise ExportError("Object '{}' has no Action to export".format(bo.name)) def blender_action(self):
if not bo.animation_data.action.fcurves: bo = self.id_data
raise ExportError("Object '{}' is animated but has no FCurves".format(bo.name)) if bo.animation_data is not None and bo.animation_data.action is not None:
return bo.animation_data.action 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(PlasmaModifierProperties): class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
pl_id = "animation" pl_id = "animation"
bl_category = "Animation" bl_category = "Animation"
@ -53,22 +60,16 @@ class PlasmaAnimationModifier(PlasmaModifierProperties):
loop_end = StringProperty(name="Loop End", loop_end = StringProperty(name="Loop End",
description="Marker indicating where the default loop ends") description="Marker indicating where the default loop ends")
@property
def requires_actor(self):
return True
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
action = _get_blender_action(bo) action = self.blender_action
markers = action.pose_markers
atcanim = exporter.mgr.find_create_object(plATCAnim, so=so, name=self.key_name) atcanim = exporter.mgr.find_create_object(plATCAnim, so=so)
atcanim.autoStart = self.auto_start atcanim.autoStart = self.auto_start
atcanim.loop = self.loop atcanim.loop = self.loop
atcanim.name = "(Entire Animation)"
atcanim.start = _convert_frame_time(action.frame_range[0])
atcanim.end = _convert_frame_time(action.frame_range[1])
# Simple start and loop info # Simple start and loop info
if action is not None:
markers = action.pose_markers
initial_marker = markers.get(self.initial_marker) initial_marker = markers.get(self.initial_marker)
if initial_marker is not None: if initial_marker is not None:
atcanim.initial = _convert_frame_time(initial_marker.frame) atcanim.initial = _convert_frame_time(initial_marker.frame)
@ -79,94 +80,29 @@ class PlasmaAnimationModifier(PlasmaModifierProperties):
if loop_start is not None: if loop_start is not None:
atcanim.loopStart = _convert_frame_time(loop_start.frame) atcanim.loopStart = _convert_frame_time(loop_start.frame)
else: else:
atcanim.loopStart = _convert_frame_time(action.frame_range[0]) atcanim.loopStart = atcanim.start
loop_end = markers.get(self.loop_end) loop_end = markers.get(self.loop_end)
if loop_end is not None: if loop_end is not None:
atcanim.loopEnd = _convert_frame_time(loop_end.frame) atcanim.loopEnd = _convert_frame_time(loop_end.frame)
else: else:
atcanim.loopEnd = _convert_frame_time(action.frame_range[1]) atcanim.loopEnd = atcanim.end
else:
# Marker points if self.loop:
for marker in markers: atcanim.loopStart = atcanim.start
atcanim.setMarker(marker.name, _convert_frame_time(marker.frame)) atcanim.loopEnd = atcanim.end
# Fixme? Not sure if we really need to expose this...
atcanim.easeInMin = 1.0
atcanim.easeInMax = 1.0
atcanim.easeInLength = 1.0
atcanim.easeOutMin = 1.0
atcanim.easeOutMax = 1.0
atcanim.easeOutLength = 1.0
# Now for the animation data. We're mostly just going to hand this off to the controller code
matrix = bo.matrix_basis
applicator = plMatrixChannelApplicator()
applicator.enabled = True
applicator.channelName = bo.name
channel = plMatrixControllerChannel()
channel.controller = exporter.animation.convert_action2tm(action, matrix)
applicator.channel = channel
atcanim.addApplicator(applicator)
# Decompose the matrix into the 90s-era 3ds max affine parts sillyness
# All that's missing now is something like "(c) 1998 HeadSpin" oh wait...
affine = hsAffineParts()
affine.T = hsVector3(*matrix.to_translation())
affine.K = hsVector3(*matrix.to_scale())
affine.F = -1.0 if matrix.determinant() < 0.0 else 1.0
rot = matrix.to_quaternion()
affine.Q = utils.quaternion(rot)
rot.normalize()
affine.U = utils.quaternion(rot)
channel.affine = affine
# We need both an AGModifier and an AGMasterMod
# NOTE: mandatory order--otherwise the animation will not work in game!
agmod = exporter.mgr.find_create_object(plAGModifier, so=so, name=self.key_name)
agmod.channelName = bo.name
agmaster = exporter.mgr.find_create_object(plAGMasterMod, so=so, name=self.key_name)
agmaster.addPrivateAnim(atcanim.key)
@property
def key_name(self):
return "{}_(Entire Animation)".format(self.id_data.name)
def _make_physical_movable(self, so):
sim = so.sim
if sim is not None:
sim = sim.object
sim.setProperty(plSimulationInterface.kPhysAnim, True)
phys = sim.physical.object
phys.setProperty(plSimulationInterface.kPhysAnim, True)
# If the mass is zero, then we will fail to animate. Fix that.
if phys.mass == 0.0:
phys.mass = 1.0
# set kPinned so it doesn't fall through
sim.setProperty(plSimulationInterface.kPinned, True)
phys.setProperty(plSimulationInterface.kPinned, True)
# Do the same for children objects
for child in so.coord.object.children:
self.make_physical_movable(child.object)
def post_export(self, exporter, bo, so):
# If this object has a physical, we need to tell the simulation iface that it can be animated
self._make_physical_movable(so)
class AnimGroupObject(bpy.types.PropertyGroup): class AnimGroupObject(bpy.types.PropertyGroup):
object_name = StringProperty(name="Child", object_name = StringProperty(name="Child Animation",
description="Object whose action is a child animation") description="Object whose action is a child animation")
class PlasmaAnimationGroupModifier(PlasmaModifierProperties): class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
pl_id = "animation_group" pl_id = "animation_group"
pl_depends = {"animation"} pl_depends = {"animation"}
bl_category = "Animation" bl_category = "Animation"
bl_label = "Group" bl_label = "Group Master"
bl_description = "Defines related animations" bl_description = "Defines related animations"
bl_icon = "GROUP" bl_icon = "GROUP"
@ -176,17 +112,14 @@ class PlasmaAnimationGroupModifier(PlasmaModifierProperties):
active_child_index = IntProperty(options={"HIDDEN"}) active_child_index = IntProperty(options={"HIDDEN"})
def export(self, exporter, bo, so): def export(self, exporter, bo, so):
action = _get_blender_action(bo) if not exporter.animation.is_animated(bo):
key_name = bo.plasma_modifiers.animation.key_name raise ExportError("'{}': Object is not animated".format(bo.name))
# See above... AGModifier must always be inited first...
agmod = exporter.mgr.find_create_object(plAGModifier, so=so, name=key_name)
# The message forwarder is the guy that makes sure that everybody knows WTF is going on # 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) msgfwd = exporter.mgr.find_create_object(plMsgForwarder, so=so, name=self.key_name)
# Now, this is da swhiz... # Now, this is da swhiz...
agmaster = exporter.mgr.find_create_object(plAGMasterMod, so=so, name=key_name) agmod, agmaster = exporter.animation.get_anigraph_objects(bo, so)
agmaster.msgForwarder = msgfwd.key agmaster.msgForwarder = msgfwd.key
agmaster.isGrouped, agmaster.isGroupMaster = True, True agmaster.isGrouped, agmaster.isGroupMaster = True, True
for i in self.children: for i in self.children:
@ -204,9 +137,8 @@ class PlasmaAnimationGroupModifier(PlasmaModifierProperties):
msg = "Animation Group '{}' specifies an object '{}' with no Plasma Animation modifier. Ignoring..." msg = "Animation Group '{}' specifies an object '{}' with no Plasma Animation modifier. Ignoring..."
exporter.report.warn(msg.format(self.key_name, i.object_name), indent=2) exporter.report.warn(msg.format(self.key_name, i.object_name), indent=2)
continue continue
child_agmod = exporter.mgr.find_create_key(plAGModifier, bl=child_bo, name=child_animation.key_name) child_agmod, child_agmaster = exporter.animation.get_anigraph_objects(bo=child_bo)
child_agmaster = exporter.mgr.find_create_key(plAGMasterMod, bl=child_bo, name=child_animation.key_name) msgfwd.addForwardKey(child_agmaster.key)
msgfwd.addForwardKey(child_agmaster)
msgfwd.addForwardKey(agmaster.key) msgfwd.addForwardKey(agmaster.key)
@property @property
@ -223,7 +155,7 @@ class LoopMarker(bpy.types.PropertyGroup):
description="Marker name from whence the loop ends") description="Marker name from whence the loop ends")
class PlasmaAnimationLoopModifier(PlasmaModifierProperties): class PlasmaAnimationLoopModifier(ActionModifier, PlasmaModifierProperties):
pl_id = "animation_loop" pl_id = "animation_loop"
pl_depends = {"animation"} pl_depends = {"animation"}
@ -238,11 +170,12 @@ class PlasmaAnimationLoopModifier(PlasmaModifierProperties):
active_loop_index = IntProperty(options={"HIDDEN"}) active_loop_index = IntProperty(options={"HIDDEN"})
def export(self, exporter, bo, so): 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 markers = action.pose_markers
key_name = bo.plasma_modifiers.animation.key_name atcanim = exporter.mgr.find_create_object(plATCAnim, so=so)
atcanim = exporter.mgr.find_create_object(plATCAnim, so=so, name=key_name)
for loop in self.loops: for loop in self.loops:
start = markers.get(loop.loop_start) start = markers.get(loop.loop_start)
end = markers.get(loop.loop_end) end = markers.get(loop.loop_end)

26
korman/properties/modifiers/physics.py

@ -84,6 +84,32 @@ class PlasmaCollider(PlasmaModifierProperties):
if self.terrain: if self.terrain:
physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable
def _make_physical_movable(self, so):
sim = so.sim
if sim is not None:
sim = sim.object
phys = sim.physical.object
_set_phys_prop(plSimulationInterface.kPhysAnim, sim, phys)
# If the mass is zero, then we will fail to animate. Fix that.
if phys.mass == 0.0:
phys.mass = 1.0
# set kPinned so it doesn't fall through
_set_phys_prop(plSimulationInterface.kPinned, sim, phys)
# Do the same for child objects
for child in so.coord.object.children:
self._make_physical_movable(child.object)
def post_export(self, exporter, bo, so):
test_bo = bo
while test_bo is not None:
if exporter.animation.has_transform_animation(test_bo):
self._make_physical_movable(so)
break
test_bo = test_bo.parent
@property @property
def key_name(self): def key_name(self):
return "{}_Collision".format(self.id_data.name) return "{}_Collision".format(self.id_data.name)

19
korman/properties/modifiers/sound.py

@ -131,7 +131,7 @@ class PlasmaSound(bpy.types.PropertyGroup):
volume = IntProperty(name="Volume", volume = IntProperty(name="Volume",
description="Volume to play the sound", description="Volume to play the sound",
min=0, max=100, default=100, min=0, max=100, default=100,
options=set(), options={"ANIMATABLE"},
subtype="PERCENTAGE") subtype="PERCENTAGE")
fade_in = PointerProperty(type=PlasmaSfxFade, options=set()) fade_in = PointerProperty(type=PlasmaSfxFade, options=set())
@ -344,10 +344,25 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
if i.sound_data and i.enabled: if i.sound_data and i.enabled:
i.convert_sound(exporter, so, winaud) i.convert_sound(exporter, so, winaud)
def get_sound_indices(self, name): def get_sound_indices(self, name=None, sound=None):
"""Returns the index of the given sound in the plWin32Sound. This is needed because stereo """Returns the index of the given sound in the plWin32Sound. This is needed because stereo
3D sounds export as two mono sound objects -- wheeeeee""" 3D sounds export as two mono sound objects -- wheeeeee"""
assert name or sound
idx = 0 idx = 0
if name is None:
for i in self.sounds:
if i == sound:
yield idx
if i.is_3d_stereo:
yield idx + 1
break
else:
idx += 2 if i.is_3d_stereo else 1
else:
raise LookupError(sound)
if sound is None:
for i in self.sounds: for i in self.sounds:
if i.name == name: if i.name == name:
yield idx yield idx

33
korman/ui/modifiers/anim.py

@ -15,16 +15,19 @@
import bpy import bpy
def _check_for_anim(layout, context): def _check_for_anim(layout, modifier):
if context.object.animation_data is None or context.object.animation_data.action is None: try:
action = modifier.blender_action
except:
layout.label("Object has no animation data", icon="ERROR") layout.label("Object has no animation data", icon="ERROR")
return False return None
return True else:
return action if action is not None else False
def animation(modifier, layout, context): def animation(modifier, layout, context):
if not _check_for_anim(layout, context): action = _check_for_anim(layout, modifier)
if action is None:
return return
action = context.object.animation_data.action
split = layout.split() split = layout.split()
col = split.column() col = split.column()
@ -32,6 +35,7 @@ def animation(modifier, layout, context):
col = split.column() col = split.column()
col.prop(modifier, "loop") col.prop(modifier, "loop")
if action:
layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER") layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER")
col = layout.column() col = layout.column()
col.enabled = modifier.loop col.enabled = modifier.loop
@ -41,11 +45,14 @@ def animation(modifier, layout, context):
class GroupListUI(bpy.types.UIList): class GroupListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
layout.prop_search(item, "object_name", bpy.data, "objects", icon="ACTION") label = item.object_name if item.object_name else "[No Child Specified]"
icon = "ACTION" if item.object_name else "ERROR"
layout.label(text=label, icon=icon)
def animation_group(modifier, layout, context): def animation_group(modifier, layout, context):
if not _check_for_anim(layout, context): action = _check_for_anim(layout, modifier)
if action is None:
return return
row = layout.row() row = layout.row()
@ -60,6 +67,9 @@ def animation_group(modifier, layout, context):
op.collection = "children" op.collection = "children"
op.index = modifier.active_child_index op.index = modifier.active_child_index
if modifier.children:
layout.prop_search(modifier.children[modifier.active_child_index], "object_name", bpy.data, "objects", icon="ACTION")
class LoopListUI(bpy.types.UIList): class LoopListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
@ -67,7 +77,11 @@ class LoopListUI(bpy.types.UIList):
def animation_loop(modifier, layout, context): 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 return
row = layout.row() row = layout.row()
@ -86,7 +100,6 @@ def animation_loop(modifier, layout, context):
# Modify the loop points # Modify the loop points
if modifier.loops: if modifier.loops:
action = context.object.animation_data.action
loop = modifier.loops[modifier.active_loop_index] loop = modifier.loops[modifier.active_loop_index]
layout.prop_search(loop, "loop_start", action, "pose_markers", icon="PMARKER") layout.prop_search(loop, "loop_start", action, "pose_markers", icon="PMARKER")
layout.prop_search(loop, "loop_end", action, "pose_markers", icon="PMARKER") layout.prop_search(loop, "loop_end", action, "pose_markers", icon="PMARKER")

Loading…
Cancel
Save