Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Duroure <julien.duroure@gmail.com>2020-01-24 00:10:48 +0300
committerJulien Duroure <julien.duroure@gmail.com>2020-01-24 00:10:48 +0300
commit75855d723895e25da855087bccbd0266773bad15 (patch)
treec4d159ce2e5921f2d9239cc641c02746f173d5ed
parentb3b274c5739de01685572032ac26ac5dcb50b950 (diff)
glTF importer: fix skinning & hierarchy issues
See https://github.com/KhronosGroup/glTF-Blender-IO/pull/857 for details
-rwxr-xr-xio_scene_gltf2/__init__.py2
-rwxr-xr-xio_scene_gltf2/blender/com/gltf2_blender_conversion.py32
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_animation.py45
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py122
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_animation_node.py39
-rw-r--r--io_scene_gltf2/blender/imp/gltf2_blender_animation_weight.py10
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_camera.py4
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_gltf.py156
-rw-r--r--io_scene_gltf2/blender/imp/gltf2_blender_light.py5
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_mesh.py9
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_node.py322
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_primitive.py108
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_scene.py188
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_skin.py182
-rw-r--r--io_scene_gltf2/blender/imp/gltf2_blender_vnode.py324
-rwxr-xr-xio_scene_gltf2/io/com/gltf2_io_trs.py68
-rwxr-xr-xio_scene_gltf2/io/imp/gltf2_io_gltf.py11
17 files changed, 763 insertions, 864 deletions
diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py
index 092bb830..f051b08e 100755
--- a/io_scene_gltf2/__init__.py
+++ b/io_scene_gltf2/__init__.py
@@ -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',
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_conversion.py b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py
index 6d9a901a..fccfce95 100755
--- a/io_scene_gltf2/blender/com/gltf2_blender_conversion.py
+++ b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py
@@ -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
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py
index 6394145d..ed1d938d 100755
--- a/io_scene_gltf2/blender/imp/gltf2_blender_animation.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py
@@ -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)
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py
index 465a801d..7e32487c 100755
--- a/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py
@@ -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
-
- # 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
+ translation_keyframes = (gltf.loc_gltf_to_blender(vals) for vals in values)
+
+ bind_trans, bind_rot, _ = vnode.trs
+ bind_rot_inv = bind_rot.conjugated()
+
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]))
- 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
+ gltf.scale_gltf_to_blender(values[idx * 3 + 1])
+ for idx in range(0, len(keys))
]
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)
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py
index 23a37228..6a0959c2 100755
--- a/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py
@@ -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)
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_weight.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_weight.py
index bd8586ab..c89e4dfe 100644
--- a/io_scene_gltf2/blender/imp/gltf2_blender_animation_weight.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_weight.py
@@ -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]
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_camera.py b/io_scene_gltf2/blender/imp/gltf2_blender_camera.py
index ec5f0ee4..78946858 100755
--- a/io_scene_gltf2/blender/imp/gltf2_blender_camera.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_camera.py
@@ -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
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py
index 5904a974..7b259759 100755
--- a/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py
@@ -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 = "?"
-
- 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
-
- # 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]
+ 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]])
+
+ # Same convention, no correction needed.
+ gltf.camera_correction = None
+
+ 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
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_light.py b/io_scene_gltf2/blender/imp/gltf2_blender_light.py
index 6213091e..e75864f9 100644
--- a/io_scene_gltf2/blender/imp/gltf2_blender_light.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_light.py
@@ -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
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py
index 318e7049..e069069e 100755
--- a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py
@@ -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:
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_node.py b/io_scene_gltf2/blender/imp/gltf2_blender_node.py
index a02514de..2f5d893a 100755
--- a/io_scene_gltf2/blender/imp/gltf2_blender_node.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_node.py
@@ -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:
- 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")
+ elif vnode.type == VNode.Bone:
+ BlenderNode.create_bone(gltf, vnode_id)
- mesh = BlenderMesh.create(gltf, pynode.mesh, node_idx, parent)
+ elif vnode.type == VNode.DummyRoot:
+ # Don't actually create this
+ vnode.blender_object = None
- 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
+ for child in vnode.children:
+ BlenderNode.create_vnode(gltf, child)
- if pynode.name:
- name = 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)
+ @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
- obj = bpy.data.objects.new(name, mesh)
+ # Set extras (if came from a glTF node)
+ if isinstance(vnode_id, int):
+ pynode = gltf.data.nodes[vnode_id]
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)
- # 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)
+ # Set transform
+ trans, rot, scale = vnode.trs
+ obj.location = trans
+ obj.rotation_mode = 'QUATERNION'
+ obj.rotation_quaternion = rot
+ obj.scale = scale
- if instance == False:
- BlenderMesh.set_mesh(gltf, gltf.data.meshes[pynode.mesh], mesh, obj)
+ # 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
- if pynode.children:
- for child_idx in pynode.children:
- BlenderNode.create(gltf, child_idx, node_idx)
+ # Nodes with a bone parent need to be translated
+ # backwards by their bone length (always 1 currently)
+ obj.location += Vector((0, -1, 0))
- return
+ bpy.data.scenes[gltf.blender_scene].collection.objects.link(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)
+ return obj
- if pynode.children:
- for child_idx in pynode.children:
- BlenderNode.create(gltf, child_idx, node_idx)
+ @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)
- return
+ @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:
+ instance = True
+ mesh = bpy.data.meshes[gltf.data.meshes[pynode.mesh].blender_name[pynode.skin]]
- if pynode.is_joint:
+ if instance is False:
if pynode.name:
- gltf.log.info("Blender create Bone node " + pynode.name)
+ gltf.log.info("Blender create Mesh 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
+ gltf.log.info("Blender create Mesh node")
- 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)
+ mesh = BlenderMesh.create(gltf, pynode.mesh, pynode.skin)
- if pynode.children:
- for child_idx in pynode.children:
- BlenderNode.create(gltf, child_idx, node_idx)
+ 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
- return
+ mesh_name = gltf.data.meshes[pynode.mesh].name
+ if not name and mesh_name:
+ name = mesh_name
- # No mesh, no camera, no light. For now, create empty #TODO
+ obj = bpy.data.objects.new(name, mesh)
- 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)
+ if instance == False:
+ BlenderMesh.set_mesh(gltf, gltf.data.meshes[pynode.mesh], obj)
- @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
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py b/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py
index 58fc9b9c..a046204b 100755
--- a/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py
@@ -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):
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_scene.py b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py
index d2d35f64..ca2f8052 100755
--- a/io_scene_gltf2/blender/imp/gltf2_blender_scene.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py
@@ -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
-
- # 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"
-
- 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
+ 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"
- 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
+ compute_vnodes(gltf)
+
+ 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
-
- 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
-
- roots = []
- for idx, node in enumerate(gltf.data.nodes):
- if idx not in parents.keys():
- roots.append(idx)
-
- return roots
+ 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]
+
+ else:
+ vnode = gltf.vnodes['root']
+ if vnode.type == VNode.DummyRoot:
+ if not vnode.children:
+ return # no nodes
+ vnode = gltf.vnodes[vnode.children[0]]
+
+ bpy.context.view_layer.objects.active = vnode.blender_object
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_skin.py b/io_scene_gltf2/blender/imp/gltf2_blender_skin.py
index 1d88e0e7..a9c50b58 100755
--- a/io_scene_gltf2/blender/imp/gltf2_blender_skin.py
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_skin.py
@@ -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]
-
- 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]
-
- @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]
-
- obj = bpy.data.objects[pyskin.blender_armature_name]
-
- # 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
+ 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]
+
+ 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 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]
+ 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]
+
+ first_bone = gltf.vnodes[pyskin.joints[0]]
+ arma = gltf.vnodes[first_bone.bone_arma]
+
+ obj = vnode.blender_object
+ mod = obj.modifiers.new(name="Armature", type="ARMATURE")
+ mod.object = arma.blender_object
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_vnode.py b/io_scene_gltf2/blender/imp/gltf2_blender_vnode.py
new file mode 100644
index 00000000..114d7193
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_vnode.py
@@ -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
+
diff --git a/io_scene_gltf2/io/com/gltf2_io_trs.py b/io_scene_gltf2/io/com/gltf2_io_trs.py
deleted file mode 100755
index 59f30830..00000000
--- a/io_scene_gltf2/io/com/gltf2_io_trs.py
+++ /dev/null
@@ -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]
-
diff --git a/io_scene_gltf2/io/imp/gltf2_io_gltf.py b/io_scene_gltf2/io/imp/gltf2_io_gltf.py
index 1a30f258..dc5c5f8f 100755
--- a/io_scene_gltf2/io/imp/gltf2_io_gltf.py
+++ b/io_scene_gltf2/io/imp/gltf2_io_gltf.py
@@ -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]