glTF importer: better multi-object animation management

This commit is contained in:
Julien Duroure 2019-09-17 22:18:19 +02:00
parent 12c0374e16
commit 87ffe55fd5
8 changed files with 264 additions and 221 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": (0, 9, 67),
"version": (0, 9, 68),
'blender': (2, 81, 6),
'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0',

View File

@ -12,8 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import bpy
from .gltf2_blender_animation_bone import BlenderBoneAnim
from .gltf2_blender_animation_node import BlenderNodeAnim
from .gltf2_blender_animation_weight import BlenderWeightAnim
from .gltf2_blender_animation_utils import restore_animation_on_object
class BlenderAnimation():
@ -28,32 +32,28 @@ class BlenderAnimation():
BlenderBoneAnim.anim(gltf, anim_idx, node_idx)
else:
BlenderNodeAnim.anim(gltf, anim_idx, node_idx)
BlenderWeightAnim.anim(gltf, anim_idx, node_idx)
if gltf.data.nodes[node_idx].children:
for child in gltf.data.nodes[node_idx].children:
BlenderAnimation.anim(gltf, anim_idx, child)
@staticmethod
def stash_action(gltf, anim_idx, node_idx, action_name):
def restore_animation(gltf, node_idx, animation_name):
"""Restores the actions for an animation by its track name on
the subtree starting at node_idx."""
node = gltf.data.nodes[node_idx]
if gltf.data.nodes[node_idx].is_joint:
BlenderBoneAnim.stash_action(gltf, anim_idx, node_idx, action_name)
if node.is_joint:
obj = bpy.data.objects[gltf.data.skins[node.skin_id].blender_armature_name]
else:
BlenderNodeAnim.stash_action(gltf, anim_idx, node_idx, action_name)
obj = bpy.data.objects[node.blender_object]
restore_animation_on_object(obj, animation_name)
if obj.data and hasattr(obj.data, 'shape_keys'):
restore_animation_on_object(obj.data.shape_keys, animation_name)
if gltf.data.nodes[node_idx].children:
for child in gltf.data.nodes[node_idx].children:
BlenderAnimation.stash_action(gltf, anim_idx, child, action_name)
@staticmethod
def restore_last_action(gltf, node_idx):
if gltf.data.nodes[node_idx].is_joint:
BlenderBoneAnim.restore_last_action(gltf, node_idx)
else:
BlenderNodeAnim.restore_last_action(gltf, node_idx)
if gltf.data.nodes[node_idx].children:
for child in gltf.data.nodes[node_idx].children:
BlenderAnimation.restore_last_action(gltf, child)
BlenderAnimation.restore_animation(gltf, child, animation_name)

View File

@ -18,7 +18,7 @@ from mathutils import Matrix
from ..com.gltf2_blender_conversion import loc_gltf_to_blender, quaternion_gltf_to_blender, scale_to_matrix
from ...io.imp.gltf2_io_binary import BinaryData
from .gltf2_blender_animation_utils import simulate_stash, restore_last_action
from .gltf2_blender_animation_utils import simulate_stash
class BlenderBoneAnim():
@ -40,31 +40,6 @@ class BlenderBoneAnim():
else:
kf.interpolation = 'LINEAR'
@staticmethod
def stash_action(gltf, anim_idx, node_idx, action_name):
node = gltf.data.nodes[node_idx]
obj = bpy.data.objects[gltf.data.skins[node.skin_id].blender_armature_name]
if anim_idx not in node.animations.keys():
return
if (obj.name, action_name) in gltf.actions_stashed.keys():
return
start_frame = bpy.context.scene.frame_start
animation_name = gltf.data.animations[anim_idx].name
simulate_stash(obj, animation_name, bpy.data.actions[action_name], start_frame)
gltf.actions_stashed[(obj.name, action_name)] = True
@staticmethod
def restore_last_action(gltf, node_idx):
node = gltf.data.nodes[node_idx]
obj = bpy.data.objects[gltf.data.skins[node.skin_id].blender_armature_name]
restore_last_action(obj)
@staticmethod
def parse_translation_channel(gltf, node, obj, bone, channel, animation):
"""Manage Location animation."""
@ -239,7 +214,8 @@ class BlenderBoneAnim():
def anim(gltf, anim_idx, node_idx):
"""Manage animation."""
node = gltf.data.nodes[node_idx]
obj = bpy.data.objects[gltf.data.skins[node.skin_id].blender_armature_name]
blender_armature_name = gltf.data.skins[node.skin_id].blender_armature_name
obj = bpy.data.objects[blender_armature_name]
bone = obj.pose.bones[node.blender_bone_name]
if anim_idx not in node.animations.keys():
@ -247,38 +223,16 @@ class BlenderBoneAnim():
animation = gltf.data.animations[anim_idx]
if animation.name:
name = animation.name + "_" + obj.name
else:
name = "Animation_" + str(anim_idx) + "_" + obj.name
if len(name) >= 63:
# Name is too long to be kept, we are going to keep only animation name for now
name = animation.name
if len(name) >= 63:
# Very long name!
name = "Animation_" + str(anim_idx)
if name not in bpy.data.actions:
action = gltf.arma_cache.get(blender_armature_name)
if not action:
name = animation.track_name + "_" + obj.name
action = bpy.data.actions.new(name)
else:
if name in gltf.animation_managed:
# multiple animation with same name in glTF file
# Create a new action with new name if needed
if name in gltf.current_animation_names.keys():
action = bpy.data.actions[gltf.current_animation_names[name]]
name = gltf.current_animation_names[name]
else:
action = bpy.data.actions.new(name)
else:
action = bpy.data.actions[name]
# Check if this action has some users.
# If no user (only 1 indeed), that means that this action must be deleted
# (is an action from a deleted object)
if action.users == 1:
bpy.data.actions.remove(action)
action = bpy.data.actions.new(name)
gltf.needs_stash.append((obj, animation.track_name, action))
gltf.arma_cache[blender_armature_name] = action
if not obj.animation_data:
obj.animation_data_create()
obj.animation_data.action = bpy.data.actions[action.name]
obj.animation_data.action = action
for channel_idx in node.animations[anim_idx]:
channel = animation.channels[channel_idx]
@ -292,6 +246,3 @@ class BlenderBoneAnim():
elif channel.target.path == "scale":
BlenderBoneAnim.parse_scale_channel(gltf, node, obj, bone, channel, animation)
if action.name not in gltf.current_animation_names.keys():
gltf.current_animation_names[name] = action.name

View File

@ -18,7 +18,7 @@ from mathutils import Vector
from ..com.gltf2_blender_conversion import loc_gltf_to_blender, quaternion_gltf_to_blender, scale_gltf_to_blender
from ..com.gltf2_blender_conversion import correction_rotation
from ...io.imp.gltf2_io_binary import BinaryData
from .gltf2_blender_animation_utils import simulate_stash, restore_last_action
from .gltf2_blender_animation_utils import simulate_stash
class BlenderNodeAnim():
@ -40,31 +40,6 @@ class BlenderNodeAnim():
else:
kf.interpolation = 'LINEAR'
@staticmethod
def stash_action(gltf, anim_idx, node_idx, action_name):
node = gltf.data.nodes[node_idx]
obj = bpy.data.objects[node.blender_object]
if anim_idx not in node.animations.keys():
return
if (obj.name, action_name) in gltf.actions_stashed.keys():
return
start_frame = bpy.context.scene.frame_start
animation_name = gltf.data.animations[anim_idx].name
simulate_stash(obj, animation_name, bpy.data.actions[action_name], start_frame)
gltf.actions_stashed[(obj.name, action_name)] = True
@staticmethod
def restore_last_action(gltf, node_idx):
node = gltf.data.nodes[node_idx]
obj = bpy.data.objects[node.blender_object]
restore_last_action(obj)
@staticmethod
def anim(gltf, anim_idx, node_idx):
"""Manage animation."""
@ -72,31 +47,25 @@ class BlenderNodeAnim():
obj = bpy.data.objects[node.blender_object]
fps = bpy.context.scene.render.fps
animation = gltf.data.animations[anim_idx]
if anim_idx not in node.animations.keys():
return
animation = gltf.data.animations[anim_idx]
if animation.name:
name = animation.name + "_" + obj.name
for channel_idx in node.animations[anim_idx]:
channel = animation.channels[channel_idx]
if channel.target.path in ['translation', 'rotation', 'scale']:
break
else:
name = "Animation_" + str(anim_idx) + "_" + obj.name
if len(name) >= 63:
# Name is too long to be kept, we are going to keep only animation name for now
name = animation.name
if len(name) >= 63:
# Very long name!
name = "Animation_" + str(anim_idx)
return
name = animation.track_name + "_" + obj.name
action = bpy.data.actions.new(name)
# Check if this action has some users.
# If no user (only 1 indeed), that means that this action must be deleted
# (is an action from a deleted object)
if action.users == 1:
bpy.data.actions.remove(action)
action = bpy.data.actions.new(name)
gltf.needs_stash.append((obj, animation.track_name, action))
if not obj.animation_data:
obj.animation_data_create()
obj.animation_data.action = bpy.data.actions[action.name]
obj.animation_data.action = action
for channel_idx in node.animations[anim_idx]:
channel = animation.channels[channel_idx]
@ -104,92 +73,64 @@ class BlenderNodeAnim():
keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input)
values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output)
if channel.target.path in ['translation', 'rotation', 'scale']:
if channel.target.path not in ['translation', 'rotation', 'scale']:
continue
# There is an animation on object
# We can't remove Yup2Zup object
gltf.animation_object = True
# There is an animation on object
# We can't remove Yup2Zup object
gltf.animation_object = True
if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
# TODO manage tangent?
values = [values[idx * 3 + 1] for idx in range(0, len(keys))]
if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
# TODO manage tangent?
values = [values[idx * 3 + 1] for idx in range(0, len(keys))]
if channel.target.path == "translation":
blender_path = "location"
group_name = "Location"
num_components = 3
values = [loc_gltf_to_blender(vals) for vals in values]
if channel.target.path == "translation":
blender_path = "location"
group_name = "Location"
num_components = 3
values = [loc_gltf_to_blender(vals) for vals in values]
elif channel.target.path == "rotation":
blender_path = "rotation_quaternion"
group_name = "Rotation"
num_components = 4
if node.correction_needed is True:
values = [
(quaternion_gltf_to_blender(vals).to_matrix().to_4x4() @ correction_rotation()).to_quaternion()
for vals in values
]
else:
values = [quaternion_gltf_to_blender(vals) for vals in values]
# Manage antipodal quaternions
for i in range(1, len(values)):
if values[i].dot(values[i-1]) < 0:
values[i] = -values[i]
elif channel.target.path == "scale":
blender_path = "scale"
group_name = "Scale"
num_components = 3
values = [scale_gltf_to_blender(vals) for vals in values]
coords = [0] * (2 * len(keys))
coords[::2] = (key[0] * fps for key in keys)
if group_name not in action.groups:
action.groups.new(group_name)
group = action.groups[group_name]
for i in range(0, num_components):
fcurve = action.fcurves.new(data_path=blender_path, index=i)
fcurve.group = group
fcurve.keyframe_points.add(len(keys))
coords[1::2] = (vals[i] for vals in values)
fcurve.keyframe_points.foreach_set('co', coords)
# Setting interpolation
for kf in fcurve.keyframe_points:
BlenderNodeAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
fcurve.update() # force updating tangents (this may change when tangent will be managed)
elif channel.target.path == 'weights':
# retrieve number of targets
nb_targets = 0
for prim in gltf.data.meshes[gltf.data.nodes[node_idx].mesh].primitives:
if prim.targets:
if len(prim.targets) > nb_targets:
nb_targets = len(prim.targets)
if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
factor = 3
delta = nb_targets
elif channel.target.path == "rotation":
blender_path = "rotation_quaternion"
group_name = "Rotation"
num_components = 4
if node.correction_needed is True:
values = [
(quaternion_gltf_to_blender(vals).to_matrix().to_4x4() @ correction_rotation()).to_quaternion()
for vals in values
]
else:
factor = 1
delta = 0
values = [quaternion_gltf_to_blender(vals) for vals in values]
for idx, key in enumerate(keys):
for sk in range(nb_targets):
if gltf.shapekeys[sk] is not None: # Do not animate shapekeys not created
obj.data.shape_keys.key_blocks[gltf.shapekeys[sk]].value = values[factor * idx * nb_targets + delta + sk][0]
obj.data.shape_keys.key_blocks[gltf.shapekeys[sk]].keyframe_insert(
"value",
frame=key[0] * fps,
group='ShapeKeys'
)
if action.name not in gltf.current_animation_names.keys():
gltf.current_animation_names[name] = action.name
# Manage antipodal quaternions
for i in range(1, len(values)):
if values[i].dot(values[i-1]) < 0:
values[i] = -values[i]
elif channel.target.path == "scale":
blender_path = "scale"
group_name = "Scale"
num_components = 3
values = [scale_gltf_to_blender(vals) for vals in values]
coords = [0] * (2 * len(keys))
coords[::2] = (key[0] * fps for key in keys)
if group_name not in action.groups:
action.groups.new(group_name)
group = action.groups[group_name]
for i in range(0, num_components):
fcurve = action.fcurves.new(data_path=blender_path, index=i)
fcurve.group = group
fcurve.keyframe_points.add(len(keys))
coords[1::2] = (vals[i] for vals in values)
fcurve.keyframe_points.foreach_set('co', coords)
# Setting interpolation
for kf in fcurve.keyframe_points:
BlenderNodeAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
fcurve.update() # force updating tangents (this may change when tangent will be managed)

View File

@ -12,7 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
def simulate_stash(obj, gltf_animation_name, action, start_frame):
import bpy
def simulate_stash(obj, track_name, action, start_frame=None):
# Simulate stash :
# * add a track
# * add an action on track
@ -20,20 +22,26 @@ def simulate_stash(obj, gltf_animation_name, action, start_frame):
# * remove active action from object
tracks = obj.animation_data.nla_tracks
new_track = tracks.new(prev=None)
new_track.name = gltf_animation_name if gltf_animation_name is not None else action.name
new_track.name = track_name
if start_frame is None:
start_frame = bpy.context.scene.frame_start
strip = new_track.strips.new(action.name, start_frame, action)
new_track.lock = True
new_track.mute = True
obj.animation_data.action = None
def restore_last_action(obj):
def restore_animation_on_object(obj, anim_name):
if not getattr(obj, 'animation_data', None):
return
if not obj.animation_data:
return
tracks = obj.animation_data.nla_tracks
if len(tracks) == 0:
return
if len(tracks[0].strips) == 0:
return
obj.animation_data.action = tracks[0].strips[0].action
for track in obj.animation_data.nla_tracks:
if track.name != anim_name:
continue
if not track.strips:
continue
obj.animation_data.action = track.strips[0].action
return
obj.animation_data.action = None

View File

@ -0,0 +1,109 @@
# Copyright 2018-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.
import json
import bpy
from ...io.imp.gltf2_io_binary import BinaryData
from .gltf2_blender_animation_utils import simulate_stash
class BlenderWeightAnim():
"""Blender ShapeKey Animation."""
def __new__(cls, *args, **kwargs):
raise RuntimeError("%s should not be instantiated" % cls)
@staticmethod
def set_interpolation(interpolation, kf):
"""Manage interpolation."""
if interpolation == "LINEAR":
kf.interpolation = 'LINEAR'
elif interpolation == "STEP":
kf.interpolation = 'CONSTANT'
elif interpolation == "CUBICSPLINE":
kf.interpolation = 'BEZIER'
kf.handle_right_type = 'AUTO'
kf.handle_left_type = 'AUTO'
else:
kf.interpolation = 'LINEAR'
@staticmethod
def anim(gltf, anim_idx, node_idx):
"""Manage animation."""
node = gltf.data.nodes[node_idx]
obj = bpy.data.objects[node.blender_object]
fps = bpy.context.scene.render.fps
animation = gltf.data.animations[anim_idx]
if anim_idx not in node.animations.keys():
return
for channel_idx in node.animations[anim_idx]:
channel = animation.channels[channel_idx]
if channel.target.path == "weights":
break
else:
return
name = animation.track_name + "_" + obj.name
action = bpy.data.actions.new(name)
action.id_root = "KEY"
gltf.needs_stash.append((obj.data.shape_keys, animation.track_name, action))
if not obj.data.shape_keys.animation_data:
obj.data.shape_keys.animation_data_create()
obj.data.shape_keys.animation_data.action = action
keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input)
values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output)
# retrieve number of targets
nb_targets = 0
for prim in gltf.data.meshes[gltf.data.nodes[node_idx].mesh].primitives:
if prim.targets:
if len(prim.targets) > nb_targets:
nb_targets = len(prim.targets)
if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
offset = nb_targets
stride = 3 * nb_targets
else:
offset = 0
stride = nb_targets
coords = [0] * (2 * len(keys))
coords[::2] = (key[0] * fps for key in keys)
group_name = "ShapeKeys"
if group_name not in action.groups:
action.groups.new(group_name)
group = action.groups[group_name]
for sk in range(nb_targets):
if gltf.shapekeys[sk] is not None: # Do not animate shapekeys not created
kb_name = obj.data.shape_keys.key_blocks[gltf.shapekeys[sk]].name
data_path = "key_blocks[" + json.dumps(kb_name) + "].value"
fcurve = action.fcurves.new(data_path=data_path)
fcurve.group = group
fcurve.keyframe_points.add(len(keys))
coords[1::2] = (values[offset + stride * i + sk][0] for i in range(len(keys)))
fcurve.keyframe_points.foreach_set('co', coords)
# Setting interpolation
for kf in fcurve.keyframe_points:
BlenderWeightAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
fcurve.update() # force updating tangents (this may change when tangent will be managed)

View File

@ -256,7 +256,14 @@ class BlenderGlTF():
for node_idx, node in enumerate(gltf.data.nodes):
node.animations = {}
track_names = set()
for anim_idx, anim in enumerate(gltf.data.animations):
# Pick pair-wise unique name for each animation to use as a name
# for its NLA tracks.
desired_name = anim.name or "Anim_%d" % anim_idx
anim.track_name = BlenderGlTF.find_unused_name(track_names, desired_name)
track_names.add(anim.track_name)
for channel_idx, channel in enumerate(anim.channels):
if channel.target.node is None:
continue
@ -274,3 +281,25 @@ class BlenderGlTF():
mesh.blender_name = None
mesh.is_weight_animated = False
@staticmethod
def find_unused_name(haystack, desired_name):
"""Finds a name not in haystack and <= 63 UTF-8 bytes.
(the limit on the size of a Blender name.)
If a is taken, tries a.001, then a.002, etc.
"""
stem = desired_name[:63]
suffix = ''
cntr = 1
while True:
name = stem + suffix
if len(name.encode('utf-8')) > 63:
stem = stem[:-1]
continue
if name not in haystack:
return name
suffix = '.%03d' % cntr
cntr += 1

View File

@ -18,6 +18,7 @@ from mathutils import Quaternion
from .gltf2_blender_node import BlenderNode
from .gltf2_blender_skin import BlenderSkin
from .gltf2_blender_animation import BlenderAnimation
from .gltf2_blender_animation_utils import simulate_stash
class BlenderScene():
@ -93,19 +94,23 @@ class BlenderScene():
BlenderSkin.create_armature_modifiers(gltf, skin_id)
if gltf.data.animations:
gltf.animation_managed = []
for anim_idx, anim in enumerate(gltf.data.animations):
gltf.current_animation_names = {}
gltf.actions_stashed= {}
# Blender armature name -> action all its bones should use
gltf.arma_cache = {}
# Things we need to stash when we're done.
gltf.needs_stash = []
if list_nodes is not None:
for node_idx in list_nodes:
BlenderAnimation.anim(gltf, anim_idx, node_idx)
for an in gltf.current_animation_names.values():
gltf.animation_managed.append(an)
for node_idx in list_nodes:
BlenderAnimation.stash_action(gltf, anim_idx, node_idx, an)
for (obj, anim_name, action) in gltf.needs_stash:
simulate_stash(obj, anim_name, action)
# Restore first animation
anim_name = gltf.data.animations[0].track_name
for node_idx in list_nodes:
BlenderAnimation.restore_last_action(gltf, node_idx)
BlenderAnimation.restore_animation(gltf, node_idx, anim_name)
if bpy.app.debug_value != 100:
# Parent root node to rotation object