glTF importer: fix skinning & hierarchy issues

See https://github.com/KhronosGroup/glTF-Blender-IO/pull/857 for details
This commit is contained in:
Julien Duroure 2020-01-23 22:10:48 +01:00
parent b3b274c573
commit 75855d7238
17 changed files with 764 additions and 865 deletions

View File

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

View File

@ -15,38 +15,6 @@
from mathutils import Matrix, Quaternion
from math import sqrt, sin, cos
def matrix_gltf_to_blender(mat_input):
"""Matrix from glTF format to Blender format."""
mat = Matrix([mat_input[0:4], mat_input[4:8], mat_input[8:12], mat_input[12:16]])
mat.transpose()
return mat
def loc_gltf_to_blender(loc):
"""Location."""
return loc
def scale_gltf_to_blender(scale):
"""Scaling."""
return scale
def quaternion_gltf_to_blender(q):
"""Quaternion from glTF to Blender."""
return Quaternion([q[3], q[0], q[1], q[2]])
def scale_to_matrix(scale):
"""Scale to matrix."""
mat = Matrix()
for i in range(3):
mat[i][i] = scale[i]
return mat
def correction_rotation():
"""Correction of Rotation."""
# Correction is needed for lamps, because Yup2Zup is not written in vertices
# and lamps has no vertices :)
return Quaternion((sqrt(2)/2, -sqrt(2)/2, 0.0, 0.0)).to_matrix().to_4x4()
def texture_transform_blender_to_gltf(mapping_transform):
"""
Converts the offset/rotation/scale from a Mapping node applied in Blender's

View File

@ -18,6 +18,7 @@ from .gltf2_blender_animation_bone import BlenderBoneAnim
from .gltf2_blender_animation_node import BlenderNodeAnim
from .gltf2_blender_animation_weight import BlenderWeightAnim
from .gltf2_blender_animation_utils import restore_animation_on_object
from .gltf2_blender_vnode import VNode
class BlenderAnimation():
@ -26,34 +27,36 @@ class BlenderAnimation():
raise RuntimeError("%s should not be instantiated" % cls)
@staticmethod
def anim(gltf, anim_idx, node_idx):
def anim(gltf, anim_idx, vnode_id):
"""Dispatch Animation to bone or object."""
if gltf.data.nodes[node_idx].is_joint:
BlenderBoneAnim.anim(gltf, anim_idx, node_idx)
else:
BlenderNodeAnim.anim(gltf, anim_idx, node_idx)
BlenderWeightAnim.anim(gltf, anim_idx, node_idx)
if isinstance(vnode_id, int):
if gltf.vnodes[vnode_id].type == VNode.Bone:
BlenderBoneAnim.anim(gltf, anim_idx, vnode_id)
elif gltf.vnodes[vnode_id].type == VNode.Object:
BlenderNodeAnim.anim(gltf, anim_idx, vnode_id)
if gltf.data.nodes[node_idx].children:
for child in gltf.data.nodes[node_idx].children:
BlenderAnimation.anim(gltf, anim_idx, child)
BlenderWeightAnim.anim(gltf, anim_idx, vnode_id)
for child in gltf.vnodes[vnode_id].children:
BlenderAnimation.anim(gltf, anim_idx, child)
@staticmethod
def restore_animation(gltf, node_idx, animation_name):
def restore_animation(gltf, vnode_id, animation_name):
"""Restores the actions for an animation by its track name on
the subtree starting at node_idx."""
node = gltf.data.nodes[node_idx]
vnode = gltf.vnodes[vnode_id]
if node.is_joint:
obj = bpy.data.objects[gltf.data.skins[node.skin_id].blender_armature_name]
else:
obj = bpy.data.objects[node.blender_object]
obj = None
if vnode.type == VNode.Bone:
obj = gltf.vnodes[vnode.bone_arma].blender_object
elif vnode.type == VNode.Object:
obj = vnode.blender_object
restore_animation_on_object(obj, animation_name)
if obj.data and hasattr(obj.data, 'shape_keys'):
restore_animation_on_object(obj.data.shape_keys, animation_name)
if obj is not None:
restore_animation_on_object(obj, animation_name)
if obj.data and hasattr(obj.data, 'shape_keys'):
restore_animation_on_object(obj.data.shape_keys, animation_name)
if gltf.data.nodes[node_idx].children:
for child in gltf.data.nodes[node_idx].children:
BlenderAnimation.restore_animation(gltf, child, animation_name)
for child in gltf.vnodes[vnode_id].children:
BlenderAnimation.restore_animation(gltf, child, animation_name)

View File

@ -14,51 +14,56 @@
import json
import bpy
from mathutils import Matrix
from mathutils import Vector
from ..com.gltf2_blender_conversion import loc_gltf_to_blender, quaternion_gltf_to_blender, scale_to_matrix
from ...io.imp.gltf2_io_binary import BinaryData
from .gltf2_blender_animation_utils import simulate_stash, make_fcurve
# The glTF curves store the value of the final transform, but in Blender
# curves animate a pose bone that is relative to the edit bone
#
# Final = EditBone * PoseBone
# where
# Final = Trans[ft] Rot[fr] Scale[fs]
# EditBone = Trans[et] Rot[er] (edit bones have no scale)
# PoseBone = Trans[pt] Rot[pr] Scale[ps]
#
# Solving for the PoseBone gives the change we need to apply to the curves
#
# pt = Rot[er^{-1}] (ft - et)
# pr = er^{-1} fr
# ps = fs
class BlenderBoneAnim():
"""Blender Bone Animation."""
def __new__(cls, *args, **kwargs):
raise RuntimeError("%s should not be instantiated" % cls)
@staticmethod
def parse_translation_channel(gltf, node, obj, bone, channel, animation):
def parse_translation_channel(gltf, vnode, obj, bone, channel, animation):
"""Manage Location animation."""
blender_path = "pose.bones[" + json.dumps(bone.name) + "].location"
group_name = bone.name
keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input)
values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output)
inv_bind_matrix = node.blender_bone_matrix.to_quaternion().to_matrix().to_4x4().inverted() \
@ Matrix.Translation(node.blender_bone_matrix.to_translation()).inverted()
if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
# TODO manage tangent?
translation_keyframes = (
loc_gltf_to_blender(values[idx * 3 + 1])
gltf.loc_gltf_to_blender(values[idx * 3 + 1])
for idx in range(0, len(keys))
)
else:
translation_keyframes = (loc_gltf_to_blender(vals) for vals in values)
if node.parent is None:
parent_mat = Matrix()
else:
if not gltf.data.nodes[node.parent].is_joint:
parent_mat = Matrix()
else:
parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix
translation_keyframes = (gltf.loc_gltf_to_blender(vals) for vals in values)
bind_trans, bind_rot, _ = vnode.trs
bind_rot_inv = bind_rot.conjugated()
# Pose is in object (armature) space and it's value if the offset from the bind pose
# (which is also in object space)
# Scale is not taken into account
final_translations = [
inv_bind_matrix @ (parent_mat @ Matrix.Translation(translation_keyframe)).to_translation()
for translation_keyframe in translation_keyframes
bind_rot_inv @ (trans - bind_trans)
for trans in translation_keyframes
]
BlenderBoneAnim.fill_fcurves(
@ -71,48 +76,31 @@ class BlenderBoneAnim():
)
@staticmethod
def parse_rotation_channel(gltf, node, obj, bone, channel, animation):
def parse_rotation_channel(gltf, vnode, obj, bone, channel, animation):
"""Manage rotation animation."""
blender_path = "pose.bones[" + json.dumps(bone.name) + "].rotation_quaternion"
group_name = bone.name
keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input)
values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output)
bind_rotation = node.blender_bone_matrix.to_quaternion()
if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
# TODO manage tangent?
quat_keyframes = [
quaternion_gltf_to_blender(values[idx * 3 + 1])
gltf.quaternion_gltf_to_blender(values[idx * 3 + 1])
for idx in range(0, len(keys))
]
else:
quat_keyframes = [quaternion_gltf_to_blender(vals) for vals in values]
quat_keyframes = [gltf.quaternion_gltf_to_blender(vals) for vals in values]
_, bind_rot, _ = vnode.trs
bind_rot_inv = bind_rot.conjugated()
if node.parent is None:
final_rots = [
bind_rotation.inverted() @ quat_keyframe
for quat_keyframe in quat_keyframes
]
else:
if not gltf.data.nodes[node.parent].is_joint:
parent_mat = Matrix()
else:
parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix
if parent_mat != parent_mat.inverted():
final_rots = [
bind_rotation.rotation_difference(
(parent_mat @ quat_keyframe.to_matrix().to_4x4()).to_quaternion()
)
for quat_keyframe in quat_keyframes
]
else:
final_rots = [
bind_rotation.rotation_difference(quat_keyframe)
for quat_keyframe in quat_keyframes
]
final_rots = [
bind_rot_inv @ rot
for rot in quat_keyframes
]
# Manage antipodal quaternions
for i in range(1, len(final_rots)):
@ -129,38 +117,22 @@ class BlenderBoneAnim():
)
@staticmethod
def parse_scale_channel(gltf, node, obj, bone, channel, animation):
def parse_scale_channel(gltf, vnode, obj, bone, channel, animation):
"""Manage scaling animation."""
blender_path = "pose.bones[" + json.dumps(bone.name) + "].scale"
group_name = bone.name
keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input)
values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output)
bind_scale = scale_to_matrix(node.blender_bone_matrix.to_scale())
if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
# TODO manage tangent?
scale_mats = (
scale_to_matrix(loc_gltf_to_blender(values[idx * 3 + 1]))
final_scales = [
gltf.scale_gltf_to_blender(values[idx * 3 + 1])
for idx in range(0, len(keys))
)
else:
scale_mats = (scale_to_matrix(loc_gltf_to_blender(vals)) for vals in values)
if node.parent is None:
final_scales = [
(bind_scale.inverted() @ scale_mat).to_scale()
for scale_mat in scale_mats
]
else:
if not gltf.data.nodes[node.parent].is_joint:
parent_mat = Matrix()
else:
parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix
final_scales = [
(bind_scale.inverted() @ scale_to_matrix(parent_mat.to_scale()) @ scale_mat).to_scale()
for scale_mat in scale_mats
]
final_scales = [gltf.scale_gltf_to_blender(vals) for vals in values]
BlenderBoneAnim.fill_fcurves(
obj.animation_data.action,
@ -194,21 +166,21 @@ class BlenderBoneAnim():
def anim(gltf, anim_idx, node_idx):
"""Manage animation."""
node = gltf.data.nodes[node_idx]
blender_armature_name = gltf.data.skins[node.skin_id].blender_armature_name
obj = bpy.data.objects[blender_armature_name]
bone = obj.pose.bones[node.blender_bone_name]
vnode = gltf.vnodes[node_idx]
obj = gltf.vnodes[vnode.bone_arma].blender_object
bone = obj.pose.bones[vnode.blender_bone_name]
if anim_idx not in node.animations.keys():
return
animation = gltf.data.animations[anim_idx]
action = gltf.arma_cache.get(blender_armature_name)
action = gltf.action_cache.get(obj.name)
if not action:
name = animation.track_name + "_" + obj.name
action = bpy.data.actions.new(name)
gltf.needs_stash.append((obj, animation.track_name, action))
gltf.arma_cache[blender_armature_name] = action
gltf.action_cache[obj.name] = action
if not obj.animation_data:
obj.animation_data_create()
@ -218,11 +190,11 @@ class BlenderBoneAnim():
channel = animation.channels[channel_idx]
if channel.target.path == "translation":
BlenderBoneAnim.parse_translation_channel(gltf, node, obj, bone, channel, animation)
BlenderBoneAnim.parse_translation_channel(gltf, vnode, obj, bone, channel, animation)
elif channel.target.path == "rotation":
BlenderBoneAnim.parse_rotation_channel(gltf, node, obj, bone, channel, animation)
BlenderBoneAnim.parse_rotation_channel(gltf, vnode, obj, bone, channel, animation)
elif channel.target.path == "scale":
BlenderBoneAnim.parse_scale_channel(gltf, node, obj, bone, channel, animation)
BlenderBoneAnim.parse_scale_channel(gltf, vnode, obj, bone, channel, animation)

View File

@ -15,10 +15,9 @@
import bpy
from mathutils import Vector
from ..com.gltf2_blender_conversion import loc_gltf_to_blender, quaternion_gltf_to_blender, scale_gltf_to_blender
from ..com.gltf2_blender_conversion import correction_rotation
from ...io.imp.gltf2_io_binary import BinaryData
from .gltf2_blender_animation_utils import simulate_stash, make_fcurve
from .gltf2_blender_vnode import VNode
class BlenderNodeAnim():
@ -30,7 +29,8 @@ class BlenderNodeAnim():
def anim(gltf, anim_idx, node_idx):
"""Manage animation."""
node = gltf.data.nodes[node_idx]
obj = bpy.data.objects[node.blender_object]
vnode = gltf.vnodes[node_idx]
obj = vnode.blender_object
fps = bpy.context.scene.render.fps
animation = gltf.data.animations[anim_idx]
@ -45,9 +45,12 @@ class BlenderNodeAnim():
else:
return
name = animation.track_name + "_" + obj.name
action = bpy.data.actions.new(name)
gltf.needs_stash.append((obj, animation.track_name, action))
action = gltf.action_cache.get(obj.name)
if not action:
name = animation.track_name + "_" + obj.name
action = bpy.data.actions.new(name)
gltf.needs_stash.append((obj, animation.track_name, action))
gltf.action_cache[obj.name] = action
if not obj.animation_data:
obj.animation_data_create()
@ -62,10 +65,6 @@ class BlenderNodeAnim():
if channel.target.path not in ['translation', 'rotation', 'scale']:
continue
# There is an animation on object
# We can't remove Yup2Zup object
gltf.animation_object = True
if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
# TODO manage tangent?
values = [values[idx * 3 + 1] for idx in range(0, len(keys))]
@ -74,20 +73,20 @@ class BlenderNodeAnim():
blender_path = "location"
group_name = "Location"
num_components = 3
values = [loc_gltf_to_blender(vals) for vals in values]
if vnode.parent is not None and gltf.vnodes[vnode.parent].type == VNode.Bone:
# Nodes with a bone parent need to be translated
# backwards by their bone length (always 1 currently)
off = Vector((0, -1, 0))
values = [gltf.loc_gltf_to_blender(vals) + off for vals in values]
else:
values = [gltf.loc_gltf_to_blender(vals) for vals in values]
elif channel.target.path == "rotation":
blender_path = "rotation_quaternion"
group_name = "Rotation"
num_components = 4
if node.correction_needed is True:
values = [
(quaternion_gltf_to_blender(vals).to_matrix().to_4x4() @ correction_rotation()).to_quaternion()
for vals in values
]
else:
values = [quaternion_gltf_to_blender(vals) for vals in values]
values = [gltf.quaternion_gltf_to_blender(vals) for vals in values]
# Manage antipodal quaternions
for i in range(1, len(values)):
@ -98,7 +97,7 @@ class BlenderNodeAnim():
blender_path = "scale"
group_name = "Scale"
num_components = 3
values = [scale_gltf_to_blender(vals) for vals in values]
values = [gltf.scale_gltf_to_blender(vals) for vals in values]
coords = [0] * (2 * len(keys))
coords[::2] = (key[0] * fps for key in keys)

View File

@ -25,10 +25,16 @@ class BlenderWeightAnim():
raise RuntimeError("%s should not be instantiated" % cls)
@staticmethod
def anim(gltf, anim_idx, node_idx):
def anim(gltf, anim_idx, vnode_id):
"""Manage animation."""
vnode = gltf.vnodes[vnode_id]
node_idx = vnode.mesh_node_idx
if node_idx is None:
return
node = gltf.data.nodes[node_idx]
obj = bpy.data.objects[node.blender_object]
obj = vnode.blender_object
fps = bpy.context.scene.render.fps
animation = gltf.data.animations[anim_idx]

View File

@ -44,9 +44,5 @@ class BlenderCamera():
cam.clip_end = pycamera.zfar
obj = bpy.data.objects.new(pycamera.name, cam)
if gltf.blender_active_collection is not None:
bpy.data.collections[gltf.blender_active_collection].objects.link(obj)
else:
bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj)
return obj

View File

@ -13,8 +13,8 @@
# limitations under the License.
import bpy
from mathutils import Vector, Quaternion, Matrix
from .gltf2_blender_scene import BlenderScene
from ...io.com.gltf2_io_trs import TRS
class BlenderGlTF():
@ -25,79 +25,52 @@ class BlenderGlTF():
@staticmethod
def create(gltf):
"""Create glTF main method."""
if bpy.context.scene.render.engine not in ['CYCLES', 'BLENDER_EEVEE']:
bpy.context.scene.render.engine = 'BLENDER_EEVEE'
BlenderGlTF.set_convert_functions(gltf)
BlenderGlTF.pre_compute(gltf)
BlenderScene.create(gltf)
@staticmethod
def set_convert_functions(gltf):
yup2zup = bpy.app.debug_value != 100
if yup2zup:
# glTF Y-Up space --> Blender Z-up space
# X,Y,Z --> X,-Z,Y
def convert_loc(x): return Vector([x[0], -x[2], x[1]])
def convert_quat(q): return Quaternion([q[3], q[0], -q[2], q[1]])
def convert_normal(n): return Vector([n[0], -n[2], n[1]])
def convert_scale(s): return Vector([s[0], s[2], s[1]])
def convert_matrix(m):
return Matrix([
[ m[0], -m[ 8], m[4], m[12]],
[-m[2], m[10], -m[6], -m[14]],
[ m[1], -m[ 9], m[5], m[13]],
[ m[3], -m[11], m[7], m[15]],
])
# Correction for cameras and lights.
# glTF: right = +X, forward = -Z, up = +Y
# glTF after Yup2Zup: right = +X, forward = +Y, up = +Z
# Blender: right = +X, forward = -Z, up = +Y
# Need to carry Blender --> glTF after Yup2Zup
gltf.camera_correction = Quaternion((2**0.5/2, 2**0.5/2, 0.0, 0.0))
gltf.display_current_node = 0
if gltf.data.nodes is not None:
gltf.display_total_nodes = len(gltf.data.nodes)
else:
gltf.display_total_nodes = "?"
def convert_loc(x): return Vector(x)
def convert_quat(q): return Quaternion([q[3], q[0], q[1], q[2]])
def convert_normal(n): return Vector(n)
def convert_scale(s): return Vector(s)
def convert_matrix(m):
return Matrix([m[0::4], m[1::4], m[2::4], m[3::4]])
active_object_name_at_end = None
if gltf.data.scenes is not None:
for scene_idx, scene in enumerate(gltf.data.scenes):
BlenderScene.create(gltf, scene_idx)
# keep active object name if needed (to be able to set as active object at end)
if gltf.data.scene is not None:
if scene_idx == gltf.data.scene:
active_object_name_at_end = bpy.context.view_layer.objects.active.name
else:
if scene_idx == 0:
active_object_name_at_end = bpy.context.view_layer.objects.active.name
else:
# special case where there is no scene in glTF file
# generate all objects in current scene
BlenderScene.create(gltf, None)
active_object_name_at_end = bpy.context.view_layer.objects.active.name
# Same convention, no correction needed.
gltf.camera_correction = None
# Armature correction
# Try to detect bone chains, and set bone lengths
# To detect if a bone is in a chain, we try to detect if a bone head is aligned
# with parent_bone :
# Parent bone defined a line (between head & tail)
# Bone head defined a point
# Calcul of distance between point and line
# If < threshold --> In a chain
# Based on an idea of @Menithal, but added alignment detection to avoid some bad cases
threshold = 0.001
for armobj in [obj for obj in bpy.data.objects if obj.type == "ARMATURE"]:
# Take into account only armature from this scene
if armobj.name not in bpy.context.view_layer.objects:
continue
bpy.context.view_layer.objects.active = armobj
armature = armobj.data
bpy.ops.object.mode_set(mode="EDIT")
for bone in armature.edit_bones:
if bone.parent is None:
continue
parent = bone.parent
# case where 2 bones are aligned (not in chain, same head)
if (bone.head - parent.head).length < threshold:
continue
u = (parent.tail - parent.head).normalized()
point = bone.head
distance = ((point - parent.head).cross(u)).length / u.length
if distance < threshold:
save_parent_direction = (parent.tail - parent.head).normalized().copy()
save_parent_tail = parent.tail.copy()
parent.tail = bone.head
# case where 2 bones are aligned (not in chain, same head)
# bone is no more is same direction
if (parent.tail - parent.head).normalized().dot(save_parent_direction) < 0.9:
parent.tail = save_parent_tail
bpy.ops.object.mode_set(mode="OBJECT")
# Set active object
if active_object_name_at_end is not None:
bpy.context.view_layer.objects.active = bpy.data.objects[active_object_name_at_end]
gltf.loc_gltf_to_blender = convert_loc
gltf.quaternion_gltf_to_blender = convert_quat
gltf.normal_gltf_to_blender = convert_normal
gltf.scale_gltf_to_blender = convert_scale
gltf.matrix_gltf_to_blender = convert_matrix
@staticmethod
def pre_compute(gltf):
@ -208,45 +181,6 @@ class BlenderGlTF():
# Lights management
node.correction_needed = False
# transform management
if node.matrix:
node.transform = node.matrix
continue
# No matrix, but TRS
mat = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] # init
if node.scale:
mat = TRS.scale_to_matrix(node.scale)
if node.rotation:
q_mat = TRS.quaternion_to_matrix(node.rotation)
mat = TRS.matrix_multiply(q_mat, mat)
if node.translation:
loc_mat = TRS.translation_to_matrix(node.translation)
mat = TRS.matrix_multiply(loc_mat, mat)
node.transform = mat
# joint management
for node_idx, node in enumerate(gltf.data.nodes):
is_joint, skin_idx = gltf.is_node_joint(node_idx)
if is_joint:
node.is_joint = True
node.skin_id = skin_idx
else:
node.is_joint = False
if gltf.data.skins:
for skin_id, skin in enumerate(gltf.data.skins):
# init blender values
skin.blender_armature_name = None
# if skin.skeleton and skin.skeleton not in skin.joints:
# gltf.data.nodes[skin.skeleton].is_joint = True
# gltf.data.nodes[skin.skeleton].skin_id = skin_id
# Dispatch animation
if gltf.data.animations:
for node_idx, node in enumerate(gltf.data.nodes):
@ -274,7 +208,7 @@ class BlenderGlTF():
# Meshes
if gltf.data.meshes:
for mesh in gltf.data.meshes:
mesh.blender_name = None
mesh.blender_name = {} # cache Blender mesh (keyed by skin_idx)
mesh.is_weight_animated = False
# Calculate names for each mesh's shapekeys

View File

@ -42,11 +42,6 @@ class BlenderLight():
# TODO range
if gltf.blender_active_collection is not None:
bpy.data.collections[gltf.blender_active_collection].objects.link(obj)
else:
bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj)
set_extras(obj.data, pylight.get('extras'))
return obj

View File

@ -20,7 +20,6 @@ from ..com.gltf2_blender_extras import set_extras
from .gltf2_blender_material import BlenderMaterial
from .gltf2_blender_primitive import BlenderPrimitive
from ...io.imp.gltf2_io_binary import BinaryData
from ..com.gltf2_blender_conversion import loc_gltf_to_blender
class BlenderMesh():
@ -29,7 +28,7 @@ class BlenderMesh():
raise RuntimeError("%s should not be instantiated" % cls)
@staticmethod
def create(gltf, mesh_idx, node_idx, parent):
def create(gltf, mesh_idx, skin_idx):
"""Mesh creation."""
pymesh = gltf.data.meshes[mesh_idx]
@ -64,7 +63,7 @@ class BlenderMesh():
materials.append(material.name)
material_idx = len(materials) - 1
BlenderPrimitive.add_primitive_to_bmesh(gltf, bme, pymesh, prim, material_idx)
BlenderPrimitive.add_primitive_to_bmesh(gltf, bme, pymesh, prim, skin_idx, material_idx)
name = pymesh.name or 'Mesh_' + str(mesh_idx)
mesh = bpy.data.meshes.new(name)
@ -76,7 +75,7 @@ class BlenderMesh():
set_extras(mesh, pymesh.extras, exclude=['targetNames'])
pymesh.blender_name = mesh.name
pymesh.blender_name[skin_idx] = mesh.name
# Clear accessor cache after all primitives are done
gltf.accessor_cache = {}
@ -84,7 +83,7 @@ class BlenderMesh():
return mesh
@staticmethod
def set_mesh(gltf, pymesh, mesh, obj):
def set_mesh(gltf, pymesh, obj):
"""Sets mesh data after creation."""
# set default weights for shape keys, and names, if not set by convention on extras data
if pymesh.weights is not None:

View File

@ -13,13 +13,12 @@
# limitations under the License.
import bpy
from mathutils import Vector
from ..com.gltf2_blender_extras import set_extras
from .gltf2_blender_mesh import BlenderMesh
from .gltf2_blender_camera import BlenderCamera
from .gltf2_blender_skin import BlenderSkin
from .gltf2_blender_light import BlenderLight
from ..com.gltf2_blender_conversion import scale_to_matrix, matrix_gltf_to_blender, correction_rotation
from .gltf2_blender_vnode import VNode
class BlenderNode():
"""Blender Node."""
@ -27,216 +26,161 @@ class BlenderNode():
raise RuntimeError("%s should not be instantiated" % cls)
@staticmethod
def create(gltf, node_idx, parent):
"""Node creation."""
pynode = gltf.data.nodes[node_idx]
# Blender attributes initialization
pynode.blender_object = ""
pynode.parent = parent
def create_vnode(gltf, vnode_id):
"""Create VNode and all its descendants."""
vnode = gltf.vnodes[vnode_id]
name = vnode.name
gltf.display_current_node += 1
if bpy.app.debug_value == 101:
gltf.log.critical("Node " + str(gltf.display_current_node) + " of " + str(gltf.display_total_nodes) + " (idx " + str(node_idx) + ")")
gltf.log.critical("Node %d of %d (id %s)", gltf.display_current_node, len(gltf.vnodes), vnode_id)
if pynode.mesh is not None:
if vnode.type == VNode.Object:
BlenderNode.create_object(gltf, vnode_id)
instance = False
if gltf.data.meshes[pynode.mesh].blender_name is not None:
# Mesh is already created, only create instance
# Except is current node is animated with path weight
# Or if previous instance is animation at node level
if pynode.weight_animation is True:
elif vnode.type == VNode.Bone:
BlenderNode.create_bone(gltf, vnode_id)
elif vnode.type == VNode.DummyRoot:
# Don't actually create this
vnode.blender_object = None
for child in vnode.children:
BlenderNode.create_vnode(gltf, child)
@staticmethod
def create_object(gltf, vnode_id):
vnode = gltf.vnodes[vnode_id]
if vnode.mesh_node_idx is not None:
pynode = gltf.data.nodes[vnode.mesh_node_idx]
obj = BlenderNode.create_mesh_object(gltf, pynode, name=vnode.name)
elif vnode.camera_node_idx is not None:
pynode = gltf.data.nodes[vnode.camera_node_idx]
obj = BlenderCamera.create(gltf, pynode.camera)
elif vnode.light_node_idx is not None:
pynode = gltf.data.nodes[vnode.light_node_idx]
obj = BlenderLight.create(gltf, pynode.extensions['KHR_lights_punctual']['light'])
elif vnode.is_arma:
armature = bpy.data.armatures.new(vnode.arma_name)
obj = bpy.data.objects.new(vnode.name, armature)
else:
obj = bpy.data.objects.new(vnode.name, None)
vnode.blender_object = obj
# Set extras (if came from a glTF node)
if isinstance(vnode_id, int):
pynode = gltf.data.nodes[vnode_id]
set_extras(obj, pynode.extras)
# Set transform
trans, rot, scale = vnode.trs
obj.location = trans
obj.rotation_mode = 'QUATERNION'
obj.rotation_quaternion = rot
obj.scale = scale
# Set parent
if vnode.parent is not None:
parent_vnode = gltf.vnodes[vnode.parent]
if parent_vnode.type == VNode.Object:
obj.parent = parent_vnode.blender_object
elif parent_vnode.type == VNode.Bone:
arma_vnode = gltf.vnodes[parent_vnode.bone_arma]
obj.parent = arma_vnode.blender_object
obj.parent_type = 'BONE'
obj.parent_bone = parent_vnode.blender_bone_name
# Nodes with a bone parent need to be translated
# backwards by their bone length (always 1 currently)
obj.location += Vector((0, -1, 0))
bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj)
return obj
@staticmethod
def create_bone(gltf, vnode_id):
vnode = gltf.vnodes[vnode_id]
blender_arma = gltf.vnodes[vnode.bone_arma].blender_object
armature = blender_arma.data
# Switch into edit mode to create edit bone
if bpy.context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.window.scene = bpy.data.scenes[gltf.blender_scene]
bpy.context.view_layer.objects.active = blender_arma
bpy.ops.object.mode_set(mode="EDIT")
editbone = armature.edit_bones.new(vnode.name)
vnode.blender_bone_name = editbone.name
# Set extras (if came from a glTF node)
if isinstance(vnode_id, int):
pynode = gltf.data.nodes[vnode_id]
set_extras(editbone, pynode.extras)
# TODO
editbone.use_connect = False
# Give the position of the bone in armature space
arma_mat = vnode.bone_arma_mat
editbone.head = arma_mat @ Vector((0, 0, 0))
editbone.tail = arma_mat @ Vector((0, 1, 0))
editbone.align_roll(arma_mat @ Vector((0, 0, 1)) - editbone.head)
# Set parent
parent_vnode = gltf.vnodes[vnode.parent]
if parent_vnode.type == VNode.Bone:
editbone.parent = armature.edit_bones[parent_vnode.blender_bone_name]
bpy.ops.object.mode_set(mode="OBJECT")
pose_bone = blender_arma.pose.bones[vnode.blender_bone_name]
# Put scale on the pose bone (can't go on the edit bone)
_, _, s = vnode.trs
pose_bone.scale = s
if isinstance(vnode_id, int):
pynode = gltf.data.nodes[vnode_id]
set_extras(pose_bone, pynode.extras)
@staticmethod
def create_mesh_object(gltf, pynode, name):
instance = False
if gltf.data.meshes[pynode.mesh].blender_name.get(pynode.skin) is not None:
# Mesh is already created, only create instance
# Except is current node is animated with path weight
# Or if previous instance is animation at node level
if pynode.weight_animation is True:
instance = False
else:
if gltf.data.meshes[pynode.mesh].is_weight_animated is True:
instance = False
else:
if gltf.data.meshes[pynode.mesh].is_weight_animated is True:
instance = False
else:
instance = True
mesh = bpy.data.meshes[gltf.data.meshes[pynode.mesh].blender_name]
if instance is False:
if pynode.name:
gltf.log.info("Blender create Mesh node " + pynode.name)
else:
gltf.log.info("Blender create Mesh node")
mesh = BlenderMesh.create(gltf, pynode.mesh, node_idx, parent)
if pynode.weight_animation is True:
# flag this mesh instance as created only for this node, because of weight animation
gltf.data.meshes[pynode.mesh].is_weight_animated = True
instance = True
mesh = bpy.data.meshes[gltf.data.meshes[pynode.mesh].blender_name[pynode.skin]]
if instance is False:
if pynode.name:
name = pynode.name
gltf.log.info("Blender create Mesh node " + pynode.name)
else:
# Take mesh name if exist
if gltf.data.meshes[pynode.mesh].name:
name = gltf.data.meshes[pynode.mesh].name
else:
name = "Object_" + str(node_idx)
gltf.log.info("Blender create Mesh node")
obj = bpy.data.objects.new(name, mesh)
set_extras(obj, pynode.extras)
obj.rotation_mode = 'QUATERNION'
if gltf.blender_active_collection is not None:
bpy.data.collections[gltf.blender_active_collection].objects.link(obj)
else:
bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj)
mesh = BlenderMesh.create(gltf, pynode.mesh, pynode.skin)
# Transforms apply only if this mesh is not skinned
# See implementation node of gltf2 specification
if not (pynode.mesh is not None and pynode.skin is not None):
BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent)
pynode.blender_object = obj.name
BlenderNode.set_parent(gltf, obj, parent)
if pynode.weight_animation is True:
# flag this mesh instance as created only for this node, because of weight animation
gltf.data.meshes[pynode.mesh].is_weight_animated = True
if instance == False:
BlenderMesh.set_mesh(gltf, gltf.data.meshes[pynode.mesh], mesh, obj)
mesh_name = gltf.data.meshes[pynode.mesh].name
if not name and mesh_name:
name = mesh_name
if pynode.children:
for child_idx in pynode.children:
BlenderNode.create(gltf, child_idx, node_idx)
obj = bpy.data.objects.new(name, mesh)
return
if instance == False:
BlenderMesh.set_mesh(gltf, gltf.data.meshes[pynode.mesh], obj)
if pynode.camera is not None:
if pynode.name:
gltf.log.info("Blender create Camera node " + pynode.name)
else:
gltf.log.info("Blender create Camera node")
obj = BlenderCamera.create(gltf, pynode.camera)
set_extras(obj, pynode.extras)
BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent) # TODO default rotation of cameras ?
pynode.blender_object = obj.name
BlenderNode.set_parent(gltf, obj, parent)
if pynode.children:
for child_idx in pynode.children:
BlenderNode.create(gltf, child_idx, node_idx)
return
if pynode.is_joint:
if pynode.name:
gltf.log.info("Blender create Bone node " + pynode.name)
else:
gltf.log.info("Blender create Bone node")
# Check if corresponding armature is already created, create it if needed
if gltf.data.skins[pynode.skin_id].blender_armature_name is None:
BlenderSkin.create_armature(gltf, pynode.skin_id, parent)
BlenderSkin.create_bone(gltf, pynode.skin_id, node_idx, parent)
if pynode.children:
for child_idx in pynode.children:
BlenderNode.create(gltf, child_idx, node_idx)
return
if pynode.extensions is not None:
if 'KHR_lights_punctual' in pynode.extensions.keys():
obj = BlenderLight.create(gltf, pynode.extensions['KHR_lights_punctual']['light'])
set_extras(obj, pynode.extras)
obj.rotation_mode = 'QUATERNION'
BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent, correction=True)
pynode.blender_object = obj.name
pynode.correction_needed = True
BlenderNode.set_parent(gltf, obj, parent)
if pynode.children:
for child_idx in pynode.children:
BlenderNode.create(gltf, child_idx, node_idx)
return
# No mesh, no camera, no light. For now, create empty #TODO
if pynode.name:
gltf.log.info("Blender create Empty node " + pynode.name)
obj = bpy.data.objects.new(pynode.name, None)
else:
gltf.log.info("Blender create Empty node")
obj = bpy.data.objects.new("Node", None)
set_extras(obj, pynode.extras)
obj.rotation_mode = 'QUATERNION'
if gltf.blender_active_collection is not None:
bpy.data.collections[gltf.blender_active_collection].objects.link(obj)
else:
bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj)
BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent)
pynode.blender_object = obj.name
BlenderNode.set_parent(gltf, obj, parent)
if pynode.children:
for child_idx in pynode.children:
BlenderNode.create(gltf, child_idx, node_idx)
@staticmethod
def set_parent(gltf, obj, parent):
"""Set parent."""
if parent is None:
return
for node_idx, node in enumerate(gltf.data.nodes):
if node_idx == parent:
if node.is_joint is True:
bpy.ops.object.select_all(action='DESELECT')
bpy.data.objects[node.blender_armature_name].select_set(True)
bpy.context.view_layer.objects.active = bpy.data.objects[node.blender_armature_name]
bpy.ops.object.mode_set(mode='EDIT')
bpy.data.objects[node.blender_armature_name].data.edit_bones.active = \
bpy.data.objects[node.blender_armature_name].data.edit_bones[node.blender_bone_name]
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.data.objects[node.blender_armature_name].select_set(True)
bpy.context.view_layer.objects.active = bpy.data.objects[node.blender_armature_name]
bpy.context.view_layer.update()
bpy.ops.object.parent_set(type='BONE_RELATIVE', keep_transform=True)
# From world transform to local (-armature transform -bone transform)
bone_trans = bpy.data.objects[node.blender_armature_name] \
.pose.bones[node.blender_bone_name].matrix.to_translation().copy()
bone_rot = bpy.data.objects[node.blender_armature_name] \
.pose.bones[node.blender_bone_name].matrix.to_quaternion().copy()
bone_scale_mat = scale_to_matrix(node.blender_bone_matrix.to_scale())
obj.location = bone_scale_mat @ obj.location
obj.location = bone_rot @ obj.location
obj.location += bone_trans
obj.location = bpy.data.objects[node.blender_armature_name].matrix_world.to_quaternion() \
@ obj.location
obj.rotation_quaternion = obj.rotation_quaternion \
@ bpy.data.objects[node.blender_armature_name].matrix_world.to_quaternion()
obj.scale = bone_scale_mat @ obj.scale
return
if node.blender_object:
obj.parent = bpy.data.objects[node.blender_object]
return
gltf.log.error("ERROR, parent not found")
@staticmethod
def set_transforms(gltf, node_idx, pynode, obj, parent, correction=False):
"""Set transforms."""
if parent is None:
obj.matrix_world = matrix_gltf_to_blender(pynode.transform)
if correction is True:
obj.matrix_world = obj.matrix_world @ correction_rotation()
return
for idx, node in enumerate(gltf.data.nodes):
if idx == parent:
if node.is_joint is True:
obj.matrix_world = matrix_gltf_to_blender(pynode.transform)
if correction is True:
obj.matrix_world = obj.matrix_world @ correction_rotation()
return
else:
if correction is True:
obj.matrix_world = obj.matrix_world @ correction_rotation()
obj.matrix_world = matrix_gltf_to_blender(pynode.transform)
return
return obj

View File

@ -13,10 +13,9 @@
# limitations under the License.
import bpy
from mathutils import Vector
from mathutils import Vector, Matrix
from .gltf2_blender_material import BlenderMaterial
from ..com.gltf2_blender_conversion import loc_gltf_to_blender
from ...io.imp.gltf2_io_binary import BinaryData
from ...io.com.gltf2_io_color_management import color_linear_to_srgb
from ...io.com import gltf2_io_debug
@ -37,7 +36,7 @@ class BlenderPrimitive():
return bme_layers[name]
@staticmethod
def add_primitive_to_bmesh(gltf, bme, pymesh, pyprimitive, material_index):
def add_primitive_to_bmesh(gltf, bme, pymesh, pyprimitive, skin_idx, material_index):
attributes = pyprimitive.attributes
if 'POSITION' not in attributes:
@ -57,6 +56,62 @@ class BlenderPrimitive():
bme_edges = bme.edges
bme_faces = bme.faces
# Gather up the joints/weights (multiple sets allow >4 influences)
joint_sets = []
weight_sets = []
set_num = 0
while 'JOINTS_%d' % set_num in attributes and 'WEIGHTS_%d' % set_num in attributes:
joint_data = BinaryData.get_data_from_accessor(gltf, attributes['JOINTS_%d' % set_num], cache=True)
weight_data = BinaryData.get_data_from_accessor(gltf, attributes['WEIGHTS_%d' % set_num], cache=True)
joint_sets.append(joint_data)
weight_sets.append(weight_data)
set_num += 1
# For skinned meshes, we will need to calculate the position of the
# verts in the bind pose, ie. the pose the edit bones are in.
if skin_idx is not None:
pyskin = gltf.data.skins[skin_idx]
if pyskin.inverse_bind_matrices is not None:
inv_binds = BinaryData.get_data_from_accessor(gltf, pyskin.inverse_bind_matrices)
inv_binds = [gltf.matrix_gltf_to_blender(m) for m in inv_binds]
else:
inv_binds = [Matrix.Identity(4) for i in range(len(pyskin.joints))]
arma_mats = [gltf.vnodes[joint].bone_arma_mat for joint in pyskin.joints]
joint_mats = [arma_mat @ inv_bind for arma_mat, inv_bind in zip(arma_mats, inv_binds)]
def skin_vert(pos, pidx):
out = Vector((0, 0, 0))
# Spec says weights should already sum to 1 but some models
# don't do it (ex. CesiumMan), so normalize.
weight_sum = 0
for joint_set, weight_set in zip(joint_sets, weight_sets):
for j in range(0, 4):
weight = weight_set[pidx][j]
if weight != 0.0:
weight_sum += weight
joint = joint_set[pidx][j]
out += weight * (joint_mats[joint] @ pos)
out /= weight_sum
return out
def skin_normal(norm, pidx):
# TODO: not sure this is right
norm = Vector([norm[0], norm[1], norm[2], 0])
out = Vector((0, 0, 0, 0))
weight_sum = 0
for joint_set, weight_set in zip(joint_sets, weight_sets):
for j in range(0, 4):
weight = weight_set[pidx][j]
if weight != 0.0:
weight_sum += weight
joint = joint_set[pidx][j]
out += weight * (joint_mats[joint] @ norm)
out /= weight_sum
out = out.to_3d().normalized()
return out
# Every vertex has an index into the primitive's attribute arrays and a
# *different* index into the BMesh's list of verts. Call the first one the
# pidx and the second the bidx. Need to keep them straight!
@ -74,7 +129,11 @@ class BlenderPrimitive():
used_pidxs = list(used_pidxs)
used_pidxs.sort()
for pidx in used_pidxs:
bme_verts.new(positions[pidx])
pos = gltf.loc_gltf_to_blender(positions[pidx])
if skin_idx is not None:
pos = skin_vert(pos, pidx)
bme_verts.new(pos)
vert_idxs.append((bidx, pidx))
pidx_to_bidx[pidx] = bidx
bidx += 1
@ -114,8 +173,13 @@ class BlenderPrimitive():
if 'NORMAL' in attributes:
normals = BinaryData.get_data_from_accessor(gltf, attributes['NORMAL'], cache=True)
for bidx, pidx in vert_idxs:
bme_verts[bidx].normal = normals[pidx]
if skin_idx is None:
for bidx, pidx in vert_idxs:
bme_verts[bidx].normal = gltf.normal_gltf_to_blender(normals[pidx])
else:
for bidx, pidx in vert_idxs:
normal = gltf.normal_gltf_to_blender(normals[pidx])
bme_verts[bidx].normal = skin_normal(normal, pidx)
# Set vertex colors. Add them in the order COLOR_0, COLOR_1, etc.
set_num = 0
@ -176,19 +240,7 @@ class BlenderPrimitive():
set_num += 1
# Set joints/weights for skinning (multiple sets allow > 4 influences)
joint_sets = []
weight_sets = []
set_num = 0
while 'JOINTS_%d' % set_num in attributes and 'WEIGHTS_%d' % set_num in attributes:
joint_data = BinaryData.get_data_from_accessor(gltf, attributes['JOINTS_%d' % set_num], cache=True)
weight_data = BinaryData.get_data_from_accessor(gltf, attributes['WEIGHTS_%d' % set_num], cache=True)
joint_sets.append(joint_data)
weight_sets.append(weight_data)
set_num += 1
# Set joints/weights for skinning
if joint_sets:
layer = BlenderPrimitive.get_layer(bme.verts.layers.deform, 'Vertex Weights')
@ -210,11 +262,19 @@ class BlenderPrimitive():
morph_positions = BinaryData.get_data_from_accessor(gltf, target['POSITION'], cache=True)
for bidx, pidx in vert_idxs:
bme_verts[bidx][layer] = (
Vector(positions[pidx]) +
Vector(morph_positions[pidx])
)
if skin_idx is None:
for bidx, pidx in vert_idxs:
bme_verts[bidx][layer] = (
gltf.loc_gltf_to_blender(positions[pidx]) +
gltf.loc_gltf_to_blender(morph_positions[pidx])
)
else:
for bidx, pidx in vert_idxs:
pos = (
gltf.loc_gltf_to_blender(positions[pidx]) +
gltf.loc_gltf_to_blender(morph_positions[pidx])
)
bme_verts[bidx][layer] = skin_vert(pos, pidx)
@staticmethod
def edges_and_faces(mode, indices):

View File

@ -19,6 +19,7 @@ from .gltf2_blender_node import BlenderNode
from .gltf2_blender_skin import BlenderSkin
from .gltf2_blender_animation import BlenderAnimation
from .gltf2_blender_animation_utils import simulate_stash
from .gltf2_blender_vnode import VNode, compute_vnodes
class BlenderScene():
@ -27,168 +28,67 @@ class BlenderScene():
raise RuntimeError("%s should not be instantiated" % cls)
@staticmethod
def create(gltf, scene_idx):
def create(gltf):
"""Scene creation."""
gltf.blender_active_collection = None
if scene_idx is not None:
pyscene = gltf.data.scenes[scene_idx]
list_nodes = pyscene.nodes
scene = bpy.context.scene
gltf.blender_scene = scene.name
if bpy.context.collection.name in bpy.data.collections: # avoid master collection
gltf.blender_active_collection = bpy.context.collection.name
if scene.render.engine not in ['CYCLES', 'BLENDER_EEVEE']:
scene.render.engine = "BLENDER_EEVEE"
# Create a new scene only if not already exists in .blend file
# TODO : put in current scene instead ?
if pyscene.name not in [scene.name for scene in bpy.data.scenes]:
# TODO: There is a bug in 2.8 alpha that break CLEAR_KEEP_TRANSFORM
# if we are creating a new scene
scene = bpy.context.scene
if bpy.context.collection.name in bpy.data.collections: # avoid master collection
gltf.blender_active_collection = bpy.context.collection.name
if scene.render.engine not in ['CYCLES', 'BLENDER_EEVEE']:
scene.render.engine = "BLENDER_EEVEE"
compute_vnodes(gltf)
gltf.blender_scene = scene.name
else:
gltf.blender_scene = pyscene.name
# Switch to newly created main scene
bpy.context.window.scene = bpy.data.scenes[gltf.blender_scene]
if bpy.context.collection.name in bpy.data.collections: # avoid master collection
gltf.blender_active_collection = bpy.context.collection.name
else:
# No scene in glTF file, create all objects in current scene
scene = bpy.context.scene
if scene.render.engine not in ['CYCLES', 'BLENDER_EEVEE']:
scene.render.engine = "BLENDER_EEVEE"
if bpy.context.collection.name in bpy.data.collections: # avoid master collection
gltf.blender_active_collection = bpy.context.collection.name
gltf.blender_scene = scene.name
list_nodes = BlenderScene.get_root_nodes(gltf)
if bpy.app.debug_value != 100:
# Create Yup2Zup empty
obj_rotation = bpy.data.objects.new("Yup2Zup", None)
obj_rotation.rotation_mode = 'QUATERNION'
obj_rotation.rotation_quaternion = Quaternion((sqrt(2) / 2, sqrt(2) / 2, 0.0, 0.0))
if gltf.blender_active_collection is not None:
bpy.data.collections[gltf.blender_active_collection].objects.link(obj_rotation)
else:
bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj_rotation)
if list_nodes is not None:
for node_idx in list_nodes:
BlenderNode.create(gltf, node_idx, None) # None => No parent
gltf.display_current_node = 0 # for debugging
BlenderNode.create_vnode(gltf, 'root')
# Now that all mesh / bones are created, create vertex groups on mesh
if gltf.data.skins:
for skin_id, skin in enumerate(gltf.data.skins):
if hasattr(skin, "node_ids"):
BlenderSkin.create_vertex_groups(gltf, skin_id)
BlenderSkin.create_vertex_groups(gltf)
BlenderSkin.create_armature_modifiers(gltf)
for skin_id, skin in enumerate(gltf.data.skins):
if hasattr(skin, "node_ids"):
BlenderSkin.create_armature_modifiers(gltf, skin_id)
BlenderScene.create_animations(gltf)
if bpy.context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
BlenderScene.set_active_object(gltf)
@staticmethod
def create_animations(gltf):
"""Create animations."""
if gltf.data.animations:
for anim_idx, anim in enumerate(gltf.data.animations):
# Blender armature name -> action all its bones should use
gltf.arma_cache = {}
# Caches the action for each object (keyed by object name)
gltf.action_cache = {}
# Things we need to stash when we're done.
gltf.needs_stash = []
if list_nodes is not None:
for node_idx in list_nodes:
BlenderAnimation.anim(gltf, anim_idx, node_idx)
BlenderAnimation.anim(gltf, anim_idx, 'root')
for (obj, anim_name, action) in gltf.needs_stash:
simulate_stash(obj, anim_name, action)
# Restore first animation
anim_name = gltf.data.animations[0].track_name
for node_idx in list_nodes:
BlenderAnimation.restore_animation(gltf, node_idx, anim_name)
if bpy.app.debug_value != 100:
# Parent root node to rotation object
if list_nodes is not None:
exclude_nodes = []
for node_idx in list_nodes:
if gltf.data.nodes[node_idx].is_joint:
# Do not change parent if root node is already parented (can be the case for skinned mesh)
if not bpy.data.objects[gltf.data.nodes[node_idx].blender_armature_name].parent:
bpy.data.objects[gltf.data.nodes[node_idx].blender_armature_name].parent = obj_rotation
else:
exclude_nodes.append(node_idx)
else:
# Do not change parent if root node is already parented (can be the case for skinned mesh)
if not bpy.data.objects[gltf.data.nodes[node_idx].blender_object].parent:
bpy.data.objects[gltf.data.nodes[node_idx].blender_object].parent = obj_rotation
else:
exclude_nodes.append(node_idx)
if gltf.animation_object is False:
# Avoid rotation bug if collection is hidden or disabled
if gltf.blender_active_collection is not None:
gltf.collection_hide_viewport = bpy.data.collections[gltf.blender_active_collection].hide_viewport
bpy.data.collections[gltf.blender_active_collection].hide_viewport = False
# TODO for visibility ... but seems not exposed on bpy for now
for node_idx in list_nodes:
if node_idx in exclude_nodes:
continue # for root node that are parented by the process
# for example skinned meshes
for obj_ in bpy.context.scene.objects:
obj_.select_set(False)
if gltf.data.nodes[node_idx].is_joint:
bpy.data.objects[gltf.data.nodes[node_idx].blender_armature_name].select_set(True)
bpy.context.view_layer.objects.active = bpy.data.objects[gltf.data.nodes[node_idx].blender_armature_name]
else:
bpy.data.objects[gltf.data.nodes[node_idx].blender_object].select_set(True)
bpy.context.view_layer.objects.active = bpy.data.objects[gltf.data.nodes[node_idx].blender_object]
bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
# remove object
#bpy.context.scene.collection.objects.unlink(obj_rotation)
bpy.data.objects.remove(obj_rotation)
# Restore collection hidden / disabled values
if gltf.blender_active_collection is not None:
bpy.data.collections[gltf.blender_active_collection].hide_viewport = gltf.collection_hide_viewport
# TODO restore visibility when expose in bpy
# Make first root object the new active one
if list_nodes is not None:
if gltf.data.nodes[list_nodes[0]].blender_object:
bl_name = gltf.data.nodes[list_nodes[0]].blender_object
else:
bl_name = gltf.data.nodes[list_nodes[0]].blender_armature_name
bpy.context.view_layer.objects.active = bpy.data.objects[bl_name]
BlenderAnimation.restore_animation(gltf, 'root', anim_name)
@staticmethod
def get_root_nodes(gltf):
if gltf.data.nodes is None:
return None
def set_active_object(gltf):
"""Make the first root object from the default glTF scene active.
If no default scene, use the first scene, or just any root object.
"""
if gltf.data.scenes:
pyscene = gltf.data.scenes[gltf.data.scene or 0]
vnode = gltf.vnodes[pyscene.nodes[0]]
if gltf.vnodes[vnode.parent].type != VNode.DummyRoot:
vnode = gltf.vnodes[vnode.parent]
parents = {}
for idx, node in enumerate(gltf.data.nodes):
pynode = gltf.data.nodes[idx]
if pynode.children:
for child_idx in pynode.children:
parents[child_idx] = idx
else:
vnode = gltf.vnodes['root']
if vnode.type == VNode.DummyRoot:
if not vnode.children:
return # no nodes
vnode = gltf.vnodes[vnode.children[0]]
roots = []
for idx, node in enumerate(gltf.data.nodes):
if idx not in parents.keys():
roots.append(idx)
return roots
bpy.context.view_layer.objects.active = vnode.blender_object

View File

@ -14,10 +14,6 @@
import bpy
from mathutils import Vector, Matrix
from ..com.gltf2_blender_conversion import matrix_gltf_to_blender, scale_to_matrix
from ...io.imp.gltf2_io_binary import BinaryData
from ..com.gltf2_blender_extras import set_extras
class BlenderSkin():
"""Blender Skinning / Armature."""
@ -25,154 +21,36 @@ class BlenderSkin():
raise RuntimeError("%s should not be instantiated" % cls)
@staticmethod
def create_armature(gltf, skin_id, parent):
"""Armature creation."""
pyskin = gltf.data.skins[skin_id]
def create_vertex_groups(gltf):
"""Create vertex groups for all skinned meshes."""
for vnode in gltf.vnodes.values():
if vnode.mesh_node_idx is None:
continue
pynode = gltf.data.nodes[vnode.mesh_node_idx]
if pynode.skin is None:
continue
pyskin = gltf.data.skins[pynode.skin]
if pyskin.name is not None:
name = pyskin.name
else:
name = "Armature_" + str(skin_id)
armature = bpy.data.armatures.new(name)
obj = bpy.data.objects.new(name, armature)
if gltf.blender_active_collection is not None:
bpy.data.collections[gltf.blender_active_collection].objects.link(obj)
else:
bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj)
pyskin.blender_armature_name = obj.name
if parent is not None:
obj.parent = bpy.data.objects[gltf.data.nodes[parent].blender_object]
obj = vnode.blender_object
for node_idx in pyskin.joints:
bone = gltf.vnodes[node_idx]
obj.vertex_groups.new(name=bone.blender_bone_name)
@staticmethod
def set_bone_transforms(gltf, skin_id, bone, node_id, parent):
"""Set bone transformations."""
pyskin = gltf.data.skins[skin_id]
pynode = gltf.data.nodes[node_id]
def create_armature_modifiers(gltf):
"""Create Armature modifiers for all skinned meshes."""
for vnode in gltf.vnodes.values():
if vnode.mesh_node_idx is None:
continue
pynode = gltf.data.nodes[vnode.mesh_node_idx]
if pynode.skin is None:
continue
pyskin = gltf.data.skins[pynode.skin]
obj = bpy.data.objects[pyskin.blender_armature_name]
first_bone = gltf.vnodes[pyskin.joints[0]]
arma = gltf.vnodes[first_bone.bone_arma]
# Set bone bind_pose by inverting bindpose matrix
if node_id in pyskin.joints:
index_in_skel = pyskin.joints.index(node_id)
if pyskin.inverse_bind_matrices is not None:
inverse_bind_matrices = BinaryData.get_data_from_accessor(gltf, pyskin.inverse_bind_matrices)
# Needed to keep scale in matrix, as bone.matrix seems to drop it
if index_in_skel < len(inverse_bind_matrices):
pynode.blender_bone_matrix = matrix_gltf_to_blender(
inverse_bind_matrices[index_in_skel]
).inverted()
bone.matrix = pynode.blender_bone_matrix
else:
gltf.log.error("Error with inverseBindMatrix for skin " + pyskin)
else:
pynode.blender_bone_matrix = Matrix() # 4x4 identity matrix
else:
print('No invBindMatrix for bone ' + str(node_id))
pynode.blender_bone_matrix = Matrix()
# Parent the bone
if parent is not None and hasattr(gltf.data.nodes[parent], "blender_bone_name"):
bone.parent = obj.data.edit_bones[gltf.data.nodes[parent].blender_bone_name] # TODO if in another scene
# Switch to Pose mode
bpy.ops.object.mode_set(mode="POSE")
obj.data.pose_position = 'POSE'
# Set posebone location/rotation/scale (in armature space)
# location is actual bone location minus it's original (bind) location
bind_location = Matrix.Translation(pynode.blender_bone_matrix.to_translation())
bind_rotation = pynode.blender_bone_matrix.to_quaternion()
bind_scale = scale_to_matrix(pynode.blender_bone_matrix.to_scale())
location, rotation, scale = matrix_gltf_to_blender(pynode.transform).decompose()
if parent is not None and hasattr(gltf.data.nodes[parent], "blender_bone_matrix"):
parent_mat = gltf.data.nodes[parent].blender_bone_matrix
# Get armature space location (bindpose + pose)
# Then, remove original bind location from armspace location, and bind rotation
final_location = (bind_location.inverted() @ parent_mat @ Matrix.Translation(location)).to_translation()
obj.pose.bones[pynode.blender_bone_name].location = \
bind_rotation.inverted().to_matrix().to_4x4() @ final_location
# Do the same for rotation & scale
obj.pose.bones[pynode.blender_bone_name].rotation_quaternion = \
(pynode.blender_bone_matrix.inverted() @ parent_mat @
matrix_gltf_to_blender(pynode.transform)).to_quaternion()
obj.pose.bones[pynode.blender_bone_name].scale = \
(bind_scale.inverted() @ parent_mat @ scale_to_matrix(scale)).to_scale()
else:
obj.pose.bones[pynode.blender_bone_name].location = bind_location.inverted() @ location
obj.pose.bones[pynode.blender_bone_name].rotation_quaternion = bind_rotation.inverted() @ rotation
obj.pose.bones[pynode.blender_bone_name].scale = bind_scale.inverted() @ scale
@staticmethod
def create_bone(gltf, skin_id, node_id, parent):
"""Bone creation."""
pyskin = gltf.data.skins[skin_id]
pynode = gltf.data.nodes[node_id]
scene = bpy.data.scenes[gltf.blender_scene]
obj = bpy.data.objects[pyskin.blender_armature_name]
bpy.context.window.scene = scene
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="EDIT")
if pynode.name:
name = pynode.name
else:
name = "Bone_" + str(node_id)
bone = obj.data.edit_bones.new(name)
pynode.blender_bone_name = bone.name
pynode.blender_armature_name = pyskin.blender_armature_name
bone.tail = Vector((0.0, 1.0 / obj.matrix_world.to_scale()[1], 0.0)) # Needed to keep bone alive
# Custom prop on edit bone
set_extras(bone, pynode.extras)
# set bind and pose transforms
BlenderSkin.set_bone_transforms(gltf, skin_id, bone, node_id, parent)
bpy.ops.object.mode_set(mode="OBJECT")
# Custom prop on pose bone
if pynode.blender_bone_name in obj.pose.bones:
set_extras(obj.pose.bones[pynode.blender_bone_name], pynode.extras)
@staticmethod
def create_vertex_groups(gltf, skin_id):
"""Vertex Group creation."""
pyskin = gltf.data.skins[skin_id]
for node_id in pyskin.node_ids:
obj = bpy.data.objects[gltf.data.nodes[node_id].blender_object]
for bone in pyskin.joints:
obj.vertex_groups.new(name=gltf.data.nodes[bone].blender_bone_name)
@staticmethod
def create_armature_modifiers(gltf, skin_id):
"""Create Armature modifier."""
pyskin = gltf.data.skins[skin_id]
if pyskin.blender_armature_name is None:
# TODO seems something is wrong
# For example, some joints are in skin 0, and are in another skin too
# Not sure this is glTF compliant, will check it
return
for node_id in pyskin.node_ids:
node = gltf.data.nodes[node_id]
obj = bpy.data.objects[node.blender_object]
for obj_sel in bpy.context.scene.objects:
obj_sel.select_set(False)
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
# bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
# Reparent skinned mesh to it's armature to avoid breaking
# skinning with interleaved transforms
obj.parent = bpy.data.objects[pyskin.blender_armature_name]
arma = obj.modifiers.new(name="Armature", type="ARMATURE")
arma.object = bpy.data.objects[pyskin.blender_armature_name]
obj = vnode.blender_object
mod = obj.modifiers.new(name="Armature", type="ARMATURE")
mod.object = arma.blender_object

View File

@ -0,0 +1,324 @@
# Copyright 2018-2019 The glTF-Blender-IO authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import bpy
from mathutils import Vector, Quaternion, Matrix
def compute_vnodes(gltf):
"""Computes the tree of virtual nodes.
Copies the glTF nodes into a tree of VNodes, then performs a series of
passes to transform it into a form that we can import into Blender.
"""
init_vnodes(gltf)
mark_bones_and_armas(gltf)
move_skinned_meshes(gltf)
fixup_multitype_nodes(gltf)
correct_cameras_and_lights(gltf)
calc_bone_matrices(gltf)
class VNode:
"""A "virtual" node.
These are what eventually get turned into nodes
in the Blender scene.
"""
# Types
Object = 0
Bone = 1
DummyRoot = 2
def __init__(self):
self.name = ''
self.children = []
self.parent = None
self.type = VNode.Object
self.is_arma = False
self.trs = (
Vector((0, 0, 0)),
Quaternion((1, 0, 0, 0)),
Vector((1, 1, 1)),
)
# Indices of the glTF node where the mesh, etc. came from.
# (They can get moved around.)
self.mesh_node_idx = None
self.camera_node_idx = None
self.light_node_idx = None
def init_vnodes(gltf):
# Map of all VNodes. The keys are arbitrary IDs.
# Nodes coming from glTF use the index into gltf.data.nodes for an ID.
gltf.vnodes = {}
for i, pynode in enumerate(gltf.data.nodes or []):
vnode = VNode()
gltf.vnodes[i] = vnode
vnode.name = pynode.name or 'Node_%d' % i
vnode.children = list(pynode.children or [])
vnode.trs = get_node_trs(gltf, pynode)
if pynode.mesh is not None:
vnode.mesh_node_idx = i
if pynode.camera is not None:
vnode.camera_node_idx = i
if 'KHR_lights_punctual' in (pynode.extensions or {}):
vnode.light_node_idx = i
for id in gltf.vnodes:
for child in gltf.vnodes[id].children:
assert gltf.vnodes[child].parent is None
gltf.vnodes[child].parent = id
# Inserting a root node will simplify things.
roots = [id for id in gltf.vnodes if gltf.vnodes[id].parent is None]
gltf.vnodes['root'] = VNode()
gltf.vnodes['root'].type = VNode.DummyRoot
gltf.vnodes['root'].name = 'Root'
gltf.vnodes['root'].children = roots
for root in roots:
gltf.vnodes[root].parent = 'root'
def get_node_trs(gltf, pynode):
if pynode.matrix is not None:
m = gltf.matrix_gltf_to_blender(pynode.matrix)
return m.decompose()
t = gltf.loc_gltf_to_blender(pynode.translation or [0, 0, 0])
r = gltf.quaternion_gltf_to_blender(pynode.rotation or [0, 0, 0, 1])
s = gltf.scale_gltf_to_blender(pynode.scale or [1, 1, 1])
return t, r, s
def mark_bones_and_armas(gltf):
"""
Mark nodes as armatures so that every node that is used as joint is a
descendant of an armature. Mark everything between an armature and a
joint as a bone.
"""
for skin in gltf.data.skins or []:
descendants = list(skin.joints)
if skin.skeleton is not None:
descendants.append(skin.skeleton)
arma_id = deepest_common_ancestor(gltf, descendants)
if arma_id in skin.joints:
arma_id = gltf.vnodes[arma_id].parent
if gltf.vnodes[arma_id].type != VNode.Bone:
gltf.vnodes[arma_id].type = VNode.Object
gltf.vnodes[arma_id].is_arma = True
gltf.vnodes[arma_id].arma_name = skin.name or 'Armature'
for joint in skin.joints:
while joint != arma_id:
gltf.vnodes[joint].type = VNode.Bone
gltf.vnodes[joint].is_arma = False
joint = gltf.vnodes[joint].parent
# Mark the armature each bone is a descendant of.
def visit(vnode_id, cur_arma): # Depth-first walk
vnode = gltf.vnodes[vnode_id]
if vnode.is_arma:
cur_arma = vnode_id
elif vnode.type == VNode.Bone:
vnode.bone_arma = cur_arma
else:
cur_arma = None
for child in vnode.children:
visit(child, cur_arma)
visit('root', cur_arma=None)
def deepest_common_ancestor(gltf, vnode_ids):
"""Find the deepest (improper) ancestor of a set of vnodes."""
path_to_ancestor = [] # path to deepest ancestor so far
for vnode_id in vnode_ids:
path = path_from_root(gltf, vnode_id)
if not path_to_ancestor:
path_to_ancestor = path
else:
path_to_ancestor = longest_common_prefix(path, path_to_ancestor)
return path_to_ancestor[-1]
def path_from_root(gltf, vnode_id):
"""Returns the ids of all vnodes from the root to vnode_id."""
path = []
while vnode_id is not None:
path.append(vnode_id)
vnode_id = gltf.vnodes[vnode_id].parent
path.reverse()
return path
def longest_common_prefix(list1, list2):
i = 0
while i != min(len(list1), len(list2)):
if list1[i] != list2[i]:
break
i += 1
return list1[:i]
def move_skinned_meshes(gltf):
"""
In glTF, where in the node hierarchy a skinned mesh is instantiated has
no effect on its world space position: only the world transforms of the
joints in its skin affect it.
To do this in Blender:
* Move a skinned mesh to become a child of the armature that affects it
* When we do mesh creation, we will also need to put all the verts in
their rest pose (ie. the pose the edit bones are in)
"""
# TODO: this leaves behind empty "husk" nodes where the skinned meshes
# used to be, which is ugly.
ids = list(gltf.vnodes.keys())
for id in ids:
vnode = gltf.vnodes[id]
if vnode.mesh_node_idx is None:
continue
mesh = gltf.data.nodes[vnode.mesh_node_idx].mesh
skin = gltf.data.nodes[vnode.mesh_node_idx].skin
if skin is None:
continue
pyskin = gltf.data.skins[skin]
arma = gltf.vnodes[pyskin.joints[0]].bone_arma
new_id = str(id) + '.skinned'
gltf.vnodes[new_id] = VNode()
gltf.vnodes[new_id].name = gltf.data.meshes[mesh].name or 'Mesh_%d' % mesh
gltf.vnodes[new_id].parent = arma
gltf.vnodes[arma].children.append(new_id)
gltf.vnodes[new_id].mesh_node_idx = vnode.mesh_node_idx
vnode.mesh_node_idx = None
def fixup_multitype_nodes(gltf):
"""
Blender only lets each object have one of: an armature, a mesh, a
camera, a light. Also bones cannot have any of these either. Find any
nodes like this and move the mesh/camera/light onto new children.
"""
ids = list(gltf.vnodes.keys())
for id in ids:
vnode = gltf.vnodes[id]
needs_move = False
if vnode.is_arma or vnode.type == VNode.Bone:
needs_move = True
if vnode.mesh_node_idx is not None:
if needs_move:
new_id = str(id) + '.mesh'
gltf.vnodes[new_id] = VNode()
gltf.vnodes[new_id].name = vnode.name + ' Mesh'
gltf.vnodes[new_id].mesh_node_idx = vnode.mesh_node_idx
gltf.vnodes[new_id].parent = id
vnode.children.append(new_id)
vnode.mesh_node_idx = None
needs_move = True
if vnode.camera_node_idx is not None:
if needs_move:
new_id = str(id) + '.camera'
gltf.vnodes[new_id] = VNode()
gltf.vnodes[new_id].name = vnode.name + ' Camera'
gltf.vnodes[new_id].camera_node_idx = vnode.camera_node_idx
gltf.vnodes[new_id].parent = id
vnode.children.append(new_id)
vnode.camera_node_idx = None
needs_move = True
if vnode.light_node_idx is not None:
if needs_move:
new_id = str(id) + '.light'
gltf.vnodes[new_id] = VNode()
gltf.vnodes[new_id].name = vnode.name + ' Light'
gltf.vnodes[new_id].light_node_idx = vnode.light_node_idx
gltf.vnodes[new_id].parent = id
vnode.children.append(new_id)
vnode.light_node_idx = None
needs_move = True
def correct_cameras_and_lights(gltf):
"""
Depending on the coordinate change, lights and cameras might need to be
rotated to match Blender conventions for which axes they point along.
"""
if gltf.camera_correction is None:
return
trs = (Vector((0, 0, 0)), gltf.camera_correction, Vector((1, 1, 1)))
ids = list(gltf.vnodes.keys())
for id in ids:
vnode = gltf.vnodes[id]
# Move the camera/light onto a new child and set its rotation
# TODO: "hard apply" the rotation without creating a new node
# (like we'll need to do for bones)
if vnode.camera_node_idx is not None:
new_id = str(id) + '.camera-correction'
gltf.vnodes[new_id] = VNode()
gltf.vnodes[new_id].name = vnode.name + ' Correction'
gltf.vnodes[new_id].trs = trs
gltf.vnodes[new_id].camera_node_idx = vnode.camera_node_idx
gltf.vnodes[new_id].parent = id
vnode.children.append(new_id)
vnode.camera_node_idx = None
if vnode.light_node_idx is not None:
new_id = str(id) + '.light-correction'
gltf.vnodes[new_id] = VNode()
gltf.vnodes[new_id].name = vnode.name + ' Correction'
gltf.vnodes[new_id].trs = trs
gltf.vnodes[new_id].light_node_idx = vnode.light_node_idx
gltf.vnodes[new_id].parent = id
vnode.children.append(new_id)
vnode.light_node_idx = None
def calc_bone_matrices(gltf):
"""
Calculate bone_arma_mat, the transformation from bone space to armature
space for the edit bone, for each bone.
"""
def visit(vnode_id): # Depth-first walk
vnode = gltf.vnodes[vnode_id]
if vnode.type == VNode.Bone:
if gltf.vnodes[vnode.parent].type == VNode.Bone:
parent_arma_mat = gltf.vnodes[vnode.parent].bone_arma_mat
else:
parent_arma_mat = Matrix.Identity(4)
t, r, _ = vnode.trs
local_to_parent = Matrix.Translation(t) @ Quaternion(r).to_matrix().to_4x4()
vnode.bone_arma_mat = parent_arma_mat @ local_to_parent
for child in vnode.children:
visit(child)
visit('root')
# TODO: add pass to rotate/resize bones so they look pretty

View File

@ -1,68 +0,0 @@
# Copyright 2018 The glTF-Blender-IO authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class TRS:
def __new__(cls, *args, **kwargs):
raise RuntimeError("{} should not be instantiated".format(cls.__name__))
@staticmethod
def scale_to_matrix(scale):
# column major !
return [scale[0], 0, 0, 0,
0, scale[1], 0, 0,
0, 0, scale[2], 0,
0, 0, 0, 1]
@staticmethod
def quaternion_to_matrix(q):
x, y, z, w = q
# TODO : is q normalized ? --> if not, multiply by 1/(w*w + x*x + y*y + z*z)
# column major !
return [
1 - 2 * y * y - 2 * z * z, 2 * x * y + 2 * w * z, 2 * x * z - 2 * w * y, 0,
2 * x * y - 2 * w * z, 1 - 2 * x * x - 2 * z * z, 2 * y * z + 2 * w * x, 0,
2 * x * z + 2 * y * w, 2 * y * z - 2 * w * x, 1 - 2 * x * x - 2 * y * y, 0,
0, 0, 0, 1]
@staticmethod
def matrix_multiply(m, n):
# column major !
return [
m[0] * n[0] + m[4] * n[1] + m[8] * n[2] + m[12] * n[3],
m[1] * n[0] + m[5] * n[1] + m[9] * n[2] + m[13] * n[3],
m[2] * n[0] + m[6] * n[1] + m[10] * n[2] + m[14] * n[3],
m[3] * n[0] + m[7] * n[1] + m[11] * n[2] + m[15] * n[3],
m[0] * n[4] + m[4] * n[5] + m[8] * n[6] + m[12] * n[7],
m[1] * n[4] + m[5] * n[5] + m[9] * n[6] + m[13] * n[7],
m[2] * n[4] + m[6] * n[5] + m[10] * n[6] + m[14] * n[7],
m[3] * n[4] + m[7] * n[5] + m[11] * n[6] + m[15] * n[7],
m[0] * n[8] + m[4] * n[9] + m[8] * n[10] + m[12] * n[11],
m[1] * n[8] + m[5] * n[9] + m[9] * n[10] + m[13] * n[11],
m[2] * n[8] + m[6] * n[9] + m[10] * n[10] + m[14] * n[11],
m[3] * n[8] + m[7] * n[9] + m[11] * n[10] + m[15] * n[11],
m[0] * n[12] + m[4] * n[13] + m[8] * n[14] + m[12] * n[15],
m[1] * n[12] + m[5] * n[13] + m[9] * n[14] + m[13] * n[15],
m[2] * n[12] + m[6] * n[13] + m[10] * n[14] + m[14] * n[15],
m[3] * n[12] + m[7] * n[13] + m[11] * n[14] + m[15] * n[15],
]
@staticmethod
def translation_to_matrix(translation):
# column major !
return [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
translation[0], translation[1], translation[2], 1.0]

View File

@ -174,17 +174,6 @@ class glTFImporter():
self.content = None
return success, txt
def is_node_joint(self, node_idx):
"""Check if node is a joint."""
if not self.data.skins: # if no skin in gltf file
return False, None
for skin_idx, skin in enumerate(self.data.skins):
if node_idx in skin.joints:
return True, skin_idx
return False, None
def load_buffer(self, buffer_idx):
"""Load buffer."""
buffer = self.data.buffers[buffer_idx]