diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 0934de1..70e524c 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -61,7 +61,7 @@ class AnimationConverter: # 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_controller(rot_curves, keyframes, bez_chans, default_xform.to_euler()) + ctrl = self._make_scalar_compound_controller(rot_curves, keyframes, bez_chans, default_xform.to_euler()) else: ctrl = self._make_quat_controller(rot_curves, keyframes, default_xform.to_euler()) return ctrl @@ -76,6 +76,14 @@ 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) + if not keyframes: + return None + + ctrl = self._make_scalar_leaf_controller(keyframes, bezier) + return ctrl + def _make_point3_controller(self, fcurves, keyframes, bezier, default_xform): ctrl = plLeafController() subctrls = ("X", "Y", "Z") @@ -141,7 +149,7 @@ class AnimationConverter: ctrl.keys = (exported_frames, keyframe_type) return ctrl - def _make_scalar_controller(self, fcurves, keyframes, bez_chans, default_xform): + def _make_scalar_compound_controller(self, fcurves, keyframes, bez_chans, default_xform): ctrl = plCompoundController() subctrls = ("X", "Y", "Z") for i in subctrls: @@ -176,6 +184,23 @@ class AnimationConverter: getattr(ctrl, subctrl).keys = (my_keyframes, my_keyframes[0].type) return ctrl + def _make_scalar_leaf_controller(self, keyframes, bezier): + ctrl = plLeafController() + keyframe_type = hsKeyFrame.kBezScalarKeyFrame if bezier else hsKeyFrame.kScalarKeyFrame + exported_frames = [] + + for keyframe in keyframes: + exported = hsScalarKey() + exported.frame = keyframe.frame_num + exported.frameTime = keyframe.frame_time + exported.inTan = keyframe.in_tan + exported.outTan = keyframe.out_tan + exported.type = keyframe_type + exported.value = keyframe.value + 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") keyframe_type = hsKeyFrame.kBezScaleKeyFrame if bez_chans else hsKeyFrame.kScaleKeyFrame @@ -218,6 +243,35 @@ class AnimationConverter: ctrl.keys = (exported_frames, keyframe_type) return ctrl + def _process_fcurve(self, fcurve): + """Like _process_keyframes, but for one fcurve""" + keyframe_data = type("KeyFrameData", (), {}) + fps = self._bl_fps + pi = 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 + keyframes[frame_num] = keyframe + final_keyframes = [keyframes[i] for i in sorted(keyframes)] + return (final_keyframes, bezier) + def _process_keyframes(self, fcurves): """Groups all FCurves for the same frame together""" keyframe_data = type("KeyFrameData", (), {}) diff --git a/korman/exporter/material.py b/korman/exporter/material.py index 13bbaa0..6e443dc 100644 --- a/korman/exporter/material.py +++ b/korman/exporter/material.py @@ -176,6 +176,9 @@ class MaterialConverter: "IMAGE": self._export_texture_type_image, "NONE": self._export_texture_type_none, } + self._animation_exporters = { + "opacityCtl": self._export_layer_opacity_animation, + } def export_material(self, bo, bm): """Exports a Blender Material as an hsGMaterial""" @@ -261,9 +264,80 @@ class MaterialConverter: # Export the specific texture type self._tex_exporters[texture.type](bo, hsgmat, layer, slot) + + # Export any layer animations + layer = self._export_layer_animations(bo, bm, slot, idx, hsgmat, layer) + hsgmat.addLayer(layer.key) return num_exported + def _export_layer_animations(self, bo, bm, tex_slot, idx, hsgmat, base_layer): + """Exports animations on this texture and chains the Plasma layers as needed""" + + def harvest_fcurves(bl_id, collection, data_path=None): + anim = bl_id.animation_data + if anim is not None: + action = anim.action + if action is not None: + if data_path is None: + collection.extend(action.fcurves) + else: + collection.extend([i for i in action.fcurves if i.data_path.startswith(data_path)]) + return action + return None + + # First, we must gather relevant FCurves from both the material and the texture itself + # Because, you know, that totally makes sense... + fcurves = [] + mat_action = harvest_fcurves(bm, fcurves, "texture_slots[{}]".format(idx)) + tex_action = harvest_fcurves(tex_slot.texture, fcurves) + + # No fcurves, no animation + if not fcurves: + return base_layer + + # Okay, so we have some FCurves. We'll loop through our known layer animation converters + # and chain this biotch up as best we can. + layer_animation = None + for attr, converter in self._animation_exporters.items(): + ctrl = converter(bm, tex_slot, fcurves) + if ctrl is not None: + if layer_animation is None: + name = "{}_LayerAnim".format(base_layer.key.name) + layer_animation = self._mgr.add_object(plLayerAnimation, bl=bo, name=name) + setattr(layer_animation, attr, ctrl) + + # Alrighty, if we exported any controllers, layer_animation is a plLayerAnimation. We need to do + # the common schtuff now. + if layer_animation is not None: + layer_animation.underLay = base_layer.key + + fps = bpy.context.scene.render.fps + atc = layer_animation.timeConvert + if tex_action is not None: + start, end = tex_action.frame_range + else: + start, end = mat_action.frame_range + atc.begin = start / fps + atc.end = end / fps + + 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 + return layer_animation + + # Well, we had some FCurves but they were garbage... Too bad. + return base_layer + + def _export_layer_opacity_animation(self, bm, tex_slot, fcurves): + opacity_fcurve = next((i for i in fcurves if i.data_path == "plasma_layer.opacity" and i.keyframe_points), None) + ctrl = self._exporter().animation.make_scalar_leaf_controller(opacity_fcurve) + return ctrl + def _export_texture_type_environment_map(self, bo, hsgmat, layer, slot): """Exports a Blender EnvironmentMapTexture to a plLayer""" diff --git a/korman/properties/prop_texture.py b/korman/properties/prop_texture.py index 41af233..96839ea 100644 --- a/korman/properties/prop_texture.py +++ b/korman/properties/prop_texture.py @@ -25,3 +25,10 @@ class PlasmaLayer(bpy.types.PropertyGroup): min=0, max=100, subtype="PERCENTAGE") + + anim_auto_start = BoolProperty(name="Auto Start", + description="Automatically start layer animation", + default=True) + anim_loop = BoolProperty(name="Loop", + description="Loop layer animation", + default=True) diff --git a/korman/ui/ui_texture.py b/korman/ui/ui_texture.py index ab4b8a3..ab7e0f3 100644 --- a/korman/ui/ui_texture.py +++ b/korman/ui/ui_texture.py @@ -28,5 +28,17 @@ class PlasmaLayerPanel(TextureButtonsPanel, bpy.types.Panel): bl_label = "Plasma Layer Options" def draw(self, context): + layer_props = context.texture.plasma_layer layout = self.layout - layout.prop(context.texture.plasma_layer, "opacity") + + split = layout.split() + col = split.column() + col.label("Animation:") + col.enabled = context.texture.animation_data is not None or \ + context.material.animation_data is not None + col.prop(layer_props, "anim_auto_start") + col.prop(layer_props, "anim_loop") + + col = split.column() + col.label("General:") + col.prop(layer_props, "opacity", text="Opacity")