Browse Source

Improve logging around layer animation export.

This should help folks diagnose "no controllable animations" a little
better.
master
Adam Johnson 1 week ago
parent
commit
f87f090dcd
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 45
      korman/exporter/animation.py
  2. 59
      korman/exporter/material.py

45
korman/exporter/animation.py

@ -479,7 +479,8 @@ class AnimationConverter:
return 0.0, 0.0
def make_matrix44_controller(self, fcurves, pos_path: str, scale_path: str, pos_default, scale_default,
*, start: Optional[int] = None, end: Optional[int] = None) -> Optional[plLeafController]:
*, start: Optional[int] = None, end: Optional[int] = None,
name: str = "") -> Optional[plLeafController]:
def convert_matrix_keyframe(**kwargs) -> hsMatrix44:
pos = kwargs[pos_path]
scale = kwargs[scale_path]
@ -499,7 +500,7 @@ class AnimationConverter:
channels = { pos_path: 3, scale_path: 3 }
default_values = { pos_path: pos_default, scale_path: scale_default }
keyframes = self._process_fcurves(fcurves, channels, 1, convert_matrix_keyframe,
default_values, start=start, end=end)
default_values, start=start, end=end, name=name)
if not keyframes:
return None
@ -508,10 +509,10 @@ class AnimationConverter:
def make_pos_controller(self, fcurves, data_path: str, default_xform,
convert: Optional[Callable] = None, *, start: Optional[int] = None,
end: Optional[int] = None) -> Optional[plLeafController]:
end: Optional[int] = None, name: str = "") -> Optional[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,
start=start, end=end)
start=start, end=end, name=name)
if not keyframes:
return None
@ -522,7 +523,7 @@ class AnimationConverter:
def make_rot_controller(self, fcurves, rotation_mode: str, default_xform,
convert: Optional[Callable] = None, *, start: Optional[int] = None,
end: Optional[int] = None) -> Union[None, plCompoundController, plLeafController]:
end: Optional[int] = None, name: str = "") -> 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:
@ -543,7 +544,7 @@ class AnimationConverter:
# 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,
start=start, end=end)
start=start, end=end, name=name)
if keyframes:
return self._make_quat_controller(keyframes)
else:
@ -564,7 +565,7 @@ class AnimationConverter:
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, start=start, end=end)
euler_convert, start=start, end=end, name=name)
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
@ -576,10 +577,11 @@ class AnimationConverter:
def make_scale_controller(self, fcurves, data_path: str, default_xform,
convert: Optional[Callable] = None, *, start: Optional[int] = None,
end: Optional[int] = None) -> Optional[plLeafController]:
end: Optional[int] = None,
name: str = "") -> Optional[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,
start=start, end=end)
start=start, end=end, name=name)
if not keyframes:
return None
@ -590,8 +592,9 @@ class AnimationConverter:
def make_scalar_leaf_controller(self, fcurve: bpy.types.FCurve,
convert: Optional[Callable] = None, *,
start: Optional[int] = None,
end: Optional[int] = None) -> Optional[plLeafController]:
keyframes, bezier = self._process_fcurve(fcurve, convert, start=start, end=end)
end: Optional[int] = None,
name: str = "") -> Optional[plLeafController]:
keyframes, bezier = self._process_fcurve(fcurve, convert, start=start, end=end, name=name)
if not keyframes:
return None
@ -741,7 +744,8 @@ class AnimationConverter:
return [keyframes_sorted[i] for i in filtered_indices]
def _process_fcurve(self, fcurve: bpy.types.FCurve, convert: Optional[Callable] = None, *,
start: Optional[int] = None, end: Optional[int] = None) -> Tuple[Sequence, AbstractSet]:
start: Optional[int] = None, end: Optional[int] = None,
name: str = "") -> Tuple[Sequence, AbstractSet]:
"""Like _process_keyframes, but for one fcurve"""
# Adapt from incoming single item sequence to a single argument.
@ -750,7 +754,7 @@ class AnimationConverter:
else:
single_convert = None
# Can't proxy to _process_fcurves because it only supports linear interoplation.
return self._process_keyframes([fcurve], 1, [0.0], single_convert, start=start, end=end)
return self._process_keyframes([fcurve], 1, [0.0], single_convert, start=start, end=end, name=name)
def _santize_converted_values(self, num_channels: int, raw_values: Union[Dict, Sequence], convert: Callable):
assert convert is not None
@ -772,7 +776,8 @@ class AnimationConverter:
def _process_fcurves(self, fcurves: Sequence, channels: Dict[str, int], result_channels: int,
convert: Callable, defaults: Dict[str, Union[float, Sequence]], *,
start: Optional[int] = None, end: Optional[int] = None) -> Sequence:
start: Optional[int] = None, end: Optional[int] = None,
name: str = "") -> Sequence:
"""This consumes a sequence of Blender FCurves that map to a single Plasma controller.
Like `_process_keyframes()`, except the converter function is mandatory, and each
Blender `data_path` must have a fixed number of channels.
@ -838,11 +843,14 @@ class AnimationConverter:
keyframe.out_tans = [0.0] * result_channels
keyframes[frame_num] = keyframe
return self._sort_and_dedupe_keyframes(keyframes)
sorted_keyframes = self._sort_and_dedupe_keyframes(keyframes)
if keyframes and not sorted_keyframes and name:
self._exporter().report.warn(f"All keyframes for '{name}' are identical and have been discarded!")
return sorted_keyframes
def _process_keyframes(self, fcurves, num_channels: int, default_values: Sequence,
convert: Optional[Callable] = None, *, start: Optional[int] = None,
end: Optional[int] = None) -> Tuple[Sequence, AbstractSet]:
end: Optional[int] = None, name: str = "") -> Tuple[Sequence, AbstractSet]:
"""Groups all FCurves for the same frame together"""
keyframe_data = type("KeyFrameData", (), {})
fps, pi = self._bl_fps, math.pi
@ -901,7 +909,10 @@ class AnimationConverter:
keyframes[frame_num] = keyframe
# Return the keyframes in a sequence sorted by frame number
return (self._sort_and_dedupe_keyframes(keyframes), bez_chans)
sorted_keyframes = self._sort_and_dedupe_keyframes(keyframes)
if keyframes and not sorted_keyframes and name:
self._exporter().report.warn(f"All keyframes for '{name}' are identical and have been discarded!")
return (sorted_keyframes, bez_chans)
@property
def _mgr(self):

59
korman/exporter/material.py

@ -529,7 +529,9 @@ class MaterialConverter:
# Export any layer animations
# NOTE: animated stencils and bumpmaps are nonsense.
if not slot.use_stencil and not wantBumpmap:
layer = self._export_layer_animations(bo, bm, slot, idx, layer)
self._report.msg("Exporting layer animations")
with self._report.indent():
layer = self._export_layer_animations(bo, bm, slot, idx, layer)
# Stash the top of the stack for later in the export
if bm is not None:
@ -562,12 +564,18 @@ class MaterialConverter:
if texture is not None:
layer_props = texture.plasma_layer
for anim in layer_props.subanimations:
self._report.msg(f"Exporting '{anim.animation_name}'")
if not anim.is_entire_animation:
start, end = anim.start, anim.end
else:
start, end = None, None
controllers = self._export_layer_controllers(bo, bm, tex_slot, idx, base_layer,
start=start, end=end)
with self._report.indent():
controllers = self._export_layer_controllers(
bo, bm, tex_slot, idx, base_layer,
start=start, end=end
)
if not controllers:
continue
@ -584,7 +592,9 @@ class MaterialConverter:
top_layer.varName = anim.sdl_var
else:
# Crappy automatic entire layer animation. Loop it by default.
controllers = self._export_layer_controllers(bo, bm, tex_slot, idx, base_layer)
self._report.msg(f"Exporting crappy '(Entire Animation)'")
with self._report.indent():
controllers = self._export_layer_controllers(bo, bm, tex_slot, idx, base_layer)
if controllers:
attach_layer(plLayerAnimation, "(Entire Animation)", controllers)
atc = top_layer.timeConvert
@ -630,14 +640,21 @@ class MaterialConverter:
# Take the FCurves and ram them through our converters, hopefully returning some valid
# animation controllers.
controllers = {}
controllers: Dict[str, plController] = {}
for attr, converter in self._animation_exporters.items():
ctrl = converter(bo, bm, tex_slot, base_layer, fcurves, start=start, end=end)
ctrl = converter(bo, bm, tex_slot, base_layer, fcurves, start=start, end=end, ctrlName=attr)
if ctrl is not None:
if isinstance(ctrl, plLeafController):
self._report.msg(f"'{attr}': {len(ctrl.keys)} keyframes")
elif isinstance(ctrl, plCompoundController):
# Shouldn't happen, but for completeness' sake.
self._report.msg(f"{attr}: X={len(ctrl.X.keys)}, Y={len(ctrl.Y.keys)}, Z={len(ctrl.Z.keys)} keyframes")
else:
raise RuntimeError()
controllers[attr] = ctrl
return controllers
def _export_layer_diffuse_animation(self, bo, bm, tex_slot, base_layer, fcurves, *, start, end, converter):
def _export_layer_diffuse_animation(self, bo, bm, tex_slot, base_layer, fcurves, *, start, end, converter, ctrlName: str = ""):
assert converter is not None
# If there's no material, then this is simply impossible.
@ -649,12 +666,15 @@ class MaterialConverter:
result = converter(bo, bm, tex_slot, mathutils.Color(color_sequence))
return result.red, result.green, result.blue
ctrl = self._exporter().animation.make_pos_controller(fcurves, "diffuse_color",
bm.diffuse_color, translate_color,
start=start, end=end)
ctrl = self._exporter().animation.make_pos_controller(
fcurves, "diffuse_color",
bm.diffuse_color, translate_color,
start=start, end=end,
name=ctrlName
)
return ctrl
def _export_layer_opacity_animation(self, bo, bm, tex_slot, base_layer, fcurves, *, start, end):
def _export_layer_opacity_animation(self, bo, bm, tex_slot, base_layer, fcurves, *, start, end, ctrlName: str):
# Dumb function to intercept the opacity values and properly flag the base layer
def process_opacity(value):
self._handle_layer_opacity(base_layer, value)
@ -662,20 +682,27 @@ class MaterialConverter:
for i in fcurves:
if i.data_path == "plasma_layer.opacity":
ctrl = self._exporter().animation.make_scalar_leaf_controller(i, process_opacity, start=start, end=end)
ctrl = self._exporter().animation.make_scalar_leaf_controller(
i, process_opacity,
start=start, end=end,
name=ctrlName
)
return ctrl
return None
def _export_layer_transform_animation(self, bo, bm, tex_slot, base_layer, fcurves, *, start, end):
def _export_layer_transform_animation(self, bo, bm, tex_slot, base_layer, fcurves, *, start, end, ctrlName: str):
if tex_slot is not None:
path = tex_slot.path_from_id()
pos_path = "{}.offset".format(path)
scale_path = "{}.scale".format(path)
# Plasma uses the controller to generate a matrix44... so we have to produce a leaf controller
ctrl = self._exporter().animation.make_matrix44_controller(fcurves, pos_path, scale_path,
tex_slot.offset, tex_slot.scale,
start=start, end=end)
ctrl = self._exporter().animation.make_matrix44_controller(
fcurves, pos_path, scale_path,
tex_slot.offset, tex_slot.scale,
start=start, end=end,
name=ctrlName
)
return ctrl
return None

Loading…
Cancel
Save