- 
                Notifications
    You must be signed in to change notification settings 
- Fork 16
Armatures and skinned objects #436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
c3ac168
              ca28135
              a6ddffe
              a6c7c7b
              8c74677
              3ed604f
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|  | @@ -16,27 +16,31 @@ | |||||
| import bpy | ||||||
|  | ||||||
| from collections import defaultdict | ||||||
| from contextlib import ExitStack | ||||||
| import functools | ||||||
| import itertools | ||||||
| import math | ||||||
| import mathutils | ||||||
| from typing import * | ||||||
| import weakref | ||||||
| import re | ||||||
|  | ||||||
| from PyHSPlasma import * | ||||||
|  | ||||||
| from . import utils | ||||||
| from ..helpers import * | ||||||
|  | ||||||
| class AnimationConverter: | ||||||
| def __init__(self, exporter): | ||||||
| self._exporter = weakref.ref(exporter) | ||||||
| self._bl_fps = bpy.context.scene.render.fps | ||||||
| self._bone_data_path_regex = re.compile('^pose\\.bones\\["(.*)"]\\.(.*)$') | ||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 
        Suggested change
       
 | ||||||
|  | ||||||
| def convert_frame_time(self, frame_num: int) -> float: | ||||||
| return frame_num / self._bl_fps | ||||||
|  | ||||||
| def convert_object_animations(self, bo: bpy.types.Object, so: plSceneObject, anim_name: str, *, | ||||||
| start: Optional[int] = None, end: Optional[int] = None) -> Iterable[plAGApplicator]: | ||||||
| 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 [] | ||||||
|  | ||||||
|  | @@ -50,6 +54,10 @@ def fetch_animation_data(id_data): | |||||
| 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 | ||||||
|  | @@ -71,6 +79,80 @@ def fetch_animation_data(id_data): | |||||
|  | ||||||
| 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. | ||||||
| anim = arm_bo.plasma_modifiers.animation | ||||||
| anim_group = arm_bo.plasma_modifiers.animation_group | ||||||
| do_bake = anim.bake | ||||||
| exit_stack = ExitStack() | ||||||
| toggle = GoodNeighbor() | ||||||
| toggle.track(anim, "bake", False) | ||||||
| toggle.track(anim_group, "enabled", True) | ||||||
| 
      Comment on lines
    
      +91
     to 
      +92
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do these changes need to be tracked and reset after the entire export process, or could we get away with only tracking them in this function? | ||||||
| handle_temporary(toggle) | ||||||
| handle_temporary(exit_stack) | ||||||
| 
      Comment on lines
    
      +93
     to 
      +94
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I like this idea, to be honest. | ||||||
|  | ||||||
| armature_action = arm_bo.animation_data.action | ||||||
| if do_bake: | ||||||
| armature_action = self._bake_animation_data(arm_bo, anim.bake_frame_step, None, None) | ||||||
|  | ||||||
| for bone_name, bone in generated_bones.items(): | ||||||
| fcurves = [] | ||||||
| for fcurve in armature_action.fcurves: | ||||||
| match = self._bone_data_path_regex.match(fcurve.data_path) | ||||||
| if not match: | ||||||
| continue | ||||||
| name, data_path = match.groups() | ||||||
| if name != bone_name: | ||||||
| continue | ||||||
| fcurves.append((fcurve, data_path)) | ||||||
|  | ||||||
| if not fcurves: | ||||||
| # No animation data for this bone. | ||||||
| continue | ||||||
|  | ||||||
| # Copy animation data. | ||||||
| anim_data = bone.animation_data_create() | ||||||
| action = bpy.data.actions.new("{}_action".format(bone.name)) | ||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 
        Suggested change
       
 | ||||||
| handle_temporary(action) | ||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a fan of doing this. Korman prefers to use generators to handle temporary object book-keeping. | ||||||
| anim_data.action = action | ||||||
| for fcurve, data_path in fcurves: | ||||||
| new_curve = action.fcurves.new(data_path, fcurve.array_index) | ||||||
| for point in fcurve.keyframe_points: | ||||||
| # Thanks to bone_parent we can just copy the animation without a care in the world ! :P | ||||||
| p = new_curve.keyframe_points.insert(point.co[0], point.co[1]) | ||||||
| for original_marker in armature_action.pose_markers: | ||||||
| marker = action.pose_markers.new(original_marker.name) | ||||||
| marker.frame = original_marker.frame | ||||||
|  | ||||||
| # Copy animation modifier and its properties if it exists. | ||||||
| # Cheating ? Very much yes. Do not try this at home, kids. | ||||||
| bone.plasma_modifiers["animation"] = arm_bo.plasma_modifiers["animation"] | ||||||
| bone.plasma_modifiers["animation_loop"] = arm_bo.plasma_modifiers["animation_loop"] | ||||||
| child = exit_stack.enter_context(TemporaryCollectionItem(anim_group.children)) | ||||||
| child.child_anim = bone | ||||||
|  | ||||||
| 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... | ||||||
| with GoodNeighbor() as toggle: | ||||||
| # Make sure we have only this object selected. | ||||||
| toggle.track(bo, "hide", False) | ||||||
| for i in bpy.data.objects: | ||||||
| i.select = i == bo | ||||||
| bpy.context.scene.objects.active = bo | ||||||
|  | ||||||
| # Do bake, but make sure we don't mess the user's data. | ||||||
| old_action = bo.animation_data.action | ||||||
| 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)) | ||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer this over passing a  | ||||||
| return baked_anim | ||||||
|  | ||||||
| def _convert_camera_animation(self, bo, so, obj_fcurves, data_fcurves, anim_name: str, | ||||||
| start: Optional[int], end: Optional[int]): | ||||||
| has_fov_anim = False | ||||||
|  | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,123 @@ | ||||||||||
| # This file is part of Korman. | ||||||||||
| # | ||||||||||
| # Korman is free software: you can redistribute it and/or modify | ||||||||||
| # it under the terms of the GNU General Public License as published by | ||||||||||
| # the Free Software Foundation, either version 3 of the License, or | ||||||||||
| # (at your option) any later version. | ||||||||||
| # | ||||||||||
| # Korman is distributed in the hope that it will be useful, | ||||||||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||||||
| # GNU General Public License for more details. | ||||||||||
| # | ||||||||||
| # You should have received a copy of the GNU General Public License | ||||||||||
| # along with Korman. If not, see <http://www.gnu.org/licenses/>. | ||||||||||
|  | ||||||||||
| import bpy | ||||||||||
| from mathutils import Matrix | ||||||||||
| import weakref | ||||||||||
| from PyHSPlasma import * | ||||||||||
|  | ||||||||||
| from . import utils | ||||||||||
|  | ||||||||||
| class ArmatureConverter: | ||||||||||
| def __init__(self, exporter): | ||||||||||
| self._exporter = weakref.ref(exporter) | ||||||||||
| self._skinned_objects_modifiers = {} | ||||||||||
| self._bones_local_to_world = {} | ||||||||||
|  | ||||||||||
| 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 created objects will be cleaned up afterwards. | ||||||||||
| armature = bo.data | ||||||||||
| generated_bones = {} # name: Blender empty. | ||||||||||
| 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), 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. | ||||||||||
| self._exporter().animation.copy_armature_animation_to_temporary_bones(bo, generated_bones, handle_temporary) | ||||||||||
| finally: | ||||||||||
| for bone in temporary_bones: | ||||||||||
| handle_temporary(bone) | ||||||||||
| 
      Comment on lines
    
      +40
     to 
      +51
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of this  for bone in (i for i in armature.bones if not i.parent):
     yield self._export_bone(foo, bar) | ||||||||||
|  | ||||||||||
| def get_bone_local_to_world(self, bo): | ||||||||||
| return self._bones_local_to_world[bo] | ||||||||||
|  | ||||||||||
| def get_skin_modifiers(self, bo): | ||||||||||
| if self.is_skinned(bo): | ||||||||||
| return self._skinned_objects_modifiers[bo] | ||||||||||
| return [] | ||||||||||
|  | ||||||||||
| def is_skinned(self, bo): | ||||||||||
| if bo.type != "MESH": | ||||||||||
| return False | ||||||||||
| if bo in self._skinned_objects_modifiers: | ||||||||||
| return True | ||||||||||
|  | ||||||||||
| # We need to cache the armature modifiers, because mesh.py will likely fiddle with them later. | ||||||||||
| armatures = [] | ||||||||||
| for mod in bo.modifiers: | ||||||||||
| # Armature modifiers only result in exporting skinning if they are linked to an exported armature. | ||||||||||
| # If the armature is not exported, the deformation will simply get baked into the exported mesh. | ||||||||||
| if mod.type == "ARMATURE" and mod.object is not None and mod.object.plasma_object.enabled and mod.use_vertex_groups and mod.show_render: | ||||||||||
| armatures.append(mod) | ||||||||||
| 
      Comment on lines
    
      +68
     to 
      +73
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could clean this up with something like armatures = [
    mod for mod in bo.modifiers
    if mod.type == "ARMATURE" and mod.object and mod.object.plasma_object.enabled and mod.use_vertex_groups and mod.show_render
] | ||||||||||
| if len(armatures): | ||||||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 
        Suggested change
       
 | ||||||||||
| self._skinned_objects_modifiers[bo] = armatures | ||||||||||
| return True | ||||||||||
| return False | ||||||||||
|  | ||||||||||
| 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 | ||||||||||
| 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) | ||||||||||
|  | ||||||||||
| # Grmbl, animation is relative to rest pose in Blender, and relative to parent in Plasma... | ||||||||||
| # Using matrix_parent_inverse or manually adjust keyframes will just mess up rotation keyframes, | ||||||||||
| # so let's just insert an extra empty object to correct all that. This is why the CoordinateInterface caches computed matrices, after all... | ||||||||||
| bone_parent = bpy.data.objects.new(bone_empty.name + "_REST", None) | ||||||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 
        Suggested change
       
 String concats are bad in Python. | ||||||||||
| bpy.context.scene.objects.link(bone_parent) | ||||||||||
| bone_parent.plasma_object.enabled = True | ||||||||||
| bone_parent.parent = parent | ||||||||||
| bone_empty.parent = bone_parent | ||||||||||
| bone_empty.matrix_local = Matrix.Identity(4) | ||||||||||
| 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_bones.append(bone_empty) | ||||||||||
| generated_bones[bone.name] = bone_empty | ||||||||||
|  | ||||||||||
| for child in bone.children: | ||||||||||
| self._export_bone(bo, child, bone_empty, bone.matrix_local.inverted(), pose, pose_mode, generated_bones, temporary_bones) | ||||||||||
| 
      Comment on lines
    
      +79
     to 
      +109
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These should probably be some form of  | ||||||||||
|  | ||||||||||
| @staticmethod | ||||||||||
| def get_bone_name(bo, bone): | ||||||||||
| if isinstance(bone, str): | ||||||||||
| return "{}_{}".format(bo.name, bone) | ||||||||||
| return "{}_{}".format(bo.name, bone.name) | ||||||||||
| 
      Comment on lines
    
      +114
     to 
      +115
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 
        Suggested change
       
 | ||||||||||
|  | ||||||||||
| @property | ||||||||||
| def _mgr(self): | ||||||||||
| return self._exporter().mgr | ||||||||||
|  | ||||||||||
| @property | ||||||||||
| def _report(self): | ||||||||||
| return self._exporter().report | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be between
mathutilsandtyping.