Browse Source

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.
pull/254/head
Adam Johnson 3 years ago
parent
commit
22ac9ae735
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 105
      korman/exporter/animation.py
  2. 2
      korman/exporter/camera.py
  3. 2
      korman/properties/prop_object.py

105
korman/exporter/animation.py

@ -57,7 +57,7 @@ class AnimationConverter:
if isinstance(bo.data, bpy.types.Camera): if isinstance(bo.data, bpy.types.Camera):
applicators.append(self._convert_camera_animation(bo, so, obj_fcurves, data_fcurves)) applicators.append(self._convert_camera_animation(bo, so, obj_fcurves, data_fcurves))
else: 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: if bo.plasma_modifiers.soundemit.enabled:
applicators.extend(self._convert_sound_volume_animation(bo.name, obj_fcurves, bo.plasma_modifiers.soundemit)) applicators.extend(self._convert_sound_volume_animation(bo.name, obj_fcurves, bo.plasma_modifiers.soundemit))
if isinstance(bo.data, bpy.types.Lamp): 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 # 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 # returned from here... At bare minimum, we'll need the applicator with an empty
# CompoundController. This should be sufficient to keep CWE from crashing... # 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 = locals().get("camera", self._mgr.find_create_object(plCameraModifier, so=so))
camera.animated = applicator is not None camera.animated = applicator is not None
return applicator return applicator
@ -369,14 +369,14 @@ class AnimationConverter:
applicator.channel = channel applicator.channel = channel
yield applicator yield applicator
def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False) -> Union[None, plMatrixChannelApplicator]: def _convert_transform_animation(self, bo, fcurves, xform, allow_empty=False) -> Union[None, plMatrixChannelApplicator]:
tm = self.convert_transform_controller(fcurves, xform, allow_empty) tm = self.convert_transform_controller(fcurves, bo.rotation_mode, xform, allow_empty)
if tm is None and not allow_empty: if tm is None and not allow_empty:
return None return None
applicator = plMatrixChannelApplicator() applicator = plMatrixChannelApplicator()
applicator.enabled = True applicator.enabled = True
applicator.channelName = name applicator.channelName = bo.name
channel = plMatrixControllerChannel() channel = plMatrixControllerChannel()
channel.controller = tm channel.controller = tm
applicator.channel = channel applicator.channel = channel
@ -384,18 +384,13 @@ class AnimationConverter:
return applicator 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: if not fcurves and not allow_empty:
return None return None
rotation_quaternion = any((i.data_path == "rotation_quaternion" for i in fcurves)) pos = self.make_pos_controller(fcurves, xform.to_translation())
rot = self.make_rot_controller(fcurves, rotation_mode, xform)
pos = self.make_pos_controller(fcurves, "location", xform.to_translation()) scale = self.make_scale_controller(fcurves, xform.to_scale())
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 pos is None and rot is None and scale is None:
if not allow_empty: if not allow_empty:
return None return None
@ -452,8 +447,8 @@ class AnimationConverter:
# Now we make the controller # Now we make the controller
return self._make_matrix44_controller(keyframes) return self._make_matrix44_controller(keyframes)
def make_pos_controller(self, fcurves, data_path : str, default_xform, convert=None) -> Union[None, plLeafController]: def make_pos_controller(self, fcurves, default_xform, convert=None) -> Union[None, plLeafController]:
pos_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] 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) keyframes, bez_chans = self._process_keyframes(pos_curves, 3, default_xform, convert)
if not keyframes: if not keyframes:
return None return None
@ -463,22 +458,57 @@ class AnimationConverter:
ctrl = self._make_point3_controller(keyframes, bez_chans) ctrl = self._make_point3_controller(keyframes, bez_chans)
return ctrl return ctrl
def make_rot_controller(self, fcurves, data_path : str, default_xform, num_channels, convert=None) -> Union[None, plCompoundController, plLeafController]: def make_rot_controller(self, fcurves, rotation_mode: str, default_xform, convert=None) -> Union[None, plCompoundController, plLeafController]:
rot_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] if rotation_mode in {"AXIS_ANGLE", "QUATERNION"}:
keyframes, bez_chans = self._process_keyframes(rot_curves, num_channels, default_xform, convert=None) rot_curves = [i for i in fcurves if i.data_path == "rotation_{}".format(rotation_mode.lower()) and i.keyframe_points]
if not keyframes: if not rot_curves:
return None 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 # Just dropping bezier stuff on the floor because Plasma does not support it, and
# many users will actually see the benefit here? Makes me sad. # I think that opting into quaternion keyframes is a good enough indication that
if bez_chans: # you're OK with that.
ctrl = self._make_scalar_compound_controller(keyframes, bez_chans) keyframes, bez_chans = self._process_keyframes(rot_curves, 4, default_xform, convert)
if keyframes:
return self._make_quat_controller(keyframes)
else: else:
ctrl = self._make_quat_controller(keyframes, num_channels) rot_curves = [i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points]
return ctrl 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: def make_scale_controller(self, fcurves, default_xform, convert=None) -> plLeafController:
scale_curves = [i for i in fcurves if i.data_path == data_path and i.keyframe_points] 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) keyframes, bez_chans = self._process_keyframes(scale_curves, 3, default_xform, convert)
if not keyframes: if not keyframes:
return None return None
@ -527,7 +557,7 @@ class AnimationConverter:
ctrl.keys = (exported_frames, keyframe_type) ctrl.keys = (exported_frames, keyframe_type)
return ctrl return ctrl
def _make_quat_controller(self, keyframes, num_channels) -> plLeafController: def _make_quat_controller(self, keyframes) -> plLeafController:
ctrl = plLeafController() ctrl = plLeafController()
keyframe_type = hsKeyFrame.kQuatKeyFrame keyframe_type = hsKeyFrame.kQuatKeyFrame
exported_frames = [] exported_frames = []
@ -539,12 +569,19 @@ class AnimationConverter:
exported.type = keyframe_type exported.type = keyframe_type
# NOTE: quat keyframes don't do bezier nonsense # NOTE: quat keyframes don't do bezier nonsense
values = keyframe.values
num_channels = len(values)
if num_channels == 3: if num_channels == 3:
value = mathutils.Euler(keyframe.values) value = mathutils.Euler(values)
exported.value = utils.quaternion(value.to_quaternion()) exported.value = utils.quaternion(value.to_quaternion())
else: elif num_channels == 4:
value = mathutils.Quaternion(keyframe.values) # 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) exported.value = utils.quaternion(value)
else:
raise ValueError("Unexpected number of channels in quaternion keyframe {}".format(num_channels))
exported_frames.append(exported) exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type) ctrl.keys = (exported_frames, keyframe_type)
return ctrl return ctrl

2
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 # path object, but it makes more sense to me to just animate the camera with
# the details of the path... # the details of the path...
pos_fcurves = tuple(i for i in helpers.fetch_fcurves(bo, False) if i.data_path == "location") 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: if pos_ctrl is None:
raise ExportError("'{}': Rail Camera lacks appropriate rail keyframes".format(bo.name)) raise ExportError("'{}': Rail Camera lacks appropriate rail keyframes".format(bo.name))
path = plAnimPath() path = plAnimPath()

2
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 is not None:
if bo.animation_data.action 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)) 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 return False
@property @property

Loading…
Cancel
Save