Browse Source

Implement rudimentary object animation

pull/10/head
Adam Johnson 9 years ago
parent
commit
5b5577f6f5
  1. 1
      korman/exporter/__init__.py
  2. 119
      korman/exporter/animation.py
  3. 2
      korman/exporter/convert.py
  4. 41
      korman/operators/op_modifier.py
  5. 1
      korman/properties/modifiers/__init__.py
  6. 172
      korman/properties/modifiers/anim.py
  7. 1
      korman/ui/modifiers/__init__.py
  8. 70
      korman/ui/modifiers/anim.py

1
korman/exporter/__init__.py

@ -19,3 +19,4 @@ from PyHSPlasma import *
from .convert import *
from .explosions import *
from . import utils

119
korman/exporter/animation.py

@ -0,0 +1,119 @@
# This file is part of Korman.
#
# Korman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Korman is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
import math
from PyHSPlasma import *
import weakref
class AnimationConverter:
def __init__(self, exporter):
self._exporter = weakref.ref(exporter)
self._bl_fps = bpy.context.scene.render.fps
def _check_scalar_subcontrollers(self, ctrl, default_xform):
"""Ensures that all scalar subcontrollers have at least one keyframe in the default state"""
for i in ("X", "Y", "Z"):
sub = getattr(ctrl, i)
if not sub.hasKeys():
keyframe = hsScalarKey()
keyframe.frame = 0
keyframe.frameTime = 0.0
keyframe.type = hsKeyFrame.kScalarKeyFrame
keyframe.value = getattr(default_xform, i.lower())
sub.keys = ([keyframe,], hsKeyFrame.kScalarKeyFrame)
def convert_action2tm(self, action, default_xform):
"""Converts a Blender Action to a plCompoundController."""
fcurves = action.fcurves
if not fcurves:
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
def _is_bezier_curve(self, keyframes):
for i in keyframes:
if i.interpolation == "BEZIER":
return True
return False
def make_pos_controller(self, fcurves, default_xform):
pos_curves = (i for i in fcurves if i.data_path == "location" and i.keyframe_points)
ctrl = self.make_scalar_controller(pos_curves)
if ctrl is not None and default_xform is not None:
self._check_scalar_subcontrollers(ctrl, default_xform.to_transpose())
def make_rot_controller(self, fcurves, default_xform):
rot_curves = (i for i in fcurves if i.data_path == "rotation_euler" and i.keyframe_points)
ctrl = self.make_scalar_controller(rot_curves)
if ctrl is not None and default_xform is not None:
self._check_scalar_subcontrollers(ctrl, default_xform.to_euler("XYZ"))
return ctrl
def make_scale_controller(self, fcurves, default_xform):
# ... TODO ...
# who needs this anyway?
return None
def make_scalar_controller(self, fcurves):
ctrl = plCompoundController()
subctls = ("X", "Y", "Z")
# this ensures that all subcontrollers are populated -- otherwise KABLOOEY!
for i in subctls:
setattr(ctrl, i, plLeafController())
for fcurve in fcurves:
fcurve.update()
if self._is_bezier_curve(fcurve.keyframe_points):
key_type = hsKeyFrame.kScalarKeyFrame
else:
key_type = hsKeyFrame.kBezScalarKeyFrame
frames = []
pi = math.pi
fps = self._bl_fps
for i in fcurve.keyframe_points:
bl_frame_num, value = i.co
frame = hsScalarKey()
if i.interpolation == "BEZIER":
frame.inTan = -(value - i.handle_left[1]) / (bl_frame_num - i.handle_left[0]) / fps / (2 * pi)
frame.outTan = (value - i.handle_right[1]) / (bl_frame_num - i.handle_right[0]) / fps / (2 * pi)
else:
frame.inTan = 0.0
frame.outTan = 0.0
frame.type = key_type
frame.frame = int(bl_frame_num * (30.0 / fps))
frame.frameTime = bl_frame_num / fps
frame.value = value
frames.append(frame)
controller = plLeafController()
getattr(ctrl, subctls[fcurve.array_index]).keys = (frames, key_type)
# Compact this bamf
if not ctrl.X.hasKeys() and not ctrl.Y.hasKeys() and not ctrl.Z.hasKeys():
return None
else:
return ctrl
@property
def _mgr(self):
return self._exporter().mgr

2
korman/exporter/convert.py

@ -18,6 +18,7 @@ import os.path
from PyHSPlasma import *
import time
from . import animation
from . import explosions
from . import logger
from . import manager
@ -47,6 +48,7 @@ class Exporter:
self.report = logger.ExportAnalysis()
self.physics = physics.PhysicsConverter(self)
self.light = rtlight.LightConverter(self)
self.animation = animation.AnimationConverter(self)
self.sumfile = sumfile.SumFile()
# Step 1: Create the age info and the pages

41
korman/operators/op_modifier.py

@ -148,3 +148,44 @@ class ModifierLogicWizOperator(ModifierOperator, bpy.types.Operator):
end = time.process_time()
print("\nLogicWiz finished in {:.2f} seconds".format(end-start))
return {"FINISHED"}
class ModifierCollectionAddOperator(ModifierOperator, bpy.types.Operator):
bl_idname = "object.plasma_modifier_collection_add"
bl_label = "Add Item"
bl_description = "Adds an item to the collection"
modifier = StringProperty(name="Modifier", description="Attribute name of the Plasma Modifier")
collection = StringProperty(name="Collection", description="Attribute name of the collection property")
name_prefix = StringProperty(name="Name Prefix", description="Prefix for autogenerated item names", default="Item")
name_prop = StringProperty(name="Name Property", description="Attribute name of the item name property")
def execute(self, context):
obj = context.active_object
mod = getattr(obj.plasma_modifiers, self.modifier)
collection = getattr(mod, self.collection)
idx = len(collection)
collection.add()
if self.name_prop:
setattr(collection[idx], self.name_prop, "{} {}".format(self.name_prefix, idx+1))
return {"FINISHED"}
class ModifierCollectionRemoveOperator(ModifierOperator, bpy.types.Operator):
bl_idname = "object.plasma_modifier_collection_remove"
bl_label = "Remove Item"
bl_description = "Removes an item from the collection"
modifier = StringProperty(name="Modifier", description="Attribute name of the Plasma Modifier")
collection = StringProperty(name="Collection", description="Attribute name of the collection property")
index = IntProperty(name="Index", description="Item index to remove")
def execute(self, context):
obj = context.active_object
mod = getattr(obj.plasma_modifiers, self.modifier)
collection = getattr(mod, self.collection)
if len(collection) > self.index:
collection.remove(self.index)
return {"FINISHED"}
else:
return {"CANCELLED"}

1
korman/properties/modifiers/__init__.py

@ -17,6 +17,7 @@ import bpy
import inspect
from .base import PlasmaModifierProperties
from .anim import *
from .avatar import *
from .logic import *
from .physics import *

172
korman/properties/modifiers/anim.py

@ -0,0 +1,172 @@
# This file is part of Korman.
#
# Korman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Korman is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bpy.props import *
from PyHSPlasma import *
from .base import PlasmaModifierProperties
from ...exporter import ExportError, utils
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))
return bo.animation_data.action
class PlasmaAnimationModifier(PlasmaModifierProperties):
pl_id = "animation"
bl_category = "Animation"
bl_label = "Animation"
bl_description = "Object animation"
bl_icon = "ACTION"
auto_start = BoolProperty(name="Auto Start",
description="Automatically start this animation on link-in",
default=True)
loop = BoolProperty(name="Loop Anim",
description="Loop the animation",
default=True)
initial_marker = StringProperty(name="Start Marker",
description="Marker indicating the default start point")
loop_start = StringProperty(name="Loop Start",
description="Marker indicating where the default loop begins")
loop_end = StringProperty(name="Loop End",
description="Marker indicating where the default loop ends")
def created(self, obj):
self.display_name = "{}_(Entire Animation)".format(obj.name)
@property
def requires_actor(self):
return True
def export(self, exporter, bo, so):
action = _get_blender_action(bo)
markers = action.pose_markers
atcanim = exporter.mgr.find_create_key(plATCAnim, so=so, name=self.display_name).object
atcanim.autoStart = self.auto_start
atcanim.loop = self.loop
atcanim.name = "(Entire Animation)"
atcanim.start = 0.0
atcanim.end = _convert_frame_time(action.frame_range[1])
# Simple start and loop info
initial_marker = markers.get(self.initial_marker)
if initial_marker is not None:
atcanim.initial = _convert_fame_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)
else:
atcanim.loopStart = 0.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])
# 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 = utils.vector3(matrix.to_translation())
affine.Q = utils.quaternion(matrix.to_quaternion())
affine.K = utils.vector3(matrix.to_scale())
affine.F = -1.0 if matrix.determinant() < 0.0 else 1.0
channel.affine = affine
# We need both an AGModifier and an AGMasterMod
# TODO: grouped animations (eg one door, two objects)
agmod = exporter.mgr.add_object(plAGModifier, so=so, name=self.display_name)
agmod.channelName = bo.name
agmaster = exporter.mgr.add_object(plAGMasterMod, so=so, name=self.display_name)
agmaster.addPrivateAnim(atcanim.key)
class LoopMarker(bpy.types.PropertyGroup):
loop_name = StringProperty(name="Loop Name",
description="Name of this loop")
loop_start = StringProperty(name="Loop Start",
description="Marker name from whence the loop begins")
loop_end = StringProperty(name="Loop End",
description="Marker name from whence the loop ends")
class PlasmaAnimationLoopModifier(PlasmaModifierProperties):
pl_id = "animation_loop"
bl_category = "Animation"
bl_label = "Loop Markers"
bl_description = "Animation loop settings"
bl_icon = "PMARKER_SEL"
loops = CollectionProperty(name="Loops",
description="Loop points within the animation",
type=LoopMarker)
active_loop_index = IntProperty(options={"HIDDEN"})
def created(self, obj):
# who cares, this modifier creates no Keys...
self.display_name = "AnimLoops"
def export(self, exporter, bo, so):
action = _get_blender_action(bo)
markers = action.pose_markers
key_name = bo.plasma_modifiers.animation.display_name
atcanim = exporter.mgr.find_create_key(plATCAnim, so=so, name=key_name).object
for loop in self.loops:
start = markers.get(loop.loop_start)
end = markers.get(loop.loop_end)
if start is None:
exporter.report.warn("Animation '{}' Loop '{}': Marker '{}' not found. This loop will not be exported".format(
action.name, loop.loop_name, loop.loop_start), indent=2)
if end is None:
exporter.report.warn("Animation '{}' Loop '{}': Marker '{}' not found. This loop will not be exported".format(
action.name, loop.loop_name, loop.loop_end), indent=2)
if start is None or end is None:
continue
atcanim.setLoop(loop.loop_name, _convert_frame_time(start.frame), _convert_frame_time(end.frame))

1
korman/ui/modifiers/__init__.py

@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
from .anim import *
from .avatar import *
from .logic import *
from .physics import *

70
korman/ui/modifiers/anim.py

@ -0,0 +1,70 @@
# This file is part of Korman.
#
# Korman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Korman is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
def _check_for_anim(layout, context):
if context.object.animation_data is None or context.object.animation_data.action is None:
layout.label("Object has no animation data", icon="ERROR")
return False
return True
def animation(modifier, layout, context):
if not _check_for_anim(layout, context):
return
action = context.object.animation_data.action
split = layout.split()
col = split.column()
col.prop(modifier, "auto_start")
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")
class LoopListUI(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
layout.prop(item, "loop_name", emboss=False, text="", icon="PMARKER_ACT")
def animation_loop(modifier, layout, context):
if not _check_for_anim(layout, context):
return
row = layout.row()
row.template_list("LoopListUI", "loops", modifier, "loops", modifier, "active_loop_index",
rows=2, maxrows=3)
col = row.column(align=True)
op = col.operator("object.plasma_modifier_collection_add", icon="ZOOMIN", text="")
op.modifier = modifier.pl_id
op.collection = "loops"
op.name_prefix = "Loop"
op.name_prop = "loop_name"
op = col.operator("object.plasma_modifier_collection_remove", icon="ZOOMOUT", text="")
op.modifier = modifier.pl_id
op.collection = "loops"
op.index = modifier.active_loop_index
# 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")
Loading…
Cancel
Save