Browse Source

Improve handling of temporary objects, fix various issues

pull/436/head
Jrius 2 weeks ago
parent
commit
a6c7c7ba99
  1. 107
      korman/exporter/animation.py
  2. 33
      korman/exporter/armature.py
  3. 4
      korman/exporter/convert.py

107
korman/exporter/animation.py

@ -39,11 +39,50 @@ class AnimationConverter:
def convert_frame_time(self, frame_num: int) -> float:
return frame_num / self._bl_fps
def copy_armature_animation_to_temporary_bones(self, arm_bo: bpy.types.Object, generated_bones):
def convert_object_animations(self, bo: bpy.types.Object, so: plSceneObject, anim_name: str, *,
start: Optional[int] = None, end: Optional[int] = None, bake_frame_step: Optional[int] = None) -> Iterable[plAGApplicator]:
if not bo.plasma_object.has_animation_data:
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", [])
return None, []
obj_action, obj_fcurves = fetch_animation_data(bo)
data_action, data_fcurves = fetch_animation_data(bo.data)
if bake_frame_step is not None and obj_action is not None:
obj_action = self._bake_animation_data(bo, bake_frame_step, start, end)
obj_fcurves = obj_action.fcurves
# 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 = []
if isinstance(bo.data, bpy.types.Camera):
applicators.append(self._convert_camera_animation(bo, so, obj_fcurves, data_fcurves, anim_name, start, end))
else:
applicators.append(self._convert_transform_animation(bo, obj_fcurves, bo.matrix_local, bo.matrix_parent_inverse, start=start, end=end))
if bo.plasma_modifiers.soundemit.enabled:
applicators.extend(self._convert_sound_volume_animation(bo.name, obj_fcurves, bo.plasma_modifiers.soundemit, start, end))
if isinstance(bo.data, bpy.types.Lamp):
lamp = bo.data
applicators.extend(self._convert_lamp_color_animation(bo.name, data_fcurves, lamp, start, end))
if isinstance(lamp, bpy.types.SpotLamp):
applicators.extend(self._convert_spot_lamp_animation(bo.name, data_fcurves, lamp, start, end))
if isinstance(lamp, bpy.types.PointLamp):
applicators.extend(self._convert_omni_lamp_animation(bo.name, data_fcurves, lamp, start, end))
return [i for i in applicators if i is not None]
def copy_armature_animation_to_temporary_bones(self, arm_bo: bpy.types.Object, generated_bones, handle_temporary):
# Enable the anim group. We will need it to reroute anim messages to children bones.
# Please note: Plasma supports grouping many animation channels into a single ATC anim, but only programmatically.
# So instead we will do it the Cyan Way(tm), which is to make dozens or maybe even hundreds of small ATC anims. Derp.
temporary_objects = []
anim = arm_bo.plasma_modifiers.animation
anim_group = arm_bo.plasma_modifiers.animation_group
do_bake = anim.bake
@ -51,14 +90,13 @@ class AnimationConverter:
toggle = GoodNeighbor()
toggle.track(anim, "bake", False)
toggle.track(anim_group, "enabled", True)
temporary_objects.append(toggle)
temporary_objects.append(exit_stack)
handle_temporary(toggle)
handle_temporary(exit_stack)
armature_action = arm_bo.animation_data.action
if do_bake:
armature_action = self._bake_animation_data(arm_bo, anim.bake_frame_step, None, None)
try:
for bone_name, bone in generated_bones.items():
fcurves = []
for fcurve in armature_action.fcurves:
@ -77,7 +115,7 @@ class AnimationConverter:
# Copy animation data.
anim_data = bone.animation_data_create()
action = bpy.data.actions.new("{}_action".format(bone.name))
temporary_objects.append(action)
handle_temporary(action)
anim_data.action = action
for fcurve, data_path in fcurves:
new_curve = action.fcurves.new(data_path, fcurve.array_index)
@ -94,58 +132,6 @@ class AnimationConverter:
bone.plasma_modifiers["animation_loop"] = arm_bo.plasma_modifiers["animation_loop"]
child = exit_stack.enter_context(TemporaryCollectionItem(anim_group.children))
child.child_anim = bone
finally:
if do_bake:
bpy.data.actions.remove(armature_action)
return temporary_objects
def convert_object_animations(self, bo: bpy.types.Object, so: plSceneObject, anim_name: str, *,
start: Optional[int] = None, end: Optional[int] = None, bake_frame_step: Optional[int] = None) -> Iterable[plAGApplicator]:
if not bo.plasma_object.has_animation_data:
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", [])
return None, []
obj_action, obj_fcurves = fetch_animation_data(bo)
data_action, data_fcurves = fetch_animation_data(bo.data)
temporary_action = None
try:
if bake_frame_step is not None and obj_action is not None:
temporary_action = obj_action = self._bake_animation_data(bo, bake_frame_step, start, end)
obj_fcurves = obj_action.fcurves
# 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 = []
if isinstance(bo.data, bpy.types.Camera):
applicators.append(self._convert_camera_animation(bo, so, obj_fcurves, data_fcurves, anim_name, start, end))
else:
applicators.append(self._convert_transform_animation(bo, obj_fcurves, bo.matrix_local, bo.matrix_parent_inverse, start=start, end=end))
if bo.plasma_modifiers.soundemit.enabled:
applicators.extend(self._convert_sound_volume_animation(bo.name, obj_fcurves, bo.plasma_modifiers.soundemit, start, end))
if isinstance(bo.data, bpy.types.Lamp):
lamp = bo.data
applicators.extend(self._convert_lamp_color_animation(bo.name, data_fcurves, lamp, start, end))
if isinstance(lamp, bpy.types.SpotLamp):
applicators.extend(self._convert_spot_lamp_animation(bo.name, data_fcurves, lamp, start, end))
if isinstance(lamp, bpy.types.PointLamp):
applicators.extend(self._convert_omni_lamp_animation(bo.name, data_fcurves, lamp, start, end))
finally:
if temporary_action is not None:
# Baking data is temporary, but the lifetime of our user's data is eternal !
bpy.data.actions.remove(temporary_action)
return [i for i in applicators if i is not None]
def _bake_animation_data(self, bo, bake_frame_step: int, start: Optional[int] = None, end: Optional[int] = None):
# Baking animations is a Blender operator, so requires a bit of boilerplate...
@ -158,15 +144,14 @@ class AnimationConverter:
# Do bake, but make sure we don't mess the user's data.
old_action = bo.animation_data.action
try:
toggle.track(bo.animation_data, "action", old_action)
keyframes = [keyframe.co[0] for curve in old_action.fcurves for keyframe in curve.keyframe_points]
frame_start = start if start is not None else min(keyframes)
frame_end = end if end is not None else max(keyframes)
bpy.ops.nla.bake(frame_start=frame_start, frame_end=frame_end, step=bake_frame_step, only_selected=False, visual_keying=True, bake_types={"POSE", "OBJECT"})
baked_anim = bo.animation_data.action
self._exporter().exit_stack.enter_context(TemporaryObject(baked_anim, bpy.data.actions.remove))
return baked_anim
finally:
bo.animation_data.action = old_action
def _convert_camera_animation(self, bo, so, obj_fcurves, data_fcurves, anim_name: str,
start: Optional[int], end: Optional[int]):

33
korman/exporter/armature.py

@ -26,26 +26,29 @@ class ArmatureConverter:
self._skinned_objects_modifiers = {}
self._bones_local_to_world = {}
def convert_armature_to_empties(self, bo):
def convert_armature_to_empties(self, bo, handle_temporary):
# Creates Blender equivalents to each bone of the armature, adjusting a whole bunch of stuff along the way.
# Yes, this is ugly, but required to get anims to export properly. I tried other ways to export armatures,
# but AFAICT sooner or later you have to implement similar hacks. Might as well generate something that the
# animation exporter can already deal with, with no modification...
# Obviously the returned temporary_objects will be cleaned up afterwards.
# Obviously the created objects will be cleaned up afterwards.
armature = bo.data
pose = bo.pose if armature.pose_position == "POSE" else None
generated_bones = {} # name: Blender empty.
temporary_objects = []
temporary_bones = []
# Note: ideally we would give the temporary bone objects to handle_temporary as soon as they are created.
# However we need to delay until we actually create their animation modifiers, so that these get exported.
try:
for bone in armature.bones:
if bone.parent:
continue
self._export_bone(bo, bone, bo, Matrix.Identity(4), pose, generated_bones, temporary_objects)
self._export_bone(bo, bone, bo, Matrix.Identity(4), bo.pose, armature.pose_position == "POSE", generated_bones, temporary_bones)
if bo.plasma_modifiers.animation.enabled and bo.animation_data is not None and bo.animation_data.action is not None:
# Let the anim exporter handle the anim crap.
temporary_objects.extend(self._exporter().animation.copy_armature_animation_to_temporary_bones(bo, generated_bones))
return temporary_objects
self._exporter().animation.copy_armature_animation_to_temporary_bones(bo, generated_bones, handle_temporary)
finally:
for bone in temporary_bones:
handle_temporary(bone)
def get_bone_local_to_world(self, bo):
return self._bones_local_to_world[bo]
@ -73,14 +76,14 @@ class ArmatureConverter:
return True
return False
def _export_bone(self, bo, bone, parent, matrix, pose, generated_bones, temporary_objects):
def _export_bone(self, bo, bone, parent, matrix, pose, pose_mode, generated_bones, temporary_bones):
bone_empty = bpy.data.objects.new(ArmatureConverter.get_bone_name(bo, bone), None)
bpy.context.scene.objects.link(bone_empty)
bone_empty.plasma_object.enabled = True
if pose is not None:
pose_bone = pose.bones[bone.name]
bone_empty.rotation_mode = pose_bone.rotation_mode
if pose_mode:
pose_matrix = pose_bone.matrix_basis
else:
pose_matrix = Matrix.Identity(4)
@ -94,18 +97,16 @@ class ArmatureConverter:
bone_parent.parent = parent
bone_empty.parent = bone_parent
bone_empty.matrix_local = Matrix.Identity(4)
temporary_objects.append(bone_parent)
temporary_bones.append(bone_parent)
bone_parent.matrix_local = matrix * bone.matrix_local * pose_matrix
# The bone's local to world matrix may change when we copy animations over, which we don't want.
# Cache the matrix so we can use it when exporting meshes.
self._bones_local_to_world[bone_empty] = bo.matrix_world * bone.matrix_local
temporary_objects.append(bone_empty)
temporary_bones.append(bone_empty)
generated_bones[bone.name] = bone_empty
for child in bone.children:
child_empty = self._export_bone(bo, child, bone_empty, bone.matrix_local.inverted(), pose, generated_bones, temporary_objects)
return bone_empty
self._export_bone(bo, child, bone_empty, bone.matrix_local.inverted(), pose, pose_mode, generated_bones, temporary_bones)
@staticmethod
def get_bone_name(bo, bone):

4
korman/exporter/convert.py

@ -560,9 +560,7 @@ class Exporter:
so = self.mgr.find_create_object(plSceneObject, bl=bo)
self._export_actor(so, bo)
# Bake all armature bones to empties - this will make it easier to export animations and such.
for temporary_object in self.armature.convert_armature_to_empties(bo):
# This could be a Blender object, an action, a context manager... I have no idea, handle_temporary will figure it out !
handle_temporary(temporary_object, bo)
self.armature.convert_armature_to_empties(bo, lambda obj: handle_temporary(obj, bo))
for mod in bo.plasma_modifiers.modifiers:
proc = getattr(mod, "pre_export", None)
if proc is not None:

Loading…
Cancel
Save