From 4c621f64578d730b46f441fef67fb56b04057487 Mon Sep 17 00:00:00 2001 From: Hazado Date: Tue, 8 Jun 2021 20:54:23 -0700 Subject: [PATCH 1/4] Support for quaternion rotation animations Put this together, through trial and error There are probably better way to do this Was able to export quaternion animation with it --- korman/exporter/animation.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 96b1dbd..f6fc4a8 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -387,10 +387,15 @@ class AnimationConverter: def convert_transform_controller(self, fcurves, xform, allow_empty=False) -> Union[None, plCompoundController]: if not fcurves and not allow_empty: return None - + + rotation_quaternion = (i for i in fcurves if i.data_path == "rotation_quaternion") + pos = self.make_pos_controller(fcurves, "location", xform.to_translation()) # TODO: support rotation_quaternion - rot = self.make_rot_controller(fcurves, "rotation_euler", xform.to_euler()) + if rotation_quaternion: + rot = self.make_rot_controller(fcurves, "rotation_quaternion", xform.to_quaternion(), 4) + else: + rot = self.make_rot_controller(fcurves, "rotation_euler", xform.to_euler(), 3) scale = self.make_scale_controller(fcurves, "scale", xform.to_scale()) if pos is None and rot is None and scale is None: if not allow_empty: @@ -459,9 +464,9 @@ class AnimationConverter: ctrl = self._make_point3_controller(keyframes, bez_chans) return ctrl - def make_rot_controller(self, fcurves, data_path : str, default_xform, convert=None) -> Union[None, plCompoundController, plLeafController]: + def make_rot_controller(self, fcurves, data_path : str, default_xform, num_channels, convert=None) -> Union[None, plCompoundController, plLeafController]: rot_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] - keyframes, bez_chans = self._process_keyframes(rot_curves, 3, default_xform, convert=None) + keyframes, bez_chans = self._process_keyframes(rot_curves, num_channels, default_xform, convert=None) if not keyframes: return None @@ -470,7 +475,7 @@ class AnimationConverter: if bez_chans: ctrl = self._make_scalar_compound_controller(keyframes, bez_chans) else: - ctrl = self._make_quat_controller( keyframes) + ctrl = self._make_quat_controller( keyframes, num_channels) return ctrl def make_scale_controller(self, fcurves, data_path : str, default_xform, convert=None) -> plLeafController: @@ -523,7 +528,7 @@ class AnimationConverter: ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_quat_controller(self, keyframes) -> plLeafController: + def _make_quat_controller(self, keyframes, num_channels) -> plLeafController: ctrl = plLeafController() keyframe_type = hsKeyFrame.kQuatKeyFrame exported_frames = [] @@ -535,8 +540,12 @@ class AnimationConverter: exported.type = keyframe_type # NOTE: quat keyframes don't do bezier nonsense - value = mathutils.Euler(keyframe.values) - exported.value = utils.quaternion(value.to_quaternion()) + if num_channels == 3: + value = mathutils.Euler(keyframe.values) + exported.value = utils.quaternion(value.to_quaternion()) + else: + value = mathutils.Quaternion(keyframe.values) + exported.value = utils.quaternion(value) exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl From f90a689afaf7edbd741b695d2b0d22840c582d45 Mon Sep 17 00:00:00 2001 From: Hazado Date: Wed, 9 Jun 2021 12:43:38 -0700 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Adam Johnson --- korman/exporter/animation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index f6fc4a8..5f029de 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -388,10 +388,9 @@ class AnimationConverter: if not fcurves and not allow_empty: return None - rotation_quaternion = (i for i in fcurves if i.data_path == "rotation_quaternion") + rotation_quaternion = any((i.data_path == "rotation_quaternion" for i in fcurves)) pos = self.make_pos_controller(fcurves, "location", xform.to_translation()) - # TODO: support rotation_quaternion if rotation_quaternion: rot = self.make_rot_controller(fcurves, "rotation_quaternion", xform.to_quaternion(), 4) else: @@ -475,7 +474,7 @@ class AnimationConverter: if bez_chans: ctrl = self._make_scalar_compound_controller(keyframes, bez_chans) else: - ctrl = self._make_quat_controller( keyframes, num_channels) + ctrl = self._make_quat_controller(keyframes, num_channels) return ctrl def make_scale_controller(self, fcurves, data_path : str, default_xform, convert=None) -> plLeafController: From 22ac9ae73581b2d4ab058082f45842672b613ee4 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 11 Jun 2021 23:14:32 -0400 Subject: [PATCH 3/4] Support all types of rotation animations. This allows you to explicitly select quaternion animations. These appear to be the default for bone animations and were silently dropped on the floor before this change. A selection of quaternion or axis angle implicitly disables bezier interpolation. Also fix the assumption that Eulers are always in XYZ order. --- korman/exporter/animation.py | 105 +++++++++++++++++++++---------- korman/exporter/camera.py | 2 +- korman/properties/prop_object.py | 2 +- 3 files changed, 73 insertions(+), 36 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 5f029de..8e59d96 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -57,7 +57,7 @@ class AnimationConverter: if isinstance(bo.data, bpy.types.Camera): applicators.append(self._convert_camera_animation(bo, so, obj_fcurves, data_fcurves)) else: - applicators.append(self._convert_transform_animation(bo.name, obj_fcurves, bo.matrix_basis)) + applicators.append(self._convert_transform_animation(bo, 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): @@ -184,7 +184,7 @@ class AnimationConverter: # If we exported any FOV animation at all, then we need to ensure there is an applicator # returned from here... At bare minimum, we'll need the applicator with an empty # CompoundController. This should be sufficient to keep CWE from crashing... - applicator = self._convert_transform_animation(bo.name, obj_fcurves, bo.matrix_basis, allow_empty=has_fov_anim) + applicator = self._convert_transform_animation(bo, obj_fcurves, bo.matrix_basis, allow_empty=has_fov_anim) camera = locals().get("camera", self._mgr.find_create_object(plCameraModifier, so=so)) camera.animated = applicator is not None return applicator @@ -369,14 +369,14 @@ class AnimationConverter: applicator.channel = channel yield applicator - def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False) -> Union[None, plMatrixChannelApplicator]: - tm = self.convert_transform_controller(fcurves, xform, allow_empty) + def _convert_transform_animation(self, bo, fcurves, xform, allow_empty=False) -> Union[None, plMatrixChannelApplicator]: + tm = self.convert_transform_controller(fcurves, bo.rotation_mode, xform, allow_empty) if tm is None and not allow_empty: return None applicator = plMatrixChannelApplicator() applicator.enabled = True - applicator.channelName = name + applicator.channelName = bo.name channel = plMatrixControllerChannel() channel.controller = tm applicator.channel = channel @@ -384,18 +384,13 @@ class AnimationConverter: return applicator - def convert_transform_controller(self, fcurves, xform, allow_empty=False) -> Union[None, plCompoundController]: + def convert_transform_controller(self, fcurves, rotation_mode: str, xform, allow_empty=False) -> Union[None, plCompoundController]: if not fcurves and not allow_empty: return None - - rotation_quaternion = any((i.data_path == "rotation_quaternion" for i in fcurves)) - - pos = self.make_pos_controller(fcurves, "location", xform.to_translation()) - if rotation_quaternion: - rot = self.make_rot_controller(fcurves, "rotation_quaternion", xform.to_quaternion(), 4) - else: - rot = self.make_rot_controller(fcurves, "rotation_euler", xform.to_euler(), 3) - scale = self.make_scale_controller(fcurves, "scale", xform.to_scale()) + + pos = self.make_pos_controller(fcurves, xform.to_translation()) + rot = self.make_rot_controller(fcurves, rotation_mode, xform) + scale = self.make_scale_controller(fcurves, xform.to_scale()) if pos is None and rot is None and scale is None: if not allow_empty: return None @@ -452,8 +447,8 @@ class AnimationConverter: # Now we make the controller return self._make_matrix44_controller(keyframes) - def make_pos_controller(self, fcurves, data_path : str, default_xform, convert=None) -> Union[None, plLeafController]: - pos_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] + def make_pos_controller(self, fcurves, default_xform, convert=None) -> Union[None, plLeafController]: + pos_curves = [i for i in fcurves if i.data_path == "location" and i.keyframe_points] keyframes, bez_chans = self._process_keyframes(pos_curves, 3, default_xform, convert) if not keyframes: return None @@ -463,22 +458,57 @@ class AnimationConverter: ctrl = self._make_point3_controller(keyframes, bez_chans) return ctrl - def make_rot_controller(self, fcurves, data_path : str, default_xform, num_channels, convert=None) -> Union[None, plCompoundController, plLeafController]: - rot_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] - keyframes, bez_chans = self._process_keyframes(rot_curves, num_channels, default_xform, convert=None) - if not keyframes: - return None + def make_rot_controller(self, fcurves, rotation_mode: str, default_xform, convert=None) -> Union[None, plCompoundController, plLeafController]: + if rotation_mode in {"AXIS_ANGLE", "QUATERNION"}: + rot_curves = [i for i in fcurves if i.data_path == "rotation_{}".format(rotation_mode.lower()) and i.keyframe_points] + if not rot_curves: + return None + + default_xform = default_xform.to_quaternion() + if rotation_mode == "AXIS_ANGLE": + default_xform = default_xform.to_axis_angle() + default_xform = (default_xform[1], default_xform[0].x, default_xform[0].y, default_xform[0].z) + + if convert is not None: + convert = lambda x: convert(mathutils.Quaternion(x[1:4], x[0]))[:] + else: + convert = lambda x: mathutils.Quaternion(x[1:4], x[0])[:] - # Ugh. Unfortunately, it appears Blender's default interpolation is bezier. So who knows if - # many users will actually see the benefit here? Makes me sad. - if bez_chans: - ctrl = self._make_scalar_compound_controller(keyframes, bez_chans) + # Just dropping bezier stuff on the floor because Plasma does not support it, and + # I think that opting into quaternion keyframes is a good enough indication that + # you're OK with that. + keyframes, bez_chans = self._process_keyframes(rot_curves, 4, default_xform, convert) + if keyframes: + return self._make_quat_controller(keyframes) else: - ctrl = self._make_quat_controller(keyframes, num_channels) - return ctrl + rot_curves = [i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points] + if not rot_curves: + return None + + # OK, so life is complicated with Euler keyframes because apparently they can store + # different "orders" that really only become apparent when the engine converts them + # into a quaternion to use in an animation. Converting orders isn't as simple as swapping + # XYZ around, so we have to bus this through quaternion??? Ugh. + def convert_euler_keyframe(euler_array: Tuple[float, float, float]): + euler = mathutils.Euler(euler_array, rotation_mode) + result = euler.to_quaternion().to_euler("XYZ") + if convert is not None: + result = convert(result) + return result[:] + + euler_convert = convert_euler_keyframe if rotation_mode != "XYZ" else convert + keyframes, bez_chans = self._process_keyframes(rot_curves, 3, default_xform.to_euler(rotation_mode), euler_convert) + if keyframes: + # Once again, quaternion keyframes do not support bezier interpolation. Ideally, + # we would just drop support for rotation beziers entirely to simplify all this + # Euler crap, but some artists may require bezier interpolation... + if bez_chans: + return self._make_scalar_compound_controller(keyframes, bez_chans) + else: + return self._make_quat_controller(keyframes) - def make_scale_controller(self, fcurves, data_path : str, default_xform, convert=None) -> plLeafController: - scale_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] + def make_scale_controller(self, fcurves, default_xform, convert=None) -> plLeafController: + scale_curves = [i for i in fcurves if i.data_path == "scale" and i.keyframe_points] keyframes, bez_chans = self._process_keyframes(scale_curves, 3, default_xform, convert) if not keyframes: return None @@ -527,7 +557,7 @@ class AnimationConverter: ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_quat_controller(self, keyframes, num_channels) -> plLeafController: + def _make_quat_controller(self, keyframes) -> plLeafController: ctrl = plLeafController() keyframe_type = hsKeyFrame.kQuatKeyFrame exported_frames = [] @@ -539,12 +569,19 @@ class AnimationConverter: exported.type = keyframe_type # NOTE: quat keyframes don't do bezier nonsense + values = keyframe.values + num_channels = len(values) if num_channels == 3: - value = mathutils.Euler(keyframe.values) + value = mathutils.Euler(values) exported.value = utils.quaternion(value.to_quaternion()) - else: - value = mathutils.Quaternion(keyframe.values) + elif num_channels == 4: + # Blender orders its quats WXYZ (nonstandard) but Plasma uses XYZW (standard) + # Also note that manual incoming quat data might be goofy, so renormalize + value = mathutils.Quaternion(values) + value.normalize() exported.value = utils.quaternion(value) + else: + raise ValueError("Unexpected number of channels in quaternion keyframe {}".format(num_channels)) exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py index 4097963..cfbf18b 100644 --- a/korman/exporter/camera.py +++ b/korman/exporter/camera.py @@ -203,7 +203,7 @@ class CameraConverter: # path object, but it makes more sense to me to just animate the camera with # the details of the path... pos_fcurves = tuple(i for i in helpers.fetch_fcurves(bo, False) if i.data_path == "location") - pos_ctrl = self._exporter().animation.convert_transform_controller(pos_fcurves, bo.matrix_basis) + pos_ctrl = self._exporter().animation.convert_transform_controller(pos_fcurves, bo.rotation_mode, bo.matrix_basis) if pos_ctrl is None: raise ExportError("'{}': Rail Camera lacks appropriate rail keyframes".format(bo.name)) path = plAnimPath() diff --git a/korman/properties/prop_object.py b/korman/properties/prop_object.py index 4413160..3498479 100644 --- a/korman/properties/prop_object.py +++ b/korman/properties/prop_object.py @@ -76,7 +76,7 @@ class PlasmaObject(bpy.types.PropertyGroup): 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 {"location", "rotation_euler", "rotation_quaternion", "rotation_axis_angle", "scale"} & data_paths return False @property From 715ac3f4146ae38a166917122f8cd0f993a2da08 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 13 Jun 2021 21:30:03 -0400 Subject: [PATCH 4/4] Re-add `data_path` arguments for pos and scale controllers. --- korman/exporter/animation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 8e59d96..5a9630c 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -388,9 +388,9 @@ class AnimationConverter: if not fcurves and not allow_empty: return None - pos = self.make_pos_controller(fcurves, xform.to_translation()) + pos = self.make_pos_controller(fcurves, "location", xform.to_translation()) rot = self.make_rot_controller(fcurves, rotation_mode, xform) - scale = self.make_scale_controller(fcurves, xform.to_scale()) + scale = self.make_scale_controller(fcurves, "scale", xform.to_scale()) if pos is None and rot is None and scale is None: if not allow_empty: return None @@ -447,8 +447,8 @@ class AnimationConverter: # Now we make the controller return self._make_matrix44_controller(keyframes) - def make_pos_controller(self, fcurves, default_xform, convert=None) -> Union[None, plLeafController]: - pos_curves = [i for i in fcurves if i.data_path == "location" and i.keyframe_points] + def make_pos_controller(self, fcurves, data_path: str, default_xform, convert=None) -> Union[None, plLeafController]: + pos_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] keyframes, bez_chans = self._process_keyframes(pos_curves, 3, default_xform, convert) if not keyframes: return None @@ -507,8 +507,8 @@ class AnimationConverter: else: return self._make_quat_controller(keyframes) - def make_scale_controller(self, fcurves, default_xform, convert=None) -> plLeafController: - scale_curves = [i for i in fcurves if i.data_path == "scale" and i.keyframe_points] + def make_scale_controller(self, fcurves, data_path: str, default_xform, convert=None) -> plLeafController: + scale_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] keyframes, bez_chans = self._process_keyframes(scale_curves, 3, default_xform, convert) if not keyframes: return None