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. 99
      korman/exporter/animation.py
  2. 2
      korman/exporter/camera.py
  3. 2
      korman/properties/prop_object.py

99
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:
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])[:]
# 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:
rot_curves = [i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points]
if not rot_curves:
return None
# 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.
# 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:
ctrl = self._make_scalar_compound_controller(keyframes, bez_chans)
return self._make_scalar_compound_controller(keyframes, bez_chans)
else:
ctrl = self._make_quat_controller(keyframes, num_channels)
return ctrl
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

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
# 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()

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.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

Loading…
Cancel
Save