From 22ac9ae73581b2d4ab058082f45842672b613ee4 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 11 Jun 2021 23:14:32 -0400 Subject: [PATCH] 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