Browse Source

Merge remote-tracking branch 'hoikas/anim'

pull/10/head
Adam Johnson 10 years ago
parent
commit
2c5a56d6cb
  1. 1
      korman/exporter/__init__.py
  2. 261
      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. 175
      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

261
korman/exporter/animation.py

@ -0,0 +1,261 @@
# 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
import mathutils
from PyHSPlasma import *
import weakref
from . import utils
class AnimationConverter:
def __init__(self, exporter):
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."""
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 make_pos_controller(self, fcurves, default_xform):
pos_curves = (i for i in fcurves if i.data_path == "location" and i.keyframe_points)
keyframes, bez_chans = self._process_keyframes(pos_curves)
if not keyframes:
return None
# At one point, I had some... insanity here to try to crush bezier channels and hand off to
# blah blah blah... As it turns out, point3 keyframe's tangents are vector3s :)
ctrl = self._make_point3_controller(keyframes, bez_chans, default_xform.to_translation())
return ctrl
def make_rot_controller(self, fcurves, default_xform):
# 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)
if not keyframes:
return None
# Ugh. Unfortunately, it appears Blender's default interpolation is bezier. So who knows if
# many users will actually see the benefit here? Makes me sad.
if bez_chans:
ctrl = self._make_scalar_controller(keyframes, bez_chans, default_xform.to_euler("XYZ"))
else:
ctrl = self._make_quat_controller(keyframes, default_xform.to_euler("XYZ"))
return ctrl
def make_scale_controller(self, fcurves, default_xform):
scale_curves = (i for i in fcurves if i.data_path == "scale" and i.keyframe_points)
keyframes, bez_chans = self._process_keyframes(scale_curves)
if not keyframes:
return None
# There is no such thing as a compound scale controller... in Plasma, anyway.
ctrl = self._make_scale_value_controller(keyframes, bez_chans, default_xform)
return ctrl
def _make_point3_controller(self, keyframes, bezier, default_xform):
ctrl = plLeafController()
subctrls = ("X", "Y", "Z")
keyframe_type = hsKeyFrame.kBezPoint3KeyFrame if bezier else hsKeyFrame.kPoint3KeyFrame
exported_frames = []
last_xform = [default_xform[0], default_xform[1], default_xform[2]]
for keyframe in keyframes:
exported = hsPoint3Key()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.type = keyframe_type
in_tan = hsVector3()
out_tan = hsVector3()
value = hsVector3()
for i, subctrl in enumerate(subctrls):
fkey = keyframe.values.get(i, None)
if fkey is not None:
v = fkey.co[1]
last_xform[i] = v
setattr(value, subctrl, v)
setattr(in_tan, subctrl, keyframe.in_tans[i])
setattr(out_tan, subctrl, keyframe.out_tans[i])
else:
setattr(value, subctrl, last_xform[i])
setattr(in_tan, subctrl, 0.0)
setattr(out_tan, subctrl, 0.0)
exported.inTan = in_tan
exported.outTan = out_tan
exported.value = value
exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _make_quat_controller(self, keyframes, default_xform):
ctrl = plLeafController()
keyframe_type = hsKeyFrame.kQuatKeyFrame
exported_frames = []
last_xform = [default_xform[0], default_xform[1], default_xform[2]]
for keyframe in keyframes:
exported = hsQuatKey()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.type = keyframe_type
# NOTE: quat keyframes don't do bezier nonsense
value = mathutils.Euler(last_xform, default_xform.order)
for i in range(3):
fkey = keyframe.values.get(i, None)
if fkey is not None:
v = fkey.co[1]
last_xform[i] = v
value[i] = v
quat = value.to_quaternion()
exported.value = utils.quaternion(quat)
exported_frames.append(exported)
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _make_scalar_controller(self, keyframes, bez_chans, default_xform):
ctrl = plCompoundController()
subctrls = ("X", "Y", "Z")
for i in subctrls:
setattr(ctrl, i, plLeafController())
exported_frames = ([], [], [])
for keyframe in keyframes:
for i, subctrl in enumerate(subctrls):
fkey = keyframe.values.get(i, None)
if fkey is not None:
keyframe_type = hsKeyFrame.kBezScalarKeyFrame if i in bez_chans else hsKeyFrame.kScalarKeyFrame
exported = hsScalarKey()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.inTan = keyframe.in_tans[i]
exported.outTan = keyframe.out_tans[i]
exported.type = keyframe_type
exported.value = fkey.co[1]
exported_frames[i].append(exported)
for i, subctrl in enumerate(subctrls):
my_keyframes = exported_frames[i]
# ensure this controller has at least ONE keyframe
if not my_keyframes:
hack_frame = hsScalarKey()
hack_frame.frame = 0
hack_frame.frameTime = 0.0
hack_frame.type = hsKeyFrame.kScalarKeyFrame
hack_frame.value = default_xform[i]
my_keyframes.append(hack_frame)
getattr(ctrl, subctrl).keys = (my_keyframes, my_keyframes[0].type)
return ctrl
def _make_scale_value_controller(self, keyframes, bez_chans, default_xform):
subctrls = ("X", "Y", "Z")
keyframe_type = hsKeyFrame.kBezScaleKeyFrame if bez_chans else hsKeyFrame.kScaleKeyFrame
exported_frames = []
_scale = default_xform.to_scale()
last_xform = [_scale[0], _scale[1], _scale[2]]
unit_quat = default_xform.to_quaternion()
unit_quat.normalize()
unit_quat = utils.quaternion(unit_quat)
for keyframe in keyframes:
exported = hsScaleKey()
exported.frame = keyframe.frame_num
exported.frameTime = keyframe.frame_time
exported.type = keyframe_type
in_tan = hsVector3()
out_tan = hsVector3()
value = hsVector3()
for i, subctrl in enumerate(subctrls):
fkey = keyframe.values.get(i, None)
if fkey is not None:
v = fkey.co[1]
last_xform[i] = v
setattr(value, subctrl, v)
setattr(in_tan, subctrl, keyframe.in_tans[i])
setattr(out_tan, subctrl, keyframe.out_tans[i])
else:
setattr(value, subctrl, last_xform[i])
setattr(in_tan, subctrl, 0.0)
setattr(out_tan, subctrl, 0.0)
exported.inTan = in_tan
exported.outTan = out_tan
exported.value = (value, unit_quat)
exported_frames.append(exported)
ctrl = plLeafController()
ctrl.keys = (exported_frames, keyframe_type)
return ctrl
def _process_keyframes(self, fcurves):
"""Groups all FCurves for the same frame together"""
keyframe_data = type("KeyFrameData", (), {})
fps = self._bl_fps
pi = math.pi
keyframes = {}
bez_chans = set()
for fcurve in fcurves:
fcurve.update()
for fkey in fcurve.keyframe_points:
frame_num, value = fkey.co
if fps == 30.0:
# hope you don't have a frame 29.9 and frame 30.0...
frame_num = int(frame_num)
else:
frame_num = int(frame_num * (30.0 / fps))
keyframe = keyframes.get(frame_num, None)
if keyframe is None:
keyframe = keyframe_data()
keyframe.frame_num = frame_num
keyframe.frame_time = frame_num / fps
keyframe.in_tans = {}
keyframe.out_tans = {}
keyframe.values = {}
keyframes[frame_num] = keyframe
idx = fcurve.array_index
keyframe.values[idx] = fkey
# Calculate the bezier interpolation nonsense
if fkey.interpolation == "BEZIER":
og_frame = fkey.co[0]
keyframe.in_tans[idx] = -(value - fkey.handle_left[1]) / (og_frame - fkey.handle_left[0]) / fps / (2 * pi)
keyframe.out_tans[idx] = (value - fkey.handle_right[1]) / (og_frame - fkey.handle_right[0]) / fps / (2 * pi)
else:
keyframe.in_tans[idx] = 0.0
keyframe.out_tans[idx] = 0.0
if keyframe.in_tans[idx] != 0.0 or keyframe.out_tans[idx] != 0.0:
bez_chans.add(idx)
# Return the keyframes in a sequence sorted by frame number
final_keyframes = [keyframes[i] for i in sorted(keyframes)]
return (final_keyframes, bez_chans)
@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 *

175
korman/properties/modifiers/anim.py

@ -0,0 +1,175 @@
# 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.K = utils.vector3(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
# 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