glTF exporter: Basic SK driver export (driven by armature animation)

This commit is contained in:
Julien Duroure 2019-12-14 09:02:16 +01:00
parent 76fc4142b5
commit f505743b2f
8 changed files with 226 additions and 34 deletions

View File

@ -15,7 +15,7 @@
bl_info = {
'name': 'glTF 2.0 format',
'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
"version": (1, 1, 25),
"version": (1, 1, 26),
'blender': (2, 81, 6),
'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0',

View File

@ -81,3 +81,15 @@ def texture_transform_gltf_to_blender(texture_transform):
'scale': [scale[0], scale[1]],
}
def get_target(property):
return {
"delta_location": "translation",
"delta_rotation_euler": "rotation",
"location": "translation",
"rotation_axis_angle": "rotation",
"rotation_euler": "rotation",
"rotation_quaternion": "rotation",
"scale": "scale",
"value": "weights"
}.get(property)

View File

@ -27,13 +27,14 @@ def gather_animation_channel_target(channels: typing.Tuple[bpy.types.FCurve],
blender_object: bpy.types.Object,
bake_bone: typing.Union[str, None],
bake_channel: typing.Union[str, None],
driver_obj,
export_settings
) -> gltf2_io.AnimationChannelTarget:
animation_channel_target = gltf2_io.AnimationChannelTarget(
extensions=__gather_extensions(channels, blender_object, export_settings, bake_bone),
extras=__gather_extras(channels, blender_object, export_settings, bake_bone),
node=__gather_node(channels, blender_object, export_settings, bake_bone),
node=__gather_node(channels, blender_object, export_settings, bake_bone, driver_obj),
path=__gather_path(channels, blender_object, export_settings, bake_bone, bake_channel)
)
@ -66,8 +67,13 @@ def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
def __gather_node(channels: typing.Tuple[bpy.types.FCurve],
blender_object: bpy.types.Object,
export_settings,
bake_bone: typing.Union[str, None]
bake_bone: typing.Union[str, None],
driver_obj
) -> gltf2_io.Node:
if driver_obj is not None:
return gltf2_blender_gather_nodes.gather_node(driver_obj, None, export_settings)
if blender_object.type == "ARMATURE":
# TODO: get joint from fcurve data_path and gather_joint

View File

@ -23,6 +23,7 @@ from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_samplers
from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channel_target
from io_scene_gltf2.blender.exp import gltf2_blender_get
from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
from io_scene_gltf2.blender.exp import gltf2_blender_gather_drivers
from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions
@ -77,18 +78,41 @@ def gather_animation_channels(blender_action: bpy.types.Action,
p,
bake_range_start,
bake_range_end,
blender_action.name)
blender_action.name,
None)
channels.append(channel)
# Retrieve channels for drivers, if needed
drivers_to_manage = gltf2_blender_gather_drivers.get_sk_drivers(blender_object)
for obj, fcurves in drivers_to_manage:
channel = __gather_animation_channel(
fcurves,
blender_object,
export_settings,
None,
None,
bake_range_start,
bake_range_end,
blender_action.name,
obj)
channels.append(channel)
else:
for channel_group in __get_channel_groups(blender_action, blender_object, export_settings):
channel_group_sorted = __get_channel_group_sorted(channel_group, blender_object)
if len(channel_group_sorted) == 0:
# Only errors on channels, ignoring
continue
channel = __gather_animation_channel(channel_group_sorted, blender_object, export_settings, None, None, bake_range_start, bake_range_end, blender_action.name)
channel = __gather_animation_channel(channel_group_sorted, blender_object, export_settings, None, None, bake_range_start, bake_range_end, blender_action.name, None)
if channel is not None:
channels.append(channel)
# resetting driver caches
gltf2_blender_gather_drivers.get_sk_driver_values.reset_cache()
gltf2_blender_gather_drivers.get_sk_drivers.reset_cache()
return channels
def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender_object: bpy.types.Object):
@ -145,7 +169,8 @@ def __gather_animation_channel(channels: typing.Tuple[bpy.types.FCurve],
bake_channel: typing.Union[str, None],
bake_range_start,
bake_range_end,
action_name: str
action_name: str,
driver_obj
) -> typing.Union[gltf2_io.AnimationChannel, None]:
if not __filter_animation_channel(channels, blender_object, export_settings):
return None
@ -153,8 +178,8 @@ def __gather_animation_channel(channels: typing.Tuple[bpy.types.FCurve],
animation_channel = gltf2_io.AnimationChannel(
extensions=__gather_extensions(channels, blender_object, export_settings, bake_bone),
extras=__gather_extras(channels, blender_object, export_settings, bake_bone),
sampler=__gather_sampler(channels, blender_object, export_settings, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name),
target=__gather_target(channels, blender_object, export_settings, bake_bone, bake_channel)
sampler=__gather_sampler(channels, blender_object, export_settings, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name, driver_obj),
target=__gather_target(channels, blender_object, export_settings, bake_bone, bake_channel, driver_obj)
)
export_user_extensions('gather_animation_channel_hook',
@ -201,7 +226,8 @@ def __gather_sampler(channels: typing.Tuple[bpy.types.FCurve],
bake_channel: typing.Union[str, None],
bake_range_start,
bake_range_end,
action_name
action_name,
driver_obj
) -> gltf2_io.AnimationSampler:
return gltf2_blender_gather_animation_samplers.gather_animation_sampler(
channels,
@ -211,6 +237,7 @@ def __gather_sampler(channels: typing.Tuple[bpy.types.FCurve],
bake_range_start,
bake_range_end,
action_name,
driver_obj,
export_settings
)
@ -219,10 +246,11 @@ def __gather_target(channels: typing.Tuple[bpy.types.FCurve],
blender_object: bpy.types.Object,
export_settings,
bake_bone: typing.Union[str, None],
bake_channel: typing.Union[str, None]
bake_channel: typing.Union[str, None],
driver_obj
) -> gltf2_io.AnimationChannelTarget:
return gltf2_blender_gather_animation_channel_target.gather_animation_channel_target(
channels, blender_object, bake_bone, bake_channel, export_settings)
channels, blender_object, bake_bone, bake_channel, driver_obj, export_settings)
def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings):

View File

@ -20,6 +20,7 @@ from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, boneca
from io_scene_gltf2.blender.com import gltf2_blender_math
from io_scene_gltf2.blender.exp import gltf2_blender_get
from io_scene_gltf2.blender.exp import gltf2_blender_extract
from io_scene_gltf2.blender.exp.gltf2_blender_gather_drivers import get_sk_drivers, get_sk_driver_values
from . import gltf2_blender_export_keys
from io_scene_gltf2.io.com import gltf2_io_debug
@ -164,6 +165,13 @@ def get_bone_matrix(blender_object_if_armature: typing.Optional[bpy.types.Object
matrix = pbone.matrix
matrix = blender_object_if_armature.convert_space(pose_bone=pbone, matrix=matrix, from_space='POSE', to_space='LOCAL')
data[frame][pbone.name] = matrix
# If some drivers must be evaluated, do it here, to avoid to have to change frame by frame later
drivers_to_manage = get_sk_drivers(blender_object_if_armature)
for dr_obj, dr_fcurves in drivers_to_manage:
vals = get_sk_driver_values(dr_obj, frame, dr_fcurves)
frame += step
return data
@ -178,10 +186,11 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
bake_range_start,
bake_range_end,
action_name: str,
driver_obj,
export_settings
) -> typing.List[Keyframe]:
"""Convert the blender action groups' fcurves to keyframes for use in glTF."""
if bake_bone is None:
if bake_bone is None and driver_obj is None:
# Find the start and end of the whole action group
# Note: channels has some None items only for SK if some SK are not animated
ranges = [channel.range() for channel in channels if channel is not None]
@ -197,7 +206,7 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
# Bake the animation, by evaluating the animation for all frames
# TODO: maybe baking can also be done with FCurve.convert_to_samples
if blender_object_if_armature is not None:
if blender_object_if_armature is not None and driver_obj is None:
if bake_bone is None:
pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature,
channels[0].data_path)
@ -238,9 +247,13 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
"scale": scale
}[target_property]
else:
# Note: channels has some None items only for SK if some SK are not animated
key.value = [c.evaluate(frame) for c in channels if c is not None]
complete_key(key, non_keyed_values)
if driver_obj is None:
# Note: channels has some None items only for SK if some SK are not animated
key.value = [c.evaluate(frame) for c in channels if c is not None]
complete_key(key, non_keyed_values)
else:
key.value = get_sk_driver_values(driver_obj, frame, channels)
complete_key(key, non_keyed_values)
keyframes.append(key)
frame += step
else:

View File

@ -38,11 +38,12 @@ def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve],
bake_range_start,
bake_range_end,
action_name: str,
driver_obj,
export_settings
) -> gltf2_io.AnimationSampler:
blender_object_if_armature = blender_object if blender_object.type == "ARMATURE" else None
if blender_object_if_armature is not None:
if blender_object_if_armature is not None and driver_obj is None:
if bake_bone is None:
pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature,
channels[0].data_path)
@ -53,6 +54,7 @@ def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve],
non_keyed_values = __gather_non_keyed_values(channels, blender_object,
blender_object_if_armature, pose_bone_if_armature,
bake_channel,
driver_obj,
export_settings)
@ -60,7 +62,7 @@ def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve],
extensions=__gather_extensions(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel),
extras=__gather_extras(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel),
input=__gather_input(channels, blender_object_if_armature, non_keyed_values,
bake_bone, bake_channel, bake_range_start, bake_range_end, action_name, export_settings),
bake_bone, bake_channel, bake_range_start, bake_range_end, action_name, driver_obj, export_settings),
interpolation=__gather_interpolation(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel),
output=__gather_output(channels, blender_object.matrix_parent_inverse.copy().freeze(),
blender_object_if_armature,
@ -70,6 +72,7 @@ def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve],
bake_range_start,
bake_range_end,
action_name,
driver_obj,
export_settings)
)
@ -91,16 +94,23 @@ def __gather_non_keyed_values(channels: typing.Tuple[bpy.types.FCurve],
blender_object_if_armature: typing.Optional[bpy.types.Object],
pose_bone_if_armature: typing.Optional[bpy.types.PoseBone],
bake_channel: typing.Union[str, None],
driver_obj,
export_settings
) -> typing.Tuple[typing.Optional[float]]:
non_keyed_values = []
obj = blender_object if driver_obj is None else driver_obj
# Note: channels has some None items only for SK if some SK are not animated
if None not in channels:
# classic case for object TRS or bone TRS
# Or if all morph target are animated
if driver_obj is not None:
# driver of SK
return tuple([None] * len(channels))
if bake_channel is None:
target = channels[0].data_path.split('.')[-1]
else:
@ -129,26 +139,26 @@ def __gather_non_keyed_values(channels: typing.Tuple[bpy.types.FCurve],
for i in range(0, length):
if bake_channel is not None:
non_keyed_values.append({
"delta_location" : blender_object.delta_location,
"delta_rotation_euler" : blender_object.delta_rotation_euler,
"location" : blender_object.location,
"rotation_axis_angle" : blender_object.rotation_axis_angle,
"rotation_euler" : blender_object.rotation_euler,
"rotation_quaternion" : blender_object.rotation_quaternion,
"scale" : blender_object.scale
"delta_location" : obj.delta_location,
"delta_rotation_euler" : obj.delta_rotation_euler,
"location" : obj.location,
"rotation_axis_angle" : obj.rotation_axis_angle,
"rotation_euler" : obj.rotation_euler,
"rotation_quaternion" : obj.rotation_quaternion,
"scale" : obj.scale
}[target][i])
elif i in indices:
non_keyed_values.append(None)
else:
if blender_object_if_armature is None:
non_keyed_values.append({
"delta_location" : blender_object.delta_location,
"delta_rotation_euler" : blender_object.delta_rotation_euler,
"location" : blender_object.location,
"rotation_axis_angle" : blender_object.rotation_axis_angle,
"rotation_euler" : blender_object.rotation_euler,
"rotation_quaternion" : blender_object.rotation_quaternion,
"scale" : blender_object.scale
"delta_location" : obj.delta_location,
"delta_rotation_euler" : obj.delta_rotation_euler,
"location" : obj.location,
"rotation_axis_angle" : obj.rotation_axis_angle,
"rotation_euler" : obj.rotation_euler,
"rotation_quaternion" : obj.rotation_quaternion,
"scale" : obj.scale
}[target][i])
else:
# TODO, this is not working if the action is not active (NLA case for example)
@ -171,7 +181,7 @@ def __gather_non_keyed_values(channels: typing.Tuple[bpy.types.FCurve],
if object_path:
shapekeys_idx = {}
cpt_sk = 0
for sk in blender_object.data.shape_keys.key_blocks:
for sk in obj.data.shape_keys.key_blocks:
if sk == sk.relative_key:
continue
if sk.mute is True:
@ -181,7 +191,7 @@ def __gather_non_keyed_values(channels: typing.Tuple[bpy.types.FCurve],
for idx_c, channel in enumerate(channels):
if channel is None:
non_keyed_values.append(blender_object.data.shape_keys.key_blocks[shapekeys_idx[idx_c]].value)
non_keyed_values.append(obj.data.shape_keys.key_blocks[shapekeys_idx[idx_c]].value)
else:
non_keyed_values.append(None)
@ -214,6 +224,7 @@ def __gather_input(channels: typing.Tuple[bpy.types.FCurve],
bake_range_start,
bake_range_end,
action_name,
driver_obj,
export_settings
) -> gltf2_io.Accessor:
"""Gather the key time codes."""
@ -225,6 +236,7 @@ def __gather_input(channels: typing.Tuple[bpy.types.FCurve],
bake_range_start,
bake_range_end,
action_name,
driver_obj,
export_settings)
times = [k.seconds for k in keyframes]
@ -276,6 +288,7 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
bake_range_start,
bake_range_end,
action_name,
driver_obj,
export_settings
) -> gltf2_io.Accessor:
"""Gather the data of the keyframes."""
@ -287,6 +300,7 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
bake_range_start,
bake_range_end,
action_name,
driver_obj,
export_settings)
if bake_bone is not None:
target_datapath = "pose.bones['" + bake_bone + "']." + bake_channel

View File

@ -95,3 +95,48 @@ def bonecache(func):
call_or_fetch = cached
unique = cached
def skdriverdiscovercache(func):
def reset_cache_skdriverdiscovercache():
func.__current_armature_name = None
func.__skdriverdiscover = {}
func.reset_cache = reset_cache_skdriverdiscovercache
@functools.wraps(func)
def wrapper_skdriverdiscover(*args, **kwargs):
if not hasattr(func, "__current_armature_name") or func.__current_armature_name is None:
func.__current_armature_name = None
func.reset_cache()
if args[0] != func.__current_armature_name:
result = func(*args)
func.__skdriverdiscover[args[0]] = result
func.__current_armature_name = args[0]
return result
else:
return func.__skdriverdiscover[args[0]]
return wrapper_skdriverdiscover
def skdrivervalues(func):
def reset_cache_skdrivervalues():
func.__skdrivervalues = {}
func.reset_cache = reset_cache_skdrivervalues
@functools.wraps(func)
def wrapper_skdrivervalues(*args, **kwargs):
if not hasattr(func, "__skdrivervalues") or func.__skdrivervalues is None:
func.reset_cache()
if args[0].name not in func.__skdrivervalues.keys():
func.__skdrivervalues[args[0].name] = {}
if args[1] not in func.__skdrivervalues[args[0].name]:
vals = func(*args)
func.__skdrivervalues[args[0].name][args[1]] = vals
return vals
else:
return func.__skdrivervalues[args[0].name][args[1]]
return wrapper_skdrivervalues

View File

@ -0,0 +1,74 @@
# Copyright 2019 The glTF-Blender-IO authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes
from io_scene_gltf2.blender.com import gltf2_blender_conversion
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import skdriverdiscovercache, skdrivervalues
from io_scene_gltf2.blender.com.gltf2_blender_data_path import get_target_object_path
@skdriverdiscovercache
def get_sk_drivers(blender_armature):
drivers = []
for child in blender_armature.children:
if not child.data:
continue
if not child.data.shape_keys:
continue
if not child.data.shape_keys.animation_data:
continue
if not child.data.shape_keys.animation_data.drivers:
continue
if len(child.data.shape_keys.animation_data.drivers) <= 0:
continue
shapekeys_idx = {}
cpt_sk = 0
for sk in child.data.shape_keys.key_blocks:
if sk == sk.relative_key:
continue
if sk.mute is True:
continue
shapekeys_idx[sk.name] = cpt_sk
cpt_sk += 1
# Note: channels will have some None items only for SK if some SK are not animated
idx_channel_mapping = []
all_sorted_channels = []
for sk_c in child.data.shape_keys.animation_data.drivers:
sk_name = child.data.shape_keys.path_resolve(get_target_object_path(sk_c.data_path)).name
idx = shapekeys_idx[sk_name]
idx_channel_mapping.append((shapekeys_idx[sk_name], sk_c))
existing_idx = dict(idx_channel_mapping)
for i in range(0, cpt_sk):
if i not in existing_idx.keys():
all_sorted_channels.append(None)
else:
all_sorted_channels.append(existing_idx[i])
drivers.append((child, tuple(all_sorted_channels)))
return tuple(drivers)
@skdrivervalues
def get_sk_driver_values(blender_object, frame, fcurves):
sk_values = []
for f in [f for f in fcurves if f is not None]:
sk_values.append(blender_object.data.shape_keys.path_resolve(get_target_object_path(f.data_path)).value)
return tuple(sk_values)