diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 6f4a292..8099423 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -353,6 +353,21 @@ class AnimationConverter: yield applicator def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False): + tm = self.convert_transform_controller(fcurves, xform, allow_empty) + if tm is None and not allow_empty: + return None + + applicator = plMatrixChannelApplicator() + applicator.enabled = True + applicator.channelName = name + channel = plMatrixControllerChannel() + channel.controller = tm + applicator.channel = channel + channel.affine = utils.affine_parts(xform) + + return applicator + + def convert_transform_controller(self, fcurves, xform, allow_empty=False): if not fcurves and not allow_empty: return None @@ -367,27 +382,7 @@ class AnimationConverter: 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 + return tm def get_anigraph_keys(self, bo=None, so=None): mod = self._mgr.find_create_key(plAGModifier, so=so, bl=bo) diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py index 7b6715c..a39485a 100644 --- a/korman/exporter/camera.py +++ b/korman/exporter/camera.py @@ -19,6 +19,8 @@ from PyHSPlasma import * import weakref from .explosions import * +from .. import helpers +from . import utils class CameraConverter: def __init__(self, exporter): @@ -67,7 +69,7 @@ class CameraConverter: brain.setFlags(plCameraBrain1.kSpeedUpWhenRunning, True) def export_camera(self, so, bo, camera_type, camera_props): - brain = getattr(self, "_export_{}_camera".format(camera_type))(so, bo, camera_props, allow_anim) + brain = getattr(self, "_export_{}_camera".format(camera_type))(so, bo, camera_props) mod = self._export_camera_modifier(so, bo, camera_props) mod.brain = brain.key @@ -138,6 +140,7 @@ class CameraConverter: return brain def _export_fixed_camera(self, so, bo, props): + self._exporter().animation.convert_object_animations(bo, so) brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so) self._convert_brain(so, bo, props, brain) return brain @@ -150,6 +153,44 @@ class CameraConverter: brain.offset = hsVector3(*camera_props.pos_offset) return brain + def _export_rail_camera(self, so, bo, props): + brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so) + self._convert_brain(so, bo, props, brain) + + rail = self._mgr.find_create_object(plRailCameraMod, so=so) + rail.followFlags |= plLineFollowMod.kForceToLine + if props.poa_type == "object": + rail.followMode = plLineFollowMod.kFollowObject + rail.refObj = self._mgr.find_create_key(plSceneObject, bl=props.poa_object) + else: + rail.followMode = plLineFollowMod.kFollowLocalAvatar + if bo.parent: + rail.pathParent = self._mgr.find_create_key(plSceneObject, bl=bo.parent) + + # The rail is defined by a position controller in Plasma. Cyan uses a separate + # path object, but it makes more sense to me to just animate the camera with + # the details of the path... + pos_fcurves = tuple(i for i in helpers.fetch_fcurves(bo, False) if i.data_path == "location") + pos_ctrl = self._exporter().animation.convert_transform_controller(pos_fcurves, bo.matrix_basis) + if pos_ctrl is None: + raise ExportError("'{}': Rail Camera lacks appropriate rail keyframes".format(bo.name)) + path = plAnimPath() + path.controller = pos_ctrl + path.affineParts = utils.affine_parts(bo.matrix_local) + begin, end = bo.animation_data.action.frame_range + for fcurve in pos_fcurves: + f1, f2 = fcurve.evaluate(begin), fcurve.evaluate(end) + if abs(f1 - f2) > 0.001: + break + else: + # The animation is a loop + path.flags |= plAnimPath.kWrap + path.length = end / bpy.context.scene.render.fps + rail.path = path + brain.rail = rail.key + + return brain + @property def _mgr(self): return self._exporter().mgr diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index f939fd2..cc526bc 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -229,7 +229,6 @@ class Exporter: # sort, and barf out a CI. sceneobject = self.mgr.find_create_object(plSceneObject, bl=bl_obj) self._export_actor(sceneobject, bl_obj) - self.animation.convert_object_animations(bl_obj, sceneobject) export_fn(sceneobject, bl_obj) # And now we puke out the modifiers... @@ -240,19 +239,19 @@ class Exporter: def _export_camera_blobj(self, so, bo): # Hey, guess what? Blender's camera data is utter crap! + # NOTE: Animation export is dependent on camera type, so we'll do that later. camera = bo.data.plasma_camera self.camera.export_camera(so, bo, camera.camera_type, camera.settings) def _export_empty_blobj(self, so, bo): - # We don't need to do anything here. This function just makes sure we don't error out - # or add a silly special case :( - pass + self.animation.convert_object_animations(bo, so) def _export_lamp_blobj(self, so, bo): - # We'll just redirect this to the RT Light converter... + self.animation.convert_object_animations(bo, so) self.light.export_rtlight(so, bo) def _export_mesh_blobj(self, so, bo): + self.animation.convert_object_animations(bo, so) if bo.data.materials: self.mesh.export_object(bo) else: diff --git a/korman/exporter/utils.py b/korman/exporter/utils.py index 3471a33..569201d 100644 --- a/korman/exporter/utils.py +++ b/korman/exporter/utils.py @@ -15,6 +15,19 @@ from PyHSPlasma import * +def affine_parts(xform): + # 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 = quaternion(rot) + rot.normalize() + affine.U = quaternion(rot) + return affine + def color(blcolor, alpha=1.0): """Converts a Blender Color into an hsColorRGBA""" return hsColorRGBA(blcolor.r, blcolor.g, blcolor.b, alpha) diff --git a/korman/helpers.py b/korman/helpers.py index 6ae2bf2..fcd60c3 100644 --- a/korman/helpers.py +++ b/korman/helpers.py @@ -51,6 +51,19 @@ class TemporaryObject: def ensure_power_of_two(value): return pow(2, math.floor(math.log(value, 2))) +def fetch_fcurves(id_data, data_fcurves=True): + """Given a Blender ID, yields its FCurves""" + def _fetch(source): + if source is not None and source.action is not None: + for i in source.action.fcurves: + yield i + + # This seems rather unpythonic IMO + for i in _fetch(id_data.animation_data): + yield i + if data_fcurves: + for i in _fetch(id_data.data.animation_data): + yield i def find_modifier(bo, modid): """Given a Blender Object, finds a given modifier and returns it or None""" diff --git a/korman/properties/prop_camera.py b/korman/properties/prop_camera.py index b7a0f68..6b88549 100644 --- a/korman/properties/prop_camera.py +++ b/korman/properties/prop_camera.py @@ -19,7 +19,8 @@ import math camera_types = [("circle", "Circle Camera", "The camera circles a fixed point"), ("follow", "Follow Camera", "The camera follows an object"), - ("fixed", "Fixed Camera", "The camera is fixed in one location")] + ("fixed", "Fixed Camera", "The camera is fixed in one location"), + ("rail", "Rail Camera", "The camera follows an object by moving along a line")] class PlasmaTransition(bpy.types.PropertyGroup): poa_acceleration = FloatProperty(name="PoA Acceleration", diff --git a/korman/ui/ui_camera.py b/korman/ui/ui_camera.py index 18143c9..e4ef144 100644 --- a/korman/ui/ui_camera.py +++ b/korman/ui/ui_camera.py @@ -47,6 +47,18 @@ class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel): def draw_camera_properties(cam_type, props, layout, context, force_no_anim=False): trans = props.transition + def _draw_alert_prop(layout, props, the_prop, cam="", min=None, max=None, **kwargs): + can_alert = not cam or cam == cam_type + if can_alert: + value = getattr(props, the_prop) + if min is not None and value < min: + layout.alert = True + if max is not None and value > max: + layout.alert = True + layout.prop(props, the_prop, **kwargs) + layout.alert = False + else: + layout.prop(props, the_prop, **kwargs) def _draw_gated_prop(layout, props, gate_prop, actual_prop): row = layout.row(align=True) row.prop(props, gate_prop, text="") @@ -111,9 +123,9 @@ def draw_camera_properties(cam_type, props, layout, context, force_no_anim=False # Position Transitions col.active = cam_type != "circle" col.label("Default Position Transition:") - col.prop(trans, "pos_acceleration", text="Acceleration") - col.prop(trans, "pos_deceleration", text="Deceleration") - col.prop(trans, "pos_velocity", text="Maximum Velocity") + _draw_alert_prop(col, trans, "pos_acceleration", cam="rail", max=10.0, text="Acceleration") + _draw_alert_prop(col, trans, "pos_deceleration", cam="rail", max=10.0, text="Deceleration") + _draw_alert_prop(col, trans, "pos_velocity", cam="rail", max=10.0, text="Maximum Velocity") col.prop(trans, "pos_cut") # Position Offsets