support transform animation

This commit is contained in:
Jason0214
2018-05-03 15:27:26 +08:00
parent a9c12f0b0c
commit d04d2229ca
5 changed files with 367 additions and 0 deletions

View File

@@ -81,6 +81,20 @@ class ExportGodot(bpy.types.Operator, ExportHelper):
"layers if that applies).",
default=False,
)
use_export_animation = BoolProperty(
name="Export Animation",
description="Export all the animation actions (include those "
"in nla_tracks), notice if an animated object has "
"an ancestor also has animated, its animation would "
"go into the ancetor's AnimationPlayer",
default=True,
)
use_seperate_animation_player = BoolProperty(
name="Seperate AnimationPlayer For Each Object",
description="Create a seperate AnimationPlayer node for every"
"blender object which has animtion data",
default=False,
)
use_mesh_modifiers = BoolProperty(
name="Apply Modifiers",
description="Apply modifiers to mesh objects (on a copy!).",

View File

@@ -19,6 +19,7 @@ from .simple_nodes import * # pylint: disable=wildcard-import
from .mesh import export_mesh_node
from .physics import export_physics_properties
from .armature import export_armature_node, export_bone_attachment
from .animation import export_animation_data
BLENDER_TYPE_TO_EXPORTER = {
@@ -30,3 +31,5 @@ BLENDER_TYPE_TO_EXPORTER = {
}
BONE_ATTACHMENT_EXPORTER = export_bone_attachment
ANIMATION_DATA_EXPORTER = export_animation_data

View File

@@ -0,0 +1,336 @@
"""Export animation into Godot scene tree"""
import collections
import re
import copy
import bpy
import mathutils
from ..structures import (NodeTemplate, NodePath,
InternalResource, Array, fix_matrix)
LINEAR_INTERPOLATION = 1
class Track:
"""Animation track, with a type track and a frame list
the element in frame list is not strictly typed, for example,
a transform track would have frame with type mathutils.Matrix()"""
def __init__(self, track_type, track_path, frame_begin, frame_list):
self.type = track_type
self.path = track_path
self.frame_begin = frame_begin
self.frames = frame_list
def last_frame(self):
"""The number of last frame"""
return self.frame_begin + len(self.frames)
class AnimationResource(InternalResource):
"""Internal resource with type Animation"""
def __init__(self):
super().__init__('Animation')
self['step'] = 0.1
self['length'] = 0
self.track_count = 0
def add_track(self, track):
"""add a track to animation resource"""
track_length = track.last_frame() / bpy.context.scene.render.fps
if track_length > self['length']:
self['length'] = track_length
track_id_str = 'tracks/{}'.format(self.track_count)
self.track_count += 1
self[track_id_str + '/type'] = '"{}"'.format(track.type)
if track.type == 'transform':
self[track_id_str + '/path'] = track.path
self[track_id_str + '/interp'] = LINEAR_INTERPOLATION
self[track_id_str + '/keys'] = transform_frames_to_keys(
track.frame_begin, track.frames
)
class AnimationPlayer(NodeTemplate):
"""Godot scene node with type AnimationPlayer"""
def __init__(self, name, parent):
super().__init__(name, "AnimationPlayer", parent)
# use parent node as the animation root node
self['root_node'] = NodePath(self.get_path(), self.parent.get_path())
# blender actions not in nla_tracks are treated as default
self.default_animation = None
def add_default_animation_resource(self, escn_file, action):
"""Default animation resource may hold animation from children
objects"""
self.default_animation = self.create_animation_resource(
escn_file, action)
def create_animation_resource(self, escn_file, action):
"""Create a new animation resource and add it into escn file"""
new_anim_resource = AnimationResource()
resource_id = escn_file.add_internal_resource(
new_anim_resource, action)
self['anims/{}'.format(action.name)] = (
"SubResource({})".format(resource_id))
return new_anim_resource
def transform_frames_to_keys(first_frame, frame_list):
"""Convert a list of transform matrix to the keyframes
of an animation track"""
array = Array(prefix='[', suffix=']')
for index, mat in enumerate(frame_list):
if index > 0 and frame_list[index] == frame_list[index - 1]:
# do not export same keyframe
continue
frame = first_frame + index
array.append(frame / bpy.context.scene.render.fps)
# transition default 1.0
array.append(1.0)
# convert from z-up to y-up
transform_mat = fix_matrix(mat)
location = transform_mat.to_translation()
quaternion = transform_mat.to_quaternion()
scale = transform_mat.to_scale()
array.append(location.x)
array.append(location.y)
array.append(location.z)
array.append(quaternion.x)
array.append(quaternion.y)
array.append(quaternion.z)
array.append(quaternion.w)
array.append(scale.x)
array.append(scale.y)
array.append(scale.z)
return array
def get_animation_player(escn_file, export_settings, godot_node):
"""Get a AnimationPlayer node, if not existed, a new
one will be created and returned"""
animation_player = None
# looking for a existed AnimationPlayer
if not export_settings['use_seperate_animation_player']:
node_ptr = godot_node
while node_ptr is not None:
for child in node_ptr.children:
if child.get_type() == 'AnimationPlayer':
animation_player = child
break
if animation_player is not None:
break
node_ptr = node_ptr.parent
if animation_player is None:
animation_player = AnimationPlayer(
godot_node.get_name() + 'Animation',
godot_node.parent,
)
escn_file.add_node(animation_player)
return animation_player
def blender_path_to_bone_name(blender_object_path):
"""Find the bone name inside a fcurve data path,
the parameter blender_object_path is part of
the fcurve.data_path generated through
split_fcurve_data_path()"""
return re.search(r'pose.bones\["([^"]+)"\]',
blender_object_path).group(1)
def split_fcurve_data_path(data_path):
"""Split fcurve data path into a blender
object path and an attribute name"""
path_list = data_path.rsplit('.', 1)
if len(path_list) == 1:
return '', path_list[0]
return path_list[0], path_list[1]
def get_frame_range(action):
"""Return the frame range of the action"""
return int(action.frame_range[0]), int(action.frame_range[1])
def export_transform_action(godot_node, animation_player,
blender_object, action, animation_resource):
"""Export a action with bone and object transform"""
class TransformFrame:
"""A data structure hold transform values of an animation key,
it is used as an intermedia data structure, being updated during
parsing the fcurve data and finally being converted to a transform
matrix, notice itself uses location, scale, rotation not matrix"""
ATTRIBUTES = {
'location', 'scale', 'rotation_quaternion', 'rotation_euler'}
def __init__(self, default_transform, rotation_mode):
self.location = default_transform.to_translation()
# fixme: lose negative scale
self.scale = default_transform.to_scale()
# quaternion and euler fcurves may both exist in fcurves
self.rotation_mode = rotation_mode
self.rotation_quaternion = default_transform.to_quaternion()
if rotation_mode == 'QUATERNION':
self.rotation_euler = default_transform.to_euler()
else:
self.rotation_euler = default_transform.to_euler(
rotation_mode
)
def update(self, attribute, array_index, value):
"""Use fcurve data to update the frame"""
if attribute == 'location':
self.location[array_index] = value
elif attribute == 'scale':
self.scale[array_index] = value
elif attribute == 'rotation_quaternion':
self.rotation_quaternion[array_index] = value
elif attribute == 'rotation_euler':
self.rotation_euler[array_index] = value
def to_matrix(self):
"""Convert location, scale, rotation to a transform matrix"""
if self.rotation_mode == 'QUATERNION':
rot_mat = self.rotation_quaternion.to_matrix().to_4x4()
else:
rot_mat = self.rotation_euler.to_matrix().to_4x4()
loc_mat = mathutils.Matrix.Translation(self.location)
sca_mat = mathutils.Matrix.Scale(1, 4, self.scale)
return loc_mat * rot_mat * sca_mat
first_frame, last_frame = get_frame_range(action)
transform_frames_map = collections.OrderedDict()
for fcurve in action.fcurves:
# fcurve data are seperated into different channels,
# for example a transform action would have several fcurves
# (location.x, location.y, rotation.x ...), so here fcurves
# are aggregated to object while being evaluted
object_path, attribute = split_fcurve_data_path(fcurve.data_path)
if object_path not in transform_frames_map:
if attribute in TransformFrame.ATTRIBUTES:
default_frame = None
if object_path.startswith('pose'):
bone_id = blender_object.pose.bones.find(
blender_path_to_bone_name(object_path)
)
pose_bone = blender_object.pose.bones[bone_id]
default_frame = TransformFrame(
pose_bone.matrix_basis,
pose_bone.rotation_mode
)
else:
# the fcurve location is matrix_basis.to_translation()
default_frame = TransformFrame(
blender_object.matrix_basis,
blender_object.rotation_mode
)
transform_frames_map[object_path] = [
copy.deepcopy(default_frame)
for _ in range(last_frame - first_frame + 1)
]
if attribute in TransformFrame.ATTRIBUTES:
for frame in range(first_frame, last_frame + 1):
transform_frames_map[
object_path][frame - first_frame].update(
attribute,
fcurve.array_index,
fcurve.evaluate(frame)
)
for object_path, frame_list in transform_frames_map.items():
if object_path == '':
# object_path equals '' represents node itself
# convert matrix_basis to matrix_local(parent space transform)
normalized_frame_list = [
blender_object.matrix_parent_inverse *
x.to_matrix() for x in frame_list]
track_path = NodePath(
animation_player.parent.get_path(),
godot_node.get_path()
)
elif object_path.startswith('pose'):
track_path = NodePath(
animation_player.parent.get_path(),
godot_node.get_path(),
blender_path_to_bone_name(object_path)
)
normalized_frame_list = [x.to_matrix() for x in frame_list]
animation_resource.add_track(
Track(
'transform',
track_path,
first_frame,
normalized_frame_list
)
)
# ----------------------------------------------
ACTION_EXPORTER_MAP = {
'transform': export_transform_action,
}
def export_animation_data(escn_file, export_settings, godot_node,
blender_object, action_type):
"""Export the action and nla_tracks in blender_object.animation_data,
it will further call the action exporting function in AnimationDataExporter
given by `func_name`"""
animation_player = get_animation_player(
escn_file, export_settings, godot_node)
exporter_func = ACTION_EXPORTER_MAP[action_type]
exported_actions = set()
action = blender_object.animation_data.action
if action is not None:
if animation_player.default_animation is None:
# choose a arbitrary action as the hash key for animation resource
animation_player.add_default_animation_resource(
escn_file, action)
exported_actions.add(action)
exporter_func(godot_node, animation_player, blender_object,
action, animation_player.default_animation)
# export actions in nla_tracks, each exported to seperate
# animation resources
for nla_track in blender_object.animation_data.nla_tracks:
for nla_strip in nla_track.strips:
# make sure no duplicate action exported
if nla_strip.action not in exported_actions:
exported_actions.add(nla_strip.action)
anim_resource = animation_player.create_animation_resource(
escn_file, nla_strip.action
)
exporter_func(godot_node, animation_player, blender_object,
nla_strip.action, anim_resource)

View File

@@ -89,6 +89,18 @@ class GodotExporter:
exported_node = exporter(self.escn_file, self.config, node,
parent_gd_node)
# if the blender node is exported and it has animation data
if (self.config["use_export_animation"] and
exported_node != parent_gd_node and
node.animation_data is not None):
converters.ANIMATION_DATA_EXPORTER(
self.escn_file,
self.config,
exported_node,
node,
"transform"
)
for child in node.children:
self.export_node(child, exported_node)

View File

@@ -18,6 +18,8 @@ def export_escn(out_file):
use_active_layers=False,
use_export_selected=False,
use_mesh_modifiers=True,
use_export_animation=True,
use_seperate_animation_player=False,
material_search_paths = 'PROJECT_DIR'
)