|
@@ -1,29 +1,70 @@
|
|
|
"""Export animation into Godot scene tree"""
|
|
|
import collections
|
|
|
import re
|
|
|
+import math
|
|
|
import copy
|
|
|
+from functools import partial
|
|
|
import bpy
|
|
|
import mathutils
|
|
|
from . import armature
|
|
|
from ..structures import (NodeTemplate, NodePath, fix_directional_transform,
|
|
|
- InternalResource, Array, fix_matrix)
|
|
|
+ InternalResource, Array, Map, fix_matrix)
|
|
|
|
|
|
+NEAREST_INTERPOLATION = 0
|
|
|
LINEAR_INTERPOLATION = 1
|
|
|
|
|
|
|
|
|
+# attribute converted as a bool, no interpolation
|
|
|
+CONVERT_AS_BOOL = 0
|
|
|
+# attribute converted as a float
|
|
|
+CONVERT_AS_FLOAT = 1
|
|
|
+# attribute is a vec or mat, mapping to several fcurves in animation
|
|
|
+CONVERT_AS_MULTI_VALUE = 2
|
|
|
+# a quad tuple contains information to convert an attribute
|
|
|
+# or a fcurve of blender to godot things
|
|
|
+AttributeConvertInfo = collections.namedtuple(
|
|
|
+ 'AttributeConvertInfo',
|
|
|
+ ['bl_name', 'gd_name', 'converter_function', 'attribute_type']
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
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):
|
|
|
+ """Animation track, has track type, track path, interpolation
|
|
|
+ method, a list of frames and a list of frame values.
|
|
|
+
|
|
|
+ Note that element in value_list is not strictly typed, for example,
|
|
|
+ a transform track would have value with type mathutils.Matrix(),
|
|
|
+ while some track would just have a float value"""
|
|
|
+ def __init__(self, track_type, track_path,
|
|
|
+ frames=(), values=()):
|
|
|
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)
|
|
|
+ # default to linear
|
|
|
+ self.interp = LINEAR_INTERPOLATION
|
|
|
+ self.frames = list()
|
|
|
+ self.values = list()
|
|
|
+
|
|
|
+ for frame in frames:
|
|
|
+ self.frames.append(frame)
|
|
|
+ for value in values:
|
|
|
+ self.values.append(value)
|
|
|
+
|
|
|
+ def add_frame_data(self, frame, value):
|
|
|
+ """Add add frame to track"""
|
|
|
+ self.frames.append(frame)
|
|
|
+ self.values.append(value)
|
|
|
+
|
|
|
+ def frame_end(self):
|
|
|
+ """The frame number of last frame"""
|
|
|
+ if not self.frames:
|
|
|
+ return 0
|
|
|
+ return self.frames[-1]
|
|
|
+
|
|
|
+ def frame_begin(self):
|
|
|
+ """The frame number of first frame"""
|
|
|
+ if not self.frames:
|
|
|
+ return 0
|
|
|
+ return self.frames[0]
|
|
|
|
|
|
|
|
|
class AnimationResource(InternalResource):
|
|
@@ -36,7 +77,7 @@ class AnimationResource(InternalResource):
|
|
|
|
|
|
def add_track(self, track):
|
|
|
"""add a track to animation resource"""
|
|
|
- track_length = track.last_frame() / bpy.context.scene.render.fps
|
|
|
+ track_length = track.frame_end() / bpy.context.scene.render.fps
|
|
|
if track_length > self['length']:
|
|
|
self['length'] = track_length
|
|
|
|
|
@@ -44,12 +85,55 @@ class AnimationResource(InternalResource):
|
|
|
self.track_count += 1
|
|
|
|
|
|
self[track_id_str + '/type'] = '"{}"'.format(track.type)
|
|
|
+ self[track_id_str + '/path'] = track.path
|
|
|
+ self[track_id_str + '/interp'] = track.interp
|
|
|
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
|
|
|
+ track.frames, track.values, track.interp
|
|
|
)
|
|
|
+ elif track.type == 'value':
|
|
|
+ self[track_id_str + '/keys'] = value_frames_to_keys(
|
|
|
+ track.frames, track.values, track.interp
|
|
|
+ )
|
|
|
+
|
|
|
+ def add_track_via_attr_mapping(self, fcurves, conv_quad_tuple_list,
|
|
|
+ base_node_path):
|
|
|
+ """Accepts some attribute mapping relation between blender and godot,
|
|
|
+ and further call `add_simple_value_track` export tracks.
|
|
|
+
|
|
|
+ `conv_quad_tuple_list` is a list of quad tuple which compose of
|
|
|
+ of(bl_attr_name, gd_attr_name, converter_lambda, attr_type)"""
|
|
|
+ for item in conv_quad_tuple_list:
|
|
|
+ bl_attr, gd_attr, converter, val_type = item
|
|
|
+ # vector vallue animation need special treatment
|
|
|
+ if val_type in (CONVERT_AS_FLOAT, CONVERT_AS_BOOL):
|
|
|
+ if val_type == CONVERT_AS_FLOAT:
|
|
|
+ track_builder = build_linear_interp_value_track
|
|
|
+ else:
|
|
|
+ track_builder = build_const_interp_value_track
|
|
|
+ self.add_simple_value_track(
|
|
|
+ fcurves,
|
|
|
+ bl_attr,
|
|
|
+ partial(
|
|
|
+ track_builder,
|
|
|
+ base_node_path.new_copy(gd_attr),
|
|
|
+ converter
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ def add_simple_value_track(self, fcurves, fcurve_data_path,
|
|
|
+ fcurve_to_track_func):
|
|
|
+ """Add a simple value track into AnimationResource, simple value
|
|
|
+ track means it have a one-one mapping to fcurve.
|
|
|
+
|
|
|
+ Note that the fcurve_to_track_func is a partial of
|
|
|
+ function like build_linear_interp_value_track and
|
|
|
+ build_const_interp_value_track which create a track
|
|
|
+ from fcurve"""
|
|
|
+ fcurve = fcurves.find(fcurve_data_path)
|
|
|
+ if fcurve is not None:
|
|
|
+ new_track = fcurve_to_track_func(fcurve)
|
|
|
+ self.add_track(new_track)
|
|
|
|
|
|
|
|
|
class AnimationPlayer(NodeTemplate):
|
|
@@ -78,22 +162,47 @@ class AnimationPlayer(NodeTemplate):
|
|
|
return new_anim_resource
|
|
|
|
|
|
|
|
|
-def transform_frames_to_keys(first_frame, frame_list):
|
|
|
+def value_frames_to_keys(frame_list, value_list, interp):
|
|
|
+ """Serialize a value list to a track keys object"""
|
|
|
+ time_array = Array(prefix='PoolRealArray(', suffix=')')
|
|
|
+ transition_array = Array(prefix='PoolRealArray(', suffix=')')
|
|
|
+ value_array = Array(prefix='[', suffix=']')
|
|
|
+ for index, frame in enumerate(frame_list):
|
|
|
+ if (interp == LINEAR_INTERPOLATION and index > 0 and
|
|
|
+ value_list[index] == value_list[index - 1]):
|
|
|
+ continue
|
|
|
+
|
|
|
+ time = frame / bpy.context.scene.render.fps
|
|
|
+ time_array.append(time)
|
|
|
+ transition_array.append(1)
|
|
|
+ value_array.append(value_list[index])
|
|
|
+
|
|
|
+ keys_map = Map()
|
|
|
+ keys_map["times"] = time_array.to_string()
|
|
|
+ keys_map["transitions"] = transition_array.to_string()
|
|
|
+ keys_map["update"] = 0
|
|
|
+ keys_map["values"] = value_array.to_string()
|
|
|
+
|
|
|
+ return keys_map
|
|
|
+
|
|
|
+
|
|
|
+def transform_frames_to_keys(frame_list, value_list, interp):
|
|
|
"""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]:
|
|
|
+ for index, frame in enumerate(frame_list):
|
|
|
+ if (interp == LINEAR_INTERPOLATION and index > 0 and
|
|
|
+ value_list[index] == value_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
|
|
|
+ mat = value_list[index]
|
|
|
transform_mat = fix_matrix(mat)
|
|
|
location = transform_mat.to_translation()
|
|
|
quaternion = transform_mat.to_quaternion()
|
|
@@ -160,9 +269,46 @@ def split_fcurve_data_path(data_path):
|
|
|
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 get_action_frame_range(action):
|
|
|
+ """Return the a tuple denoting the frame range of action"""
|
|
|
+ # in blender `last_frame` is included, here plus one to make it
|
|
|
+ # excluded to fit python convention
|
|
|
+ return int(action.frame_range[0]), int(action.frame_range[1]) + 1
|
|
|
+
|
|
|
+
|
|
|
+def get_fcurve_frame_range(fcurve):
|
|
|
+ """Return the a tuple denoting the frame range of fcurve"""
|
|
|
+ return int(fcurve.range()[0]), int(fcurve.range()[1]) + 1
|
|
|
+
|
|
|
+
|
|
|
+def build_const_interp_value_track(track_path, map_func, fcurve):
|
|
|
+ """Build a godot value track from a Blender const interpolation fcurve"""
|
|
|
+ track = Track('value', track_path)
|
|
|
+ track.interp = NEAREST_INTERPOLATION
|
|
|
+
|
|
|
+ if map_func is None:
|
|
|
+ for keyframe in fcurve.keyframe_points:
|
|
|
+ track.add_frame_data(int(keyframe.co[0]), keyframe.co[1])
|
|
|
+ else:
|
|
|
+ for keyframe in fcurve.keyframe_points:
|
|
|
+ track.add_frame_data(int(keyframe.co[0]), map_func(keyframe.co[1]))
|
|
|
+
|
|
|
+ return track
|
|
|
+
|
|
|
+
|
|
|
+def build_linear_interp_value_track(track_path, map_func, fcurve):
|
|
|
+ """Build a godot value track by evaluate every frame of Blender fcurve"""
|
|
|
+ track = Track('value', track_path)
|
|
|
+
|
|
|
+ frame_range = get_fcurve_frame_range(fcurve)
|
|
|
+ if map_func is None:
|
|
|
+ for frame in range(frame_range[0], frame_range[1]):
|
|
|
+ track.add_frame_data(frame, fcurve.evaluate(frame))
|
|
|
+ else:
|
|
|
+ for frame in range(frame_range[0], frame_range[1]):
|
|
|
+ track.add_frame_data(frame, map_func(fcurve.evaluate(frame)))
|
|
|
+
|
|
|
+ return track
|
|
|
|
|
|
|
|
|
def export_transform_action(godot_node, animation_player,
|
|
@@ -213,12 +359,12 @@ def export_transform_action(godot_node, animation_player,
|
|
|
sca_mat = mathutils.Matrix.Scale(1, 4, self.scale)
|
|
|
return loc_mat * rot_mat * sca_mat
|
|
|
|
|
|
- first_frame, last_frame = get_frame_range(action)
|
|
|
+ first_frame, last_frame = get_action_frame_range(action)
|
|
|
|
|
|
# if no skeleton node exist, it will be None
|
|
|
skeleton_node = armature.find_skeletion_node(godot_node)
|
|
|
|
|
|
- transform_frames_map = collections.OrderedDict()
|
|
|
+ transform_frame_values_map = collections.OrderedDict()
|
|
|
for fcurve in action.fcurves:
|
|
|
# fcurve data are seperated into different channels,
|
|
|
# for example a transform action would have several fcurves
|
|
@@ -226,7 +372,7 @@ def export_transform_action(godot_node, animation_player,
|
|
|
# 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 object_path not in transform_frame_values_map:
|
|
|
if attribute in TransformFrame.ATTRIBUTES:
|
|
|
|
|
|
default_frame = None
|
|
@@ -253,37 +399,37 @@ def export_transform_action(godot_node, animation_player,
|
|
|
pose_bone.rotation_mode
|
|
|
)
|
|
|
|
|
|
- transform_frames_map[object_path] = [
|
|
|
+ transform_frame_values_map[object_path] = [
|
|
|
copy.deepcopy(default_frame)
|
|
|
- for _ in range(last_frame - first_frame + 1)
|
|
|
+ for _ in range(last_frame - first_frame)
|
|
|
]
|
|
|
|
|
|
if attribute in TransformFrame.ATTRIBUTES:
|
|
|
|
|
|
- for frame in range(first_frame, last_frame + 1):
|
|
|
- transform_frames_map[
|
|
|
+ for frame in range(first_frame, last_frame):
|
|
|
+ transform_frame_values_map[
|
|
|
object_path][frame - first_frame].update(
|
|
|
attribute,
|
|
|
fcurve.array_index,
|
|
|
fcurve.evaluate(frame)
|
|
|
)
|
|
|
|
|
|
- for object_path, frame_list in transform_frames_map.items():
|
|
|
+ for object_path, frame_value_list in transform_frame_values_map.items():
|
|
|
if object_path == '':
|
|
|
# object_path equals '' represents node itself
|
|
|
|
|
|
# convert matrix_basis to matrix_local(parent space transform)
|
|
|
if (godot_node.get_type()
|
|
|
in ("SpotLight", "DirectionalLight", "Camera")):
|
|
|
- normalized_frame_list = [
|
|
|
+ normalized_value_list = [
|
|
|
fix_directional_transform(
|
|
|
blender_object.matrix_parent_inverse * x.to_matrix()
|
|
|
- ) for x in frame_list
|
|
|
+ ) for x in frame_value_list
|
|
|
]
|
|
|
else:
|
|
|
- normalized_frame_list = [
|
|
|
+ normalized_value_list = [
|
|
|
blender_object.matrix_parent_inverse *
|
|
|
- x.to_matrix() for x in frame_list
|
|
|
+ x.to_matrix() for x in frame_value_list
|
|
|
]
|
|
|
|
|
|
track_path = NodePath(
|
|
@@ -298,22 +444,184 @@ def export_transform_action(godot_node, animation_player,
|
|
|
blender_path_to_bone_name(object_path)
|
|
|
)
|
|
|
|
|
|
- normalized_frame_list = [x.to_matrix() for x in frame_list]
|
|
|
+ normalized_value_list = [x.to_matrix() for x in frame_value_list]
|
|
|
|
|
|
animation_resource.add_track(
|
|
|
Track(
|
|
|
'transform',
|
|
|
track_path,
|
|
|
- first_frame,
|
|
|
- normalized_frame_list
|
|
|
+ range(first_frame, last_frame),
|
|
|
+ normalized_value_list
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def export_shapekey_action(godot_node, animation_player,
|
|
|
+ blender_object, action, animation_resource):
|
|
|
+ """Export shapekey value action"""
|
|
|
+ first_frame, last_frame = get_action_frame_range(action)
|
|
|
+
|
|
|
+ for fcurve in action.fcurves:
|
|
|
+
|
|
|
+ object_path, attribute = split_fcurve_data_path(fcurve.data_path)
|
|
|
+
|
|
|
+ if attribute == 'value':
|
|
|
+ shapekey_name = re.search(r'key_blocks\["([^"]+)"\]',
|
|
|
+ object_path).group(1)
|
|
|
+
|
|
|
+ track_path = NodePath(
|
|
|
+ animation_player.parent.get_path(),
|
|
|
+ godot_node.get_path(),
|
|
|
+ "blend_shapes/{}".format(shapekey_name)
|
|
|
+ )
|
|
|
+
|
|
|
+ value_track = Track(
|
|
|
+ 'value',
|
|
|
+ track_path,
|
|
|
)
|
|
|
+
|
|
|
+ for frame in range(first_frame, last_frame):
|
|
|
+ value_track.add_frame_data(frame, fcurve.evaluate(frame))
|
|
|
+
|
|
|
+ animation_resource.add_track(value_track)
|
|
|
+
|
|
|
+
|
|
|
+def export_light_action(light_node, animation_player,
|
|
|
+ blender_lamp, action, animation_resource):
|
|
|
+ """Export light(lamp in Blender) action"""
|
|
|
+ if blender_lamp.animation_data is None:
|
|
|
+ return
|
|
|
+
|
|
|
+ first_frame, last_frame = get_action_frame_range(action)
|
|
|
+ base_node_path = NodePath(
|
|
|
+ animation_player.parent.get_path(), light_node.get_path()
|
|
|
+ )
|
|
|
+
|
|
|
+ animation_resource.add_simple_value_track(
|
|
|
+ action.fcurves, 'use_negative',
|
|
|
+ partial(
|
|
|
+ build_const_interp_value_track,
|
|
|
+ base_node_path.new_copy('light_negative'),
|
|
|
+ lambda x: x > 0.0,
|
|
|
)
|
|
|
+ )
|
|
|
+
|
|
|
+ animation_resource.add_track_via_attr_mapping(
|
|
|
+ action.fcurves,
|
|
|
+ light_node.attribute_conversion,
|
|
|
+ base_node_path
|
|
|
+ )
|
|
|
+
|
|
|
+ # color tracks is not one-one mapping to fcurve, they
|
|
|
+ # need to be treated like transform track
|
|
|
+ color_frame_values_map = collections.OrderedDict()
|
|
|
+
|
|
|
+ for fcurve in action.fcurves:
|
|
|
+ _, attribute = split_fcurve_data_path(fcurve.data_path)
|
|
|
+
|
|
|
+ if attribute in ('color', 'shadow_color'):
|
|
|
+ if attribute not in color_frame_values_map:
|
|
|
+ color_frame_values_map[attribute] = [
|
|
|
+ mathutils.Color()
|
|
|
+ for _ in range(first_frame, last_frame)
|
|
|
+ ]
|
|
|
+ color_list = color_frame_values_map[attribute]
|
|
|
+ for frame in range(first_frame, last_frame):
|
|
|
+ color_list[frame - first_frame][
|
|
|
+ fcurve.array_index] = fcurve.evaluate(frame)
|
|
|
+
|
|
|
+ for attribute, frame_value_list in color_frame_values_map.items():
|
|
|
+ if attribute == 'color':
|
|
|
+ track_path = base_node_path.new_copy('light_color')
|
|
|
+ else:
|
|
|
+ track_path = base_node_path.new_copy('shadow_color')
|
|
|
+
|
|
|
+ animation_resource.add_track(
|
|
|
+ Track(
|
|
|
+ 'value',
|
|
|
+ track_path,
|
|
|
+ range(first_frame, last_frame),
|
|
|
+ frame_value_list
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def export_camera_action(camera_node, animation_player,
|
|
|
+ blender_cam, action, animation_resource):
|
|
|
+ """Export camera action"""
|
|
|
+ if blender_cam.animation_data is None:
|
|
|
+ return
|
|
|
+
|
|
|
+ first_frame, last_frame = get_action_frame_range(action)
|
|
|
+ base_node_path = NodePath(
|
|
|
+ animation_player.parent.get_path(), camera_node.get_path()
|
|
|
+ )
|
|
|
+
|
|
|
+ animation_resource.add_track_via_attr_mapping(
|
|
|
+ action.fcurves,
|
|
|
+ camera_node.attribute_conversion,
|
|
|
+ base_node_path
|
|
|
+ )
|
|
|
+
|
|
|
+ animation_resource.add_simple_value_track(
|
|
|
+ action.fcurves, 'type',
|
|
|
+ partial(
|
|
|
+ build_const_interp_value_track,
|
|
|
+ base_node_path.new_copy('projection'),
|
|
|
+ lambda x: 0 if x == 0.0 else 1,
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ # blender use sensor_width and f_lens to animate fov
|
|
|
+ # while godot directly use fov
|
|
|
+ fov_animated = False
|
|
|
+ focal_len_list = list()
|
|
|
+ sensor_size_list = list()
|
|
|
+
|
|
|
+ if action.fcurves.find('lens') is not None:
|
|
|
+ fcurve = action.fcurves.find('lens')
|
|
|
+ fov_animated = True
|
|
|
+ for frame in range(first_frame, last_frame):
|
|
|
+ focal_len_list.append(fcurve.evaluate(frame))
|
|
|
+ if action.fcurves.find('sensor_width') is not None:
|
|
|
+ fcurve = action.fcurves.find('sensor_width')
|
|
|
+ fov_animated = True
|
|
|
+ for frame in range(first_frame, last_frame):
|
|
|
+ sensor_size_list.append(fcurve.evaluate(frame))
|
|
|
+
|
|
|
+ if fov_animated:
|
|
|
+ # export fov track
|
|
|
+ if not focal_len_list:
|
|
|
+ focal_len_list = [blender_cam.lens
|
|
|
+ for _ in range(first_frame, last_frame)]
|
|
|
+ if not sensor_size_list:
|
|
|
+ sensor_size_list = [blender_cam.sensor_width
|
|
|
+ for _ in range(first_frame, last_frame)]
|
|
|
+
|
|
|
+ fov_list = list()
|
|
|
+ for index, flen in enumerate(focal_len_list):
|
|
|
+ fov_list.append(2 * math.degrees(
|
|
|
+ math.atan(
|
|
|
+ sensor_size_list[index]/2/flen
|
|
|
+ )
|
|
|
+ ))
|
|
|
+
|
|
|
+ animation_resource.add_track(Track(
|
|
|
+ 'value',
|
|
|
+ base_node_path.new_copy('fov'),
|
|
|
+ range(first_frame, last_frame),
|
|
|
+ fov_list
|
|
|
+ ))
|
|
|
+
|
|
|
|
|
|
# ----------------------------------------------
|
|
|
|
|
|
|
|
|
ACTION_EXPORTER_MAP = {
|
|
|
'transform': export_transform_action,
|
|
|
+ 'shapekey': export_shapekey_action,
|
|
|
+ 'light': export_light_action,
|
|
|
+ 'camera': export_camera_action,
|
|
|
}
|
|
|
|
|
|
|
|
@@ -322,6 +630,9 @@ def export_animation_data(escn_file, export_settings, godot_node,
|
|
|
"""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`"""
|
|
|
+ if (blender_object.animation_data is None or
|
|
|
+ not export_settings['use_export_animation']):
|
|
|
+ return
|
|
|
animation_player = get_animation_player(
|
|
|
escn_file, export_settings, godot_node)
|
|
|
|