glTF exporter: Big refactoring

- precompute tree before export
- manage collections / instances / linked
- use custom cache to avoid name collision
- animations are baked from world matrix

More info on https://github.com/KhronosGroup/glTF-Blender-IO
This commit is contained in:
Julien Duroure 2022-03-02 21:36:18 +01:00
parent 842c215b74
commit 782f8585f4
Notes: blender-bot 2023-02-14 18:52:20 +01:00
Referenced by issue #95050, Python Traceback when exporting to GLTF
Referenced by issue #78459, glTF Exporter won't properly export linked collections if 'Selected objects' option is toggled
19 changed files with 1236 additions and 629 deletions

View File

@ -4,7 +4,7 @@
bl_info = {
'name': 'glTF 2.0 format',
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
"version": (3, 2, 7),
"version": (3, 2, 8),
'blender': (3, 1, 0),
'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0',
@ -879,6 +879,8 @@ class GLTF_PT_export_animation_export(bpy.types.Panel):
row = layout.row()
row.active = operator.export_force_sampling
row.prop(operator, 'export_def_bones')
if operator.export_force_sampling is False and operator.export_def_bones is True:
layout.label(text="Export only deformation bones is not possible when not sampling animation")
class GLTF_PT_export_animation_shapekeys(bpy.types.Panel):

View File

@ -98,7 +98,7 @@ def swizzle_yup_value(value: typing.Any) -> typing.Any:
return value
def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Matrix = Matrix.Identity(4)) -> typing \
def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Matrix = Matrix.Identity(4), need_rotation_correction: bool = False) -> typing \
.Union[Vector, Quaternion]:
"""Manage transformations."""
target = get_target_property_name(data_path)
@ -116,25 +116,31 @@ def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Ma
if transform_func is None:
raise RuntimeError("Cannot transform values at {}".format(data_path))
return transform_func(v, transform)
return transform_func(v, transform, need_rotation_correction)
def transform_location(location: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector:
def transform_location(location: Vector, transform: Matrix = Matrix.Identity(4), need_rotation_correction:bool = False) -> Vector:
"""Transform location."""
correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0))
m = Matrix.Translation(location)
if need_rotation_correction:
m @= correction.to_matrix().to_4x4()
m = transform @ m
return m.to_translation()
def transform_rotation(rotation: Quaternion, transform: Matrix = Matrix.Identity(4)) -> Quaternion:
def transform_rotation(rotation: Quaternion, transform: Matrix = Matrix.Identity(4), need_rotation_correction: bool = False) -> Quaternion:
"""Transform rotation."""
rotation.normalize()
correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0))
m = rotation.to_matrix().to_4x4()
if need_rotation_correction:
m @= correction.to_matrix().to_4x4()
m = transform @ m
return m.to_quaternion()
def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector:
def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4), need_rotation_correction: bool = False) -> Vector:
"""Transform scale."""
m = Matrix.Identity(4)
m[0][0] = scale.x
@ -145,7 +151,7 @@ def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4)) -> Ve
return m.to_scale()
def transform_value(value: Vector, _: Matrix = Matrix.Identity(4)) -> Vector:
def transform_value(value: Vector, _: Matrix = Matrix.Identity(4), need_rotation_correction: bool = False) -> Vector:
"""Transform value."""
return value

View File

@ -19,6 +19,7 @@ VISIBLE = 'gltf_visible'
RENDERABLE = 'gltf_renderable'
ACTIVE_COLLECTION = 'gltf_active_collection'
SKINS = 'gltf_skins'
DEF_BONES_ONLY = 'gltf_def_bones'
DISPLACEMENT = 'gltf_displacement'
FORCE_SAMPLING = 'gltf_force_sampling'
FRAME_RANGE = 'gltf_frame_range'

View File

@ -9,10 +9,14 @@ from ...io.com.gltf2_io_debug import print_console
from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vertex_groups, modifiers, export_settings):
def extract_primitives(blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings):
"""Extract primitives from a mesh."""
print_console('INFO', 'Extracting primitive: ' + blender_mesh.name)
blender_object = None
if uuid_for_skined_data:
blender_object = export_settings['vtree'].nodes[uuid_for_skined_data].blender_object
use_normals = export_settings[gltf2_blender_export_keys.NORMALS]
if use_normals:
blender_mesh.calc_normals_split()
@ -57,7 +61,7 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert
armature = None
if armature:
skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings)
skin = gltf2_blender_gather_skins.gather_skin(export_settings['vtree'].nodes[uuid_for_skined_data].armature, export_settings)
if not skin:
armature = None

View File

@ -7,10 +7,12 @@ from io_scene_gltf2.io.com import gltf2_io
from io_scene_gltf2.io.com.gltf2_io_debug import print_console
from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes
from io_scene_gltf2.blender.exp import gltf2_blender_gather_animations
from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_sampler_keyframes
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
from ..com.gltf2_blender_extras import generate_extras
from io_scene_gltf2.blender.exp import gltf2_blender_export_keys
from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions
from io_scene_gltf2.blender.exp import gltf2_blender_gather_tree
def gather_gltf2(export_settings):
@ -22,12 +24,18 @@ def gather_gltf2(export_settings):
scenes = []
animations = [] # unfortunately animations in gltf2 are just as 'root' as scenes.
active_scene = None
store_user_scene = bpy.context.scene
for blender_scene in bpy.data.scenes:
scenes.append(__gather_scene(blender_scene, export_settings))
if export_settings[gltf2_blender_export_keys.ANIMATIONS]:
# resetting object cache
gltf2_blender_gather_animation_sampler_keyframes.get_object_matrix.reset_cache()
animations += __gather_animations(blender_scene, export_settings)
if bpy.context.scene.name == blender_scene.name:
active_scene = len(scenes) -1
# restore user scene
bpy.context.window.scene = store_user_scene
return active_scene, scenes, animations
@ -40,14 +48,25 @@ def __gather_scene(blender_scene, export_settings):
nodes=[]
)
for blender_object in blender_scene.objects:
if blender_object.parent is None:
node = gltf2_blender_gather_nodes.gather_node(
blender_object,
blender_object.library.name if blender_object.library else None,
blender_scene, None, export_settings)
if node is not None:
scene.nodes.append(node)
vtree = gltf2_blender_gather_tree.VExportTree(export_settings)
vtree.construct(blender_scene)
vtree.search_missing_armature() # In case armature are no parented correctly
export_user_extensions('vtree_before_filter_hook', export_settings, vtree)
# Now, we can filter tree if needed
vtree.filter()
export_user_extensions('vtree_after_filter_hook', export_settings, vtree)
export_settings['vtree'] = vtree
for r in [vtree.nodes[r] for r in vtree.roots]:
node = gltf2_blender_gather_nodes.gather_node(
r, export_settings)
if node is not None:
scene.nodes.append(node)
export_user_extensions('gather_scene_hook', export_settings, scene, blender_scene)
@ -58,15 +77,16 @@ def __gather_animations(blender_scene, export_settings):
animations = []
merged_tracks = {}
for blender_object in blender_scene.objects:
vtree = export_settings['vtree']
for obj_uuid in vtree.get_all_objects():
blender_object = vtree.nodes[obj_uuid].blender_object
# First check if this object is exported or not. Do not export animation of not exported object
obj_node = gltf2_blender_gather_nodes.gather_node(blender_object,
blender_object.library.name if blender_object.library else None,
blender_scene, None, export_settings)
if obj_node is not None:
animations_, merged_tracks = gltf2_blender_gather_animations.gather_animations(blender_object, merged_tracks, len(animations), export_settings)
animations += animations_
# Do not manage not exported objects
if vtree.nodes[obj_uuid].node is None:
continue
animations_, merged_tracks = gltf2_blender_gather_animations.gather_animations(obj_uuid, merged_tracks, len(animations), export_settings)
animations += animations_
if export_settings['gltf_nla_strips'] is False:
# Fake an animation with all animations of the scene

View File

@ -12,18 +12,20 @@ from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions
@cached
def gather_animation_channel_target(channels: typing.Tuple[bpy.types.FCurve],
blender_object: bpy.types.Object,
def gather_animation_channel_target(obj_uuid: int,
channels: typing.Tuple[bpy.types.FCurve],
bake_bone: typing.Union[str, None],
bake_channel: typing.Union[str, None],
driver_obj,
driver_obj_uuid,
export_settings
) -> gltf2_io.AnimationChannelTarget:
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
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, driver_obj),
node=__gather_node(channels, obj_uuid, export_settings, bake_bone, driver_obj_uuid),
path=__gather_path(channels, blender_object, export_settings, bake_bone, bake_channel)
)
@ -54,16 +56,16 @@ def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
def __gather_node(channels: typing.Tuple[bpy.types.FCurve],
blender_object: bpy.types.Object,
obj_uuid: str,
export_settings,
bake_bone: typing.Union[str, None],
driver_obj
driver_obj_uuid
) -> gltf2_io.Node:
if driver_obj is not None:
return gltf2_blender_gather_nodes.gather_node(driver_obj,
driver_obj.library.name if driver_obj.library else None,
None, None, export_settings)
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
if driver_obj_uuid is not None:
return export_settings['vtree'].nodes[driver_obj_uuid].node
if blender_object.type == "ARMATURE":
# TODO: get joint from fcurve data_path and gather_joint
@ -74,16 +76,9 @@ def __gather_node(channels: typing.Tuple[bpy.types.FCurve],
blender_bone = blender_object.path_resolve(channels[0].data_path.rsplit('.', 1)[0])
if isinstance(blender_bone, bpy.types.PoseBone):
if export_settings["gltf_def_bones"] is False:
return gltf2_blender_gather_joints.gather_joint(blender_object, blender_bone, export_settings)
else:
bones, _, _ = gltf2_blender_gather_skins.get_bone_tree(None, blender_object)
if blender_bone.name in [b.name for b in bones]:
return gltf2_blender_gather_joints.gather_joint(blender_object, blender_bone, export_settings)
return gltf2_blender_gather_joints.gather_joint_vnode(export_settings['vtree'].nodes[obj_uuid].bones[blender_bone.name], export_settings)
return gltf2_blender_gather_nodes.gather_node(blender_object,
blender_object.library.name if blender_object.library else None,
None, None, export_settings)
return export_settings['vtree'].nodes[obj_uuid].node
def __gather_path(channels: typing.Tuple[bpy.types.FCurve],

View File

@ -15,15 +15,18 @@ 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
from io_scene_gltf2.blender.exp.gltf2_blender_gather_tree import VExportNode
from . import gltf2_blender_export_keys
@cached
def gather_animation_channels(blender_action: bpy.types.Action,
blender_object: bpy.types.Object,
def gather_animation_channels(obj_uuid: int,
blender_action: bpy.types.Action,
export_settings
) -> typing.List[gltf2_io.AnimationChannel]:
channels = []
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
# First calculate range of animation for baking
# This is need if user set 'Force sampling' and in case we need to bake
@ -59,11 +62,8 @@ def gather_animation_channels(blender_action: bpy.types.Action,
# Then bake all bones
bones_to_be_animated = []
if export_settings["gltf_def_bones"] is False:
bones_to_be_animated = blender_object.data.bones
else:
bones_to_be_animated, _, _ = gltf2_blender_gather_skins.get_bone_tree(None, blender_object)
bones_to_be_animated = [blender_object.pose.bones[b.name] for b in bones_to_be_animated]
bones_uuid = export_settings["vtree"].get_all_bones(obj_uuid)
bones_to_be_animated = [blender_object.pose.bones[export_settings["vtree"].nodes[b].blender_bone.name] for b in bones_uuid]
list_of_animated_bone_channels = []
for channel_group in __get_channel_groups(blender_action, blender_object, export_settings):
@ -72,9 +72,9 @@ def gather_animation_channels(blender_action: bpy.types.Action,
for bone in bones_to_be_animated:
for p in ["location", "rotation_quaternion", "scale"]:
channel = __gather_animation_channel(
channel = gather_animation_channel(
obj_uuid,
(),
blender_object,
export_settings,
bone.name,
p,
@ -95,17 +95,17 @@ def gather_animation_channels(blender_action: bpy.types.Action,
if len(channel_group) == 0:
# Only errors on channels, ignoring
continue
channel = __gather_animation_channel(channel_group, blender_object, export_settings, None, None, bake_range_start, bake_range_end, force_range, blender_action.name, None, True)
channel = gather_animation_channel(obj_uuid, channel_group, export_settings, None, None, bake_range_start, bake_range_end, force_range, blender_action.name, None, True)
if channel is not 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(
drivers_to_manage = gltf2_blender_gather_drivers.get_sk_drivers(obj_uuid, export_settings)
for obj_driver_uuid, fcurves in drivers_to_manage:
channel = gather_animation_channel(
obj_uuid,
fcurves,
blender_object,
export_settings,
None,
None,
@ -113,31 +113,77 @@ def gather_animation_channels(blender_action: bpy.types.Action,
bake_range_end,
force_range,
blender_action.name,
obj,
False)
obj_driver_uuid,
True)
if channel is not None:
channels.append(channel)
else:
done_paths = []
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,
channel = gather_animation_channel(
obj_uuid,
channel_group_sorted,
export_settings,
None,
None,
bake_range_start,
bake_range_end,
force_range,
blender_action.name,
None,
True
)
if channel is not None:
channels.append(channel)
# Store already done channel path
target = [c for c in channel_group_sorted if c is not None][0].data_path.split('.')[-1]
path = {
"delta_location": "location",
"delta_rotation_euler": "rotation_quaternion",
"location": "location",
"rotation_axis_angle": "rotation_quaternion",
"rotation_euler": "rotation_quaternion",
"rotation_quaternion": "rotation_quaternion",
"scale": "scale",
"value": "weights"
}.get(target)
if path is not None:
done_paths.append(path)
done_paths = list(set(done_paths))
if export_settings['gltf_selected'] is True and export_settings['vtree'].tree_troncated is True:
start_frame = min([v[0] for v in [a.frame_range for a in bpy.data.actions]])
end_frame = max([v[1] for v in [a.frame_range for a in bpy.data.actions]])
to_be_done = ['location', 'rotation_quaternion', 'scale']
to_be_done = [c for c in to_be_done if c not in done_paths]
# In case of weight action, do nothing.
# If there is only weight --> TRS is already managed at first
if not (len(done_paths) == 1 and 'weights' in done_paths):
for p in to_be_done:
channel = gather_animation_channel(
obj_uuid,
(),
export_settings,
None,
None,
bake_range_start,
bake_range_end,
p,
start_frame,
end_frame,
force_range,
blender_action.name,
None,
False)
if channel is not None:
channels.append(channel)
False #If Object is not animated, don't keep animation for this channel
)
if channel is not None:
channels.append(channel)
# resetting driver caches
@ -198,8 +244,9 @@ def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender
# if not shapekeys, stay in same order, because order doesn't matter
return channels
def __gather_animation_channel(channels: typing.Tuple[bpy.types.FCurve],
blender_object: bpy.types.Object,
# This function can be called directly from gather_animation in case of bake animation (non animated selected object)
def gather_animation_channel(obj_uuid: str,
channels: typing.Tuple[bpy.types.FCurve],
export_settings,
bake_bone: typing.Union[str, None],
bake_channel: typing.Union[str, None],
@ -207,15 +254,18 @@ def __gather_animation_channel(channels: typing.Tuple[bpy.types.FCurve],
bake_range_end,
force_range: bool,
action_name: str,
driver_obj,
driver_obj_uuid,
node_channel_is_animated: bool
) -> typing.Union[gltf2_io.AnimationChannel, None]:
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
if not __filter_animation_channel(channels, blender_object, export_settings):
return None
__target= __gather_target(channels, blender_object, export_settings, bake_bone, bake_channel, driver_obj)
__target= __gather_target(obj_uuid, channels, export_settings, bake_bone, bake_channel, driver_obj_uuid)
if __target.path is not None:
sampler = __gather_sampler(channels, blender_object, export_settings, bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, driver_obj, node_channel_is_animated)
sampler = __gather_sampler(channels, obj_uuid, export_settings, bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, driver_obj_uuid, node_channel_is_animated)
if sampler is None:
# After check, no need to animate this node for this channel
@ -268,7 +318,7 @@ def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
def __gather_sampler(channels: typing.Tuple[bpy.types.FCurve],
blender_object: bpy.types.Object,
obj_uuid: str,
export_settings,
bake_bone: typing.Union[str, None],
bake_channel: typing.Union[str, None],
@ -276,33 +326,38 @@ def __gather_sampler(channels: typing.Tuple[bpy.types.FCurve],
bake_range_end,
force_range: bool,
action_name,
driver_obj,
driver_obj_uuid,
node_channel_is_animated: bool
) -> gltf2_io.AnimationSampler:
need_rotation_correction = (export_settings[gltf2_blender_export_keys.CAMERAS] and export_settings['vtree'].nodes[obj_uuid].blender_type == VExportNode.CAMERA) or \
(export_settings[gltf2_blender_export_keys.LIGHTS] and export_settings['vtree'].nodes[obj_uuid].blender_type == VExportNode.LIGHT)
return gltf2_blender_gather_animation_samplers.gather_animation_sampler(
channels,
blender_object,
obj_uuid,
bake_bone,
bake_channel,
bake_range_start,
bake_range_end,
force_range,
action_name,
driver_obj,
driver_obj_uuid,
node_channel_is_animated,
need_rotation_correction,
export_settings
)
def __gather_target(channels: typing.Tuple[bpy.types.FCurve],
blender_object: bpy.types.Object,
def __gather_target(obj_uuid: str,
channels: typing.Tuple[bpy.types.FCurve],
export_settings,
bake_bone: typing.Union[str, None],
bake_channel: typing.Union[str, None],
driver_obj
driver_obj_uuid
) -> gltf2_io.AnimationChannelTarget:
return gltf2_blender_gather_animation_channel_target.gather_animation_channel_target(
channels, blender_object, bake_bone, bake_channel, driver_obj, export_settings)
obj_uuid, channels, bake_bone, bake_channel, driver_obj_uuid, export_settings)
def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings):

View File

@ -5,12 +5,13 @@ import bpy
import mathutils
import typing
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, bonecache
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, bonecache, objectcache
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.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
from io_scene_gltf2.blender.exp.gltf2_blender_gather_tree import VExportNode
import numpy as np
@ -95,6 +96,10 @@ class Keyframe:
def value(self, value: typing.List[float]):
self.__value = self.__set_indexed(value)
@value.setter
def value_total(self, value: typing.List[float]):
self.__value = value
@property
def in_tangent(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]:
if self.__in_tangent is None:
@ -120,9 +125,75 @@ class Keyframe:
self.__out_tangent = self.__set_indexed(value)
@objectcache
def get_object_matrix(blender_obj_uuid: str,
action_name: str,
bake_range_start: int,
bake_range_end: int,
current_frame: int,
step: int,
export_settings
):
data = {}
# TODO : bake_range_start & bake_range_end are no more needed here
# Because we bake, we don't know exactly the frame range,
# So using min / max of all actions
start_frame = min([v[0] for v in [a.frame_range for a in bpy.data.actions]])
end_frame = max([v[1] for v in [a.frame_range for a in bpy.data.actions]])
frame = start_frame
while frame <= end_frame:
bpy.context.scene.frame_set(int(frame))
for obj_uuid in [uid for (uid, n) in export_settings['vtree'].nodes.items() if n.blender_type not in [VExportNode.BONE]]:
blender_obj = export_settings['vtree'].nodes[obj_uuid].blender_object
# if this object is not animated, do not skip :
# We need this object too in case of bake
# calculate local matrix
if export_settings['vtree'].nodes[obj_uuid].parent_uuid is None:
parent_mat = mathutils.Matrix.Identity(4).freeze()
else:
if export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_uuid].blender_type not in [VExportNode.BONE]:
parent_mat = export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_uuid].blender_object.matrix_world
else:
# Object animated is parented to a bone
blender_bone = export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_bone_uuid].blender_bone
armature_object = export_settings['vtree'].nodes[export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_bone_uuid].armature].blender_object
axis_basis_change = mathutils.Matrix(
((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))
parent_mat = armature_object.matrix_world @ blender_bone.matrix @ axis_basis_change
#For object inside collection (at root), matrix world is already expressed regarding collection parent
if export_settings['vtree'].nodes[obj_uuid].parent_uuid is not None and export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_uuid].blender_type == VExportNode.COLLECTION:
parent_mat = mathutils.Matrix.Identity(4).freeze()
mat = parent_mat.inverted_safe() @ blender_obj.matrix_world
if obj_uuid not in data.keys():
data[obj_uuid] = {}
if blender_obj.animation_data and blender_obj.animation_data.action:
if blender_obj.animation_data.action.name not in data[obj_uuid].keys():
data[obj_uuid][blender_obj.animation_data.action.name] = {}
data[obj_uuid][blender_obj.animation_data.action.name][frame] = mat
else:
# case of baking selected object.
# There is no animation, so use uuid of object as key
if obj_uuid not in data[obj_uuid].keys():
data[obj_uuid][obj_uuid] = {}
data[obj_uuid][obj_uuid][frame] = mat
frame += step
return data
@bonecache
def get_bone_matrix(blender_object_if_armature: typing.Optional[bpy.types.Object],
def get_bone_matrix(blender_obj_uuid_if_armature: typing.Optional[str],
channels: typing.Tuple[bpy.types.FCurve],
bake_bone: typing.Union[str, None],
bake_channel: typing.Union[str, None],
@ -130,9 +201,11 @@ def get_bone_matrix(blender_object_if_armature: typing.Optional[bpy.types.Object
bake_range_end,
action_name: str,
current_frame: int,
step: int
step: int,
export_settings
):
blender_object_if_armature = export_settings['vtree'].nodes[blender_obj_uuid_if_armature].blender_object if blender_obj_uuid_if_armature is not None else None
data = {}
# Always using bake_range, because some bones may need to be baked,
@ -145,35 +218,40 @@ def get_bone_matrix(blender_object_if_armature: typing.Optional[bpy.types.Object
frame = start_frame
while frame <= end_frame:
data[frame] = {}
# we need to bake in the constraints
bpy.context.scene.frame_set(int(frame))
for pbone in blender_object_if_armature.pose.bones:
if bake_bone is None:
matrix = pbone.matrix_basis.copy()
bones = export_settings['vtree'].get_all_bones(blender_obj_uuid_if_armature)
for bone_uuid in bones:
blender_bone = export_settings['vtree'].nodes[bone_uuid].blender_bone
if export_settings['vtree'].nodes[bone_uuid].parent_uuid is not None and export_settings['vtree'].nodes[export_settings['vtree'].nodes[bone_uuid].parent_uuid].blender_type == VExportNode.BONE:
blender_bone_parent = export_settings['vtree'].nodes[export_settings['vtree'].nodes[bone_uuid].parent_uuid].blender_bone
rest_mat = blender_bone_parent.bone.matrix_local.inverted_safe() @ blender_bone.bone.matrix_local
matrix = rest_mat.inverted_safe() @ blender_bone_parent.matrix.inverted_safe() @ blender_bone.matrix
else:
if (pbone.bone.use_inherit_rotation == False or pbone.bone.inherit_scale != "FULL") and pbone.parent != None:
rest_mat = (pbone.parent.bone.matrix_local.inverted_safe() @ pbone.bone.matrix_local)
matrix = (rest_mat.inverted_safe() @ pbone.parent.matrix.inverted_safe() @ pbone.matrix)
if blender_bone.parent is None:
matrix = blender_bone.bone.matrix_local.inverted_safe() @ blender_bone.matrix
else:
matrix = pbone.matrix
matrix = blender_object_if_armature.convert_space(pose_bone=pbone, matrix=matrix, from_space='POSE', to_space='LOCAL')
# Bone has a parent, but in export, after filter, is at root of armature
matrix = blender_bone.matrix.copy()
data[frame][pbone.name] = matrix
data[frame][blender_bone.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)
drivers_to_manage = get_sk_drivers(blender_obj_uuid_if_armature, export_settings)
for dr_obj_uuid, dr_fcurves in drivers_to_manage:
vals = get_sk_driver_values(dr_obj_uuid, frame, dr_fcurves, export_settings)
frame += step
return data
# cache for performance reasons
# This function is called 2 times, for input (timing) and output (key values)
@cached
def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Object],
def gather_keyframes(blender_obj_uuid: str,
is_armature: bool,
channels: typing.Tuple[bpy.types.FCurve],
non_keyed_values: typing.Tuple[typing.Optional[float]],
bake_bone: typing.Union[str, None],
@ -182,32 +260,40 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
bake_range_end,
force_range: bool,
action_name: str,
driver_obj,
driver_obj_uuid,
node_channel_is_animated: bool,
export_settings
) -> typing.List[Keyframe]:
) -> typing.Tuple[typing.List[Keyframe], bool]:
"""Convert the blender action groups' fcurves to keyframes for use in glTF."""
blender_object_if_armature = export_settings['vtree'].nodes[blender_obj_uuid].blender_object if is_armature is True is not None else None
blender_obj_uuid_if_armature = blender_obj_uuid if is_armature is True else None
if force_range is True:
start_frame = bake_range_start
end_frame = bake_range_end
else:
if bake_bone is None and driver_obj is None:
if bake_bone is None and driver_obj_uuid 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]
start_frame = min([channel.range()[0] for channel in channels if channel is not None])
end_frame = max([channel.range()[1] for channel in channels if channel is not None])
if len(channels) != 0:
start_frame = min([channel.range()[0] for channel in channels if channel is not None])
end_frame = max([channel.range()[1] for channel in channels if channel is not None])
else:
start_frame = bake_range_start
end_frame = bake_range_end
else:
start_frame = bake_range_start
end_frame = bake_range_end
keyframes = []
if needs_baking(blender_object_if_armature, channels, export_settings):
baking_is_needed = needs_baking(blender_object_if_armature, channels, export_settings)
if baking_is_needed:
# 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 and driver_obj is None:
if blender_object_if_armature is not None and driver_obj_uuid 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)
@ -224,7 +310,7 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
if isinstance(pose_bone_if_armature, bpy.types.PoseBone):
mat = get_bone_matrix(
blender_object_if_armature,
blender_obj_uuid_if_armature,
channels,
bake_bone,
bake_channel,
@ -232,7 +318,8 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
bake_range_end,
action_name,
frame,
step
step,
export_settings
)
trans, rot, scale = mat.decompose()
@ -248,12 +335,36 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
"scale": scale
}[target_property]
else:
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)
if driver_obj_uuid is None:
# If channel is TRS, we bake from world matrix, else this is SK
if len(channels) != 0:
target = [c for c in channels if c is not None][0].data_path.split('.')[-1]
else:
target = bake_channel
if target == "value": #SK
# 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:
mat = get_object_matrix(blender_obj_uuid,
action_name,
bake_range_start,
bake_range_end,
frame,
step,
export_settings)
trans, rot, sca = mat.decompose()
key.value_total = {
"location": trans,
"rotation_axis_angle": [rot.to_axis_angle()[1], rot.to_axis_angle()[0][0], rot.to_axis_angle()[0][1], rot.to_axis_angle()[0][2]],
"rotation_euler": rot.to_euler(),
"rotation_quaternion": rot,
"scale": sca
}[target]
else:
key.value = get_sk_driver_values(driver_obj, frame, channels)
key.value = get_sk_driver_values(driver_obj_uuid, frame, channels, export_settings)
complete_key(key, non_keyed_values)
keyframes.append(key)
frame += step
@ -307,7 +418,7 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
keyframes.append(key)
if not export_settings[gltf2_blender_export_keys.OPTIMIZE_ANIMS]:
return keyframes
return (keyframes, baking_is_needed)
# For armature only
# Check if all values are the same
@ -319,17 +430,20 @@ def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Objec
if node_channel_is_animated is True: # fcurve on this bone for this property
# Keep animation, but keep only 2 keyframes if data are not changing
return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes
return ([keyframes[0], keyframes[-1]], baking_is_needed) if cst is True and len(keyframes) >= 2 else (keyframes, baking_is_needed)
else: # bone is not animated (no fcurve)
# Not keeping if not changing property
return None if cst is True else keyframes
return (None, baking_is_needed) if cst is True else (keyframes, baking_is_needed)
else:
# For objects, if all values are the same, we keep only first and last
cst = fcurve_is_constant(keyframes)
return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes
if node_channel_is_animated is True:
return ([keyframes[0], keyframes[-1]], baking_is_needed) if cst is True and len(keyframes) >= 2 else (keyframes, baking_is_needed)
else:
# baked object (selected but not animated)
return (None, baking_is_needed) if cst is True else (keyframes, baking_is_needed)
return keyframes
return (keyframes, baking_is_needed)
def fcurve_is_constant(keyframes):
@ -374,6 +488,10 @@ def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object],
if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]:
return True
# If tree is troncated, sampling is forced
if export_settings['vtree'].tree_troncated is True:
return True
# Sampling due to unsupported interpolation
interpolation = [c for c in channels if c is not None][0].keyframe_points[0].interpolation
if interpolation not in ["BEZIER", "LINEAR", "CONSTANT"]:

View File

@ -3,6 +3,7 @@
import typing
from io_scene_gltf2.blender.exp.gltf2_blender_gather_tree import VExportNode
import bpy
import mathutils
@ -21,20 +22,23 @@ from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extension
@cached
def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve],
blender_object: bpy.types.Object,
obj_uuid: str,
bake_bone: typing.Union[str, None],
bake_channel: typing.Union[str, None],
bake_range_start,
bake_range_end,
force_range: bool,
action_name: str,
driver_obj,
driver_obj_uuid,
node_channel_is_animated: bool,
need_rotation_correction,
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 and driver_obj is None:
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
is_armature = True if blender_object.type == "ARMATURE" else False
blender_object_if_armature = blender_object if is_armature is True else None
if is_armature is True and driver_obj_uuid 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)
@ -45,15 +49,15 @@ 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,
driver_obj_uuid,
export_settings)
if blender_object.parent is not None:
matrix_parent_inverse = blender_object.matrix_parent_inverse.copy().freeze()
else:
matrix_parent_inverse = mathutils.Matrix.Identity(4).freeze()
input = __gather_input(channels, blender_object_if_armature, non_keyed_values,
bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, driver_obj, node_channel_is_animated, export_settings)
input = __gather_input(channels, obj_uuid, is_armature, non_keyed_values,
bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, driver_obj_uuid, node_channel_is_animated, export_settings)
if input is None:
# After check, no need to animate this node for this channel
@ -66,7 +70,8 @@ def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve],
interpolation=__gather_interpolation(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel),
output=__gather_output(channels,
matrix_parent_inverse,
blender_object_if_armature,
obj_uuid,
is_armature,
non_keyed_values,
bake_bone,
bake_channel,
@ -74,8 +79,9 @@ def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve],
bake_range_end,
force_range,
action_name,
driver_obj,
driver_obj_uuid,
node_channel_is_animated,
need_rotation_correction,
export_settings)
)
@ -97,12 +103,13 @@ 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,
driver_obj_uuid,
export_settings
) -> typing.Tuple[typing.Optional[float]]:
non_keyed_values = []
driver_obj = export_settings['vtree'].nodes[driver_obj_uuid].blender_object if driver_obj_uuid is not None else None
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
@ -217,10 +224,10 @@ def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
) -> typing.Any:
return None
@cached
def __gather_input(channels: typing.Tuple[bpy.types.FCurve],
blender_object_if_armature: typing.Optional[bpy.types.Object],
blender_obj_uuid: str,
is_armature: bool,
non_keyed_values: typing.Tuple[typing.Optional[float]],
bake_bone: typing.Union[str, None],
bake_channel: typing.Union[str, None],
@ -228,12 +235,13 @@ def __gather_input(channels: typing.Tuple[bpy.types.FCurve],
bake_range_end,
force_range: bool,
action_name,
driver_obj,
driver_obj_uuid,
node_channel_is_animated: bool,
export_settings
) -> gltf2_io.Accessor:
"""Gather the key time codes."""
keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(blender_object_if_armature,
keyframes, is_baked = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(blender_obj_uuid,
is_armature,
channels,
non_keyed_values,
bake_bone,
@ -242,7 +250,7 @@ def __gather_input(channels: typing.Tuple[bpy.types.FCurve],
bake_range_end,
force_range,
action_name,
driver_obj,
driver_obj_uuid,
node_channel_is_animated,
export_settings)
if keyframes is None:
@ -277,14 +285,15 @@ def __gather_interpolation(channels: typing.Tuple[bpy.types.FCurve],
# TODO: check if the bone was animated with CONSTANT
return 'LINEAR'
else:
max_keyframes = max([len(ch.keyframe_points) for ch in channels if ch is not None])
# If only single keyframe revert to STEP
if max_keyframes < 2:
return 'STEP'
if len(channels) != 0: # channels can be empty when baking object (non animated selected object)
max_keyframes = max([len(ch.keyframe_points) for ch in channels if ch is not None])
# If only single keyframe revert to STEP
if max_keyframes < 2:
return 'STEP'
# If all keyframes are CONSTANT, we can use STEP.
if all(all(k.interpolation == 'CONSTANT' for k in c.keyframe_points) for c in channels if c is not None):
return 'STEP'
# If all keyframes are CONSTANT, we can use STEP.
if all(all(k.interpolation == 'CONSTANT' for k in c.keyframe_points) for c in channels if c is not None):
return 'STEP'
# Otherwise, sampled keyframes use LINEAR interpolation.
return 'LINEAR'
@ -304,7 +313,8 @@ def __gather_interpolation(channels: typing.Tuple[bpy.types.FCurve],
@cached
def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
parent_inverse,
blender_object_if_armature: typing.Optional[bpy.types.Object],
blender_obj_uuid: str,
is_armature: bool,
non_keyed_values: typing.Tuple[typing.Optional[float]],
bake_bone: typing.Union[str, None],
bake_channel: typing.Union[str, None],
@ -314,10 +324,12 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
action_name,
driver_obj,
node_channel_is_animated: bool,
need_rotation_correction: bool,
export_settings
) -> gltf2_io.Accessor:
"""Gather the data of the keyframes."""
keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(blender_object_if_armature,
keyframes, is_baked = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(blender_obj_uuid,
is_armature,
channels,
non_keyed_values,
bake_bone,
@ -329,10 +341,19 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
driver_obj,
node_channel_is_animated,
export_settings)
if is_baked is True:
parent_inverse = mathutils.Matrix.Identity(4).freeze()
blender_object_if_armature = export_settings['vtree'].nodes[blender_obj_uuid].blender_object if is_armature is True else None
if bake_bone is not None:
target_datapath = "pose.bones['" + bake_bone + "']." + bake_channel
else:
target_datapath = [c for c in channels if c is not None][0].data_path
if len(channels) != 0: # channels can be empty when baking object (non animated selected object)
target_datapath = [c for c in channels if c is not None][0].data_path
else:
target_datapath = bake_channel
is_yup = export_settings[gltf2_blender_export_keys.YUP]
@ -355,6 +376,7 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
bone = blender_object_if_armature.pose.bones[bake_bone]
if isinstance(bone, bpy.types.PoseBone):
if bone.parent is None:
# bone at root of armature
axis_basis_change = mathutils.Matrix.Identity(4)
if export_settings[gltf2_blender_export_keys.YUP]:
axis_basis_change = mathutils.Matrix(
@ -364,10 +386,25 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
(0.0, 0.0, 0.0, 1.0)))
correction_matrix_local = axis_basis_change @ bone.bone.matrix_local
else:
correction_matrix_local = (
bone.parent.bone.matrix_local.inverted_safe() @
bone.bone.matrix_local
)
# Bone is not at root of armature
# There are 2 cases :
parent_uuid = export_settings['vtree'].nodes[export_settings['vtree'].nodes[blender_obj_uuid].bones[bone.name]].parent_uuid
if parent_uuid is not None and export_settings['vtree'].nodes[parent_uuid].blender_type == VExportNode.BONE:
# export bone is not at root of armature neither
correction_matrix_local = (
bone.parent.bone.matrix_local.inverted_safe() @
bone.bone.matrix_local
)
else:
# exported bone (after filter) is at root of armature
axis_basis_change = mathutils.Matrix.Identity(4)
if export_settings[gltf2_blender_export_keys.YUP]:
axis_basis_change = mathutils.Matrix(
((1.0, 0.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 0.0),
(0.0, -1.0, 0.0, 0.0),
(0.0, 0.0, 0.0, 1.0)))
correction_matrix_local = axis_basis_change
transform = correction_matrix_local
else:
@ -378,14 +415,14 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
values = []
for keyframe in keyframes:
# Transform the data and build gltf control points
value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform)
value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, need_rotation_correction)
if is_yup and not is_armature_animation:
value = gltf2_blender_math.swizzle_yup(value, target_datapath)
keyframe_value = gltf2_blender_math.mathutils_to_gltf(value)
if keyframe.in_tangent is not None:
# we can directly transform the tangent as it currently is represented by a control point
in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform)
in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform, need_rotation_correction)
if is_yup and blender_object_if_armature is None:
in_tangent = gltf2_blender_math.swizzle_yup(in_tangent, target_datapath)
# the tangent in glTF is relative to the keyframe value
@ -397,7 +434,7 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
if keyframe.out_tangent is not None:
# we can directly transform the tangent as it currently is represented by a control point
out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform)
out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform, need_rotation_correction)
if is_yup and blender_object_if_armature is None:
out_tangent = gltf2_blender_math.swizzle_yup(out_tangent, target_datapath)
# the tangent in glTF is relative to the keyframe value

View File

@ -11,7 +11,36 @@ from ..com.gltf2_blender_extras import generate_extras
from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions
def gather_animations(blender_object: bpy.types.Object,
def __gather_channels_baked(obj_uuid, export_settings):
channels = []
# If no animation in file, no need to bake
if len(bpy.data.actions) == 0:
return None
start_frame = min([v[0] for v in [a.frame_range for a in bpy.data.actions]])
end_frame = max([v[1] for v in [a.frame_range for a in bpy.data.actions]])
for p in ["location", "rotation_quaternion", "scale"]:
channel = gltf2_blender_gather_animation_channels.gather_animation_channel(
obj_uuid,
(),
export_settings,
None,
p,
start_frame,
end_frame,
False,
obj_uuid, # Use obj uuid as action name for caching
None,
False #If Object is not animated, don't keep animation for this channel
)
if channel is not None:
channels.append(channel)
return channels if len(channels) > 0 else None
def gather_animations( obj_uuid: int,
tracks: typing.Dict[str, typing.List[int]],
offset: int,
export_settings) -> typing.Tuple[typing.List[gltf2_io.Animation], typing.Dict[str, typing.List[int]]]:
@ -24,11 +53,29 @@ def gather_animations(blender_object: bpy.types.Object,
"""
animations = []
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
# Collect all 'actions' affecting this object. There is a direct mapping between blender actions and glTF animations
blender_actions = __get_blender_actions(blender_object, export_settings)
# save the current active action of the object, if any
# We will restore it after export
if len([a for a in blender_actions if a[2] == "OBJECT"]) == 0:
# No TRS animation are found for this object.
# But we need to bake, in case we export selection
if export_settings['gltf_selected'] is True and blender_object.type != "ARMATURE":
channels = __gather_channels_baked(obj_uuid, export_settings)
if channels is not None:
animation = gltf2_io.Animation(
channels=channels,
extensions=None, # as other animations
extras=None, # Because there is no animation to get extras from
name=blender_object.name, # Use object name as animation name
samplers=[]
)
__link_samplers(animation, export_settings)
if animation is not None:
animations.append(animation)
current_action = None
if blender_object.animation_data and blender_object.animation_data.action:
current_action = blender_object.animation_data.action
@ -63,7 +110,7 @@ def gather_animations(blender_object: bpy.types.Object,
# No need to set active shapekeys animations, this is needed for bone baking
animation = __gather_animation(blender_action, blender_object, export_settings)
animation = __gather_animation(obj_uuid, blender_action, export_settings)
if animation is not None:
animations.append(animation)
@ -91,21 +138,24 @@ def gather_animations(blender_object: bpy.types.Object,
return animations, tracks
def __gather_animation(blender_action: bpy.types.Action,
blender_object: bpy.types.Object,
export_settings
def __gather_animation( obj_uuid: int,
blender_action: bpy.types.Action,
export_settings
) -> typing.Optional[gltf2_io.Animation]:
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
if not __filter_animation(blender_action, blender_object, export_settings):
return None
name = __gather_name(blender_action, blender_object, export_settings)
try:
animation = gltf2_io.Animation(
channels=__gather_channels(blender_action, blender_object, export_settings),
channels=__gather_channels(obj_uuid, blender_action, export_settings),
extensions=__gather_extensions(blender_action, blender_object, export_settings),
extras=__gather_extras(blender_action, blender_object, export_settings),
name=name,
samplers=__gather_samplers(blender_action, blender_object, export_settings)
samplers=__gather_samplers(obj_uuid, blender_action, export_settings)
)
except RuntimeError as error:
print_console("WARNING", "Animation '{}' could not be exported. Cause: {}".format(name, error))
@ -134,12 +184,12 @@ def __filter_animation(blender_action: bpy.types.Action,
return True
def __gather_channels(blender_action: bpy.types.Action,
blender_object: bpy.types.Object,
def __gather_channels(obj_uuid: int,
blender_action: bpy.types.Action,
export_settings
) -> typing.List[gltf2_io.AnimationChannel]:
return gltf2_blender_gather_animation_channels.gather_animation_channels(
blender_action, blender_object, export_settings)
obj_uuid, blender_action, export_settings)
def __gather_extensions(blender_action: bpy.types.Action,
@ -166,8 +216,8 @@ def __gather_name(blender_action: bpy.types.Action,
return blender_action.name
def __gather_samplers(blender_action: bpy.types.Action,
blender_object: bpy.types.Object,
def __gather_samplers(obj_uuid: str,
blender_action: bpy.types.Action,
export_settings
) -> typing.List[gltf2_io.AnimationSampler]:
# We need to gather the samplers after gathering all channels --> populate this list in __link_samplers

View File

@ -6,83 +6,134 @@ import bpy
from io_scene_gltf2.blender.exp import gltf2_blender_get
def cached_by_key(key):
"""
Decorates functions whose result should be cached. Use it like:
@cached_by_key(key=...)
def func(..., export_settings):
...
The decorated function, func, must always take an "export_settings" arg
(the cache is stored here).
The key argument to the decorator is a function that computes the key to
cache on. It is passed all the arguments to func.
"""
def inner(func):
@functools.wraps(func)
def wrapper_cached(*args, **kwargs):
if kwargs.get("export_settings"):
export_settings = kwargs["export_settings"]
else:
export_settings = args[-1]
cache_key = key(*args, **kwargs)
# invalidate cache if export settings have changed
if not hasattr(func, "__export_settings") or export_settings != func.__export_settings:
func.__cache = {}
func.__export_settings = export_settings
# use or fill cache
if cache_key in func.__cache:
return func.__cache[cache_key]
else:
result = func(*args, **kwargs)
func.__cache[cache_key] = result
return result
return wrapper_cached
return inner
def default_key(*args, **kwargs):
"""
Default cache key for @cached functions.
Cache on all arguments (except export_settings).
"""
assert len(args) >= 2 and 0 <= len(kwargs) <= 1, "Wrong signature for cached function"
cache_key_args = args
# make a shallow copy of the keyword arguments so that 'export_settings' can be removed
cache_key_kwargs = dict(kwargs)
if kwargs.get("export_settings"):
del cache_key_kwargs["export_settings"]
else:
cache_key_args = args[:-1]
cache_key = ()
for i in cache_key_args:
cache_key += (i,)
for i in cache_key_kwargs.values():
cache_key += (i,)
return cache_key
def cached(func):
"""
Decorate the cache gather functions results.
return cached_by_key(key=default_key)(func)
def objectcache(func):
def reset_cache_objectcache():
func.__objectcache = {}
func.reset_cache = reset_cache_objectcache
The gather function is only executed if its result isn't in the cache yet
:param func: the function to be decorated. It will have a static __cache member afterwards
:return:
"""
@functools.wraps(func)
def wrapper_cached(*args, **kwargs):
assert len(args) >= 2 and 0 <= len(kwargs) <= 1, "Wrong signature for cached function"
def wrapper_objectcache(*args, **kwargs):
cache_key_args = args
# make a shallow copy of the keyword arguments so that 'export_settings' can be removed
cache_key_kwargs = dict(kwargs)
if kwargs.get("export_settings"):
export_settings = kwargs["export_settings"]
# 'export_settings' should not be cached
del cache_key_kwargs["export_settings"]
else:
export_settings = args[-1]
cache_key_args = args[:-1]
cache_key_args = args[:-1]
__by_name = [bpy.types.Object, bpy.types.Scene, bpy.types.Material, bpy.types.Action, bpy.types.Mesh, bpy.types.PoseBone]
if not hasattr(func, "__objectcache"):
func.reset_cache()
# we make a tuple from the function arguments so that they can be used as a key to the cache
cache_key = ()
for i in cache_key_args:
if type(i) in __by_name:
cache_key += (i.name,)
else:
cache_key += (i,)
for i in cache_key_kwargs.values():
if type(i) in __by_name:
cache_key += (i.name,)
else:
cache_key += (i,)
# invalidate cache if export settings have changed
if not hasattr(func, "__export_settings") or export_settings != func.__export_settings:
func.__cache = {}
func.__export_settings = export_settings
# use or fill cache
if cache_key in func.__cache:
return func.__cache[cache_key]
else:
# object is not cached yet
if cache_key_args[0] not in func.__objectcache.keys():
result = func(*args)
func.__cache[cache_key] = result
return result
return wrapper_cached
func.__objectcache = result
return result[cache_key_args[0]][cache_key_args[1]][cache_key_args[4]]
# object is in cache, but not this action
# We need to keep other actions
elif cache_key_args[1] not in func.__objectcache[cache_key_args[0]].keys():
result = func(*args)
func.__objectcache[cache_key_args[0]][cache_key_args[1]] = result[cache_key_args[0]][cache_key_args[1]]
return result[cache_key_args[0]][cache_key_args[1]][cache_key_args[4]]
# all is already cached
else:
return func.__objectcache[cache_key_args[0]][cache_key_args[1]][cache_key_args[4]]
return wrapper_objectcache
def bonecache(func):
def reset_cache_bonecache():
func.__current_action_name = None
func.__current_armature_name = None
func.__current_armature_uuid = None
func.__bonecache = {}
func.reset_cache = reset_cache_bonecache
@functools.wraps(func)
def wrapper_bonecache(*args, **kwargs):
if args[2] is None:
pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(args[0],
args[1][0].data_path)
armature = args[-1]['vtree'].nodes[args[0]].blender_object
cache_key_args = args
cache_key_args = args[:-1]
if cache_key_args[2] is None:
pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(armature,
cache_key_args[1][0].data_path)
else:
pose_bone_if_armature = args[0].pose.bones[args[2]]
pose_bone_if_armature = armature.pose.bones[cache_key_args[2]]
if not hasattr(func, "__current_action_name"):
func.reset_cache()
if args[6] != func.__current_action_name or args[0] != func.__current_armature_name:
if cache_key_args[6] != func.__current_action_name or cache_key_args[0] != func.__current_armature_uuid:
result = func(*args)
func.__bonecache = result
func.__current_action_name = args[6]
func.__current_armature_name = args[0]
return result[args[7]][pose_bone_if_armature.name]
func.__current_action_name = cache_key_args[6]
func.__current_armature_uuid = cache_key_args[0]
return result[cache_key_args[7]][pose_bone_if_armature.name]
else:
return func.__bonecache[args[7]][pose_bone_if_armature.name]
return func.__bonecache[cache_key_args[7]][pose_bone_if_armature.name]
return wrapper_bonecache
# TODO: replace "cached" with "unique" in all cases where the caching is functional and not only for performance reasons
@ -92,23 +143,27 @@ unique = cached
def skdriverdiscovercache(func):
def reset_cache_skdriverdiscovercache():
func.__current_armature_name = None
func.__current_armature_uuid = 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:
cache_key_args = args
cache_key_args = args[:-1]
if not hasattr(func, "__current_armature_uuid") or func.__current_armature_uuid is None:
func.reset_cache()
if args[0] != func.__current_armature_name:
if cache_key_args[0] != func.__current_armature_uuid:
result = func(*args)
func.__skdriverdiscover[args[0]] = result
func.__current_armature_name = args[0]
func.__skdriverdiscover[cache_key_args[0]] = result
func.__current_armature_uuid = cache_key_args[0]
return result
else:
return func.__skdriverdiscover[args[0]]
return func.__skdriverdiscover[cache_key_args[0]]
return wrapper_skdriverdiscover
def skdrivervalues(func):
@ -123,12 +178,17 @@ def skdrivervalues(func):
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]:
armature = args[-1]['vtree'].nodes[args[0]].blender_object
cache_key_args = args
cache_key_args = args[:-1]
if armature.name not in func.__skdrivervalues.keys():
func.__skdrivervalues[armature.name] = {}
if cache_key_args[1] not in func.__skdrivervalues[armature.name]:
vals = func(*args)
func.__skdrivervalues[args[0].name][args[1]] = vals
func.__skdrivervalues[armature.name][cache_key_args[1]] = vals
return vals
else:
return func.__skdrivervalues[args[0].name][args[1]]
return func.__skdrivervalues[armature.name][cache_key_args[1]]
return wrapper_skdrivervalues

View File

@ -5,13 +5,20 @@
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):
def get_sk_drivers(blender_armature_uuid, export_settings):
blender_armature = export_settings['vtree'].nodes[blender_armature_uuid].blender_object
drivers = []
for child in blender_armature.children:
for child_uuid in export_settings['vtree'].nodes[blender_armature_uuid].children:
if export_settings['vtree'].nodes[child_uuid].blender_type == "BONE":
continue
child = export_settings['vtree'].nodes[child_uuid].blender_object
if not child.data:
continue
# child.data can be an armature - which has no shapekeys
@ -63,13 +70,14 @@ def get_sk_drivers(blender_armature):
all_sorted_channels.append(existing_idx[i])
if len(all_sorted_channels) > 0:
drivers.append((child, tuple(all_sorted_channels)))
drivers.append((child_uuid, tuple(all_sorted_channels)))
return tuple(drivers)
@skdrivervalues
def get_sk_driver_values(blender_object, frame, fcurves):
def get_sk_driver_values(blender_object_uuid, frame, fcurves, export_settings):
sk_values = []
blender_object = export_settings['vtree'].nodes[blender_object_uuid].blender_object
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)

View File

@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2018-2021 The glTF-Blender-IO authors.
import mathutils
from mathutils import Matrix, Quaternion, Vector
from . import gltf2_blender_export_keys
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
@ -9,9 +9,40 @@ from io_scene_gltf2.io.com import gltf2_io
from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions
from ..com.gltf2_blender_extras import generate_extras
from io_scene_gltf2.blender.exp import gltf2_blender_gather_tree
# TODO these 3 functions move to shared file
def __convert_swizzle_location(loc, export_settings):
"""Convert a location from Blender coordinate system to glTF coordinate system."""
if export_settings[gltf2_blender_export_keys.YUP]:
return Vector((loc[0], loc[2], -loc[1]))
else:
return Vector((loc[0], loc[1], loc[2]))
def __convert_swizzle_rotation(rot, export_settings):
"""
Convert a quaternion rotation from Blender coordinate system to glTF coordinate system.
'w' is still at first position.
"""
if export_settings[gltf2_blender_export_keys.YUP]:
return Quaternion((rot[0], rot[1], rot[3], -rot[2]))
else:
return Quaternion((rot[0], rot[1], rot[2], rot[3]))
def __convert_swizzle_scale(scale, export_settings):
"""Convert a scale from Blender coordinate system to glTF coordinate system."""
if export_settings[gltf2_blender_export_keys.YUP]:
return Vector((scale[0], scale[2], scale[1]))
else:
return Vector((scale[0], scale[1], scale[2]))
@cached
def gather_joint(blender_object, blender_bone, export_settings):
def gather_joint_vnode(vnode, export_settings):
"""
Generate a glTF2 node from a blender bone, as joints in glTF2 are simply nodes.
@ -19,28 +50,19 @@ def gather_joint(blender_object, blender_bone, export_settings):
:param export_settings: the settings for this export
:return: a glTF2 node (acting as a joint)
"""
axis_basis_change = mathutils.Matrix.Identity(4)
if export_settings[gltf2_blender_export_keys.YUP]:
axis_basis_change = mathutils.Matrix(
((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))
vtree = export_settings['vtree']
blender_object = vtree.nodes[vnode].blender_object
blender_bone = vtree.nodes[vnode].blender_bone
# extract bone transform
if blender_bone.parent is None:
correction_matrix_local = axis_basis_change @ blender_bone.bone.matrix_local
else:
correction_matrix_local = (
blender_bone.parent.bone.matrix_local.inverted_safe() @
blender_bone.bone.matrix_local
)
if (blender_bone.bone.use_inherit_rotation == False or blender_bone.bone.inherit_scale != "FULL") and blender_bone.parent != None:
rest_mat = (blender_bone.parent.bone.matrix_local.inverted_safe() @ blender_bone.bone.matrix_local)
matrix_basis = (rest_mat.inverted_safe() @ blender_bone.parent.matrix.inverted_safe() @ blender_bone.matrix)
else:
matrix_basis = blender_bone.matrix
matrix_basis = blender_object.convert_space(pose_bone=blender_bone, matrix=matrix_basis, from_space='POSE', to_space='LOCAL')
mat = vtree.nodes[vtree.nodes[vnode].parent_uuid].matrix_world.inverted_safe() @ vtree.nodes[vnode].matrix_world
trans, rot, sca = mat.decompose()
trans = __convert_swizzle_location(trans, export_settings)
rot = __convert_swizzle_rotation(rot, export_settings)
sca = __convert_swizzle_scale(sca, export_settings)
trans, rot, sca = (correction_matrix_local @ matrix_basis).decompose()
translation, rotation, scale = (None, None, None)
if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
translation = [trans[0], trans[1], trans[2]]
@ -52,14 +74,8 @@ def gather_joint(blender_object, blender_bone, export_settings):
# traverse into children
children = []
if export_settings["gltf_def_bones"] is False:
for bone in blender_bone.children:
children.append(gather_joint(blender_object, bone, export_settings))
else:
_, children_, _ = gltf2_blender_gather_skins.get_bone_tree(None, blender_bone.id_data)
if blender_bone.name in children_.keys():
for bone in children_[blender_bone.name]:
children.append(gather_joint(blender_object, blender_bone.id_data.pose.bones[bone], export_settings))
for bone_uuid in [c for c in vtree.nodes[vnode].children if vtree.nodes[c].blender_type == gltf2_blender_gather_tree.VExportNode.BONE]:
children.append(gather_joint_vnode(bone_uuid, export_settings))
# finally add to the joints array containing all the joints in the hierarchy
node = gltf2_io.Node(
@ -79,6 +95,8 @@ def gather_joint(blender_object, blender_bone, export_settings):
export_user_extensions('gather_joint_hook', export_settings, node, blender_bone)
vtree.nodes[vnode].node = node
return node
def __gather_extras(blender_bone, export_settings):

View File

@ -3,7 +3,7 @@
import bpy
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, cached_by_key
from io_scene_gltf2.io.com import gltf2_io
from io_scene_gltf2.io.com.gltf2_io_extensions import Extension
from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info, gltf2_blender_export_keys
@ -16,8 +16,14 @@ from io_scene_gltf2.blender.exp import gltf2_blender_get
from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions
from io_scene_gltf2.io.com.gltf2_io_debug import print_console
@cached
def get_material_cache_key(blender_material, export_settings):
# Use id of material
# Do not use bpy.types that can be unhashable
# Do not use material name, that can be not unique (when linked)
return ((id(blender_material),))
@cached_by_key(key=get_material_cache_key)
def gather_material(blender_material, export_settings):
"""
Gather the material used by the blender primitive.

View File

@ -4,7 +4,7 @@
import bpy
from typing import Optional, Dict, List, Any, Tuple
from .gltf2_blender_export_keys import MORPH
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, cached_by_key
from io_scene_gltf2.io.com import gltf2_io
from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitives
from ..com.gltf2_blender_extras import generate_extras
@ -13,30 +13,64 @@ from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extension
@cached
def get_mesh_cache_key(blender_mesh,
blender_object,
vertex_groups,
modifiers,
skip_filter,
materials,
original_mesh,
export_settings):
# Use id of original mesh
# Do not use bpy.types that can be unhashable
# Do not use mesh name, that can be not unique (when linked)
# If materials are not exported, no need to cache by material
if export_settings['gltf_materials'] is None:
mats = None
else:
mats = tuple(id(m) if m is not None else None for m in materials)
# TODO check what is really needed for modifiers
mesh_to_id_cache = blender_mesh if original_mesh is None else original_mesh
return (
(id(mesh_to_id_cache),),
(modifiers,),
(skip_filter,), #TODO to check if still needed
mats
)
@cached_by_key(key=get_mesh_cache_key)
def gather_mesh(blender_mesh: bpy.types.Mesh,
library: Optional[str],
blender_object: Optional[bpy.types.Object],
uuid_for_skined_data,
vertex_groups: Optional[bpy.types.VertexGroups],
modifiers: Optional[bpy.types.ObjectModifiers],
skip_filter: bool,
material_names: Tuple[str],
materials: Tuple[bpy.types.Material],
original_mesh: bpy.types.Mesh,
export_settings
) -> Optional[gltf2_io.Mesh]:
if not skip_filter and not __filter_mesh(blender_mesh, library, vertex_groups, modifiers, export_settings):
if not skip_filter and not __filter_mesh(blender_mesh, vertex_groups, modifiers, export_settings):
return None
mesh = gltf2_io.Mesh(
extensions=__gather_extensions(blender_mesh, library, vertex_groups, modifiers, export_settings),
extras=__gather_extras(blender_mesh, library, vertex_groups, modifiers, export_settings),
name=__gather_name(blender_mesh, library, vertex_groups, modifiers, export_settings),
weights=__gather_weights(blender_mesh, library, vertex_groups, modifiers, export_settings),
primitives=__gather_primitives(blender_mesh, library, blender_object, vertex_groups, modifiers, material_names, export_settings),
extensions=__gather_extensions(blender_mesh, vertex_groups, modifiers, export_settings),
extras=__gather_extras(blender_mesh, vertex_groups, modifiers, export_settings),
name=__gather_name(blender_mesh, vertex_groups, modifiers, export_settings),
weights=__gather_weights(blender_mesh, vertex_groups, modifiers, export_settings),
primitives=__gather_primitives(blender_mesh, uuid_for_skined_data, vertex_groups, modifiers, materials, export_settings),
)
if len(mesh.primitives) == 0:
print_console("WARNING", "Mesh '{}' has no primitives and will be omitted.".format(mesh.name))
return None
blender_object = None
if uuid_for_skined_data:
blender_object = export_settings['vtree'].nodes[uuid_for_skined_data].blender_object
export_user_extensions('gather_mesh_hook',
export_settings,
mesh,
@ -45,13 +79,12 @@ def gather_mesh(blender_mesh: bpy.types.Mesh,
vertex_groups,
modifiers,
skip_filter,
material_names)
materials)
return mesh
def __filter_mesh(blender_mesh: bpy.types.Mesh,
library: Optional[str],
vertex_groups: Optional[bpy.types.VertexGroups],
modifiers: Optional[bpy.types.ObjectModifiers],
export_settings
@ -63,7 +96,6 @@ def __filter_mesh(blender_mesh: bpy.types.Mesh,
def __gather_extensions(blender_mesh: bpy.types.Mesh,
library: Optional[str],
vertex_groups: Optional[bpy.types.VertexGroups],
modifiers: Optional[bpy.types.ObjectModifiers],
export_settings
@ -72,7 +104,6 @@ def __gather_extensions(blender_mesh: bpy.types.Mesh,
def __gather_extras(blender_mesh: bpy.types.Mesh,
library: Optional[str],
vertex_groups: Optional[bpy.types.VertexGroups],
modifiers: Optional[bpy.types.ObjectModifiers],
export_settings
@ -100,7 +131,6 @@ def __gather_extras(blender_mesh: bpy.types.Mesh,
def __gather_name(blender_mesh: bpy.types.Mesh,
library: Optional[str],
vertex_groups: Optional[bpy.types.VertexGroups],
modifiers: Optional[bpy.types.ObjectModifiers],
export_settings
@ -109,24 +139,21 @@ def __gather_name(blender_mesh: bpy.types.Mesh,
def __gather_primitives(blender_mesh: bpy.types.Mesh,
library: Optional[str],
blender_object: Optional[bpy.types.Object],
uuid_for_skined_data,
vertex_groups: Optional[bpy.types.VertexGroups],
modifiers: Optional[bpy.types.ObjectModifiers],
material_names: Tuple[str],
materials: Tuple[bpy.types.Material],
export_settings
) -> List[gltf2_io.MeshPrimitive]:
return gltf2_blender_gather_primitives.gather_primitives(blender_mesh,
library,
blender_object,
uuid_for_skined_data,
vertex_groups,
modifiers,
material_names,
materials,
export_settings)
def __gather_weights(blender_mesh: bpy.types.Mesh,
library: Optional[str],
vertex_groups: Optional[bpy.types.VertexGroups],
modifiers: Optional[bpy.types.ObjectModifiers],
export_settings

View File

@ -13,137 +13,49 @@ from io_scene_gltf2.blender.exp import gltf2_blender_gather_cameras
from io_scene_gltf2.blender.exp import gltf2_blender_gather_mesh
from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints
from io_scene_gltf2.blender.exp import gltf2_blender_gather_lights
from io_scene_gltf2.blender.exp.gltf2_blender_gather_tree import VExportNode
from ..com.gltf2_blender_extras import generate_extras
from io_scene_gltf2.io.com import gltf2_io
from io_scene_gltf2.io.com import gltf2_io_extensions
from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions
from io_scene_gltf2.io.com.gltf2_io_debug import print_console
from io_scene_gltf2.blender.exp import gltf2_blender_gather_tree
def gather_node(blender_object, library, blender_scene, dupli_object_parent, export_settings):
# custom cache to avoid cache miss when called from animation
# with blender_scene=None
# invalidate cache if export settings have changed
if not hasattr(gather_node, "__export_settings") or export_settings != gather_node.__export_settings:
gather_node.__cache = {}
gather_node.__export_settings = export_settings
if blender_scene is None and (blender_object.name, library) in gather_node.__cache:
return gather_node.__cache[(blender_object.name, library)]
node = __gather_node(blender_object, library, blender_scene, dupli_object_parent, export_settings)
gather_node.__cache[(blender_object.name, library)] = node
return node
@cached
def __gather_node(blender_object, library, blender_scene, dupli_object_parent, export_settings):
children, only_bone_children = __gather_children(blender_object, blender_scene, export_settings)
camera = None
mesh = None
skin = None
weights = None
# If blender_scene is None, we are coming from animation export
# Check to know if object is exported is already done, so we don't check
# again if object is instanced in scene : this check was already done when exporting object itself
if not __filter_node(blender_object, blender_scene, export_settings):
if children:
# This node should be filtered out, but has un-filtered children present.
# So, export this node, excluding its camera, mesh, skin, and weights.
# The transformations and animations on this node will have visible effects on children.
# Armature always have children node(s) (that are bone(s))
# We have to check if children are only bones or not for armatures
if blender_object.type == "ARMATURE" and only_bone_children is True:
return None
pass
else:
# This node is filtered out, and has no un-filtered children or descendants.
return None
else:
# This node is being fully exported.
camera = __gather_camera(blender_object, export_settings)
mesh = __gather_mesh(blender_object, library, export_settings)
skin = __gather_skin(blender_object, export_settings)
weights = __gather_weights(blender_object, export_settings)
def gather_node(vnode, export_settings):
blender_object = vnode.blender_object
skin = __gather_skin(vnode, blender_object, export_settings)
node = gltf2_io.Node(
camera=camera,
children=children,
camera=__gather_camera(blender_object, export_settings),
children=__gather_children(vnode, blender_object, export_settings),
extensions=__gather_extensions(blender_object, export_settings),
extras=__gather_extras(blender_object, export_settings),
matrix=__gather_matrix(blender_object, export_settings),
mesh=mesh,
mesh=__gather_mesh(vnode, blender_object, export_settings),
name=__gather_name(blender_object, export_settings),
rotation=None,
scale=None,
skin=skin,
translation=None,
weights=weights
weights=__gather_weights(blender_object, export_settings)
)
# If node mesh is skined, transforms should be ignored at import, so no need to set them here
if node.skin is None:
node.translation, node.rotation, node.scale = __gather_trans_rot_scale(blender_object, export_settings)
node.translation, node.rotation, node.scale = __gather_trans_rot_scale(vnode, export_settings)
if export_settings[gltf2_blender_export_keys.YUP]:
# Checking node.extensions is making sure that the type of lamp is managed, and will be exported
if blender_object.type == 'LIGHT' and export_settings[gltf2_blender_export_keys.LIGHTS] and node.extensions:
correction_node = __get_correction_node(blender_object, export_settings)
correction_node.extensions = {"KHR_lights_punctual": node.extensions["KHR_lights_punctual"]}
del node.extensions["KHR_lights_punctual"]
node.children.append(correction_node)
if blender_object.type == 'CAMERA' and export_settings[gltf2_blender_export_keys.CAMERAS]:
correction_node = __get_correction_node(blender_object, export_settings)
correction_node.camera = node.camera
node.children.append(correction_node)
node.camera = None
export_user_extensions('gather_node_hook', export_settings, node, blender_object)
vnode.node = node
if node.skin is not None:
vnode.skin = skin
return node
def __filter_node(blender_object, blender_scene, export_settings):
if blender_object.users == 0:
return False
if blender_scene is not None:
instanced = any([blender_object.name in layer.objects for layer in blender_scene.view_layers])
if instanced is False:
# Check if object is from a linked collection
if any([blender_object.name in coll.objects for coll in bpy.data.collections if coll.library is not None]):
pass
else:
# Not instanced, not linked -> We don't keep this object
return False
if export_settings[gltf2_blender_export_keys.SELECTED] and blender_object.select_get() is False:
return False
if export_settings[gltf2_blender_export_keys.VISIBLE] and blender_object.visible_get() is False:
return False
# render_get() doesn't exist, so unfortunately this won't take into account the Collection settings
if export_settings[gltf2_blender_export_keys.RENDERABLE] and blender_object.hide_render is True:
return False
if export_settings[gltf2_blender_export_keys.ACTIVE_COLLECTION]:
found = any(x == blender_object for x in bpy.context.collection.all_objects)
if not found:
return False
if blender_object.type == 'LIGHT':
return export_settings[gltf2_blender_export_keys.LIGHTS]
if blender_object.type == 'CAMERA':
return export_settings[gltf2_blender_export_keys.CAMERAS]
return True
def __gather_camera(blender_object, export_settings):
if blender_object.type != 'CAMERA':
return None
@ -151,54 +63,35 @@ def __gather_camera(blender_object, export_settings):
return gltf2_blender_gather_cameras.gather_camera(blender_object.data, export_settings)
def __gather_children(blender_object, blender_scene, export_settings):
def __gather_children(vnode, blender_object, export_settings):
children = []
only_bone_children = True # True by default, will be set to False if needed
# standard children
for child_object in blender_object.children:
if child_object.parent_bone:
# this is handled further down,
# as the object should be a child of the specific bone,
# not the Armature object
continue
node = gather_node(child_object,
child_object.library.name if child_object.library else None,
blender_scene, None, export_settings)
vtree = export_settings['vtree']
# Standard Children / Collection
for c in [vtree.nodes[c] for c in vnode.children if vtree.nodes[c].blender_type != gltf2_blender_gather_tree.VExportNode.BONE]:
node = gather_node(c, export_settings)
if node is not None:
children.append(node)
only_bone_children = False
# blender dupli objects
if blender_object.instance_type == 'COLLECTION' and blender_object.instance_collection:
for dupli_object in blender_object.instance_collection.objects:
if dupli_object.parent is not None:
continue
if dupli_object.type == "ARMATURE":
continue # There is probably a proxy (no more existing)
node = gather_node(dupli_object,
dupli_object.library.name if dupli_object.library else None,
blender_scene, blender_object.name, export_settings)
if node is not None:
children.append(node)
only_bone_children = False
# blender bones
if blender_object.type == "ARMATURE":
# Armature --> Retrieve Blender bones
if vnode.blender_type == gltf2_blender_gather_tree.VExportNode.ARMATURE:
root_joints = []
if export_settings["gltf_def_bones"] is False:
bones = blender_object.pose.bones
else:
bones, _, _ = gltf2_blender_gather_skins.get_bone_tree(None, blender_object)
bones = [blender_object.pose.bones[b.name] for b in bones]
for blender_bone in bones:
if not blender_bone.parent:
joint = gltf2_blender_gather_joints.gather_joint(blender_object, blender_bone, export_settings)
children.append(joint)
root_joints.append(joint)
# handle objects directly parented to bones
direct_bone_children = [child for child in blender_object.children if child.parent_bone]
if len(direct_bone_children) != 0:
only_bone_children = False
all_armature_children = vnode.children
root_bones_uuid = [c for c in all_armature_children if export_settings['vtree'].nodes[c].blender_type == VExportNode.BONE]
for bone_uuid in root_bones_uuid:
joint = gltf2_blender_gather_joints.gather_joint_vnode(bone_uuid, export_settings)
children.append(joint)
root_joints.append(joint)
# Object parented to bones
direct_bone_children = []
for n in [vtree.nodes[i] for i in vtree.get_all_bones(vnode.uuid)]:
direct_bone_children.extend([c for c in n.children if vtree.nodes[c].blender_type != gltf2_blender_gather_tree.VExportNode.BONE])
def find_parent_joint(joints, name):
for joint in joints:
if joint.name == name:
@ -207,44 +100,40 @@ def __gather_children(blender_object, blender_scene, export_settings):
if parent_joint:
return parent_joint
return None
for child in direct_bone_children:
for child in direct_bone_children: # List of object that are parented to bones
# find parent joint
parent_joint = find_parent_joint(root_joints, child.parent_bone)
parent_joint = find_parent_joint(root_joints, vtree.nodes[child].blender_object.parent_bone)
if not parent_joint:
continue
child_node = gather_node(child, None, blender_scene, None, export_settings)
child_node = gather_node(vtree.nodes[child], export_settings)
if child_node is None:
continue
blender_bone = blender_object.pose.bones[parent_joint.name]
# fix rotation
if export_settings[gltf2_blender_export_keys.YUP]:
rot = child_node.rotation
if rot is None:
rot = [0, 0, 0, 1]
rot_quat = Quaternion(rot)
axis_basis_change = Matrix(
((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, -1.0, 0.0), (0.0, 1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))
mat = child.matrix_parent_inverse @ child.matrix_basis
mat = mat @ axis_basis_change
mat = vtree.nodes[vtree.nodes[child].parent_bone_uuid].matrix_world.inverted_safe() @ vtree.nodes[child].matrix_world
loc, rot_quat, scale = mat.decompose()
_, rot_quat, _ = mat.decompose()
child_node.rotation = [rot_quat[1], rot_quat[2], rot_quat[3], rot_quat[0]]
trans = __convert_swizzle_location(loc, export_settings)
rot = __convert_swizzle_rotation(rot_quat, export_settings)
sca = __convert_swizzle_scale(scale, export_settings)
# fix translation (in blender bone's tail is the origin for children)
trans, _, _ = child.matrix_local.decompose()
if trans is None:
trans = [0, 0, 0]
# bones go down their local y axis
if blender_bone.matrix.to_scale()[1] >= 1e-6:
bone_tail = [0, blender_bone.length / blender_bone.matrix.to_scale()[1], 0]
else:
bone_tail = [0,0,0] # If scale is 0, tail == head
child_node.translation = [trans[idx] + bone_tail[idx] for idx in range(3)]
translation, rotation, scale = (None, None, None)
if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
translation = [trans[0], trans[1], trans[2]]
if rot[0] != 1.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 0.0:
rotation = [rot[1], rot[2], rot[3], rot[0]]
if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0:
scale = [sca[0], sca[1], sca[2]]
child_node.translation = translation
child_node.rotation = rotation
child_node.scale = scale
parent_joint.children.append(child_node)
return children, only_bone_children
return children
def __gather_extensions(blender_object, export_settings):
@ -283,13 +172,17 @@ def __gather_matrix(blender_object, export_settings):
return []
def __gather_mesh(blender_object, library, export_settings):
def __gather_mesh(vnode, blender_object, export_settings):
if blender_object.type in ['CURVE', 'SURFACE', 'FONT']:
return __gather_mesh_from_nonmesh(blender_object, library, export_settings)
return __gather_mesh_from_nonmesh(blender_object, export_settings)
if blender_object.type != "MESH":
return None
# For duplis instancer, when show is off -> export as empty
if vnode.force_as_empty is True:
return None
# Be sure that object is valid (no NaN for example)
blender_object.data.validate()
@ -301,6 +194,8 @@ def __gather_mesh(blender_object, library, export_settings):
if len(modifiers) == 0:
modifiers = None
# TODO for objects without any modifiers, we can keep original mesh_data
# It will instance mesh in glTF
if export_settings[gltf2_blender_export_keys.APPLY]:
armature_modifiers = {}
if export_settings[gltf2_blender_export_keys.SKINS]:
@ -335,24 +230,23 @@ def __gather_mesh(blender_object, library, export_settings):
modifiers = None
materials = tuple(ms.material for ms in blender_object.material_slots)
material_names = tuple(None if mat is None else mat.name for mat in materials)
# retrieve armature
# Because mesh data will be transforms to skeleton space,
# we can't instantiate multiple object at different location, skined by same armature
blender_object_for_skined_data = None
uuid_for_skined_data = None
if export_settings[gltf2_blender_export_keys.SKINS]:
for idx, modifier in enumerate(blender_object.modifiers):
if modifier.type == 'ARMATURE':
blender_object_for_skined_data = blender_object
uuid_for_skined_data = vnode.uuid
result = gltf2_blender_gather_mesh.gather_mesh(blender_mesh,
library,
blender_object_for_skined_data,
uuid_for_skined_data,
vertex_groups,
modifiers,
skip_filter,
material_names,
materials,
None,
export_settings)
if export_settings[gltf2_blender_export_keys.APPLY]:
@ -361,7 +255,7 @@ def __gather_mesh(blender_object, library, export_settings):
return result
def __gather_mesh_from_nonmesh(blender_object, library, export_settings):
def __gather_mesh_from_nonmesh(blender_object, export_settings):
"""Handles curves, surfaces, text, etc."""
needs_to_mesh_clear = False
try:
@ -387,18 +281,18 @@ def __gather_mesh_from_nonmesh(blender_object, library, export_settings):
needs_to_mesh_clear = True
skip_filter = True
material_names = tuple([ms.material.name for ms in blender_object.material_slots if ms.material is not None])
materials = tuple([ms.material for ms in blender_object.material_slots if ms.material is not None])
vertex_groups = None
modifiers = None
blender_object_for_skined_data = None
result = gltf2_blender_gather_mesh.gather_mesh(blender_mesh,
library,
blender_object_for_skined_data,
vertex_groups,
modifiers,
skip_filter,
material_names,
materials,
blender_object.data,
export_settings)
finally:
@ -411,33 +305,15 @@ def __gather_mesh_from_nonmesh(blender_object, library, export_settings):
def __gather_name(blender_object, export_settings):
return blender_object.name
def __gather_trans_rot_scale(blender_object, export_settings):
if blender_object.matrix_parent_inverse == Matrix.Identity(4):
trans = blender_object.location
if blender_object.rotation_mode in ['QUATERNION', 'AXIS_ANGLE']:
rot = blender_object.rotation_quaternion
else:
rot = blender_object.rotation_euler.to_quaternion()
sca = blender_object.scale
def __gather_trans_rot_scale(vnode, export_settings):
if vnode.parent_uuid is None:
# No parent, so matrix is world matrix
trans, rot, sca = vnode.matrix_world.decompose()
else:
# matrix_local = matrix_parent_inverse*location*rotation*scale
# Decomposing matrix_local gives less accuracy, but is needed if matrix_parent_inverse is not the identity.
# calculate local matrix
trans, rot, sca = (export_settings['vtree'].nodes[vnode.parent_uuid].matrix_world.inverted_safe() @ vnode.matrix_world).decompose()
if blender_object.matrix_local[3][3] != 0.0:
trans, rot, sca = blender_object.matrix_local.decompose()
else:
# Some really weird cases, scale is null (if parent is null when evaluation is done)
print_console('WARNING', 'Some nodes are 0 scaled during evaluation. Result can be wrong')
trans = blender_object.location
if blender_object.rotation_mode in ['QUATERNION', 'AXIS_ANGLE']:
rot = blender_object.rotation_quaternion
else:
rot = blender_object.rotation_euler.to_quaternion()
sca = blender_object.scale
# make sure the rotation is normalized
rot.normalize()
@ -446,9 +322,9 @@ def __gather_trans_rot_scale(blender_object, export_settings):
rot = __convert_swizzle_rotation(rot, export_settings)
sca = __convert_swizzle_scale(sca, export_settings)
if blender_object.instance_type == 'COLLECTION' and blender_object.instance_collection:
if vnode.blender_object.instance_type == 'COLLECTION' and vnode.blender_object.instance_collection:
offset = -__convert_swizzle_location(
blender_object.instance_collection.instance_offset, export_settings)
vnode.blender_object.instance_collection.instance_offset, export_settings)
s = Matrix.Diagonal(sca).to_4x4()
r = rot.to_matrix().to_4x4()
@ -473,8 +349,7 @@ def __gather_trans_rot_scale(blender_object, export_settings):
scale = [sca[0], sca[1], sca[2]]
return translation, rotation, scale
def __gather_skin(blender_object, export_settings):
def __gather_skin(vnode, blender_object, export_settings):
modifiers = {m.type: m for m in blender_object.modifiers}
if "ARMATURE" not in modifiers or modifiers["ARMATURE"].object is None:
return None
@ -501,34 +376,12 @@ def __gather_skin(blender_object, export_settings):
return None
# Skins and meshes must be in the same glTF node, which is different from how blender handles armatures
return gltf2_blender_gather_skins.gather_skin(modifiers["ARMATURE"].object, export_settings)
return gltf2_blender_gather_skins.gather_skin(vnode.armature, export_settings)
def __gather_weights(blender_object, export_settings):
return None
def __get_correction_node(blender_object, export_settings):
correction_quaternion = __convert_swizzle_rotation(
Quaternion((1.0, 0.0, 0.0), math.radians(-90.0)), export_settings)
correction_quaternion = [correction_quaternion[1], correction_quaternion[2],
correction_quaternion[3], correction_quaternion[0]]
return gltf2_io.Node(
camera=None,
children=[],
extensions=None,
extras=None,
matrix=None,
mesh=None,
name=blender_object.name + '_Orientation',
rotation=correction_quaternion,
scale=None,
skin=None,
translation=None,
weights=None
)
def __convert_swizzle_location(loc, export_settings):
"""Convert a location from Blender coordinate system to glTF coordinate system."""
if export_settings[gltf2_blender_export_keys.YUP]:

View File

@ -7,7 +7,7 @@ import numpy as np
from .gltf2_blender_export_keys import NORMALS, MORPH_NORMAL, TANGENTS, MORPH_TANGENT, MORPH
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, cached_by_key
from io_scene_gltf2.blender.exp import gltf2_blender_extract
from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors
from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes
@ -20,13 +20,34 @@ from io_scene_gltf2.io.com.gltf2_io_debug import print_console
@cached
def get_primitive_cache_key(
blender_mesh,
blender_object,
vertex_groups,
modifiers,
materials,
export_settings):
# Use id of mesh
# Do not use bpy.types that can be unhashable
# Do not use mesh name, that can be not unique (when linked)
# TODO check what is really needed for modifiers
return (
(id(blender_mesh),),
(modifiers,),
tuple(id(m) if m is not None else None for m in materials)
)
@cached_by_key(key=get_primitive_cache_key)
def gather_primitives(
blender_mesh: bpy.types.Mesh,
library: Optional[str],
blender_object: Optional[bpy.types.Object],
uuid_for_skined_data,
vertex_groups: Optional[bpy.types.VertexGroups],
modifiers: Optional[bpy.types.ObjectModifiers],
material_names: Tuple[str],
materials: Tuple[bpy.types.Material],
export_settings
) -> List[gltf2_io.MeshPrimitive]:
"""
@ -36,7 +57,7 @@ def gather_primitives(
"""
primitives = []
blender_primitives = __gather_cache_primitives(blender_mesh, library, blender_object,
blender_primitives = __gather_cache_primitives(blender_mesh, uuid_for_skined_data,
vertex_groups, modifiers, export_settings)
for internal_primitive in blender_primitives:
@ -45,14 +66,13 @@ def gather_primitives(
if export_settings['gltf_materials'] == "EXPORT" and material_idx is not None:
blender_material = None
if material_names:
i = material_idx if material_idx < len(material_names) else -1
material_name = material_names[i]
if material_name is not None:
blender_material = bpy.data.materials[material_name]
if blender_material is not None:
mat = None
if materials:
i = material_idx if material_idx < len(materials) else -1
mat = materials[i]
if mat is not None:
material = gltf2_blender_gather_materials.gather_material(
blender_material,
mat,
export_settings,
)
@ -72,8 +92,7 @@ def gather_primitives(
@cached
def __gather_cache_primitives(
blender_mesh: bpy.types.Mesh,
library: Optional[str],
blender_object: Optional[bpy.types.Object],
uuid_for_skined_data,
vertex_groups: Optional[bpy.types.VertexGroups],
modifiers: Optional[bpy.types.ObjectModifiers],
export_settings
@ -84,7 +103,7 @@ def __gather_cache_primitives(
primitives = []
blender_primitives = gltf2_blender_extract.extract_primitives(
None, blender_mesh, library, blender_object, vertex_groups, modifiers, export_settings)
blender_mesh, uuid_for_skined_data, vertex_groups, modifiers, export_settings)
for internal_primitive in blender_primitives:
primitive = {

View File

@ -10,10 +10,12 @@ from io_scene_gltf2.io.com import gltf2_io_constants
from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors
from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints
from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions
from io_scene_gltf2.blender.exp import gltf2_blender_gather_tree
from io_scene_gltf2.blender.exp.gltf2_blender_gather_tree import VExportNode
@cached
def gather_skin(blender_object, export_settings):
def gather_skin(armature_uuid, export_settings):
"""
Gather armatures, bones etc into a glTF2 skin object.
@ -21,78 +23,70 @@ def gather_skin(blender_object, export_settings):
:param export_settings:
:return: a glTF2 skin object
"""
if not __filter_skin(blender_object, export_settings):
blender_armature_object = export_settings['vtree'].nodes[armature_uuid].blender_object
if not __filter_skin(blender_armature_object, export_settings):
return None
skin = gltf2_io.Skin(
extensions=__gather_extensions(blender_object, export_settings),
extras=__gather_extras(blender_object, export_settings),
inverse_bind_matrices=__gather_inverse_bind_matrices(blender_object, export_settings),
joints=__gather_joints(blender_object, export_settings),
name=__gather_name(blender_object, export_settings),
skeleton=__gather_skeleton(blender_object, export_settings)
extensions=__gather_extensions(blender_armature_object, export_settings),
extras=__gather_extras(blender_armature_object, export_settings),
inverse_bind_matrices=__gather_inverse_bind_matrices(armature_uuid, export_settings),
joints=__gather_joints(armature_uuid, export_settings),
name=__gather_name(blender_armature_object, export_settings),
skeleton=__gather_skeleton(blender_armature_object, export_settings)
)
export_user_extensions('gather_skin_hook', export_settings, skin, blender_object)
# If armature is not exported, joints will be empty.
# Do not construct skin in that case
if len(skin.joints) == 0:
return None
export_user_extensions('gather_skin_hook', export_settings, skin, blender_armature_object)
return skin
def __filter_skin(blender_object, export_settings):
def __filter_skin(blender_armature_object, export_settings):
if not export_settings[gltf2_blender_export_keys.SKINS]:
return False
if blender_object.type != 'ARMATURE' or len(blender_object.pose.bones) == 0:
if blender_armature_object.type != 'ARMATURE' or len(blender_armature_object.pose.bones) == 0:
return False
return True
def __gather_extensions(blender_object, export_settings):
def __gather_extensions(blender_armature_object, export_settings):
return None
def __gather_extras(blender_object, export_settings):
def __gather_extras(blender_armature_object, export_settings):
return None
def __gather_inverse_bind_matrices(blender_object, export_settings):
def __gather_inverse_bind_matrices(armature_uuid, export_settings):
blender_armature_object = export_settings['vtree'].nodes[armature_uuid].blender_object
axis_basis_change = mathutils.Matrix.Identity(4)
if export_settings[gltf2_blender_export_keys.YUP]:
axis_basis_change = mathutils.Matrix(
((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))
if export_settings['gltf_def_bones'] is False:
# build the hierarchy of nodes out of the bones
root_bones = []
for blender_bone in blender_object.pose.bones:
if not blender_bone.parent:
root_bones.append(blender_bone)
else:
_, children_, root_bones = get_bone_tree(None, blender_object)
matrices = []
# traverse the matrices in the same order as the joints and compute the inverse bind matrix
bones_uuid = export_settings['vtree'].get_all_bones(armature_uuid)
def __collect_matrices(bone):
inverse_bind_matrix = (
axis_basis_change @
(
blender_object.matrix_world @
blender_armature_object.matrix_world @
bone.bone.matrix_local
)
).inverted_safe()
matrices.append(inverse_bind_matrix)
if export_settings['gltf_def_bones'] is False:
for child in bone.children:
__collect_matrices(child)
else:
if bone.name in children_.keys():
for child in children_[bone.name]:
__collect_matrices(blender_object.pose.bones[child])
# start with the "root" bones and recurse into children, in the same ordering as the how joints are gathered
for root_bone in root_bones:
__collect_matrices(root_bone)
matrices = []
for b in bones_uuid:
__collect_matrices(blender_armature_object.pose.bones[export_settings['vtree'].nodes[b].blender_bone.name])
# flatten the matrices
inverse_matrices = []
@ -113,67 +107,26 @@ def __gather_inverse_bind_matrices(blender_object, export_settings):
)
def __gather_joints(blender_object, export_settings):
root_joints = []
if export_settings['gltf_def_bones'] is False:
# build the hierarchy of nodes out of the bones
for blender_bone in blender_object.pose.bones:
if not blender_bone.parent:
root_joints.append(gltf2_blender_gather_joints.gather_joint(blender_object, blender_bone, export_settings))
else:
_, children_, root_joints = get_bone_tree(None, blender_object)
root_joints = [gltf2_blender_gather_joints.gather_joint(blender_object, i, export_settings) for i in root_joints]
def __gather_joints(armature_uuid, export_settings):
# joints is a flat list containing all nodes belonging to the skin
joints = []
blender_armature_object = export_settings['vtree'].nodes[armature_uuid].blender_object
def __collect_joints(node):
joints.append(node)
if export_settings['gltf_def_bones'] is False:
for child in node.children:
__collect_joints(child)
else:
if node.name in children_.keys():
for child in children_[node.name]:
__collect_joints(gltf2_blender_gather_joints.gather_joint(blender_object, blender_object.pose.bones[child], export_settings))
all_armature_children = export_settings['vtree'].nodes[armature_uuid].children
root_bones_uuid = [c for c in all_armature_children if export_settings['vtree'].nodes[c].blender_type == VExportNode.BONE]
for joint in root_joints:
__collect_joints(joint)
# Create bone nodes
for root_bone_uuid in root_bones_uuid:
gltf2_blender_gather_joints.gather_joint_vnode(root_bone_uuid, export_settings)
bones_uuid = export_settings['vtree'].get_all_bones(armature_uuid)
joints = [export_settings['vtree'].nodes[b].node for b in bones_uuid]
return joints
def __gather_name(blender_object, export_settings):
return blender_object.name
def __gather_name(blender_armature_object, export_settings):
return blender_armature_object.name
def __gather_skeleton(blender_object, export_settings):
def __gather_skeleton(blender_armature_object, export_settings):
# In the future support the result of https://github.com/KhronosGroup/glTF/pull/1195
return None # gltf2_blender_gather_nodes.gather_node(blender_object, blender_scene, export_settings)
@cached
def get_bone_tree(blender_dummy, blender_object):
bones = []
children = {}
root_bones = []
def get_parent(bone):
bones.append(bone.name)
if bone.parent is not None:
if bone.parent.name not in children.keys():
children[bone.parent.name] = []
children[bone.parent.name].append(bone.name)
get_parent(bone.parent)
else:
root_bones.append(bone.name)
for bone in [b for b in blender_object.data.bones if b.use_deform is True]:
get_parent(bone)
# remove duplicates
for k, v in children.items():
children[k] = list(set(v))
list_ = list(set(bones))
root_ = list(set(root_bones))
return [blender_object.data.bones[b] for b in list_], children, [blender_object.pose.bones[b] for b in root_]
return None

View File

@ -0,0 +1,375 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2021 The glTF-Blender-IO authors.
import bpy
import uuid
from . import gltf2_blender_export_keys
from mathutils import Quaternion, Matrix
class VExportNode:
OBJECT = 1
ARMATURE = 2
BONE = 3
LIGHT = 4
CAMERA = 5
COLLECTION = 6
# Parent type, to be set on child regarding its parent
NO_PARENT = 54
PARENT_OBJECT = 50
PARENT_BONE = 51
PARENT_BONE_RELATIVE = 52
PARENT_ROOT_BONE = 53
PARENT_BONE_BONE = 55
def __init__(self):
self.children = []
self.blender_type = None
self.world_matrix = None
self.parent_type = None
self.blender_object = None
self.blender_bone = None
self.force_as_empty = False # Used for instancer display
# Only for bone/bone and object parented to bone
self.parent_bone_uuid = None
# Only for bones
self.use_deform = None
# Only for armature
self.bones = {}
# For deformed object
self.armature = None # for deformed object and for bone
self.skin = None
# glTF
self.node = None
def add_child(self, uuid):
self.children.append(uuid)
def set_world_matrix(self, matrix):
self.world_matrix = matrix
def set_blender_data(self, blender_object, blender_bone):
self.blender_object = blender_object
self.blender_bone = blender_bone
def recursive_display(self, tree, mode):
if mode == "simple":
for c in self.children:
print(self.blender_object.name, "/", self.blender_bone.name if self.blender_bone else "", "-->", tree.nodes[c].blender_object.name, "/", tree.nodes[c].blender_bone.name if tree.nodes[c].blender_bone else "" )
tree.nodes[c].recursive_display(tree, mode)
class VExportTree:
def __init__(self, export_settings):
self.nodes = {}
self.roots = []
self.export_settings = export_settings
self.tree_troncated = False
def add_node(self, node):
self.nodes[node.uuid] = node
def add_children(self, uuid_parent, uuid_child):
self.nodes[uuid_parent].add_child(uuid_child)
def construct(self, blender_scene):
bpy.context.window.scene = blender_scene
depsgraph = bpy.context.evaluated_depsgraph_get()
for blender_object in [obj.original for obj in depsgraph.scene_eval.objects if obj.parent is None]:
self.recursive_node_traverse(blender_object, None, None, Matrix.Identity(4))
def recursive_node_traverse(self, blender_object, blender_bone, parent_uuid, parent_coll_matrix_world, armature_uuid=None, dupli_world_matrix=None):
node = VExportNode()
node.uuid = str(uuid.uuid4())
node.parent_uuid = parent_uuid
node.set_blender_data(blender_object, blender_bone)
# add to parent if needed
if parent_uuid is not None:
self.add_children(parent_uuid, node.uuid)
else:
self.roots.append(node.uuid)
# Set blender type
if blender_bone is not None:
node.blender_type = VExportNode.BONE
self.nodes[armature_uuid].bones[blender_bone.name] = node.uuid
node.use_deform = blender_bone.id_data.data.bones[blender_bone.name].use_deform
elif blender_object.type == "ARMATURE":
node.blender_type = VExportNode.ARMATURE
elif blender_object.type == "CAMERA":
node.blender_type = VExportNode.CAMERA
elif blender_object.type == "LIGHT":
node.blender_type = VExportNode.LIGHT
elif blender_object.instance_type == "COLLECTION":
node.blender_type = VExportNode.COLLECTION
else:
node.blender_type = VExportNode.OBJECT
# For meshes with armature modifier (parent is armature), keep armature uuid
if node.blender_type == VExportNode.OBJECT:
modifiers = {m.type: m for m in blender_object.modifiers}
if "ARMATURE" in modifiers and modifiers["ARMATURE"].object is not None:
if parent_uuid is None or not self.nodes[parent_uuid].blender_type == VExportNode.ARMATURE:
# correct workflow is to parent skinned mesh to armature, but ...
# all users don't use correct workflow
print("WARNING: Armature must be the parent of skinned mesh")
print("Armature is selected by its name, but may be false in case of instances")
# Search an armature by name, and use the first found
# This will be done after all objects are setup
node.armature_needed = modifiers["ARMATURE"].object.name
else:
node.armature = parent_uuid
# For bones, store uuid of armature
if blender_bone is not None:
node.armature = armature_uuid
# for bone/bone parenting, store parent, this will help armature tree management
if parent_uuid is not None and self.nodes[parent_uuid].blender_type == VExportNode.BONE and node.blender_type == VExportNode.BONE:
node.parent_bone_uuid = parent_uuid
# Objects parented to bone
if parent_uuid is not None and self.nodes[parent_uuid].blender_type == VExportNode.BONE and node.blender_type != VExportNode.BONE:
node.parent_bone_uuid = parent_uuid
# World Matrix
# Store World Matrix for objects
if dupli_world_matrix is not None:
node.matrix_world = dupli_world_matrix
elif node.blender_type in [VExportNode.OBJECT, VExportNode.COLLECTION, VExportNode.ARMATURE, VExportNode.CAMERA, VExportNode.LIGHT]:
# Matrix World of object is expressed based on collection instance objects are
# So real world matrix is collection world_matrix @ "world_matrix" of object
node.matrix_world = parent_coll_matrix_world @ blender_object.matrix_world.copy()
if node.blender_type == VExportNode.CAMERA and self.export_settings[gltf2_blender_export_keys.CAMERAS]:
correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0))
node.matrix_world @= correction.to_matrix().to_4x4()
elif node.blender_type == VExportNode.LIGHT and self.export_settings[gltf2_blender_export_keys.LIGHTS]:
correction = Quaternion((2**0.5/2, -2**0.5/2, 0.0, 0.0))
node.matrix_world @= correction.to_matrix().to_4x4()
elif node.blender_type == VExportNode.BONE:
node.matrix_world = self.nodes[node.armature].matrix_world @ blender_bone.matrix
axis_basis_change = Matrix(
((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))
node.matrix_world = node.matrix_world @ axis_basis_change
# Force empty ?
# For duplis, if instancer is not display, we should create an empty
if blender_object.is_instancer is True and blender_object.show_instancer_for_render is False:
node.force_as_empty = True
# Storing this node
self.add_node(node)
###### Manage children ######
# standard children
if blender_bone is None and blender_object.is_instancer is False:
for child_object in blender_object.children:
if child_object.parent_bone:
# Object parented to bones
# Will be manage later
continue
else:
# Classic parenting
self.recursive_node_traverse(child_object, None, node.uuid, parent_coll_matrix_world)
# Collections
if blender_object.instance_type == 'COLLECTION' and blender_object.instance_collection:
for dupli_object in blender_object.instance_collection.objects:
if dupli_object.parent is not None:
continue
self.recursive_node_traverse(dupli_object, None, node.uuid, node.matrix_world)
# Armature : children are bones with no parent
if blender_object.type == "ARMATURE" and blender_bone is None:
for b in [b for b in blender_object.pose.bones if b.parent is None]:
self.recursive_node_traverse(blender_object, b, node.uuid, parent_coll_matrix_world, node.uuid)
# Bones
if blender_object.type == "ARMATURE" and blender_bone is not None:
for b in blender_bone.children:
self.recursive_node_traverse(blender_object, b, node.uuid, parent_coll_matrix_world, armature_uuid)
# Object parented to bone
if blender_bone is not None:
for child_object in [c for c in blender_object.children if c.parent_bone is not None and c.parent_bone == blender_bone.name]:
self.recursive_node_traverse(child_object, None, node.uuid, parent_coll_matrix_world)
# Duplis
if blender_object.is_instancer is True and blender_object.instance_type != 'COLLECTION':
depsgraph = bpy.context.evaluated_depsgraph_get()
for (dupl, mat) in [(dup.object.original, dup.matrix_world.copy()) for dup in depsgraph.object_instances if dup.parent and id(dup.parent.original) == id(blender_object)]:
self.recursive_node_traverse(dupl, None, node.uuid, parent_coll_matrix_world, dupli_world_matrix=mat)
def get_all_objects(self):
return [n.uuid for n in self.nodes.values() if n.blender_type != VExportNode.BONE]
def get_all_bones(self, uuid): #For armatue Only
if self.nodes[uuid].blender_type == VExportNode.ARMATURE:
def recursive_get_all_bones(uuid):
total = []
if self.nodes[uuid].blender_type == VExportNode.BONE:
total.append(uuid)
for child_uuid in self.nodes[uuid].children:
total.extend(recursive_get_all_bones(child_uuid))
return total
tot = []
for c_uuid in self.nodes[uuid].children:
tot.extend(recursive_get_all_bones(c_uuid))
return tot
else:
return []
def display(self, mode):
if mode == "simple":
for n in self.roots:
print("Root", self.nodes[n].blender_object.name, "/", self.nodes[n].blender_bone.name if self.nodes[n].blender_bone else "" )
self.nodes[n].recursive_display(self, mode)
def filter_tag(self):
roots = self.roots.copy()
for r in roots:
self.recursive_filter_tag(r, None)
def filter_perform(self):
roots = self.roots.copy()
for r in roots:
self.recursive_filter(r, None) # Root, so no parent
def filter(self):
self.filter_tag()
self.filter_perform()
def recursive_filter_tag(self, uuid, parent_keep_tag):
# parent_keep_tag is for collection instance
# some properties (selection, visibility, renderability)
# are defined at collection level, and we need to use these values
# for all objects of the collection instance.
# But some properties (camera, lamp ...) are not defined at collection level
if parent_keep_tag is None:
self.nodes[uuid].keep_tag = self.node_filter_not_inheritable_is_kept(uuid) and self.node_filter_inheritable_is_kept(uuid)
elif parent_keep_tag is True:
self.nodes[uuid].keep_tag = self.node_filter_not_inheritable_is_kept(uuid)
elif parent_keep_tag is False:
self.nodes[uuid].keep_tag = False
else:
print("This should not happen!")
for child in self.nodes[uuid].children:
if self.nodes[uuid].blender_type == VExportNode.COLLECTION:
self.recursive_filter_tag(child, self.nodes[uuid].keep_tag)
else:
self.recursive_filter_tag(child, parent_keep_tag)
def recursive_filter(self, uuid, parent_kept_uuid):
children = self.nodes[uuid].children.copy()
new_parent_kept_uuid = None
if self.nodes[uuid].keep_tag is False:
new_parent_kept_uuid = parent_kept_uuid
# Need to modify tree
if self.nodes[uuid].parent_uuid is not None:
self.nodes[self.nodes[uuid].parent_uuid].children.remove(uuid)
else:
# Remove from root
self.roots.remove(uuid)
else:
new_parent_kept_uuid = uuid
# If parent_uuid is not parent_kept_uuid, we need to modify children list of parent_kept_uuid
if parent_kept_uuid != self.nodes[uuid].parent_uuid and parent_kept_uuid is not None:
self.tree_troncated = True
self.nodes[parent_kept_uuid].children.append(uuid)
# If parent_kept_uuid is None, and parent_uuid was not, add to root list
if self.nodes[uuid].parent_uuid is not None and parent_kept_uuid is None:
self.tree_troncated = True
self.roots.append(uuid)
# Modify parent uuid
self.nodes[uuid].parent_uuid = parent_kept_uuid
for child in children:
self.recursive_filter(child, new_parent_kept_uuid)
def node_filter_not_inheritable_is_kept(self, uuid):
# Export Camera or not
if self.nodes[uuid].blender_type == VExportNode.CAMERA:
if self.export_settings[gltf2_blender_export_keys.CAMERAS] is False:
return False
# Export Lamp or not
if self.nodes[uuid].blender_type == VExportNode.LIGHT:
if self.export_settings[gltf2_blender_export_keys.LIGHTS] is False:
return False
# Export deform bones only
if self.nodes[uuid].blender_type == VExportNode.BONE:
if self.export_settings['gltf_def_bones'] is True and self.nodes[uuid].use_deform is False:
# Check if bone has some objected parented to bone. We need to keep it in that case, even if this is not a def bone
if len([c for c in self.nodes[uuid].children if self.nodes[c].blender_type != VExportNode.BONE]) != 0:
return True
return False
return True
def node_filter_inheritable_is_kept(self, uuid):
if self.export_settings[gltf2_blender_export_keys.SELECTED] and self.nodes[uuid].blender_object.select_get() is False:
return False
if self.export_settings[gltf2_blender_export_keys.VISIBLE]:
# The eye in outliner (object)
if self.nodes[uuid].blender_object.visible_get() is False:
return False
# The screen in outliner (object)
if self.nodes[uuid].blender_object.hide_viewport is True:
return False
# The screen in outliner (collections)
if all([c.hide_viewport for c in self.nodes[uuid].blender_object.users_collection]):
return False
# The camera in outliner (object)
if self.export_settings[gltf2_blender_export_keys.RENDERABLE]:
if self.nodes[uuid].blender_object.hide_render is True:
return False
# The camera in outliner (collections)
if all([c.hide_render for c in self.nodes[uuid].blender_object.users_collection]):
return False
if self.export_settings[gltf2_blender_export_keys.ACTIVE_COLLECTION]:
found = any(x == self.nodes[uuid].blender_object for x in bpy.context.collection.all_objects)
if not found:
return False
return True
def search_missing_armature(self):
for n in [n for n in self.nodes.values() if hasattr(n, "armature_needed") is True]:
candidates = [i for i in self.nodes.values() if i.blender_type == VExportNode.ARMATURE and i.blender_object.name == n.armature_needed]
if len(candidates) > 0:
n.armature = candidates[0].uuid
del n.armature_needed