Browse Source

Merge pull request #236 from Hoikas/moar-anims

Implement Material Diffuse Animations
pull/246/head v0.11
Adam Johnson 3 years ago committed by GitHub
parent
commit
9f97d5d5d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 567
      korman/exporter/animation.py
  2. 92
      korman/exporter/material.py

567
korman/exporter/animation.py

@ -14,10 +14,13 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
from collections import defaultdict
import functools
import itertools
import math
import mathutils
from PyHSPlasma import *
from typing import *
import weakref
from . import utils
@ -27,10 +30,10 @@ class AnimationConverter:
self._exporter = weakref.ref(exporter)
self._bl_fps = bpy.context.scene.render.fps
def _convert_frame_time(self, frame_num):
def _convert_frame_time(self, frame_num : int) -> float:
return frame_num / self._bl_fps
def convert_object_animations(self, bo, so):
def convert_object_animations(self, bo, so) -> None:
if not bo.plasma_object.has_animation_data:
return
@ -191,7 +194,7 @@ class AnimationConverter:
return None
energy_curve = next((i for i in fcurves if i.data_path == "energy" and i.keyframe_points), None)
color_curves = sorted((i for i in fcurves if i.data_path == "color" and i.keyframe_points), key=lambda x: x.array_index)
color_curves = [i for i in fcurves if i.data_path == "color" and i.keyframe_points]
if energy_curve is None and color_curves is None:
return None
elif lamp.use_only_shadow:
@ -202,10 +205,15 @@ class AnimationConverter:
return None
# OK Specular is easy. We just toss out the color as a point3.
color_keyframes, color_bez = self._process_keyframes(color_curves, convert=lambda x: x * -1.0 if lamp.use_negative else None)
def convert_specular_animation(color):
if lamp.use_negative:
return map(lambda x: x * -1.0, color)
else:
return color
color_keyframes, color_bez = self._process_keyframes(color_curves, 3, lamp.color, convert_specular_animation)
if color_keyframes and lamp.use_specular:
channel = plPointControllerChannel()
channel.controller = self._make_point3_controller(color_curves, color_keyframes, color_bez, lamp.color)
channel.controller = self._make_point3_controller(color_keyframes, color_bez)
applicator = plLightSpecularApplicator()
applicator.channelName = name
applicator.channel = channel
@ -214,18 +222,20 @@ class AnimationConverter:
# Hey, look, it's a third way to process FCurves. YAY!
def convert_diffuse_animation(color, energy):
if lamp.use_negative:
return { key: (0.0 - value) * energy[0] for key, value in color.items() }
proc = lambda x: x * -1.0 * energy[0]
else:
return { key: value * energy[0] for key, value in color.items() }
diffuse_defaults = { "color": lamp.color, "energy": lamp.energy }
proc = lambda x: x * energy[0]
return map(proc, color)
diffuse_channels = dict(color=3, energy=1)
diffuse_defaults = dict(color=lamp.color, energy=lamp.energy)
diffuse_fcurves = color_curves + [energy_curve,]
diffuse_keyframes = self._process_fcurves(diffuse_fcurves, convert_diffuse_animation, diffuse_defaults)
diffuse_keyframes = self._process_fcurves(diffuse_fcurves, diffuse_channels, 3, convert_diffuse_animation, diffuse_defaults)
if not diffuse_keyframes:
return None
# Whew.
channel = plPointControllerChannel()
channel.controller = self._make_point3_controller([], diffuse_keyframes, False, [])
channel.controller = self._make_point3_controller(diffuse_keyframes, False)
applicator = plLightDiffuseApplicator()
applicator.channelName = name
applicator.channel = channel
@ -239,8 +249,16 @@ class AnimationConverter:
distance_fcurve = next((i for i in fcurves if i.data_path == "distance"), None)
if energy_fcurve is None and distance_fcurve is None:
return None
light_converter = self._exporter().light
intensity, atten_end = light_converter.convert_attenuation(lamp)
light_converter, report = self._exporter().light, self._exporter().report
omni_fcurves = [distance_fcurve, energy_fcurve]
omni_channels = dict(distance=1, energy=1)
omni_defaults = dict(distance=lamp.distance, energy=lamp.energy)
def convert_omni_atten(distance, energy):
intens = abs(energy[0])
atten_end = distance[0] if lamp.use_sphere else distance[0] * 2
return light_converter.convert_attenuation_linear(intens, atten_end)
# All types allow animating cutoff
if distance_fcurve is not None:
@ -255,15 +273,9 @@ class AnimationConverter:
falloff = lamp.falloff_type
if falloff == "CONSTANT":
if energy_fcurve is not None:
self._exporter().report.warn("Constant attenuation cannot be animated in Plasma", ident=3)
report.warn("Constant attenuation cannot be animated in Plasma", ident=3)
elif falloff == "INVERSE_LINEAR":
def convert_linear_atten(distance, energy):
intens = abs(energy[0])
atten_end = distance[0] if lamp.use_sphere else distance[0] * 2
return light_converter.convert_attenuation_linear(intens, atten_end)
keyframes = self._process_fcurves([distance_fcurve, energy_fcurve], convert_linear_atten,
{"distance": lamp.distance, "energy": lamp.energy})
keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, omni_defaults)
if keyframes:
channel = plScalarControllerChannel()
channel.controller = self._make_scalar_leaf_controller(keyframes, False)
@ -273,13 +285,8 @@ class AnimationConverter:
yield applicator
elif falloff == "INVERSE_SQUARE":
if self._mgr.getVer() >= pvMoul:
def convert_quadratic_atten(distance, energy):
intens = abs(energy[0])
atten_end = distance[0] if lamp.use_sphere else distance[0] * 2
return light_converter.convert_attenuation_quadratic(intens, atten_end)
keyframes = self._process_fcurves([distance_fcurve, energy_fcurve], convert_quadratic_atten,
{"distance": lamp.distance, "energy": lamp.energy})
report.port("Lamp {} Falloff animations are only supported in Myst Online: Uru Live", falloff, indent=3)
keyframes = self._process_fcurves(omni_fcurves, omni_channels, 1, convert_omni_atten, omni_defaults)
if keyframes:
channel = plScalarControllerChannel()
channel.controller = self._make_scalar_leaf_controller(keyframes, False)
@ -288,9 +295,9 @@ class AnimationConverter:
applicator.channel = channel
yield applicator
else:
self._exporter().report.port("Lamp Falloff '{}' animations only partially supported for this version of Plasma", falloff, indent=3)
report.warn("Lamp {} Falloff animations are not supported for this version of Plasma", falloff, indent=3)
else:
self._exporter().report.warn("Lamp Falloff '{}' animations are not supported".format(falloff), ident=3)
report.warn("Lamp Falloff '{}' animations are not supported", falloff, ident=3)
def _convert_sound_volume_animation(self, name, fcurves, soundemit):
if not fcurves:
@ -340,8 +347,11 @@ class AnimationConverter:
size = spot_size[0]
value = size - (blend * size)
return math.degrees(value)
defaults = { "spot_blend": lamp.spot_blend, "spot_size": lamp.spot_size }
keyframes = self._process_fcurves([blend_fcurve, size_fcurve], convert_spot_inner, defaults)
inner_fcurves = [blend_fcurve, size_fcurve]
inner_channels = dict(spot_blend=1, spot_size=1)
inner_defaults = dict(spot_blend=lamp.spot_blend, spot_size=lamp.spot_size)
keyframes = self._process_fcurves(inner_fcurves, inner_channels, 1, convert_spot_inner, inner_defaults)
if keyframes:
channel = plScalarControllerChannel()
@ -351,7 +361,7 @@ class AnimationConverter:
applicator.channel = channel
yield applicator
def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False):
def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False) -> Union[None, plMatrixChannelApplicator]:
tm = self.convert_transform_controller(fcurves, xform, allow_empty)
if tm is None and not allow_empty:
return None
@ -366,13 +376,14 @@ class AnimationConverter:
return applicator
def convert_transform_controller(self, fcurves, xform, allow_empty=False):
def convert_transform_controller(self, fcurves, xform, allow_empty=False) -> Union[None, plCompoundController]:
if not fcurves and not allow_empty:
return None
pos = self.make_pos_controller(fcurves, xform)
rot = self.make_rot_controller(fcurves, xform)
scale = self.make_scale_controller(fcurves, xform)
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())
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
@ -383,17 +394,17 @@ class AnimationConverter:
tm.Z = scale
return tm
def get_anigraph_keys(self, bo=None, so=None):
def get_anigraph_keys(self, bo=None, so=None) -> Tuple[plKey, plKey]:
mod = self._mgr.find_create_key(plAGModifier, so=so, bl=bo)
master = self._mgr.find_create_key(plAGMasterMod, so=so, bl=bo)
return mod, master
def get_anigraph_objects(self, bo=None, so=None):
def get_anigraph_objects(self, bo=None, so=None) -> Tuple[plAGModifier, plAGMasterMod]:
mod = self._mgr.find_create_object(plAGModifier, so=so, bl=bo)
master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo)
return mod, master
def get_animation_key(self, bo, so=None):
def get_animation_key(self, bo, so=None) -> plKey:
# we might be controlling more than one animation. isn't that cute?
# https://www.youtube.com/watch?v=hspNaoxzNbs
# (but obviously this is not wrong...)
@ -403,72 +414,65 @@ class AnimationConverter:
else:
return self.get_anigraph_keys(bo, so)[1]
def make_matrix44_controller(self, fcurves, pos_path, scale_path, pos_default, scale_default):
def convert_matrix_keyframe(**kwargs):
pos = kwargs.get(pos_path)
scale = kwargs.get(scale_path)
# Since only some position curves may be supplied, construct dict with all positions
allpos = dict(enumerate(pos_default))
allscale = dict(enumerate(scale_default))
allpos.update(pos)
allscale.update(scale)
def make_matrix44_controller(self, fcurves, pos_path : str, scale_path : str, pos_default, scale_default) -> Union[None, plLeafController]:
def convert_matrix_keyframe(**kwargs) -> hsMatrix44:
pos = kwargs[pos_path]
scale = kwargs[scale_path]
matrix = hsMatrix44()
# Note: scale and pos are dicts, so we can't unpack
matrix.setTranslate(hsVector3(allpos[0], allpos[1], allpos[2]))
matrix.setScale(hsVector3(allscale[0], allscale[1], allscale[2]))
matrix.setTranslate(hsVector3(*pos))
matrix.setScale(hsVector3(*scale))
return matrix
fcurves = [i for i in fcurves if i.data_path == pos_path or i.data_path == scale_path]
if not fcurves:
return None
channels = { pos_path: 3, scale_path: 3 }
default_values = { pos_path: pos_default, scale_path: scale_default }
keyframes = self._process_fcurves(fcurves, convert_matrix_keyframe, default_values)
keyframes = self._process_fcurves(fcurves, channels, 1, convert_matrix_keyframe, default_values)
if not keyframes:
return None
# Now we make the controller
return self._make_matrix44_controller(keyframes)
def make_pos_controller(self, fcurves, default_xform, convert=None):
pos_curves = [i for i in fcurves if i.data_path == "location" and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(pos_curves, convert)
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
# At one point, I had some... insanity here to try to crush bezier channels and hand off to
# blah blah blah... As it turns out, point3 keyframe's tangents are vector3s :)
ctrl = self._make_point3_controller(pos_curves, keyframes, bez_chans, default_xform.to_translation())
ctrl = self._make_point3_controller(keyframes, bez_chans)
return ctrl
def make_rot_controller(self, fcurves, default_xform, convert=None):
# TODO: support rotation_quaternion
rot_curves = [i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(rot_curves, convert=None)
def make_rot_controller(self, fcurves, data_path : 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]
keyframes, bez_chans = self._process_keyframes(rot_curves, 3, default_xform, convert=None)
if not keyframes:
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.
if bez_chans:
ctrl = self._make_scalar_compound_controller(rot_curves, keyframes, bez_chans, default_xform.to_euler())
ctrl = self._make_scalar_compound_controller(keyframes, bez_chans)
else:
ctrl = self._make_quat_controller(rot_curves, keyframes, default_xform.to_euler())
ctrl = self._make_quat_controller( keyframes)
return ctrl
def make_scale_controller(self, fcurves, default_xform, convert=None):
scale_curves = [i for i in fcurves if i.data_path == "scale" and i.keyframe_points]
keyframes, bez_chans = self._process_keyframes(scale_curves, convert)
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
# There is no such thing as a compound scale controller... in Plasma, anyway.
ctrl = self._make_scale_value_controller(scale_curves, keyframes, bez_chans, default_xform)
ctrl = self._make_scale_value_controller(keyframes, bez_chans)
return ctrl
def make_scalar_leaf_controller(self, fcurve, convert=None):
def make_scalar_leaf_controller(self, fcurve, convert=None) -> Union[None, plLeafController]:
keyframes, bezier = self._process_fcurve(fcurve, convert)
if not keyframes:
return None
@ -476,7 +480,7 @@ class AnimationConverter:
ctrl = self._make_scalar_leaf_controller(keyframes, bezier)
return ctrl
def _make_matrix44_controller(self, keyframes):
def _make_matrix44_controller(self, keyframes) -> plLeafController:
ctrl = plLeafController()
keyframe_type = hsKeyFrame.kMatrix44KeyFrame
exported_frames = []
@ -486,52 +490,32 @@ class AnimationConverter:
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.type = keyframe_type
exported.value = keyframe.value
exported.value = keyframe.values[0]
exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _make_point3_controller(self, fcurves, keyframes, bezier, default_xform):
def _make_point3_controller(self, keyframes, bezier) -> plLeafController:
ctrl = plLeafController()
subctrls = ("X", "Y", "Z")
keyframe_type = hsKeyFrame.kBezPoint3KeyFrame if bezier else hsKeyFrame.kPoint3KeyFrame
exported_frames = []
ctrl_fcurves = { i.array_index: i for i in fcurves }
for keyframe in keyframes:
exported = hsPoint3Key()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.type = keyframe_type
in_tan = hsVector3()
out_tan = hsVector3()
value = hsVector3()
for i, subctrl in enumerate(subctrls):
fval = keyframe.values.get(i, None)
if fval is not None:
setattr(value, subctrl, fval)
setattr(in_tan, subctrl, keyframe.in_tans[i])
setattr(out_tan, subctrl, keyframe.out_tans[i])
else:
try:
setattr(value, subctrl, ctrl_fcurves[i].evaluate(keyframe.frame_num_blender))
except KeyError:
setattr(value, subctrl, default_xform[i])
setattr(in_tan, subctrl, 0.0)
setattr(out_tan, subctrl, 0.0)
exported.inTan = in_tan
exported.outTan = out_tan
exported.value = value
exported.inTan = hsVector3(*keyframe.in_tans)
exported.outTan = hsVector3(*keyframe.out_tans)
exported.value = hsVector3(*keyframe.values)
exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _make_quat_controller(self, fcurves, keyframes, default_xform):
def _make_quat_controller(self, keyframes) -> plLeafController:
ctrl = plLeafController()
keyframe_type = hsKeyFrame.kQuatKeyFrame
exported_frames = []
ctrl_fcurves = { i.array_index: i for i in fcurves }
for keyframe in keyframes:
exported = hsQuatKey()
@ -540,58 +524,36 @@ class AnimationConverter:
exported.type = keyframe_type
# NOTE: quat keyframes don't do bezier nonsense
value = mathutils.Euler()
for i in range(3):
fval = keyframe.values.get(i, None)
if fval is not None:
value[i] = fval
else:
try:
value[i] = ctrl_fcurves[i].evaluate(keyframe.frame_num_blender)
except KeyError:
value[i] = default_xform[i]
quat = value.to_quaternion()
exported.value = utils.quaternion(quat)
value = mathutils.Euler(keyframe.values)
exported.value = utils.quaternion(value.to_quaternion())
exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _make_scalar_compound_controller(self, fcurves, keyframes, bez_chans, default_xform):
def _make_scalar_compound_controller(self, keyframes, bez_chans) -> plCompoundController:
ctrl = plCompoundController()
subctrls = ("X", "Y", "Z")
for i in subctrls:
setattr(ctrl, i, plLeafController())
exported_frames = ([], [], [])
ctrl_fcurves = { i.array_index: i for i in fcurves }
for keyframe in keyframes:
for i, subctrl in enumerate(subctrls):
fval = keyframe.values.get(i, None)
if fval is not None:
keyframe_type = hsKeyFrame.kBezScalarKeyFrame if i in bez_chans else hsKeyFrame.kScalarKeyFrame
exported = hsScalarKey()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.inTan = keyframe.in_tans[i]
exported.outTan = keyframe.out_tans[i]
exported.type = keyframe_type
exported.value = fval
exported_frames[i].append(exported)
keyframe_type = hsKeyFrame.kBezScalarKeyFrame if i in bez_chans else hsKeyFrame.kScalarKeyFrame
exported = hsScalarKey()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.inTan = keyframe.in_tans[i]
exported.outTan = keyframe.out_tans[i]
exported.type = keyframe_type
exported.value = keyframe.values[i]
exported_frames[i].append(exported)
for i, subctrl in enumerate(subctrls):
my_keyframes = exported_frames[i]
# ensure this controller has at least ONE keyframe
if not my_keyframes:
hack_frame = hsScalarKey()
hack_frame.frame = 0
hack_frame.frameTime = 0.0
hack_frame.type = hsKeyFrame.kScalarKeyFrame
hack_frame.value = default_xform[i]
my_keyframes.append(hack_frame)
getattr(ctrl, subctrl).keys = (my_keyframes, my_keyframes[0].type)
return ctrl
def _make_scalar_leaf_controller(self, keyframes, bezier):
def _make_scalar_leaf_controller(self, keyframes, bezier) -> plLeafController:
ctrl = plLeafController()
keyframe_type = hsKeyFrame.kBezScalarKeyFrame if bezier else hsKeyFrame.kScalarKeyFrame
exported_frames = []
@ -600,239 +562,192 @@ class AnimationConverter:
exported = hsScalarKey()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.inTan = keyframe.in_tan
exported.outTan = keyframe.out_tan
exported.inTan = keyframe.in_tans[0]
exported.outTan = keyframe.out_tans[0]
exported.type = keyframe_type
exported.value = keyframe.value
exported.value = keyframe.values[0]
exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _make_scale_value_controller(self, fcurves, keyframes, bez_chans, default_xform):
subctrls = ("X", "Y", "Z")
def _make_scale_value_controller(self, keyframes, bez_chans) -> plLeafController:
keyframe_type = hsKeyFrame.kBezScaleKeyFrame if bez_chans else hsKeyFrame.kScaleKeyFrame
exported_frames = []
ctrl_fcurves = { i.array_index: i for i in fcurves }
default_scale = default_xform.to_scale()
unit_quat = default_xform.to_quaternion()
unit_quat.normalize()
unit_quat = utils.quaternion(unit_quat)
# Hmm... This smells... But it was basically doing this before the rewrite.
unit_quat = hsQuat(0.0, 0.0, 0.0, 1.0)
for keyframe in keyframes:
exported = hsScaleKey()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.type = keyframe_type
in_tan = hsVector3()
out_tan = hsVector3()
value = hsVector3()
for i, subctrl in enumerate(subctrls):
fval = keyframe.values.get(i, None)
if fval is not None:
setattr(value, subctrl, fval)
setattr(in_tan, subctrl, keyframe.in_tans[i])
setattr(out_tan, subctrl, keyframe.out_tans[i])
else:
try:
setattr(value, subctrl, ctrl_fcurves[i].evaluate(keyframe.frame_num_blender))
except KeyError:
setattr(value, subctrl, default_scale[i])
setattr(in_tan, subctrl, 0.0)
setattr(out_tan, subctrl, 0.0)
exported.inTan = in_tan
exported.outTan = out_tan
exported.value = (value, unit_quat)
exported.inTan = hsVector3(*keyframe.in_tans)
exported.outTan = hsVector3(*keyframe.out_tans)
exported.value = (hsVector3(*keyframe.values), unit_quat)
exported_frames.append(exported)
ctrl = plLeafController()
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _process_fcurve(self, fcurve, convert=None):
def _sort_and_dedupe_keyframes(self, keyframes : Dict) -> Sequence:
"""Takes in the final, unsorted keyframe sequence and sorts it. If all keyframes are
equivalent, eg due to a convert function, then they are discarded."""
num_keyframes = len(keyframes)
keyframes_sorted = [keyframes[i] for i in sorted(keyframes)]
# If any keyframe's value is equivalent to its boundary keyframes, discard it.
def filter_boundaries(i):
if i == 0 or i == num_keyframes - 1:
return False
left, me, right = keyframes_sorted[i - 1], keyframes_sorted[i], keyframes_sorted[i + 1]
return left.values == me.values == right.values
filtered_indices = list(itertools.filterfalse(filter_boundaries, range(num_keyframes)))
if len(filtered_indices) == 2:
if keyframes_sorted[filtered_indices[0]].values == keyframes_sorted[filtered_indices[1]].values:
return []
return [keyframes_sorted[i] for i in filtered_indices]
def _process_fcurve(self, fcurve, convert=None) -> Tuple[Sequence, AbstractSet]:
"""Like _process_keyframes, but for one fcurve"""
# Adapt from incoming single item sequence to a single argument.
single_convert = lambda x: convert(x[0]) if convert is not None else None
# Can't proxy to _process_fcurves because it only supports linear interoplation.
return self._process_keyframes([fcurve], 1, [0.0], single_convert)
def _santize_converted_values(self, num_channels : int, raw_values : Union[Dict, Sequence], convert : Callable):
assert convert is not None
if isinstance(raw_values, Dict):
values = convert(**raw_values)
elif isinstance(raw_values, Sequence):
values = convert(raw_values)
else:
raise AssertionError("Unexpected type for raw_values: {}".format(raw_values.__class__))
if not isinstance(values, Sequence) and isinstance(values, Iterable):
values = tuple(values)
if not isinstance(values, Sequence):
assert num_channels == 1, "Converter returned 1 value but expected {}".format(num_channels)
values = (values,)
else:
assert len(values) == num_channels, "Converter returned {} values but expected {}".format(len(values), num_channels)
return values
def _process_fcurves(self, fcurves : Sequence, channels : Dict[str, int], result_channels : int,
convert : Callable, defaults : Dict[str, Union[float, Sequence]]) -> 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.
"""
# TODO: This fxn should probably issue a warning if any keyframes use bezier interpolation.
# But there's no indication given by any other fxn when an invalid interpolation mode is
# given, so what can you do?
keyframe_data = type("KeyFrameData", (), {})
fps = self._bl_fps
pi = math.pi
fps, pi = self._bl_fps, math.pi
keyframes = {}
bezier = False
fcurve.update()
for fkey in fcurve.keyframe_points:
keyframe = keyframe_data()
frame_num, value = fkey.co
if fps == 30.0:
keyframe.frame_num = int(frame_num)
else:
keyframe.frame_num = int(frame_num * (30.0 / fps))
keyframe.frame_time = frame_num / fps
if fkey.interpolation == "BEZIER":
keyframe.in_tan = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi)
keyframe.out_tan = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi)
bezier = True
else:
keyframe.in_tan = 0.0
keyframe.out_tan = 0.0
keyframe.value = value if convert is None else convert(value)
keyframes[frame_num] = keyframe
final_keyframes = [keyframes[i] for i in sorted(keyframes)]
return (final_keyframes, bezier)
def _process_fcurves(self, fcurves, convert, defaults=None):
"""Processes FCurves of different data sets and converts them into a single list of keyframes.
This should be used when multiple Blender fields map to a single Plasma option."""
class KeyFrameData:
def __init__(self):
self.values = {}
fps = self._bl_fps
pi = math.pi
# It is assumed therefore that any multichannel FCurves will have all channels represented.
# This seems fairly safe with my experiments with Lamp colors...
grouped_fcurves = {}
for fcurve in fcurves:
if fcurve is None:
continue
grouped_fcurves = defaultdict(dict)
for fcurve in (i for i in fcurves if i is not None):
fcurve.update()
if fcurve.data_path in grouped_fcurves:
grouped_fcurves[fcurve.data_path][fcurve.array_index] = fcurve
else:
grouped_fcurves[fcurve.data_path] = { fcurve.array_index: fcurve }
grouped_fcurves[fcurve.data_path][fcurve.array_index] = fcurve
# Default values for channels that are not animated
for key, value in defaults.items():
if key not in grouped_fcurves:
if hasattr(value, "__len__"):
grouped_fcurves[key] = value
fcurve_keyframes = defaultdict(functools.partial(defaultdict, dict))
for fcurve in (i for i in fcurves if i is not None):
for fkey in fcurve.keyframe_points:
fcurve_keyframes[fkey.co[0]][fcurve.data_path][fcurve.array_index] = fkey
def iter_channel_values(frame_num : int, fcurves : Dict, fkeys : Dict, num_channels : int, defaults : Union[float, Sequence]):
for i in range(num_channels):
fkey = fkeys.get(i, None)
if fkey is None:
fcurve = fcurves.get(i, None)
if fcurve is None:
# We would like to test this to see if it makes sense, but Blender's mathutils
# types don't actually implement the sequence protocol. So, we'll have to
# just try to subscript it and see what happens.
try:
yield defaults[i]
except:
assert num_channels == 1, "Got a non-subscriptable default for a multi-channel keyframe."
yield defaults
else:
yield fcurve.evaluate(frame_num)
else:
grouped_fcurves[key] = [value,]
yield fkey.co[1]
# Assemble a dict { PlasmaFrameNum: { FCurveDataPath: KeyFrame } }
keyframe_points = {}
for fcurve in fcurves:
if fcurve is None:
continue
for keyframe in fcurve.keyframe_points:
frame_num_blender, value = keyframe.co
frame_num = int(frame_num_blender * (30.0 / fps))
# This is a temporary keyframe, so we're not going to worry about converting everything
# Only the frame number to Plasma so we can go ahead and merge any rounded dupes
entry, data = keyframe_points.get(frame_num), None
if entry is None:
entry = {}
keyframe_points[frame_num] = entry
else:
data = entry.get(fcurve.data_path)
if data is None:
data = KeyFrameData()
data.frame_num = frame_num
data.frame_num_blender = frame_num_blender
entry[fcurve.data_path] = data
data.values[fcurve.array_index] = value
# Now, we loop through our assembled keyframes and interpolate any missing data using the FCurves
fcurve_chans = { key: len(value) for key, value in grouped_fcurves.items() }
expected_values = sum(fcurve_chans.values())
all_chans = frozenset(grouped_fcurves.keys())
# We will also do the final convert here as well...
final_keyframes = []
for frame_num in sorted(keyframe_points.copy().keys()):
keyframes = keyframe_points[frame_num]
frame_num_blender = next(iter(keyframes.values())).frame_num_blender
# If any data_paths are missing, init a dummy
missing_channels = all_chans - frozenset(keyframes.keys())
for chan in missing_channels:
dummy = KeyFrameData()
dummy.frame_num = frame_num
dummy.frame_num_blender = frame_num_blender
keyframes[chan] = dummy
# Ensure all values are filled out.
num_values = sum(map(len, (i.values for i in keyframes.values())))
if num_values != expected_values:
for chan, sorted_fcurves in grouped_fcurves.items():
chan_keyframes = keyframes[chan]
chan_values = fcurve_chans[chan]
if len(chan_keyframes.values) == chan_values:
continue
for i in range(chan_values):
if i not in chan_keyframes.values:
try:
fcurve = grouped_fcurves[chan][i]
except:
chan_keyframes.values[i] = defaults[chan]
else:
if isinstance(fcurve, bpy.types.FCurve):
chan_keyframes.values[i] = fcurve.evaluate(chan_keyframes.frame_num_blender)
else:
# it's actually a default value!
chan_keyframes.values[i] = fcurve
# All values are calculated! Now we convert the disparate key data into a single keyframe.
kwargs = { data_path: keyframe.values for data_path, keyframe in keyframes.items() }
final_keyframe = KeyFrameData()
final_keyframe.frame_num = frame_num
final_keyframe.frame_num_blender = frame_num_blender
final_keyframe.frame_time = frame_num / fps
value = convert(**kwargs)
if hasattr(value, "__len__"):
final_keyframe.in_tans = [0.0] * len(value)
final_keyframe.out_tans = [0.0] * len(value)
final_keyframe.values = value
else:
final_keyframe.in_tan = 0.0
final_keyframe.out_tan = 0.0
final_keyframe.value = value
final_keyframes.append(final_keyframe)
return final_keyframes
keyframes = {}
for frame_num, fkeys in fcurve_keyframes.items():
keyframe = keyframe_data()
# hope you don't have a frame 29.9 and frame 30.0...
keyframe.frame_num = int(frame_num * (30.0 / fps))
keyframe.frame_num_blender = frame_num
keyframe.frame_time = frame_num / fps
keyframe.values_raw = { data_path: tuple(iter_channel_values(frame_num, grouped_fcurves[data_path], fkeys, num_channels, defaults[data_path]))
for data_path, num_channels in channels.items() }
keyframe.values = self._santize_converted_values(result_channels, keyframe.values_raw, convert)
# Very gnawty
keyframe.in_tans = [0.0] * result_channels
keyframe.out_tans = [0.0] * result_channels
keyframes[frame_num] = keyframe
def _process_keyframes(self, fcurves, convert=None):
return self._sort_and_dedupe_keyframes(keyframes)
def _process_keyframes(self, fcurves, num_channels : int, default_values : Sequence, convert=None) -> Tuple[Sequence, AbstractSet]:
"""Groups all FCurves for the same frame together"""
keyframe_data = type("KeyFrameData", (), {})
fps = self._bl_fps
pi = math.pi
fps, pi = self._bl_fps, math.pi
keyframes = {}
bez_chans = set()
for fcurve in fcurves:
keyframes, fcurve_keyframes = {}, defaultdict(dict)
indexed_fcurves = { fcurve.array_index: fcurve for fcurve in fcurves if fcurve is not None }
for i, fcurve in indexed_fcurves.items():
fcurve.update()
for fkey in fcurve.keyframe_points:
frame_num, value = fkey.co
keyframe = keyframes.get(frame_num, None)
if keyframe is None:
keyframe = keyframe_data()
if fps == 30.0:
# hope you don't have a frame 29.9 and frame 30.0...
keyframe.frame_num = int(frame_num)
else:
keyframe.frame_num = int(frame_num * (30.0 / fps))
keyframe.frame_num_blender = frame_num
keyframe.frame_time = frame_num / fps
keyframe.in_tans = {}
keyframe.out_tans = {}
keyframe.values = {}
keyframes[frame_num] = keyframe
idx = fcurve.array_index
keyframe.values[idx] = value if convert is None else convert(value)
# Calculate the bezier interpolation nonsense
if fkey.interpolation == "BEZIER":
keyframe.in_tans[idx] = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi)
keyframe.out_tans[idx] = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi)
bez_chans.add(idx)
fcurve_keyframes[fkey.co[0]][i] = fkey
def iter_values(frame_num, fkeys) -> Generator[float, None, None]:
for i in range(num_channels):
fkey = fkeys.get(i, None)
if fkey is not None:
yield fkey.co[1]
else:
keyframe.in_tans[idx] = 0.0
keyframe.out_tans[idx] = 0.0
fcurve = indexed_fcurves.get(i, None)
if fcurve is not None:
yield fcurve.evaluate(frame_num)
else:
yield default_values[i]
# Does this really need to be a set?
bez_chans = set()
for frame_num, fkeys in fcurve_keyframes.items():
keyframe = keyframe_data()
# hope you don't have a frame 29.9 and frame 30.0...
keyframe.frame_num = int(frame_num * (30.0 / fps))
keyframe.frame_num_blender = frame_num
keyframe.frame_time = frame_num / fps
keyframe.in_tans = [0.0] * num_channels
keyframe.out_tans = [0.0] * num_channels
keyframe.values_raw = tuple(iter_values(frame_num, fkeys))
if convert is None:
keyframe.values = keyframe.values_raw
else:
keyframe.values = self._santize_converted_values(num_channels, keyframe.values_raw, convert)
for i, fkey in ((i, fkey) for i, fkey in fkeys.items() if fkey.interpolation == "BEZIER"):
value = keyframe.values_raw[i]
keyframe.in_tans[i] = -(value - fkey.handle_left[1]) / (frame_num - fkey.handle_left[0]) / fps / (2 * pi)
keyframe.out_tans[i] = (value - fkey.handle_right[1]) / (frame_num - fkey.handle_right[0]) / fps / (2 * pi)
bez_chans.add(i)
keyframes[frame_num] = keyframe
# Return the keyframes in a sequence sorted by frame number
final_keyframes = [keyframes[i] for i in sorted(keyframes)]
return (final_keyframes, bez_chans)
return (self._sort_and_dedupe_keyframes(keyframes), bez_chans)
@property
def _mgr(self):

92
korman/exporter/material.py

@ -16,8 +16,10 @@
import bpy
import functools
import math
import mathutils
from pathlib import Path
from PyHSPlasma import *
from typing import Union
import weakref
from .explosions import *
@ -143,7 +145,10 @@ class MaterialConverter:
"NONE": self._export_texture_type_none,
}
self._animation_exporters = {
"ambientCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_ambient),
"opacityCtl": self._export_layer_opacity_animation,
"preshadeCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_preshade),
"runtimeCtl": functools.partial(self._export_layer_diffuse_animation, converter=self.get_material_runtime),
"transformCtl": self._export_layer_transform_animation,
}
@ -254,6 +259,7 @@ class MaterialConverter:
if not hsgmat.layers:
layer = self._mgr.find_create_object(plLayer, name="{}_AutoLayer".format(mat_name), bl=bo)
self._propagate_material_settings(bo, bm, layer)
layer = self._export_layer_animations(bo, bm, None, 0, layer)
hsgmat.addLayer(layer.key)
# Cache this material for later
@ -489,7 +495,7 @@ class MaterialConverter:
layer = self._export_layer_animations(bo, bm, slot, idx, layer)
return layer
def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer):
def _export_layer_animations(self, bo, bm, tex_slot, idx, base_layer) -> plLayer:
"""Exports animations on this texture and chains the Plasma layers as needed"""
def harvest_fcurves(bl_id, collection, data_path=None):
@ -508,9 +514,16 @@ class MaterialConverter:
return None
fcurves = []
texture = tex_slot.texture
mat_action = harvest_fcurves(bm, fcurves, "texture_slots[{}]".format(idx))
tex_action = harvest_fcurves(texture, fcurves)
# Base layers get all of the fcurves for animating things like the diffuse color
texture = tex_slot.texture if tex_slot is not None else None
if idx == 0:
harvest_fcurves(bm, fcurves)
harvest_fcurves(texture, fcurves)
elif tex_slot is not None:
harvest_fcurves(bm, fcurves, tex_slot.path_from_id())
harvest_fcurves(texture, fcurves)
if not fcurves:
return base_layer
@ -518,7 +531,7 @@ class MaterialConverter:
# and chain this biotch up as best we can.
layer_animation = None
for attr, converter in self._animation_exporters.items():
ctrl = converter(tex_slot, base_layer, fcurves)
ctrl = converter(bo, bm, tex_slot, base_layer, fcurves)
if ctrl is not None:
if layer_animation is None:
name = "{}_LayerAnim".format(base_layer.key.name)
@ -539,21 +552,39 @@ class MaterialConverter:
atc.begin = min((fcurve.range()[0] for fcurve in fcurves)) * (30.0 / fps) / fps
atc.end = max((fcurve.range()[1] for fcurve in fcurves)) * (30.0 / fps) / fps
layer_props = tex_slot.texture.plasma_layer
if not layer_props.anim_auto_start:
atc.flags |= plAnimTimeConvert.kStopped
if layer_props.anim_loop:
if tex_slot is not None:
layer_props = tex_slot.texture.plasma_layer
if not layer_props.anim_auto_start:
atc.flags |= plAnimTimeConvert.kStopped
if layer_props.anim_loop:
atc.flags |= plAnimTimeConvert.kLoop
atc.loopBegin = atc.begin
atc.loopEnd = atc.end
if layer_props.anim_sdl_var:
layer_animation.varName = layer_props.anim_sdl_var
else:
# Hmm... I wonder what we should do here? A reasonable default might be to just
# run the stupid thing in a loop.
atc.flags |= plAnimTimeConvert.kLoop
atc.loopBegin = atc.begin
atc.loopEnd = atc.end
if layer_props.anim_sdl_var:
layer_animation.varName = layer_props.anim_sdl_var
return layer_animation
# Well, we had some FCurves but they were garbage... Too bad.
return base_layer
def _export_layer_opacity_animation(self, tex_slot, base_layer, fcurves):
def _export_layer_diffuse_animation(self, bo, bm, tex_slot, base_layer, fcurves, converter):
assert converter is not None
def translate_color(color_sequence):
# See things like get_material_preshade
result = converter(bo, bm, 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)
return ctrl
def _export_layer_opacity_animation(self, bo, bm, tex_slot, base_layer, fcurves):
for i in fcurves:
if i.data_path == "plasma_layer.opacity":
base_layer.state.blendFlags |= hsGMatState.kBlendAlpha
@ -561,14 +592,16 @@ class MaterialConverter:
return ctrl
return None
def _export_layer_transform_animation(self, tex_slot, base_layer, fcurves):
path = tex_slot.path_from_id()
pos_path = "{}.offset".format(path)
scale_path = "{}.scale".format(path)
def _export_layer_transform_animation(self, bo, bm, tex_slot, base_layer, fcurves):
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)
return ctrl
# 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)
return ctrl
return None
def _export_texture_type_environment_map(self, bo, layer, slot):
"""Exports a Blender EnvironmentMapTexture to a plLayer"""
@ -1164,24 +1197,26 @@ class MaterialConverter:
def get_bump_layer(self, bo):
return self._bump_mats.get(bo, None)
def get_material_ambient(self, bo, bm) -> hsColorRGBA:
def get_material_ambient(self, bo, bm, color : Union[None, mathutils.Color]=None) -> hsColorRGBA:
emit_scale = bm.emit * 0.5
if emit_scale > 0.0:
return hsColorRGBA(bm.diffuse_color.r * emit_scale,
bm.diffuse_color.g * emit_scale,
bm.diffuse_color.b * emit_scale,
if color is None:
color = bm.diffuse_color
return hsColorRGBA(color.r * emit_scale,
color.g * emit_scale,
color.b * emit_scale,
1.0)
else:
return utils.color(bpy.context.scene.world.ambient_color)
def get_material_preshade(self, bo, bm, color=None) -> hsColorRGBA:
def get_material_preshade(self, bo, bm, color : Union[None, mathutils.Color]=None) -> hsColorRGBA:
if bo.plasma_modifiers.lighting.rt_lights:
return hsColorRGBA.kBlack
if color is None:
color = bm.diffuse_color
return utils.color(color)
def get_material_runtime(self, bo, bm, color=None) -> hsColorRGBA:
def get_material_runtime(self, bo, bm, color : Union[None, mathutils.Color]=None) -> hsColorRGBA:
if not bo.plasma_modifiers.lighting.preshade:
return hsColorRGBA.kBlack
if color is None:
@ -1191,19 +1226,18 @@ class MaterialConverter:
def get_texture_animation_key(self, bo, bm, texture):
"""Finds or creates the appropriate key for sending messages to an animated Texture"""
tex_name = texture.name
tex_name = texture.name if texture is not None else "AutoLayer"
if bo.type == "LAMP":
assert bm is None
bm_name = bo.name
else:
assert bm is not None
bm_name = bm.name
if not tex_name in bm.texture_slots:
if texture is not None and not tex_name in bm.texture_slots:
raise ExportError("Texture '{}' not used in Material '{}'".format(bm_name, tex_name))
name = "{}_{}_LayerAnim".format(bm_name, tex_name)
layer = texture.plasma_layer
pClass = plLayerSDLAnimation if layer.anim_sdl_var else plLayerAnimation
pClass = plLayerSDLAnimation if texture is not None and texture.plasma_layer.anim_sdl_var else plLayerAnimation
return self._mgr.find_create_key(pClass, bl=bo, name=name)
@property

Loading…
Cancel
Save