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._bl_fps = bpy.context.scene.render.fps
def convert_action2tm(self, action, default_xform):
"""Converts a Blender Action to a plCompoundController."""
fcurves = action.fcurves
def _convert_frame_time(self, frame_num):
return frame_num / self._bl_fps
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:
return None
# NOTE: plCompoundController is from Myst 5 and was backported to MOUL.
# Worry not however... libHSPlasma will do the conversion for us.
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_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.X = self.make_pos_controller(fcurves, default_xform)
tm.Y = self.make_rot_controller(fcurves, default_xform)
tm.Z = self.make_scale_controller(fcurves, default_xform)
return tm
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)
if not pos_keyframes and not scale_keyframes:
tm.X = pos
tm.Y = rot
tm.Z = scale
applicator = plMatrixChannelApplicator()
applicator.enabled = True
applicator.channelName = name
channel = plMatrixControllerChannel()
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
# 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)
# 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))
default_values = { pos_path: pos_default, scale_path: scale_default }
keyframes = self._process_fcurves(fcurves, convert_matrix_keyframe, default_values)
if not keyframes:
return None
# Now we make the controller
ctrl = self._make_matrix44_controller(pos_fcurves, scale_fcurves, keyframes, default_pos, default_scale)
return ctrl
return self._make_matrix44_controller(keyframes)
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]
keyframes, bez_chans = self._process_keyframes(pos_curves)
keyframes, bez_chans = self._process_keyframes(pos_curves, convert)
if not keyframes:
return None
@ -84,10 +360,10 @@ class AnimationConverter:
ctrl = self._make_point3_controller(pos_curves, keyframes, bez_chans, default_xform.to_translation())
return ctrl
def make_rot_controller(self, fcurves, default_xform):
def make_rot_controller(self, fcurves, default_xform, convert=None):
# TODO: support rotation_quaternion
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:
return None
@ -99,9 +375,9 @@ class AnimationConverter:
ctrl = self._make_quat_controller(rot_curves, keyframes, default_xform.to_euler())
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]
keyframes, bez_chans = self._process_keyframes(scale_curves)
keyframes, bez_chans = self._process_keyframes(scale_curves, convert)
if not keyframes:
return None
@ -109,49 +385,25 @@ class AnimationConverter:
ctrl = self._make_scale_value_controller(scale_curves, keyframes, bez_chans, default_xform)
return ctrl
def make_scalar_leaf_controller(self, fcurve):
keyframes, bezier = self._process_fcurve(fcurve)
def make_scalar_leaf_controller(self, fcurve, convert=None):
keyframes, bezier = self._process_fcurve(fcurve, convert)
if not keyframes:
return None
ctrl = self._make_scalar_leaf_controller(keyframes, bezier)
return ctrl
def _make_matrix44_controller(self, pos_fcurves, scale_fcurves, keyframes, default_pos, default_scale):
def _make_matrix44_controller(self, keyframes):
ctrl = plLeafController()
keyframe_type = hsKeyFrame.kMatrix44KeyFrame
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:
valid_key = pos_key if pos_key is not None else scale_key
for keyframe in keyframes:
exported = hsMatrix44Key()
exported.frame = valid_key.frame_num
exported.frameTime = valid_key.frame_time
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.type = keyframe_type
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.value = keyframe.value
exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
@ -315,7 +567,7 @@ class AnimationConverter:
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _process_fcurve(self, fcurve):
def _process_fcurve(self, fcurve, convert=None):
"""Like _process_keyframes, but for one fcurve"""
keyframe_data = type("KeyFrameData", (), {})
fps = self._bl_fps
@ -339,12 +591,121 @@ class AnimationConverter:
else:
keyframe.in_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
final_keyframes = [keyframes[i] for i in sorted(keyframes)]
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"""
keyframe_data = type("KeyFrameData", (), {})
fps = self._bl_fps
@ -371,7 +732,7 @@ class AnimationConverter:
keyframe.values = {}
keyframes[frame_num] = keyframe
idx = fcurve.array_index
keyframe.values[idx] = value
keyframe.values[idx] = value if convert is None else convert(value)
# Calculate the bezier interpolation nonsense
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)
self._export_actor(sceneobject, bl_obj)
export_fn(sceneobject, bl_obj)
self.animation.convert_object_animations(bl_obj, sceneobject)
# And now we puke out the modifiers...
for mod in bl_obj.plasma_modifiers.modifiers:
@ -258,6 +259,8 @@ class Exporter:
return True
if bo.name in self.actors:
return True
if self.animation.has_transform_animation(bo):
return True
for mod in bo.plasma_modifiers.modifiers:
if mod.enabled:

7
korman/exporter/material.py

@ -295,11 +295,12 @@ class MaterialConverter:
return None
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]
scale_fcurves = [i for i in fcurves if i.data_path.find("scale") != -1]
path = tex_slot.path_from_id()
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
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
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):
intens = bl.energy
if intens < 0:
intens = -intens
attenEnd = bl.distance * 2 if bl.use_sphere else bl.distance
# If you change these calculations, be sure to update the AnimationConverter!
intens, attenEnd = self.convert_attenuation(bl)
if bl.falloff_type == "CONSTANT":
print(" Attenuation: No Falloff")
pl.attenConst = intens
@ -54,18 +51,29 @@ class LightConverter:
elif bl.falloff_type == "INVERSE_LINEAR":
print(" Attenuation: Inverse Linear")
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.attenCutoff = attenEnd
elif bl.falloff_type == "INVERSE_SQUARE":
print(" Attenuation: Inverse Square")
pl.attenConst = 1.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
else:
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):
print(" [LimitedDirLightInfo '{}']".format(bl.name))
@ -104,6 +112,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]
@ -175,6 +184,11 @@ class LightConverter:
if 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*
pl_light.sceneNode = self.mgr.get_scene_node(location=so.key.location)
@ -202,7 +216,6 @@ class LightConverter:
else:
state.miscFlags |= hsGMatState.kMiscOrthoProjection
state.ZFlags |= hsGMatState.kZNoZWrite
pl_light.setProperty(plLightInfo.kLPMovable, True)
pl_light.setProperty(plLightInfo.kLPCastShadows, False)
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)
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:
@ -178,9 +177,7 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node):
# (but obviously this is not wrong...)
target = exporter.mgr.find_create_key(plMsgForwarder, bl=obj, name=group.key_name)
else:
# remember, the AGModifier MUST exist first... so just in case...
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)
_agmod_trash, target = exporter.animation.get_anigraph_keys(obj)
else:
material = bpy.data.materials.get(self.material_name, 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
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))
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(PlasmaModifierProperties):
class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties):
pl_id = "animation"
bl_category = "Animation"
@ -53,22 +60,16 @@ class PlasmaAnimationModifier(PlasmaModifierProperties):
loop_end = StringProperty(name="Loop End",
description="Marker indicating where the default loop ends")
@property
def requires_actor(self):
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, name=self.key_name)
atcanim = exporter.mgr.find_create_object(plATCAnim, so=so)
atcanim.autoStart = self.auto_start
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
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)
@ -79,94 +80,29 @@ class PlasmaAnimationModifier(PlasmaModifierProperties):
if loop_start is not None:
atcanim.loopStart = _convert_frame_time(loop_start.frame)
else:
atcanim.loopStart = _convert_frame_time(action.frame_range[0])
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 = _convert_frame_time(action.frame_range[1])
# Marker points
for marker in markers:
atcanim.setMarker(marker.name, _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
# 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)
atcanim.loopEnd = atcanim.end
else:
if self.loop:
atcanim.loopStart = atcanim.start
atcanim.loopEnd = atcanim.end
class AnimGroupObject(bpy.types.PropertyGroup):
object_name = StringProperty(name="Child",
object_name = StringProperty(name="Child Animation",
description="Object whose action is a child animation")
class PlasmaAnimationGroupModifier(PlasmaModifierProperties):
class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties):
pl_id = "animation_group"
pl_depends = {"animation"}
bl_category = "Animation"
bl_label = "Group"
bl_label = "Group Master"
bl_description = "Defines related animations"
bl_icon = "GROUP"
@ -176,17 +112,14 @@ 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
# See above... AGModifier must always be inited first...
agmod = exporter.mgr.find_create_object(plAGModifier, so=so, name=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)
# 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.isGrouped, agmaster.isGroupMaster = True, True
for i in self.children:
@ -204,9 +137,8 @@ class PlasmaAnimationGroupModifier(PlasmaModifierProperties):
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)
continue
child_agmod = exporter.mgr.find_create_key(plAGModifier, bl=child_bo, name=child_animation.key_name)
child_agmaster = exporter.mgr.find_create_key(plAGMasterMod, bl=child_bo, name=child_animation.key_name)
msgfwd.addForwardKey(child_agmaster)
child_agmod, child_agmaster = exporter.animation.get_anigraph_objects(bo=child_bo)
msgfwd.addForwardKey(child_agmaster.key)
msgfwd.addForwardKey(agmaster.key)
@property
@ -223,7 +155,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"}
@ -238,11 +170,12 @@ 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
key_name = bo.plasma_modifiers.animation.key_name
atcanim = exporter.mgr.find_create_object(plATCAnim, so=so, name=key_name)
atcanim = exporter.mgr.find_create_object(plATCAnim, so=so)
for loop in self.loops:
start = markers.get(loop.loop_start)
end = markers.get(loop.loop_end)

26
korman/properties/modifiers/physics.py

@ -84,6 +84,32 @@ class PlasmaCollider(PlasmaModifierProperties):
if self.terrain:
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
def key_name(self):
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",
description="Volume to play the sound",
min=0, max=100, default=100,
options=set(),
options={"ANIMATABLE"},
subtype="PERCENTAGE")
fade_in = PointerProperty(type=PlasmaSfxFade, options=set())
@ -344,10 +344,25 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
if i.sound_data and i.enabled:
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
3D sounds export as two mono sound objects -- wheeeeee"""
assert name or sound
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:
if i.name == name:
yield idx

33
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,6 +35,7 @@ def animation(modifier, layout, context):
col = split.column()
col.prop(modifier, "loop")
if action:
layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER")
col = layout.column()
col.enabled = modifier.loop
@ -41,11 +45,14 @@ def animation(modifier, layout, context):
class GroupListUI(bpy.types.UIList):
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):
if not _check_for_anim(layout, context):
action = _check_for_anim(layout, modifier)
if action is None:
return
row = layout.row()
@ -60,6 +67,9 @@ def animation_group(modifier, layout, context):
op.collection = "children"
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):
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):
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 +100,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