mirror of
https://github.com/godotengine/godot-blender-exporter.git
synced 2026-01-05 18:10:04 +03:00
support transform animation
This commit is contained in:
@@ -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!).",
|
||||
|
||||
@@ -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
|
||||
|
||||
336
io_scene_godot/converters/animation.py
Normal file
336
io_scene_godot/converters/animation.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user