From dbeed6f66042f4c007ede2005c2817a703598dbf Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 25 Jun 2016 20:28:17 -0400 Subject: [PATCH 01/10] Refactor ATCAnim generation Move all the logic into the AnimationConverter in preparation for exporting animated sound volumes, spot cones, light color values, etc. --- korman/exporter/animation.py | 102 +++++++++++++++++++++++++--- korman/exporter/convert.py | 3 + korman/nodes/node_messages.py | 4 +- korman/properties/modifiers/anim.py | 63 ++--------------- 4 files changed, 102 insertions(+), 70 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 7e961dc..33ee788 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -27,19 +27,103 @@ class AnimationConverter: self._exporter = weakref.ref(exporter) self._bl_fps = bpy.context.scene.render.fps - def convert_action2tm(self, action, default_xform): - """Converts a Blender Action to a plCompoundController.""" + def _convert_frame_time(self, frame_num): + return frame_num / self._bl_fps + + def convert_object_animations(self, bo, so): + anim = bo.animation_data + if anim is None: + return + action = anim.action + if action is None: + return fcurves = action.fcurves if not fcurves: + return + + # We're basically just going to throw all the FCurves at the controller converter (read: wall) + # and see what sticks. PlasmaMAX has some nice animation channel stuff that allows for some + # form of separation, but Blender's NLA editor is way confusing and appears to not work with + # things that aren't the typical position, rotation, scale animations. + applicators = [] + applicators.append(self._convert_transform_animation(bo.name, fcurves, bo.matrix_basis)) + + # Check to make sure we have some valid animation applicators before proceeding. + if not any(applicators): + return + + # There is a race condition in the client with animation loading. It expects for modifiers + # to be listed on the SceneObject in a specific order. D'OH! So, always use these funcs. + agmod, agmaster = self.get_anigraph_objects(bo, so) + atcanim = self._mgr.find_create_object(plATCAnim, so=so) + + # Add the animation data to the ATC + for i in applicators: + if i is not None: + atcanim.addApplicator(i) + agmod.channelName = bo.name + agmaster.addPrivateAnim(atcanim.key) + + # This was previously part of the Animation Modifier, however, there can be lots of animations + # Therefore we move it here. + markers = action.pose_markers + atcanim.name = "(Entire Animation)" + atcanim.start = self._convert_frame_time(action.frame_range[0]) + atcanim.end = self._convert_frame_time(action.frame_range[1]) + + # Marker points + for marker in markers: + atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) + + # Fixme? Not sure if we really need to expose this... + atcanim.easeInMin = 1.0 + atcanim.easeInMax = 1.0 + atcanim.easeInLength = 1.0 + atcanim.easeOutMin = 1.0 + atcanim.easeOutMax = 1.0 + atcanim.easeOutLength = 1.0 + + def _convert_transform_animation(self, name, fcurves, xform): + pos = self.make_pos_controller(fcurves, xform) + rot = self.make_rot_controller(fcurves, xform) + scale = self.make_scale_controller(fcurves, xform) + if pos is None and rot is None and scale is None: return None - - # NOTE: plCompoundController is from Myst 5 and was backported to MOUL. - # Worry not however... libHSPlasma will do the conversion for us. tm = plCompoundController() - tm.X = self.make_pos_controller(fcurves, default_xform) - tm.Y = self.make_rot_controller(fcurves, default_xform) - tm.Z = self.make_scale_controller(fcurves, default_xform) - return tm + tm.X = pos + tm.Y = rot + tm.Z = scale + + applicator = plMatrixChannelApplicator() + applicator.enabled = True + applicator.channelName = name + channel = plMatrixControllerChannel() + channel.controller = tm + applicator.channel = channel + + # Decompose the matrix into the 90s-era 3ds max affine parts sillyness + # All that's missing now is something like "(c) 1998 HeadSpin" oh wait... + affine = hsAffineParts() + affine.T = hsVector3(*xform.to_translation()) + affine.K = hsVector3(*xform.to_scale()) + affine.F = -1.0 if xform.determinant() < 0.0 else 1.0 + rot = xform.to_quaternion() + affine.Q = utils.quaternion(rot) + rot.normalize() + affine.U = utils.quaternion(rot) + channel.affine = affine + + return applicator + + def get_anigraph_keys(self, bo=None, so=None): + 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): + 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 make_matrix44_controller(self, pos_fcurves, scale_fcurves, default_pos, default_scale): pos_keyframes, pos_bez = self._process_keyframes(pos_fcurves) diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 7881992..4037088 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -207,6 +207,7 @@ class Exporter: sceneobject = self.mgr.find_create_object(plSceneObject, bl=bl_obj) self._export_actor(sceneobject, bl_obj) export_fn(sceneobject, bl_obj) + self.animation.convert_object_animations(bl_obj, sceneobject) # And now we puke out the modifiers... for mod in bl_obj.plasma_modifiers.modifiers: @@ -256,6 +257,8 @@ class Exporter: return True if bo.parent is not None: return True + if bo.animation_data is not None and bo.animation_data.action is not None: + return True if bo.name in self.actors: return True diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index e500637..6ea5be7 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -178,9 +178,7 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): # (but obviously this is not wrong...) target = exporter.mgr.find_create_key(plMsgForwarder, bl=obj, name=group.key_name) else: - # remember, the AGModifier MUST exist first... so just in case... - exporter.mgr.find_create_key(plAGModifier, bl=obj, name=anim.key_name) - target = exporter.mgr.find_create_key(plAGMasterMod, bl=obj, name=anim.key_name) + _agmod_trash, target = exporter.animation.get_anigraph_keys(obj) else: material = bpy.data.materials.get(self.material_name, None) if material is None: diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 77be132..e14dece 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -61,12 +61,9 @@ class PlasmaAnimationModifier(PlasmaModifierProperties): action = _get_blender_action(bo) markers = action.pose_markers - atcanim = exporter.mgr.find_create_object(plATCAnim, so=so, name=self.key_name) + atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) atcanim.autoStart = self.auto_start atcanim.loop = self.loop - atcanim.name = "(Entire Animation)" - atcanim.start = _convert_frame_time(action.frame_range[0]) - atcanim.end = _convert_frame_time(action.frame_range[1]) # Simple start and loop info initial_marker = markers.get(self.initial_marker) @@ -86,51 +83,6 @@ class PlasmaAnimationModifier(PlasmaModifierProperties): else: atcanim.loopEnd = _convert_frame_time(action.frame_range[1]) - # Marker points - for marker in markers: - atcanim.setMarker(marker.name, _convert_frame_time(marker.frame)) - - # Fixme? Not sure if we really need to expose this... - atcanim.easeInMin = 1.0 - atcanim.easeInMax = 1.0 - atcanim.easeInLength = 1.0 - atcanim.easeOutMin = 1.0 - atcanim.easeOutMax = 1.0 - atcanim.easeOutLength = 1.0 - - # Now for the animation data. We're mostly just going to hand this off to the controller code - matrix = bo.matrix_basis - applicator = plMatrixChannelApplicator() - applicator.enabled = True - applicator.channelName = bo.name - channel = plMatrixControllerChannel() - channel.controller = exporter.animation.convert_action2tm(action, matrix) - applicator.channel = channel - atcanim.addApplicator(applicator) - - # Decompose the matrix into the 90s-era 3ds max affine parts sillyness - # All that's missing now is something like "(c) 1998 HeadSpin" oh wait... - affine = hsAffineParts() - affine.T = hsVector3(*matrix.to_translation()) - affine.K = hsVector3(*matrix.to_scale()) - affine.F = -1.0 if matrix.determinant() < 0.0 else 1.0 - rot = matrix.to_quaternion() - affine.Q = utils.quaternion(rot) - rot.normalize() - affine.U = utils.quaternion(rot) - channel.affine = affine - - # We need both an AGModifier and an AGMasterMod - # NOTE: mandatory order--otherwise the animation will not work in game! - agmod = exporter.mgr.find_create_object(plAGModifier, so=so, name=self.key_name) - agmod.channelName = bo.name - agmaster = exporter.mgr.find_create_object(plAGMasterMod, so=so, name=self.key_name) - agmaster.addPrivateAnim(atcanim.key) - - @property - def key_name(self): - return "{}_(Entire Animation)".format(self.id_data.name) - def _make_physical_movable(self, so): sim = so.sim if sim is not None: @@ -179,14 +131,11 @@ class PlasmaAnimationGroupModifier(PlasmaModifierProperties): action = _get_blender_action(bo) key_name = bo.plasma_modifiers.animation.key_name - # See above... AGModifier must always be inited first... - agmod = exporter.mgr.find_create_object(plAGModifier, so=so, name=key_name) - # The message forwarder is the guy that makes sure that everybody knows WTF is going on msgfwd = exporter.mgr.find_create_object(plMsgForwarder, so=so, name=self.key_name) # Now, this is da swhiz... - agmaster = exporter.mgr.find_create_object(plAGMasterMod, so=so, name=key_name) + agmod, agmaster = exporter.animation.get_anigraph_objects(bo, so) agmaster.msgForwarder = msgfwd.key agmaster.isGrouped, agmaster.isGroupMaster = True, True for i in self.children: @@ -204,9 +153,8 @@ class PlasmaAnimationGroupModifier(PlasmaModifierProperties): msg = "Animation Group '{}' specifies an object '{}' with no Plasma Animation modifier. Ignoring..." exporter.report.warn(msg.format(self.key_name, i.object_name), indent=2) continue - child_agmod = exporter.mgr.find_create_key(plAGModifier, bl=child_bo, name=child_animation.key_name) - child_agmaster = exporter.mgr.find_create_key(plAGMasterMod, bl=child_bo, name=child_animation.key_name) - msgfwd.addForwardKey(child_agmaster) + child_agmod, child_agmaster = exporter.animation.get_anigraph_objects(bo=child_bo) + msgfwd.addForwardKey(child_agmaster.key) msgfwd.addForwardKey(agmaster.key) @property @@ -241,8 +189,7 @@ class PlasmaAnimationLoopModifier(PlasmaModifierProperties): action = _get_blender_action(bo) markers = action.pose_markers - key_name = bo.plasma_modifiers.animation.key_name - atcanim = exporter.mgr.find_create_object(plATCAnim, so=so, name=key_name) + atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) for loop in self.loops: start = markers.get(loop.loop_start) end = markers.get(loop.loop_end) From 74610a3eee49dcc71929d3b2975ffc3aacc5d47b Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 26 Jun 2016 20:50:47 -0400 Subject: [PATCH 02/10] Allow for animation value conversioner callables This is useful in the case that we store data that does not quite match what Plasma uses or expects --- korman/exporter/animation.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 33ee788..d902a4b 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -157,9 +157,9 @@ class AnimationConverter: ctrl = self._make_matrix44_controller(pos_fcurves, scale_fcurves, keyframes, default_pos, default_scale) return ctrl - def make_pos_controller(self, fcurves, default_xform): + 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) + keyframes, bez_chans = self._process_keyframes(pos_curves, convert) if not keyframes: return None @@ -168,10 +168,10 @@ class AnimationConverter: ctrl = self._make_point3_controller(pos_curves, keyframes, bez_chans, default_xform.to_translation()) return ctrl - def make_rot_controller(self, fcurves, default_xform): + 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) + keyframes, bez_chans = self._process_keyframes(rot_curves, convert=None) if not keyframes: return None @@ -183,9 +183,9 @@ class AnimationConverter: ctrl = self._make_quat_controller(rot_curves, keyframes, default_xform.to_euler()) return ctrl - def make_scale_controller(self, fcurves, default_xform): + 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) + keyframes, bez_chans = self._process_keyframes(scale_curves, convert) if not keyframes: return None @@ -193,8 +193,8 @@ class AnimationConverter: ctrl = self._make_scale_value_controller(scale_curves, keyframes, bez_chans, default_xform) return ctrl - def make_scalar_leaf_controller(self, fcurve): - keyframes, bezier = self._process_fcurve(fcurve) + def make_scalar_leaf_controller(self, fcurve, convert=None): + keyframes, bezier = self._process_fcurve(fcurve, convert) if not keyframes: return None @@ -399,7 +399,7 @@ class AnimationConverter: ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _process_fcurve(self, fcurve): + def _process_fcurve(self, fcurve, convert=None): """Like _process_keyframes, but for one fcurve""" keyframe_data = type("KeyFrameData", (), {}) fps = self._bl_fps @@ -423,12 +423,12 @@ class AnimationConverter: else: keyframe.in_tan = 0.0 keyframe.out_tan = 0.0 - keyframe.value = value + 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_keyframes(self, fcurves): + def _process_keyframes(self, fcurves, convert=None): """Groups all FCurves for the same frame together""" keyframe_data = type("KeyFrameData", (), {}) fps = self._bl_fps @@ -455,7 +455,7 @@ class AnimationConverter: keyframe.values = {} keyframes[frame_num] = keyframe idx = fcurve.array_index - keyframe.values[idx] = value + keyframe.values[idx] = value if convert is None else convert(value) # Calculate the bezier interpolation nonsense if fkey.interpolation == "BEZIER": From e2d18c898abb89cd139ce1f5ed6f23738fa30349 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 26 Jun 2016 20:51:37 -0400 Subject: [PATCH 03/10] Implement sound volume animations --- korman/exporter/animation.py | 27 ++++++++++++++++++++ korman/properties/modifiers/sound.py | 37 +++++++++++++++++++--------- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index d902a4b..f87f214 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -47,6 +47,8 @@ class AnimationConverter: # things that aren't the typical position, rotation, scale animations. applicators = [] applicators.append(self._convert_transform_animation(bo.name, fcurves, bo.matrix_basis)) + if bo.plasma_modifiers.soundemit.enabled: + applicators.extend(self._convert_sound_volume_animation(bo.name, fcurves, bo.plasma_modifiers.soundemit)) # Check to make sure we have some valid animation applicators before proceeding. if not any(applicators): @@ -83,6 +85,31 @@ class AnimationConverter: atcanim.easeOutMax = 1.0 atcanim.easeOutLength = 1.0 + def _convert_sound_volume_animation(self, name, fcurves, soundemit): + def convert_volume(value): + if value == 0.0: + return 0.0 + else: + return math.log10(value) * 20.0 + + for sound in soundemit.sounds: + path = "{}.volume".format(sound.path_from_id()) + fcurve = next((i for i in fcurves if i.data_path == path and i.keyframe_points), None) + if fcurve is None: + continue + + for i in soundemit.get_sound_indices(sound=sound): + applicator = plSoundVolumeApplicator() + applicator.channelName = name + applicator.index = i + + # libHSPlasma assumes a channel is not shared among applicators... + # so yes, we must convert the same animation data again and again. + channel = plScalarControllerChannel() + channel.controller = self.make_scalar_leaf_controller(fcurve, convert=convert_volume) + applicator.channel = channel + yield applicator + def _convert_transform_animation(self, name, fcurves, xform): pos = self.make_pos_controller(fcurves, xform) rot = self.make_rot_controller(fcurves, xform) diff --git a/korman/properties/modifiers/sound.py b/korman/properties/modifiers/sound.py index a641141..4ed90cf 100644 --- a/korman/properties/modifiers/sound.py +++ b/korman/properties/modifiers/sound.py @@ -131,7 +131,7 @@ class PlasmaSound(bpy.types.PropertyGroup): volume = IntProperty(name="Volume", description="Volume to play the sound", min=0, max=100, default=100, - options=set(), + options={"ANIMATABLE"}, subtype="PERCENTAGE") fade_in = PointerProperty(type=PlasmaSfxFade, options=set()) @@ -344,20 +344,35 @@ class PlasmaSoundEmitter(PlasmaModifierProperties): if i.sound_data and i.enabled: i.convert_sound(exporter, so, winaud) - def get_sound_indices(self, name): + def get_sound_indices(self, name=None, sound=None): """Returns the index of the given sound in the plWin32Sound. This is needed because stereo 3D sounds export as two mono sound objects -- wheeeeee""" + assert name or sound idx = 0 - for i in self.sounds: - if i.name == name: - yield idx - if i.is_3d_stereo: - yield idx + 1 - break + + if name is None: + for i in self.sounds: + if i == sound: + yield idx + if i.is_3d_stereo: + yield idx + 1 + break + else: + idx += 2 if i.is_3d_stereo else 1 else: - idx += 2 if i.is_3d_stereo else 1 - else: - raise ValueError(name) + raise LookupError(sound) + + if sound is None: + for i in self.sounds: + if i.name == name: + yield idx + if i.is_3d_stereo: + yield idx + 1 + break + else: + idx += 2 if i.is_3d_stereo else 1 + else: + raise ValueError(name) @classmethod def register(cls): From 6755453d3cea564025aba8c7d793b6a8d340d54e Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 27 Jun 2016 15:20:00 -0400 Subject: [PATCH 04/10] Add fcurve/keyframe smashing, mashing, etc. Don't even ask... --- korman/exporter/animation.py | 111 ++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index f87f214..925dc4a 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -455,6 +455,115 @@ class AnimationConverter: 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 + 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 } + + # 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 + else: + grouped_fcurves[key] = [value,] + + # 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: + fcurve = grouped_fcurves[chan][i] + 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 + + def _process_keyframes(self, fcurves, convert=None): """Groups all FCurves for the same frame together""" keyframe_data = type("KeyFrameData", (), {}) @@ -482,7 +591,7 @@ class AnimationConverter: keyframe.values = {} keyframes[frame_num] = keyframe idx = fcurve.array_index - keyframe.values[idx] = value if convert is None else convert(value) + keyframe.values[idx] = value if convert is None else convert(value) # Calculate the bezier interpolation nonsense if fkey.interpolation == "BEZIER": From 3f98e6ea9da8e6eb0646beb158b8aca8da689313 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 27 Jun 2016 15:22:49 -0400 Subject: [PATCH 05/10] Add lamp color and energy animations This makes things incredibly more complex because Blender stores those animations on the ObData ID instead of the Object ID data block. Dang. So, the animation modifier's detection code had to be pretty much scrapped. The newer code is a little hacky in places. Hopefully we can address this soon-ish. --- korman/exporter/animation.py | 107 +++++++++++++++++++++++----- korman/exporter/rtlight.py | 1 + korman/nodes/node_messages.py | 3 +- korman/properties/modifiers/anim.py | 72 +++++++++++-------- korman/ui/modifiers/anim.py | 36 ++++++---- 5 files changed, 158 insertions(+), 61 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 925dc4a..5b69f16 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -31,24 +31,32 @@ class AnimationConverter: return frame_num / self._bl_fps def convert_object_animations(self, bo, so): - anim = bo.animation_data - if anim is None: - return - action = anim.action - if action is None: - return - fcurves = action.fcurves - if not fcurves: + if not self.is_animated(bo): return + def fetch_animation_data(id_data): + if id_data is not None: + if id_data.animation_data is not None: + action = id_data.animation_data.action + return action, getattr(action, "fcurves", None) + return None, None + + # TODO: At some point, we should consider supporting NLA stuff. + # But for now, this seems sufficient. + obj_action, obj_fcurves = fetch_animation_data(bo) + data_action, data_fcurves = fetch_animation_data(bo.data) + # We're basically just going to throw all the FCurves at the controller converter (read: wall) # and see what sticks. PlasmaMAX has some nice animation channel stuff that allows for some # form of separation, but Blender's NLA editor is way confusing and appears to not work with # things that aren't the typical position, rotation, scale animations. applicators = [] - applicators.append(self._convert_transform_animation(bo.name, fcurves, bo.matrix_basis)) + applicators.append(self._convert_transform_animation(bo.name, obj_fcurves, bo.matrix_basis)) if bo.plasma_modifiers.soundemit.enabled: - applicators.extend(self._convert_sound_volume_animation(bo.name, 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): + lamp = bo.data + applicators.extend(self._convert_lamp_color_animation(bo.name, data_fcurves, lamp)) # Check to make sure we have some valid animation applicators before proceeding. if not any(applicators): @@ -68,14 +76,19 @@ class AnimationConverter: # This was previously part of the Animation Modifier, however, there can be lots of animations # Therefore we move it here. - markers = action.pose_markers + def get_ranges(*args, **kwargs): + index = kwargs.get("index", 0) + for i in args: + if i is not None: + yield i.frame_range[index] atcanim.name = "(Entire Animation)" - atcanim.start = self._convert_frame_time(action.frame_range[0]) - atcanim.end = self._convert_frame_time(action.frame_range[1]) + atcanim.start = self._convert_frame_time(min(get_ranges(obj_action, data_action, index=0))) + atcanim.end = self._convert_frame_time(max(get_ranges(obj_action, data_action, index=1))) # Marker points - for marker in markers: - atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) + if obj_action is not None: + for marker in obj_action.pose_markers: + atcanim.setMarker(marker.name, self._convert_frame_time(marker.frame)) # Fixme? Not sure if we really need to expose this... atcanim.easeInMin = 1.0 @@ -85,7 +98,55 @@ class AnimationConverter: atcanim.easeOutMax = 1.0 atcanim.easeOutLength = 1.0 + def _convert_lamp_color_animation(self, name, fcurves, lamp): + if not fcurves: + 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) + if energy_curve is None and color_curves is None: + return None + elif lamp.use_only_shadow: + self._exporter().report.warn("Cannot animate Lamp color because this lamp only casts shadows", indent=3) + return None + elif not lamp.use_specular and not lamp.use_diffuse: + self._exporter().report.warn("Cannot animate Lamp color because neither Diffuse nor Specular are enabled", indent=3) + 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) + if color_keyframes and lamp.use_specular: + channel = plPointControllerChannel() + channel.controller = self._make_point3_controller(color_curves, color_keyframes, color_bez, lamp.color) + applicator = plLightSpecularApplicator() + applicator.channelName = name + applicator.channel = channel + yield applicator + + # 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() } + else: + return { key: value * energy[0] for key, value in color.items() } + diffuse_defaults = { "color": lamp.color, "energy": lamp.energy } + diffuse_fcurves = color_curves + [energy_curve,] + diffuse_keyframes = self._process_fcurves(diffuse_fcurves, convert_diffuse_animation, diffuse_defaults) + if not diffuse_keyframes: + return None + + # Whew. + channel = plPointControllerChannel() + channel.controller = self._make_point3_controller([], diffuse_keyframes, False, []) + applicator = plLightDiffuseApplicator() + applicator.channelName = name + applicator.channel = channel + yield applicator + def _convert_sound_volume_animation(self, name, fcurves, soundemit): + if not fcurves: + return None + def convert_volume(value): if value == 0.0: return 0.0 @@ -111,6 +172,9 @@ class AnimationConverter: yield applicator def _convert_transform_animation(self, name, fcurves, xform): + if not fcurves: + return None + pos = self.make_pos_controller(fcurves, xform) rot = self.make_rot_controller(fcurves, xform) scale = self.make_scale_controller(fcurves, xform) @@ -152,6 +216,17 @@ class AnimationConverter: master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo) return mod, master + def is_animated(self, bo): + if bo.animation_data is not None: + if bo.animation_data.action is not None: + return True + data = getattr(bo, "data", None) + if data is not None: + if data.animation_data is not None: + if data.animation_data.action is not None: + return True + return False + def make_matrix44_controller(self, pos_fcurves, scale_fcurves, default_pos, default_scale): pos_keyframes, pos_bez = self._process_keyframes(pos_fcurves) scale_keyframes, scale_bez = self._process_keyframes(scale_fcurves) @@ -160,7 +235,7 @@ class AnimationConverter: # Matrix keyframes cannot do bezier schtuff if pos_bez or scale_bez: - self._exporter().report.warn("This animation cannot use bezier keyframes--forcing linear", indent=3) + self._exporter().report.warn("Matrix44 controllers cannot use bezier keyframes--forcing linear", indent=3) # Let's pair up the pos and scale schtuff based on frame numbers. I realize that we're creating # a lot of temporary objects, but until I see profiling results that this is terrible, I prefer diff --git a/korman/exporter/rtlight.py b/korman/exporter/rtlight.py index 01787e2..b37eef5 100644 --- a/korman/exporter/rtlight.py +++ b/korman/exporter/rtlight.py @@ -104,6 +104,7 @@ class LightConverter: self._converter_funcs[bl_light.type](bl_light, pl_light) # Light color nonsense + # Please note that these calculations are duplicated in the AnimationConverter energy = bl_light.energy if bl_light.use_negative: diff_color = [(0.0 - i) * energy for i in bl_light.color] diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index 6ea5be7..05e28d8 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -168,8 +168,7 @@ class PlasmaAnimCmdMsgNode(PlasmaMessageNode, bpy.types.Node): obj = bpy.data.objects.get(self.object_name, None) if obj is None: self.raise_error("invalid object: '{}'".format(self.object_name)) - anim = obj.plasma_modifiers.animation - if not anim.enabled: + if not exporter.animation.is_animated(obj): self.raise_error("invalid animation") group = obj.plasma_modifiers.animation_group if group.enabled: diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index e14dece..dd45bfe 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -24,14 +24,21 @@ def _convert_frame_time(frame_num): fps = bpy.context.scene.render.fps return frame_num / fps -def _get_blender_action(bo): - if bo.animation_data is None or bo.animation_data.action is None: - raise ExportError("Object '{}' has no Action to export".format(bo.name)) - if not bo.animation_data.action.fcurves: - raise ExportError("Object '{}' is animated but has no FCurves".format(bo.name)) - return bo.animation_data.action - -class PlasmaAnimationModifier(PlasmaModifierProperties): +class ActionModifier: + @property + def blender_action(self): + bo = self.id_data + if bo.animation_data is not None and bo.animation_data.action is not None: + return bo.animation_data.action + if bo.data is not None: + if bo.data.animation_data is not None and bo.data.animation_data.action is not None: + # we will not use this action for any animation logic. that must be stored on the Object + # datablock for simplicity's sake. + return None + raise ExportError("Object '{}' is not animated".format(bo.name)) + + +class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): pl_id = "animation" bl_category = "Animation" @@ -58,30 +65,35 @@ class PlasmaAnimationModifier(PlasmaModifierProperties): return True def export(self, exporter, bo, so): - action = _get_blender_action(bo) - markers = action.pose_markers + action = self.blender_action atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) atcanim.autoStart = self.auto_start atcanim.loop = self.loop # Simple start and loop info - initial_marker = markers.get(self.initial_marker) - if initial_marker is not None: - atcanim.initial = _convert_frame_time(initial_marker.frame) - else: - atcanim.initial = -1.0 - if self.loop: - loop_start = markers.get(self.loop_start) - if loop_start is not None: - atcanim.loopStart = _convert_frame_time(loop_start.frame) + if action is not None: + markers = action.pose_markers + initial_marker = markers.get(self.initial_marker) + if initial_marker is not None: + atcanim.initial = _convert_frame_time(initial_marker.frame) else: - atcanim.loopStart = _convert_frame_time(action.frame_range[0]) - loop_end = markers.get(self.loop_end) - if loop_end is not None: - atcanim.loopEnd = _convert_frame_time(loop_end.frame) - else: - atcanim.loopEnd = _convert_frame_time(action.frame_range[1]) + atcanim.initial = -1.0 + if self.loop: + loop_start = markers.get(self.loop_start) + if loop_start is not None: + atcanim.loopStart = _convert_frame_time(loop_start.frame) + else: + atcanim.loopStart = atcanim.start + loop_end = markers.get(self.loop_end) + if loop_end is not None: + atcanim.loopEnd = _convert_frame_time(loop_end.frame) + else: + atcanim.loopEnd = atcanim.end + else: + if self.loop: + atcanim.loopStart = atcanim.start + atcanim.loopEnd = atcanim.end def _make_physical_movable(self, so): sim = so.sim @@ -128,8 +140,8 @@ class PlasmaAnimationGroupModifier(PlasmaModifierProperties): active_child_index = IntProperty(options={"HIDDEN"}) def export(self, exporter, bo, so): - action = _get_blender_action(bo) - key_name = bo.plasma_modifiers.animation.key_name + if not exporter.animation.is_animated(bo): + raise ExportError("'{}': Object is not animated".format(bo.name)) # The message forwarder is the guy that makes sure that everybody knows WTF is going on msgfwd = exporter.mgr.find_create_object(plMsgForwarder, so=so, name=self.key_name) @@ -171,7 +183,7 @@ class LoopMarker(bpy.types.PropertyGroup): description="Marker name from whence the loop ends") -class PlasmaAnimationLoopModifier(PlasmaModifierProperties): +class PlasmaAnimationLoopModifier(ActionModifier, PlasmaModifierProperties): pl_id = "animation_loop" pl_depends = {"animation"} @@ -186,7 +198,9 @@ class PlasmaAnimationLoopModifier(PlasmaModifierProperties): active_loop_index = IntProperty(options={"HIDDEN"}) def export(self, exporter, bo, so): - action = _get_blender_action(bo) + action = self.blender_action + if action is None: + raise ExportError("'{}': No object animation data".format(bo.name)) markers = action.pose_markers atcanim = exporter.mgr.find_create_object(plATCAnim, so=so) diff --git a/korman/ui/modifiers/anim.py b/korman/ui/modifiers/anim.py index e9b2fd5..84c9edc 100644 --- a/korman/ui/modifiers/anim.py +++ b/korman/ui/modifiers/anim.py @@ -15,16 +15,19 @@ import bpy -def _check_for_anim(layout, context): - if context.object.animation_data is None or context.object.animation_data.action is None: +def _check_for_anim(layout, modifier): + try: + action = modifier.blender_action + except: layout.label("Object has no animation data", icon="ERROR") - return False - return True + return None + else: + return action if action is not None else False def animation(modifier, layout, context): - if not _check_for_anim(layout, context): + action = _check_for_anim(layout, modifier) + if action is None: return - action = context.object.animation_data.action split = layout.split() col = split.column() @@ -32,11 +35,12 @@ def animation(modifier, layout, context): col = split.column() col.prop(modifier, "loop") - layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER") - col = layout.column() - col.enabled = modifier.loop - col.prop_search(modifier, "loop_start", action, "pose_markers", icon="PMARKER") - col.prop_search(modifier, "loop_end", action, "pose_markers", icon="PMARKER") + if action: + layout.prop_search(modifier, "initial_marker", action, "pose_markers", icon="PMARKER") + col = layout.column() + col.enabled = modifier.loop + col.prop_search(modifier, "loop_start", action, "pose_markers", icon="PMARKER") + col.prop_search(modifier, "loop_end", action, "pose_markers", icon="PMARKER") class GroupListUI(bpy.types.UIList): @@ -45,7 +49,8 @@ class GroupListUI(bpy.types.UIList): def animation_group(modifier, layout, context): - if not _check_for_anim(layout, context): + action = _check_for_anim(layout, modifier) + if not action: return row = layout.row() @@ -67,7 +72,11 @@ class LoopListUI(bpy.types.UIList): def animation_loop(modifier, layout, context): - if not _check_for_anim(layout, context): + action = _check_for_anim(layout, modifier) + if action is False: + layout.label("Object must be animated, not ObData", icon="ERROR") + return + elif action is None: return row = layout.row() @@ -86,7 +95,6 @@ def animation_loop(modifier, layout, context): # Modify the loop points if modifier.loops: - action = context.object.animation_data.action loop = modifier.loops[modifier.active_loop_index] layout.prop_search(loop, "loop_start", action, "pose_markers", icon="PMARKER") layout.prop_search(loop, "loop_end", action, "pose_markers", icon="PMARKER") From e8c71eb12113f0c26282f1e337db2c60ece433f6 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 27 Jun 2016 17:20:54 -0400 Subject: [PATCH 06/10] Port matrix44 controller generation to new fcurve merging method --- korman/exporter/animation.py | 79 +++++++++++------------------------- korman/exporter/material.py | 7 ++-- 2 files changed, 27 insertions(+), 59 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 5b69f16..ba05826 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -227,37 +227,28 @@ class AnimationConverter: return True return False - def make_matrix44_controller(self, pos_fcurves, scale_fcurves, default_pos, default_scale): - pos_keyframes, pos_bez = self._process_keyframes(pos_fcurves) - scale_keyframes, scale_bez = self._process_keyframes(scale_fcurves) - if not pos_keyframes and not scale_keyframes: + 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) + + matrix = hsMatrix44() + # Note: scale and pos are dicts, so we can't unpack + matrix.setTranslate(hsVector3(pos[0], pos[1], pos[2])) + matrix.setScale(hsVector3(scale[0], scale[1], scale[2])) + 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 - # Matrix keyframes cannot do bezier schtuff - if pos_bez or scale_bez: - self._exporter().report.warn("Matrix44 controllers cannot use bezier keyframes--forcing linear", indent=3) - - # Let's pair up the pos and scale schtuff based on frame numbers. I realize that we're creating - # a lot of temporary objects, but until I see profiling results that this is terrible, I prefer - # to have code that makes sense. - keyframes = [] - for pos, scale in itertools.zip_longest(pos_keyframes, scale_keyframes, fillvalue=None): - if pos is None: - keyframes.append((None, scale)) - elif scale is None: - keyframes.append((pos, scale)) - elif pos.frame_num == scale.frame_num: - keyframes.append((pos, scale)) - elif pos.frame_num < scale.frame_num: - keyframes.append((pos, None)) - keyframes.append((None, scale)) - elif pos.frame_num > scale.frame_num: - keyframes.append((None, scale)) - keyframes.append((pos, None)) + default_values = { pos_path: pos_default, scale_path: scale_default } + keyframes = self._process_fcurves(fcurves, convert_matrix_keyframe, default_values) + if not keyframes: + return None # Now we make the controller - ctrl = self._make_matrix44_controller(pos_fcurves, scale_fcurves, keyframes, default_pos, default_scale) - return ctrl + 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] @@ -303,41 +294,17 @@ class AnimationConverter: ctrl = self._make_scalar_leaf_controller(keyframes, bezier) return ctrl - def _make_matrix44_controller(self, pos_fcurves, scale_fcurves, keyframes, default_pos, default_scale): + def _make_matrix44_controller(self, keyframes): ctrl = plLeafController() keyframe_type = hsKeyFrame.kMatrix44KeyFrame exported_frames = [] - pcurves = { i.array_index: i for i in pos_fcurves } - scurves = { i.array_index: i for i in scale_fcurves } - def eval_fcurve(fcurves, keyframe, i, default_xform): - try: - return fcurves[i].evaluate(keyframe.frame_num_blender) - except KeyError: - return default_xform[i] - - for pos_key, scale_key in keyframes: - valid_key = pos_key if pos_key is not None else scale_key + for keyframe in keyframes: exported = hsMatrix44Key() - exported.frame = valid_key.frame_num - exported.frameTime = valid_key.frame_time + exported.frame = keyframe.frame_num + exported.frameTime = keyframe.frame_time exported.type = keyframe_type - - if pos_key is not None: - pos_value = [pos_key.values[i] if i in pos_key.values else eval_fcurve(pcurves, pos_key, i, default_pos) for i in range(3)] - else: - pos_value = [eval_fcurve(pcurves, valid_key, i, default_pos) for i in range(3)] - if scale_key is not None: - scale_value = [scale_key.values[i] if i in scale_key.values else eval_fcurve(scurves, scale_key, i, default_scale) for i in range(3)] - else: - scale_value = [eval_fcurve(scurves, valid_key, i, default_scale) for i in range(3)] - pos_value = hsVector3(*pos_value) - scale_value = hsVector3(*scale_value) - - value = hsMatrix44() - value.setTranslate(pos_value) - value.setScale(scale_value) - exported.value = value + exported.value = keyframe.value exported_frames.append(exported) ctrl.keys = (exported_frames, keyframe_type) return ctrl diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 3352112..33e8963 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -295,11 +295,12 @@ class MaterialConverter: return None def _export_layer_transform_animation(self, tex_slot, base_layer, fcurves): - pos_fcurves = [i for i in fcurves if i.data_path.find("offset") != -1] - scale_fcurves = [i for i in fcurves if i.data_path.find("scale") != -1] + 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(pos_fcurves, scale_fcurves, tex_slot.offset, tex_slot.scale) + ctrl = self._exporter().animation.make_matrix44_controller(fcurves, pos_path, scale_path, tex_slot.offset, tex_slot.scale) return ctrl def _export_texture_type_environment_map(self, bo, layer, slot): From 33810c37d1d67b8fd1e9ba97cc2fecaf1a7ca9e9 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 27 Jun 2016 18:43:49 -0400 Subject: [PATCH 07/10] Implement spot cone animations --- korman/exporter/animation.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index ba05826..69d082d 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -57,6 +57,8 @@ class AnimationConverter: if isinstance(bo.data, bpy.types.Lamp): lamp = bo.data applicators.extend(self._convert_lamp_color_animation(bo.name, data_fcurves, lamp)) + if isinstance(lamp, bpy.types.SpotLamp): + applicators.extend(self._convert_spot_lamp_animation(bo.name, data_fcurves, lamp)) # Check to make sure we have some valid animation applicators before proceeding. if not any(applicators): @@ -171,6 +173,38 @@ class AnimationConverter: applicator.channel = channel yield applicator + def _convert_spot_lamp_animation(self, name, fcurves, lamp): + blend_fcurve = next((i for i in fcurves if i.data_path == "spot_blend"), None) + size_fcurve = next((i for i in fcurves if i.data_path == "spot_size"), None) + if blend_fcurve is None and size_fcurve is None: + return None + + # Spot Outer is just the size keyframes... + if size_fcurve is not None: + channel = plScalarControllerChannel() + channel.controller = self.make_scalar_leaf_controller(size_fcurve, lambda x: math.degrees(x)) + applicator = plSpotOuterApplicator() + applicator.channelName = name + applicator.channel = channel + yield applicator + + # Spot inner must be calculated... + def convert_spot_inner(spot_blend, spot_size): + blend = min(0.001, spot_blend[0]) + 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) + + if keyframes: + channel = plScalarControllerChannel() + channel.controller = self._make_scalar_leaf_controller(keyframes, False) + applicator = plSpotInnerApplicator() + applicator.channelName = name + applicator.channel = channel + yield applicator + def _convert_transform_animation(self, name, fcurves, xform): if not fcurves: return None From 0d73893ac5c826cfd649f9c9c99d198eabda6743 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 27 Jun 2016 18:53:27 -0400 Subject: [PATCH 08/10] Limit CoordInterface creation for animated objects to xform animations only. --- korman/exporter/animation.py | 8 ++++++++ korman/exporter/convert.py | 4 ++-- korman/properties/modifiers/anim.py | 28 -------------------------- korman/properties/modifiers/physics.py | 26 ++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 69d082d..c32532a 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -214,6 +214,7 @@ class AnimationConverter: scale = self.make_scale_controller(fcurves, xform) if pos is None and rot is None and scale is None: return None + tm = plCompoundController() tm.X = pos tm.Y = rot @@ -250,6 +251,13 @@ class AnimationConverter: master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo) return mod, master + def has_transform_animation(self, bo): + 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 False + def is_animated(self, bo): if bo.animation_data is not None: if bo.animation_data.action is not None: diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 4037088..24763d2 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -257,10 +257,10 @@ class Exporter: return True if bo.parent is not None: return True - if bo.animation_data is not None and bo.animation_data.action is not None: - return True if bo.name in self.actors: return True + if self.animation.has_transform_animation(bo): + return True for mod in bo.plasma_modifiers.modifiers: if mod.enabled: diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index dd45bfe..413c570 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -60,10 +60,6 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): loop_end = StringProperty(name="Loop End", description="Marker indicating where the default loop ends") - @property - def requires_actor(self): - return True - def export(self, exporter, bo, so): action = self.blender_action @@ -95,30 +91,6 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): atcanim.loopStart = atcanim.start atcanim.loopEnd = atcanim.end - def _make_physical_movable(self, so): - sim = so.sim - if sim is not None: - sim = sim.object - sim.setProperty(plSimulationInterface.kPhysAnim, True) - phys = sim.physical.object - phys.setProperty(plSimulationInterface.kPhysAnim, True) - - # If the mass is zero, then we will fail to animate. Fix that. - if phys.mass == 0.0: - phys.mass = 1.0 - - # set kPinned so it doesn't fall through - sim.setProperty(plSimulationInterface.kPinned, True) - phys.setProperty(plSimulationInterface.kPinned, True) - - # Do the same for children objects - for child in so.coord.object.children: - self.make_physical_movable(child.object) - - def post_export(self, exporter, bo, so): - # If this object has a physical, we need to tell the simulation iface that it can be animated - self._make_physical_movable(so) - class AnimGroupObject(bpy.types.PropertyGroup): object_name = StringProperty(name="Child", diff --git a/korman/properties/modifiers/physics.py b/korman/properties/modifiers/physics.py index 53a5198..c52c95e 100644 --- a/korman/properties/modifiers/physics.py +++ b/korman/properties/modifiers/physics.py @@ -84,6 +84,32 @@ class PlasmaCollider(PlasmaModifierProperties): if self.terrain: physical.LOSDBs |= plSimDefs.kLOSDBAvatarWalkable + def _make_physical_movable(self, so): + sim = so.sim + if sim is not None: + sim = sim.object + phys = sim.physical.object + _set_phys_prop(plSimulationInterface.kPhysAnim, sim, phys) + + # If the mass is zero, then we will fail to animate. Fix that. + if phys.mass == 0.0: + phys.mass = 1.0 + + # set kPinned so it doesn't fall through + _set_phys_prop(plSimulationInterface.kPinned, sim, phys) + + # Do the same for child objects + for child in so.coord.object.children: + self._make_physical_movable(child.object) + + def post_export(self, exporter, bo, so): + test_bo = bo + while test_bo is not None: + if exporter.animation.has_transform_animation(test_bo): + self._make_physical_movable(so) + break + test_bo = test_bo.parent + @property def key_name(self): return "{}_Collision".format(self.id_data.name) From 66264331a6345dc7a507da6b67ed65dea27a40bc Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 27 Jun 2016 19:19:52 -0400 Subject: [PATCH 09/10] Update Animation Group modifier Fix an issue with ObData animations and update the UI to match other list-based modifiers. --- korman/properties/modifiers/anim.py | 6 +++--- korman/ui/modifiers/anim.py | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/korman/properties/modifiers/anim.py b/korman/properties/modifiers/anim.py index 413c570..7744c9e 100644 --- a/korman/properties/modifiers/anim.py +++ b/korman/properties/modifiers/anim.py @@ -93,16 +93,16 @@ class PlasmaAnimationModifier(ActionModifier, PlasmaModifierProperties): class AnimGroupObject(bpy.types.PropertyGroup): - object_name = StringProperty(name="Child", + object_name = StringProperty(name="Child Animation", description="Object whose action is a child animation") -class PlasmaAnimationGroupModifier(PlasmaModifierProperties): +class PlasmaAnimationGroupModifier(ActionModifier, PlasmaModifierProperties): pl_id = "animation_group" pl_depends = {"animation"} bl_category = "Animation" - bl_label = "Group" + bl_label = "Group Master" bl_description = "Defines related animations" bl_icon = "GROUP" diff --git a/korman/ui/modifiers/anim.py b/korman/ui/modifiers/anim.py index 84c9edc..402b7c2 100644 --- a/korman/ui/modifiers/anim.py +++ b/korman/ui/modifiers/anim.py @@ -45,12 +45,14 @@ def animation(modifier, layout, context): class GroupListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): - layout.prop_search(item, "object_name", bpy.data, "objects", icon="ACTION") + label = item.object_name if item.object_name else "[No Child Specified]" + icon = "ACTION" if item.object_name else "ERROR" + layout.label(text=label, icon=icon) def animation_group(modifier, layout, context): action = _check_for_anim(layout, modifier) - if not action: + if action is None: return row = layout.row() @@ -65,6 +67,9 @@ def animation_group(modifier, layout, context): op.collection = "children" op.index = modifier.active_child_index + if modifier.children: + layout.prop_search(modifier.children[modifier.active_child_index], "object_name", bpy.data, "objects", icon="ACTION") + class LoopListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): From 5f8e57ef6b230058a61d8259ecd03c0731cbe973 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 28 Jun 2016 17:57:56 -0400 Subject: [PATCH 10/10] Implement Omni Light animations --- korman/exporter/animation.py | 57 ++++++++++++++++++++++++++++++++++++ korman/exporter/rtlight.py | 28 +++++++++++++----- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index c32532a..0d932bd 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -59,6 +59,8 @@ class AnimationConverter: applicators.extend(self._convert_lamp_color_animation(bo.name, data_fcurves, lamp)) if isinstance(lamp, bpy.types.SpotLamp): applicators.extend(self._convert_spot_lamp_animation(bo.name, data_fcurves, lamp)) + if isinstance(lamp, bpy.types.PointLamp): + applicators.extend(self._convert_omni_lamp_animation(bo.name, data_fcurves, lamp)) # Check to make sure we have some valid animation applicators before proceeding. if not any(applicators): @@ -145,6 +147,61 @@ class AnimationConverter: applicator.channel = channel yield applicator + def _convert_omni_lamp_animation(self, name, fcurves, lamp): + energy_fcurve = next((i for i in fcurves if i.data_path == "energy"), None) + 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) + + # All types allow animating cutoff + if distance_fcurve is not None: + channel = plScalarControllerChannel() + channel.controller = self.make_scalar_leaf_controller(distance_fcurve, + lambda x: x * 2 if lamp.use_sphere else x) + applicator = plOmniCutoffApplicator() + applicator.channelName = name + applicator.channel = channel + yield applicator + + 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) + elif falloff == "INVERSE_LINEAR": + def convert_linear_atten(distance, energy): + intens = abs(energy[0]) + atten_end = distance[0] * 2 if lamp.use_sphere else distance[0] + 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}) + if keyframes: + channel = plScalarControllerChannel() + channel.controller = self._make_scalar_leaf_controller(keyframes, False) + applicator = plOmniApplicator() + applicator.channelName = name + applicator.channel = channel + yield applicator + elif falloff == "INVERSE_SQUARE": + def convert_quadratic_atten(distance, energy): + intens = abs(energy[0]) + atten_end = distance[0] * 2 if lamp.use_sphere else distance[0] + 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}) + if keyframes: + channel = plScalarControllerChannel() + channel.controller = self._make_scalar_leaf_controller(keyframes, False) + applicator = plOmniSqApplicator() + applicator.channelName = name + applicator.channel = channel + yield applicator + else: + self._exporter().report.warn("Lamp Falloff '{}' animations are not supported".format(falloff), ident=3) + def _convert_sound_volume_animation(self, name, fcurves, soundemit): if not fcurves: return None diff --git a/korman/exporter/rtlight.py b/korman/exporter/rtlight.py index b37eef5..a253d27 100644 --- a/korman/exporter/rtlight.py +++ b/korman/exporter/rtlight.py @@ -40,11 +40,8 @@ class LightConverter: } def _convert_attenuation(self, bl, pl): - intens = bl.energy - if intens < 0: - intens = -intens - attenEnd = bl.distance * 2 if bl.use_sphere else bl.distance - + # If you change these calculations, be sure to update the AnimationConverter! + intens, attenEnd = self.convert_attenuation(bl) if bl.falloff_type == "CONSTANT": print(" Attenuation: No Falloff") pl.attenConst = intens @@ -54,18 +51,29 @@ class LightConverter: elif bl.falloff_type == "INVERSE_LINEAR": print(" Attenuation: Inverse Linear") pl.attenConst = 1.0 - pl.attenLinear = max(0.0, (intens * _FAR_POWER - 1.0) / attenEnd) + pl.attenLinear = self.convert_attenuation_linear(intens, attenEnd) pl.attenQuadratic = 0.0 pl.attenCutoff = attenEnd elif bl.falloff_type == "INVERSE_SQUARE": print(" Attenuation: Inverse Square") pl.attenConst = 1.0 pl.attenLinear = 0.0 - pl.attenQuadratic = max(0.0, (intens * _FAR_POWER - 1.0) / (attenEnd * attenEnd)) + pl.attenQuadratic = self.convert_attenuation_quadratic(intens, attenEnd) pl.attenCutoff = attenEnd else: raise BlenderOptionNotSupportedError(bl.falloff_type) + def convert_attenuation(self, lamp): + intens = abs(lamp.energy) + attenEnd = lamp.distance * 2 if lamp.use_sphere else lamp.distance + return (intens, attenEnd) + + def convert_attenuation_linear(self, intensity, end): + return max(0.0, (intensity * _FAR_POWER - 1.0) / end) + + def convert_attenuation_quadratic(self, intensity, end): + return max(0.0, (intensity * _FAR_POWER - 1.0) / pow(end, 2)) + def _convert_area_lamp(self, bl, pl): print(" [LimitedDirLightInfo '{}']".format(bl.name)) @@ -176,6 +184,11 @@ class LightConverter: if projectors: self._export_rt_projector(bo, pl_light, projectors) + # If the lamp has any sort of animation attached, then it needs to be marked movable. + # Otherwise, Plasma may not use it for lighting. + if projectors or self._exporter().animation.is_animated(bo): + pl_light.setProperty(plLightInfo.kLPMovable, True) + # *Sigh* pl_light.sceneNode = self.mgr.get_scene_node(location=so.key.location) @@ -203,7 +216,6 @@ class LightConverter: else: state.miscFlags |= hsGMatState.kMiscOrthoProjection state.ZFlags |= hsGMatState.kZNoZWrite - pl_light.setProperty(plLightInfo.kLPMovable, True) pl_light.setProperty(plLightInfo.kLPCastShadows, False) if slot.blend_type == "ADD":