mirror of
https://github.com/Doodle3D/Doodle3D-Slicer.git
synced 2024-09-19 18:20:06 +02:00
578 lines
16 KiB
Python
578 lines
16 KiB
Python
|
"""
|
||
|
Module for handling the parsing of skeletal animation data.
|
||
|
"""
|
||
|
|
||
|
import math
|
||
|
import mathutils
|
||
|
from bpy import data, context
|
||
|
from .. import constants, logger
|
||
|
|
||
|
|
||
|
def pose_animation(armature, options):
|
||
|
"""Query armature animation using pose bones
|
||
|
|
||
|
:param armature:
|
||
|
:param options:
|
||
|
:returns: list dictionaries containing animationdata
|
||
|
:rtype: [{}, {}, ...]
|
||
|
|
||
|
"""
|
||
|
logger.debug("animation.pose_animation(%s)", armature)
|
||
|
func = _parse_pose_action
|
||
|
return _parse_action(func, armature, options)
|
||
|
|
||
|
|
||
|
def rest_animation(armature, options):
|
||
|
"""Query armature animation (REST position)
|
||
|
|
||
|
:param armature:
|
||
|
:param options:
|
||
|
:returns: list dictionaries containing animationdata
|
||
|
:rtype: [{}, {}, ...]
|
||
|
|
||
|
"""
|
||
|
logger.debug("animation.rest_animation(%s)", armature)
|
||
|
func = _parse_rest_action
|
||
|
return _parse_action(func, armature, options)
|
||
|
|
||
|
|
||
|
def _parse_action(func, armature, options):
|
||
|
"""
|
||
|
|
||
|
:param func:
|
||
|
:param armature:
|
||
|
:param options:
|
||
|
|
||
|
"""
|
||
|
animations = []
|
||
|
logger.info("Parsing %d actions", len(data.actions))
|
||
|
for action in data.actions:
|
||
|
logger.info("Parsing action %s", action.name)
|
||
|
animation = func(action, armature, options)
|
||
|
animations.append(animation)
|
||
|
return animations
|
||
|
|
||
|
|
||
|
def _parse_rest_action(action, armature, options):
|
||
|
"""
|
||
|
|
||
|
:param action:
|
||
|
:param armature:
|
||
|
:param options:
|
||
|
|
||
|
"""
|
||
|
end_frame = action.frame_range[1]
|
||
|
start_frame = action.frame_range[0]
|
||
|
frame_length = end_frame - start_frame
|
||
|
rot = armature.matrix_world.decompose()[1]
|
||
|
rotation_matrix = rot.to_matrix()
|
||
|
hierarchy = []
|
||
|
parent_index = -1
|
||
|
frame_step = options.get(constants.FRAME_STEP, 1)
|
||
|
fps = context.scene.render.fps
|
||
|
|
||
|
start = int(start_frame)
|
||
|
end = int(end_frame / frame_step) + 1
|
||
|
|
||
|
for bone in armature.data.bones:
|
||
|
# I believe this was meant to skip control bones, may
|
||
|
# not be useful. needs more testing
|
||
|
if bone.use_deform is False:
|
||
|
logger.info("Skipping animation data for bone %s", bone.name)
|
||
|
continue
|
||
|
|
||
|
logger.info("Parsing animation data for bone %s", bone.name)
|
||
|
|
||
|
keys = []
|
||
|
for frame in range(start, end):
|
||
|
computed_frame = frame * frame_step
|
||
|
pos, pchange = _position(bone, computed_frame,
|
||
|
action, armature.matrix_world)
|
||
|
rot, rchange = _rotation(bone, computed_frame,
|
||
|
action, rotation_matrix)
|
||
|
rot = _normalize_quaternion(rot)
|
||
|
|
||
|
pos_x, pos_y, pos_z = pos.x, pos.z, -pos.y
|
||
|
rot_x, rot_y, rot_z, rot_w = rot.x, rot.z, -rot.y, rot.w
|
||
|
|
||
|
if frame == start_frame:
|
||
|
|
||
|
time = (frame * frame_step - start_frame) / fps
|
||
|
# @TODO: missing scale values
|
||
|
keyframe = {
|
||
|
constants.TIME: time,
|
||
|
constants.POS: [pos_x, pos_y, pos_z],
|
||
|
constants.ROT: [rot_x, rot_y, rot_z, rot_w],
|
||
|
constants.SCL: [1, 1, 1]
|
||
|
}
|
||
|
keys.append(keyframe)
|
||
|
|
||
|
# END-FRAME: needs pos, rot and scl attributes
|
||
|
# with animation length (required frame)
|
||
|
|
||
|
elif frame == end_frame / frame_step:
|
||
|
|
||
|
time = frame_length / fps
|
||
|
keyframe = {
|
||
|
constants.TIME: time,
|
||
|
constants.POS: [pos_x, pos_y, pos_z],
|
||
|
constants.ROT: [rot_x, rot_y, rot_z, rot_w],
|
||
|
constants.SCL: [1, 1, 1]
|
||
|
}
|
||
|
keys.append(keyframe)
|
||
|
|
||
|
# MIDDLE-FRAME: needs only one of the attributes,
|
||
|
# can be an empty frame (optional frame)
|
||
|
|
||
|
elif pchange is True or rchange is True:
|
||
|
|
||
|
time = (frame * frame_step - start_frame) / fps
|
||
|
|
||
|
if pchange is True and rchange is True:
|
||
|
keyframe = {
|
||
|
constants.TIME: time,
|
||
|
constants.POS: [pos_x, pos_y, pos_z],
|
||
|
constants.ROT: [rot_x, rot_y, rot_z, rot_w]
|
||
|
}
|
||
|
elif pchange is True:
|
||
|
keyframe = {
|
||
|
constants.TIME: time,
|
||
|
constants.POS: [pos_x, pos_y, pos_z]
|
||
|
}
|
||
|
elif rchange is True:
|
||
|
keyframe = {
|
||
|
constants.TIME: time,
|
||
|
constants.ROT: [rot_x, rot_y, rot_z, rot_w]
|
||
|
}
|
||
|
|
||
|
keys.append(keyframe)
|
||
|
|
||
|
hierarchy.append({
|
||
|
constants.KEYS: keys,
|
||
|
constants.PARENT: parent_index
|
||
|
})
|
||
|
parent_index += 1
|
||
|
|
||
|
animation = {
|
||
|
constants.HIERARCHY: hierarchy,
|
||
|
constants.LENGTH: frame_length / fps,
|
||
|
constants.FPS: fps,
|
||
|
constants.NAME: action.name
|
||
|
}
|
||
|
|
||
|
return animation
|
||
|
|
||
|
|
||
|
def _parse_pose_action(action, armature, options):
|
||
|
"""
|
||
|
|
||
|
:param action:
|
||
|
:param armature:
|
||
|
:param options:
|
||
|
|
||
|
"""
|
||
|
# @TODO: this seems to fail in batch mode meaning the
|
||
|
# user has to have th GUI open. need to improve
|
||
|
# this logic to allow batch processing, if Blender
|
||
|
# chooses to behave....
|
||
|
current_context = context.area.type
|
||
|
context.area.type = 'DOPESHEET_EDITOR'
|
||
|
context.space_data.mode = 'ACTION'
|
||
|
context.area.spaces.active.action = action
|
||
|
|
||
|
armature_matrix = armature.matrix_world
|
||
|
fps = context.scene.render.fps
|
||
|
|
||
|
end_frame = action.frame_range[1]
|
||
|
start_frame = action.frame_range[0]
|
||
|
frame_length = end_frame - start_frame
|
||
|
|
||
|
frame_step = options.get(constants.FRAME_STEP, 1)
|
||
|
used_frames = int(frame_length / frame_step) + 1
|
||
|
|
||
|
keys = []
|
||
|
channels_location = []
|
||
|
channels_rotation = []
|
||
|
channels_scale = []
|
||
|
|
||
|
for pose_bone in armature.pose.bones:
|
||
|
logger.info("Processing channels for %s",
|
||
|
pose_bone.bone.name)
|
||
|
keys.append([])
|
||
|
channels_location.append(
|
||
|
_find_channels(action,
|
||
|
pose_bone.bone,
|
||
|
'location'))
|
||
|
channels_rotation.append(
|
||
|
_find_channels(action,
|
||
|
pose_bone.bone,
|
||
|
'rotation_quaternion'))
|
||
|
channels_rotation.append(
|
||
|
_find_channels(action,
|
||
|
pose_bone.bone,
|
||
|
'rotation_euler'))
|
||
|
channels_scale.append(
|
||
|
_find_channels(action,
|
||
|
pose_bone.bone,
|
||
|
'scale'))
|
||
|
|
||
|
frame_step = options[constants.FRAME_STEP]
|
||
|
frame_index_as_time = options[constants.FRAME_INDEX_AS_TIME]
|
||
|
for frame_index in range(0, used_frames):
|
||
|
if frame_index == used_frames - 1:
|
||
|
frame = end_frame
|
||
|
else:
|
||
|
frame = start_frame + frame_index * frame_step
|
||
|
|
||
|
logger.info("Processing frame %d", frame)
|
||
|
|
||
|
time = frame - start_frame
|
||
|
if frame_index_as_time is False:
|
||
|
time = time / fps
|
||
|
|
||
|
context.scene.frame_set(frame)
|
||
|
|
||
|
bone_index = 0
|
||
|
|
||
|
def has_keyframe_at(channels, frame):
|
||
|
"""
|
||
|
|
||
|
:param channels:
|
||
|
:param frame:
|
||
|
|
||
|
"""
|
||
|
def find_keyframe_at(channel, frame):
|
||
|
"""
|
||
|
|
||
|
:param channel:
|
||
|
:param frame:
|
||
|
|
||
|
"""
|
||
|
for keyframe in channel.keyframe_points:
|
||
|
if keyframe.co[0] == frame:
|
||
|
return keyframe
|
||
|
return None
|
||
|
|
||
|
for channel in channels:
|
||
|
if not find_keyframe_at(channel, frame) is None:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
for pose_bone in armature.pose.bones:
|
||
|
|
||
|
logger.info("Processing bone %s", pose_bone.bone.name)
|
||
|
if pose_bone.parent is None:
|
||
|
bone_matrix = armature_matrix * pose_bone.matrix
|
||
|
else:
|
||
|
parent_matrix = armature_matrix * pose_bone.parent.matrix
|
||
|
bone_matrix = armature_matrix * pose_bone.matrix
|
||
|
bone_matrix = parent_matrix.inverted() * bone_matrix
|
||
|
|
||
|
pos, rot, scl = bone_matrix.decompose()
|
||
|
rot = _normalize_quaternion(rot)
|
||
|
|
||
|
pchange = True or has_keyframe_at(
|
||
|
channels_location[bone_index], frame)
|
||
|
rchange = True or has_keyframe_at(
|
||
|
channels_rotation[bone_index], frame)
|
||
|
schange = True or has_keyframe_at(
|
||
|
channels_scale[bone_index], frame)
|
||
|
|
||
|
pos = (pos.x, pos.z, -pos.y)
|
||
|
rot = (rot.x, rot.z, -rot.y, rot.w)
|
||
|
scl = (scl.x, scl.z, scl.y)
|
||
|
|
||
|
keyframe = {constants.TIME: time}
|
||
|
if frame == start_frame or frame == end_frame:
|
||
|
keyframe.update({
|
||
|
constants.POS: pos,
|
||
|
constants.ROT: rot,
|
||
|
constants.SCL: scl
|
||
|
})
|
||
|
elif any([pchange, rchange, schange]):
|
||
|
if pchange is True:
|
||
|
keyframe[constants.POS] = pos
|
||
|
if rchange is True:
|
||
|
keyframe[constants.ROT] = rot
|
||
|
if schange is True:
|
||
|
keyframe[constants.SCL] = scl
|
||
|
|
||
|
if len(keyframe.keys()) > 1:
|
||
|
logger.info("Recording keyframe data for %s %s",
|
||
|
pose_bone.bone.name, str(keyframe))
|
||
|
keys[bone_index].append(keyframe)
|
||
|
else:
|
||
|
logger.info("No anim data to record for %s",
|
||
|
pose_bone.bone.name)
|
||
|
|
||
|
bone_index += 1
|
||
|
|
||
|
hierarchy = []
|
||
|
bone_index = 0
|
||
|
for pose_bone in armature.pose.bones:
|
||
|
hierarchy.append({
|
||
|
constants.PARENT: bone_index - 1,
|
||
|
constants.KEYS: keys[bone_index]
|
||
|
})
|
||
|
bone_index += 1
|
||
|
|
||
|
if frame_index_as_time is False:
|
||
|
frame_length = frame_length / fps
|
||
|
|
||
|
context.scene.frame_set(start_frame)
|
||
|
context.area.type = current_context
|
||
|
|
||
|
animation = {
|
||
|
constants.HIERARCHY: hierarchy,
|
||
|
constants.LENGTH: frame_length,
|
||
|
constants.FPS: fps,
|
||
|
constants.NAME: action.name
|
||
|
}
|
||
|
|
||
|
return animation
|
||
|
|
||
|
|
||
|
def _find_channels(action, bone, channel_type):
|
||
|
"""
|
||
|
|
||
|
:param action:
|
||
|
:param bone:
|
||
|
:param channel_type:
|
||
|
|
||
|
"""
|
||
|
result = []
|
||
|
|
||
|
if len(action.groups):
|
||
|
|
||
|
group_index = -1
|
||
|
for index, group in enumerate(action.groups):
|
||
|
if group.name == bone.name:
|
||
|
group_index = index
|
||
|
# @TODO: break?
|
||
|
|
||
|
if group_index > -1:
|
||
|
for channel in action.groups[group_index].channels:
|
||
|
if channel_type in channel.data_path:
|
||
|
result.append(channel)
|
||
|
|
||
|
else:
|
||
|
bone_label = '"%s"' % bone.name
|
||
|
for channel in action.fcurves:
|
||
|
data_path = [bone_label in channel.data_path,
|
||
|
channel_type in channel.data_path]
|
||
|
if all(data_path):
|
||
|
result.append(channel)
|
||
|
|
||
|
return result
|
||
|
|
||
|
|
||
|
def _position(bone, frame, action, armature_matrix):
|
||
|
"""
|
||
|
|
||
|
:param bone:
|
||
|
:param frame:
|
||
|
:param action:
|
||
|
:param armature_matrix:
|
||
|
|
||
|
"""
|
||
|
|
||
|
position = mathutils.Vector((0, 0, 0))
|
||
|
change = False
|
||
|
|
||
|
ngroups = len(action.groups)
|
||
|
|
||
|
if ngroups > 0:
|
||
|
|
||
|
index = 0
|
||
|
|
||
|
for i in range(ngroups):
|
||
|
if action.groups[i].name == bone.name:
|
||
|
index = i
|
||
|
|
||
|
for channel in action.groups[index].channels:
|
||
|
if "location" in channel.data_path:
|
||
|
has_changed = _handle_position_channel(
|
||
|
channel, frame, position)
|
||
|
change = change or has_changed
|
||
|
|
||
|
else:
|
||
|
|
||
|
bone_label = '"%s"' % bone.name
|
||
|
|
||
|
for channel in action.fcurves:
|
||
|
data_path = channel.data_path
|
||
|
if bone_label in data_path and "location" in data_path:
|
||
|
has_changed = _handle_position_channel(
|
||
|
channel, frame, position)
|
||
|
change = change or has_changed
|
||
|
|
||
|
position = position * bone.matrix_local.inverted()
|
||
|
|
||
|
if bone.parent is None:
|
||
|
|
||
|
position.x += bone.head.x
|
||
|
position.y += bone.head.y
|
||
|
position.z += bone.head.z
|
||
|
|
||
|
else:
|
||
|
|
||
|
parent = bone.parent
|
||
|
|
||
|
parent_matrix = parent.matrix_local.inverted()
|
||
|
diff = parent.tail_local - parent.head_local
|
||
|
|
||
|
position.x += (bone.head * parent_matrix).x + diff.x
|
||
|
position.y += (bone.head * parent_matrix).y + diff.y
|
||
|
position.z += (bone.head * parent_matrix).z + diff.z
|
||
|
|
||
|
return armature_matrix*position, change
|
||
|
|
||
|
|
||
|
def _rotation(bone, frame, action, armature_matrix):
|
||
|
"""
|
||
|
|
||
|
:param bone:
|
||
|
:param frame:
|
||
|
:param action:
|
||
|
:param armature_matrix:
|
||
|
|
||
|
"""
|
||
|
|
||
|
# TODO: calculate rotation also from rotation_euler channels
|
||
|
|
||
|
rotation = mathutils.Vector((0, 0, 0, 1))
|
||
|
|
||
|
change = False
|
||
|
|
||
|
ngroups = len(action.groups)
|
||
|
|
||
|
# animation grouped by bones
|
||
|
|
||
|
if ngroups > 0:
|
||
|
|
||
|
index = -1
|
||
|
|
||
|
for i in range(ngroups):
|
||
|
if action.groups[i].name == bone.name:
|
||
|
index = i
|
||
|
|
||
|
if index > -1:
|
||
|
for channel in action.groups[index].channels:
|
||
|
if "quaternion" in channel.data_path:
|
||
|
has_changed = _handle_rotation_channel(
|
||
|
channel, frame, rotation)
|
||
|
change = change or has_changed
|
||
|
|
||
|
# animation in raw fcurves
|
||
|
|
||
|
else:
|
||
|
|
||
|
bone_label = '"%s"' % bone.name
|
||
|
|
||
|
for channel in action.fcurves:
|
||
|
data_path = channel.data_path
|
||
|
if bone_label in data_path and "quaternion" in data_path:
|
||
|
has_changed = _handle_rotation_channel(
|
||
|
channel, frame, rotation)
|
||
|
change = change or has_changed
|
||
|
|
||
|
rot3 = rotation.to_3d()
|
||
|
rotation.xyz = rot3 * bone.matrix_local.inverted()
|
||
|
rotation.xyz = armature_matrix * rotation.xyz
|
||
|
|
||
|
return rotation, change
|
||
|
|
||
|
|
||
|
def _handle_rotation_channel(channel, frame, rotation):
|
||
|
"""
|
||
|
|
||
|
:param channel:
|
||
|
:param frame:
|
||
|
:param rotation:
|
||
|
|
||
|
"""
|
||
|
|
||
|
change = False
|
||
|
|
||
|
if channel.array_index in [0, 1, 2, 3]:
|
||
|
|
||
|
for keyframe in channel.keyframe_points:
|
||
|
if keyframe.co[0] == frame:
|
||
|
change = True
|
||
|
|
||
|
value = channel.evaluate(frame)
|
||
|
|
||
|
if channel.array_index == 1:
|
||
|
rotation.x = value
|
||
|
|
||
|
elif channel.array_index == 2:
|
||
|
rotation.y = value
|
||
|
|
||
|
elif channel.array_index == 3:
|
||
|
rotation.z = value
|
||
|
|
||
|
elif channel.array_index == 0:
|
||
|
rotation.w = value
|
||
|
|
||
|
return change
|
||
|
|
||
|
|
||
|
def _handle_position_channel(channel, frame, position):
|
||
|
"""
|
||
|
|
||
|
:param channel:
|
||
|
:param frame:
|
||
|
:param position:
|
||
|
|
||
|
"""
|
||
|
|
||
|
change = False
|
||
|
|
||
|
if channel.array_index in [0, 1, 2]:
|
||
|
for keyframe in channel.keyframe_points:
|
||
|
if keyframe.co[0] == frame:
|
||
|
change = True
|
||
|
|
||
|
value = channel.evaluate(frame)
|
||
|
|
||
|
if channel.array_index == 0:
|
||
|
position.x = value
|
||
|
|
||
|
if channel.array_index == 1:
|
||
|
position.y = value
|
||
|
|
||
|
if channel.array_index == 2:
|
||
|
position.z = value
|
||
|
|
||
|
return change
|
||
|
|
||
|
|
||
|
def _quaternion_length(quat):
|
||
|
"""Calculate the length of a quaternion
|
||
|
|
||
|
:param quat: Blender quaternion object
|
||
|
:rtype: float
|
||
|
|
||
|
"""
|
||
|
return math.sqrt(quat.x * quat.x + quat.y * quat.y +
|
||
|
quat.z * quat.z + quat.w * quat.w)
|
||
|
|
||
|
|
||
|
def _normalize_quaternion(quat):
|
||
|
"""Normalize a quaternion
|
||
|
|
||
|
:param quat: Blender quaternion object
|
||
|
:returns: generic quaternion enum object with normalized values
|
||
|
:rtype: object
|
||
|
|
||
|
"""
|
||
|
enum = type('Enum', (), {'x': 0, 'y': 0, 'z': 0, 'w': 1})
|
||
|
length = _quaternion_length(quat)
|
||
|
if length is not 0:
|
||
|
length = 1 / length
|
||
|
enum.x = quat.x * length
|
||
|
enum.y = quat.y * length
|
||
|
enum.z = quat.z * length
|
||
|
enum.w = quat.w * length
|
||
|
return enum
|