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:
-rwxr-xr-xio_scene_gltf2/__init__.py579
-rwxr-xr-xio_scene_gltf2/blender/com/gltf2_blender_conversion.py42
-rwxr-xr-xio_scene_gltf2/blender/com/gltf2_blender_data_path.py28
-rwxr-xr-xio_scene_gltf2/blender/com/gltf2_blender_image.py32
-rwxr-xr-xio_scene_gltf2/blender/com/gltf2_blender_image_util.py121
-rwxr-xr-xio_scene_gltf2/blender/com/gltf2_blender_json.py38
-rwxr-xr-xio_scene_gltf2/blender/com/gltf2_blender_material_helpers.py59
-rwxr-xr-xio_scene_gltf2/blender/com/gltf2_blender_math.py159
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_animate.py638
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_export.py100
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_export_keys.py66
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_extract.py1117
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_filter.py455
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather.py63
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py82
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py131
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py198
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py166
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py169
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py60
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py124
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_image.py166
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py80
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py113
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py113
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py138
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py93
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py90
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py148
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py217
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py200
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py98
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py150
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py100
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py107
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py64
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_get.py376
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py263
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py97
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_search_scene.py89
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_utils.py68
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py327
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_animation.py35
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py188
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_animation_node.py132
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_camera.py47
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_gltf.py217
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_image.py101
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_map_emissive.py110
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_map_normal.py89
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_map_occlusion.py41
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_material.py150
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_mesh.py158
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_node.py184
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py347
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_primitive.py170
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_scene.py94
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_skin.py209
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_texture.py39
-rwxr-xr-xio_scene_gltf2/io/__init__.py16
-rwxr-xr-xio_scene_gltf2/io/com/gltf2_io.py1200
-rwxr-xr-xio_scene_gltf2/io/com/gltf2_io_constants.py132
-rwxr-xr-xio_scene_gltf2/io/com/gltf2_io_debug.py138
-rwxr-xr-xio_scene_gltf2/io/com/gltf2_io_functional.py41
-rwxr-xr-xio_scene_gltf2/io/com/gltf2_io_image.py154
-rwxr-xr-xio_scene_gltf2/io/com/gltf2_io_trs.py68
-rwxr-xr-xio_scene_gltf2/io/exp/gltf2_io_binary_data.py36
-rwxr-xr-xio_scene_gltf2/io/exp/gltf2_io_buffer.py61
-rwxr-xr-xio_scene_gltf2/io/exp/gltf2_io_export.py97
-rwxr-xr-xio_scene_gltf2/io/exp/gltf2_io_get.py316
-rwxr-xr-xio_scene_gltf2/io/exp/gltf2_io_image_data.py106
-rwxr-xr-xio_scene_gltf2/io/imp/__init__.py16
-rwxr-xr-xio_scene_gltf2/io/imp/gltf2_io_binary.py178
-rwxr-xr-xio_scene_gltf2/io/imp/gltf2_io_gltf.py199
74 files changed, 12593 insertions, 0 deletions
diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py
new file mode 100755
index 00000000..0e864cad
--- /dev/null
+++ b/io_scene_gltf2/__init__.py
@@ -0,0 +1,579 @@
+# 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.
+
+#
+# Imports
+#
+
+import os
+import bpy
+from bpy_extras.io_utils import ImportHelper, ExportHelper
+from bpy.types import Operator, AddonPreferences
+
+from .io.com.gltf2_io_debug import print_console, Log
+from .io.imp.gltf2_io_gltf import glTFImporter
+from .blender.imp.gltf2_blender_gltf import BlenderGlTF
+
+from bpy.props import (CollectionProperty,
+ StringProperty,
+ BoolProperty,
+ EnumProperty,
+ FloatProperty,
+ IntProperty)
+
+#
+# Globals
+#
+
+bl_info = {
+ 'name': 'glTF 2.0 format',
+ 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann & Moritz Becher',
+ "version": (0, 0, 1),
+ 'blender': (2, 80, 0),
+ 'location': 'File > Import-Export',
+ 'description': 'Import-Export as glTF 2.0',
+ 'warning': '',
+ 'wiki_url': ''
+ '',
+ 'support': 'COMMUNITY',
+ 'category': 'Import-Export'}
+
+
+#
+# Functions / Classes.
+#
+
+
+class GLTF2ExportSettings(bpy.types.Operator):
+ """Save the export settings on export (saved in .blend).
+Toggle off to clear settings"""
+ bl_label = "Save Settings"
+ bl_idname = "scene.gltf2_export_settings_set"
+
+ def execute(self, context):
+ operator = context.active_operator
+ operator.will_save_settings = not operator.will_save_settings
+ if not operator.will_save_settings:
+ # clear settings
+ context.scene.pop(operator.scene_key)
+ return {"FINISHED"}
+
+
+class ExportGLTF2_Base:
+
+ # TODO: refactor to avoid boilerplate
+
+ export_copyright: StringProperty(
+ name='Copyright',
+ description='',
+ default=''
+ )
+
+ export_embed_buffers: BoolProperty(
+ name='Embed buffers',
+ description='',
+ default=False
+ )
+
+ export_embed_images: BoolProperty(
+ name='Embed images',
+ description='',
+ default=False
+ )
+
+ export_strip: BoolProperty(
+ name='Strip delimiters',
+ description='',
+ default=False
+ )
+
+ export_indices: EnumProperty(
+ name='Maximum indices',
+ items=(('UNSIGNED_BYTE', 'Unsigned Byte', ''),
+ ('UNSIGNED_SHORT', 'Unsigned Short', ''),
+ ('UNSIGNED_INT', 'Unsigned Integer', '')),
+ default='UNSIGNED_INT'
+ )
+
+ export_force_indices: BoolProperty(
+ name='Force maximum indices',
+ description='',
+ default=False
+ )
+
+ export_texcoords: BoolProperty(
+ name='Export texture coordinates',
+ description='',
+ default=True
+ )
+
+ export_normals: BoolProperty(
+ name='Export normals',
+ description='',
+ default=True
+ )
+
+ export_tangents: BoolProperty(
+ name='Export tangents',
+ description='',
+ default=True
+ )
+
+ export_materials: BoolProperty(
+ name='Export materials',
+ description='',
+ default=True
+ )
+
+ export_colors: BoolProperty(
+ name='Export colors',
+ description='',
+ default=True
+ )
+
+ export_cameras: BoolProperty(
+ name='Export cameras',
+ description='',
+ default=False
+ )
+
+ export_camera_infinite: BoolProperty(
+ name='Infinite perspective Camera',
+ description='',
+ default=False
+ )
+
+ export_selected: BoolProperty(
+ name='Export selected only',
+ description='',
+ default=False
+ )
+
+ export_layers: BoolProperty(
+ name='Export all layers',
+ description='',
+ default=True
+ )
+
+ export_extras: BoolProperty(
+ name='Export extras',
+ description='',
+ default=False
+ )
+
+ export_yup: BoolProperty(
+ name='Convert Z up to Y up',
+ description='',
+ default=True
+ )
+
+ export_apply: BoolProperty(
+ name='Apply modifiers',
+ description='',
+ default=False
+ )
+
+ export_animations: BoolProperty(
+ name='Export animations',
+ description='',
+ default=True
+ )
+
+ export_frame_range: BoolProperty(
+ name='Export within playback range',
+ description='',
+ default=True
+ )
+
+ export_frame_step: IntProperty(
+ name='Frame step size',
+ description='Step size (in frames) for animation export.',
+ default=1,
+ min=1,
+ max=120
+ )
+
+ export_move_keyframes: BoolProperty(
+ name='Keyframes start with 0',
+ description='',
+ default=True
+ )
+
+ export_force_sampling: BoolProperty(
+ name='Force sample animations',
+ description='',
+ default=False
+ )
+
+ export_current_frame: BoolProperty(
+ name='Export current frame',
+ description='',
+ default=True
+ )
+
+ export_skins: BoolProperty(
+ name='Export skinning',
+ description='',
+ default=True
+ )
+
+ export_bake_skins: BoolProperty(
+ name='Bake skinning constraints',
+ description='',
+ default=False
+ )
+
+ export_morph: BoolProperty(
+ name='Export morphing',
+ description='',
+ default=True
+ )
+
+ export_morph_normal: BoolProperty(
+ name='Export morphing normals',
+ description='',
+ default=True
+ )
+
+ export_morph_tangent: BoolProperty(
+ name='Export morphing tangents',
+ description='',
+ default=True
+ )
+
+ export_lights: BoolProperty(
+ name='Export KHR_lights_punctual',
+ description='',
+ default=False
+ )
+
+ export_texture_transform: BoolProperty(
+ name='Export KHR_texture_transform',
+ description='',
+ default=False
+ )
+
+ export_displacement: BoolProperty(
+ name='Export KHR_materials_displacement',
+ description='',
+ default=False
+ )
+
+ will_save_settings: BoolProperty(default=False)
+
+ # Custom scene property for saving settings
+ scene_key = "glTF2ExportSettings"
+
+ #
+
+ def invoke(self, context, event):
+ settings = context.scene.get(self.scene_key)
+ self.will_save_settings = False
+ if settings:
+ try:
+ for (k, v) in settings.items():
+ setattr(self, k, v)
+ self.will_save_settings = True
+
+ except AttributeError:
+ self.report({"ERROR"}, "Loading export settings failed. Removed corrupted settings")
+ del context.scene[self.scene_key]
+
+ return ExportHelper.invoke(self, context, event)
+
+ def save_settings(self, context):
+ # find all export_ props
+ all_props = self.properties
+ export_props = {x: all_props.get(x) for x in dir(all_props)
+ if x.startswith("export_") and all_props.get(x) is not None}
+
+ context.scene[self.scene_key] = export_props
+
+ def execute(self, context):
+ from .blender.exp import gltf2_blender_export
+
+ if self.will_save_settings:
+ self.save_settings(context)
+
+ # All custom export settings are stored in this container.
+ export_settings = {}
+
+ export_settings['gltf_filepath'] = bpy.path.ensure_ext(self.filepath, self.filename_ext)
+ export_settings['gltf_filedirectory'] = os.path.dirname(export_settings['gltf_filepath']) + '/'
+
+ export_settings['gltf_format'] = self.export_format
+ export_settings['gltf_copyright'] = self.export_copyright
+ export_settings['gltf_embed_buffers'] = self.export_embed_buffers
+ export_settings['gltf_embed_images'] = self.export_embed_images
+ export_settings['gltf_strip'] = self.export_strip
+ export_settings['gltf_indices'] = self.export_indices
+ export_settings['gltf_force_indices'] = self.export_force_indices
+ export_settings['gltf_texcoords'] = self.export_texcoords
+ export_settings['gltf_normals'] = self.export_normals
+ export_settings['gltf_tangents'] = self.export_tangents and self.export_normals
+ export_settings['gltf_materials'] = self.export_materials
+ export_settings['gltf_colors'] = self.export_colors
+ export_settings['gltf_cameras'] = self.export_cameras
+ if self.export_cameras:
+ export_settings['gltf_camera_infinite'] = self.export_camera_infinite
+ else:
+ export_settings['gltf_camera_infinite'] = False
+ export_settings['gltf_selected'] = self.export_selected
+ export_settings['gltf_layers'] = self.export_layers
+ export_settings['gltf_extras'] = self.export_extras
+ export_settings['gltf_yup'] = self.export_yup
+ export_settings['gltf_apply'] = self.export_apply
+ export_settings['gltf_animations'] = self.export_animations
+ if self.export_animations:
+ export_settings['gltf_current_frame'] = False
+ export_settings['gltf_frame_range'] = self.export_frame_range
+ export_settings['gltf_move_keyframes'] = self.export_move_keyframes
+ export_settings['gltf_force_sampling'] = self.export_force_sampling
+ else:
+ export_settings['gltf_current_frame'] = self.export_current_frame
+ export_settings['gltf_frame_range'] = False
+ export_settings['gltf_move_keyframes'] = False
+ export_settings['gltf_force_sampling'] = False
+ export_settings['gltf_skins'] = self.export_skins
+ if self.export_skins:
+ export_settings['gltf_bake_skins'] = self.export_bake_skins
+ else:
+ export_settings['gltf_bake_skins'] = False
+ export_settings['gltf_frame_step'] = self.export_frame_step
+ export_settings['gltf_morph'] = self.export_morph
+ if self.export_morph:
+ export_settings['gltf_morph_normal'] = self.export_morph_normal
+ else:
+ export_settings['gltf_morph_normal'] = False
+ if self.export_morph and self.export_morph_normal:
+ export_settings['gltf_morph_tangent'] = self.export_morph_tangent
+ else:
+ export_settings['gltf_morph_tangent'] = False
+
+ export_settings['gltf_lights'] = self.export_lights
+ export_settings['gltf_texture_transform'] = self.export_texture_transform
+ export_settings['gltf_displacement'] = self.export_displacement
+
+ export_settings['gltf_binary'] = bytearray()
+ export_settings['gltf_binaryfilename'] = os.path.splitext(os.path.basename(self.filepath))[0] + '.bin'
+
+ return gltf2_blender_export.save(self, context, export_settings)
+
+ def draw(self, context):
+ layout = self.layout
+
+ #
+
+ col = layout.box().column()
+ col.label(text='Embedding:') # , icon='PACKAGE')
+ col.prop(self, 'export_copyright')
+ if self.export_format == 'ASCII':
+ col.prop(self, 'export_embed_buffers')
+ col.prop(self, 'export_embed_images')
+ col.prop(self, 'export_strip')
+
+ col = layout.box().column()
+ col.label(text='Nodes:') # , icon='OOPS')
+ col.prop(self, 'export_selected')
+ col.prop(self, 'export_layers')
+ col.prop(self, 'export_extras')
+ col.prop(self, 'export_yup')
+
+ col = layout.box().column()
+ col.label(text='Meshes:') # , icon='MESH_DATA')
+ col.prop(self, 'export_apply')
+ col.prop(self, 'export_indices')
+ col.prop(self, 'export_force_indices')
+
+ col = layout.box().column()
+ col.label(text='Attributes:') # , icon='SURFACE_DATA')
+ col.prop(self, 'export_texcoords')
+ col.prop(self, 'export_normals')
+ if self.export_normals:
+ col.prop(self, 'export_tangents')
+ col.prop(self, 'export_colors')
+
+ col = layout.box().column()
+ col.label(text='Objects:') # , icon='OBJECT_DATA')
+ col.prop(self, 'export_cameras')
+ if self.export_cameras:
+ col.prop(self, 'export_camera_infinite')
+
+ col = layout.box().column()
+ col.label(text='Materials:') # , icon='MATERIAL_DATA')
+ col.prop(self, 'export_materials')
+ col.prop(self, 'export_texture_transform')
+
+ col = layout.box().column()
+ col.label(text='Animation:') # , icon='OUTLINER_DATA_POSE')
+ col.prop(self, 'export_animations')
+ if self.export_animations:
+ col.prop(self, 'export_frame_range')
+ col.prop(self, 'export_frame_step')
+ col.prop(self, 'export_move_keyframes')
+ col.prop(self, 'export_force_sampling')
+ else:
+ col.prop(self, 'export_current_frame')
+ col.prop(self, 'export_skins')
+ if self.export_skins:
+ col.prop(self, 'export_bake_skins')
+ col.prop(self, 'export_morph')
+ if self.export_morph:
+ col.prop(self, 'export_morph_normal')
+ if self.export_morph_normal:
+ col.prop(self, 'export_morph_tangent')
+
+ addon_prefs = context.user_preferences.addons[__name__].preferences
+ if addon_prefs.experimental:
+ col = layout.box().column()
+ col.label(text='Experimental:') # , icon='RADIO')
+ col.prop(self, 'export_lights')
+ col.prop(self, 'export_displacement')
+
+ row = layout.row()
+ row.operator(
+ GLTF2ExportSettings.bl_idname,
+ text=GLTF2ExportSettings.bl_label,
+ icon="%s" % "PINNED" if self.will_save_settings else "UNPINNED")
+
+# TODO: refactor operators to single operator for both cases
+
+class ExportGLTF2_GLTF(bpy.types.Operator, ExportGLTF2_Base, ExportHelper):
+ """Export scene as glTF 2.0 file"""
+ bl_idname = 'export_scene.gltf'
+ bl_label = 'Export glTF 2.0'
+
+ filename_ext = '.gltf'
+ filter_glob: StringProperty(default='*.gltf', options={'HIDDEN'})
+
+ export_format = 'ASCII'
+
+
+class ExportGLTF2_GLB(bpy.types.Operator, ExportGLTF2_Base, ExportHelper):
+ """Export scene as glTF 2.0 file"""
+ bl_idname = 'export_scene.glb'
+ bl_label = 'Export glTF 2.0 binary'
+
+ filename_ext = '.glb'
+ filter_glob: StringProperty(default='*.glb', options={'HIDDEN'})
+
+ export_format = 'BINARY'
+
+
+def menu_func_export_gltf(self, context):
+ self.layout.operator(ExportGLTF2_GLTF.bl_idname, text='glTF 2.0 (.gltf)')
+
+
+def menu_func_export_glb(self, context):
+ self.layout.operator(ExportGLTF2_GLB.bl_idname, text='glTF 2.0 (.glb)')
+
+
+class ExportGLTF2_AddonPreferences(AddonPreferences):
+ bl_idname = __name__
+
+ experimental: BoolProperty(name='Enable experimental glTF export settings', default=False)
+
+ def draw(self, context):
+ layout = self.layout
+ layout.prop(self, "experimental")
+
+
+class ImportglTF2(Operator, ImportHelper):
+ bl_idname = 'import_scene.gltf'
+ bl_label = "glTF 2.0 (.gltf/.glb)"
+
+ filter_glob: StringProperty(default="*.gltf;*.glb", options={'HIDDEN'})
+
+ loglevel: EnumProperty(items=Log.get_levels(), name="Log Level", default=Log.default())
+
+ import_pack_images: BoolProperty(
+ name='Pack images',
+ description='',
+ default=True
+ )
+
+ import_shading: EnumProperty(
+ name="Shading",
+ items=(("NORMALS", "Use Normal Data", ""),
+ ("FLAT", "Flat Shading", ""),
+ ("SMOOTH", "Smooth Shading", "")),
+ default="NORMALS")
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.prop(self, 'loglevel')
+ layout.prop(self, 'import_pack_images')
+ layout.prop(self, 'import_shading')
+
+ def execute(self, context):
+ return self.import_gltf2(context)
+
+ def import_gltf2(self, context):
+ import_settings = self.as_keywords()
+
+ self.gltf_importer = glTFImporter(self.filepath, import_settings)
+ success, txt = self.gltf_importer.read()
+ if not success:
+ self.report({'ERROR'}, txt)
+ return {'CANCELLED'}
+ success, txt = self.gltf_importer.checks()
+ if not success:
+ self.report({'ERROR'}, txt)
+ return {'CANCELLED'}
+ self.gltf_importer.log.critical("Data are loaded, start creating Blender stuff")
+ BlenderGlTF.create(self.gltf_importer)
+ self.gltf_importer.log.critical("glTF import is now finished")
+ self.gltf_importer.log.removeHandler(self.gltf_importer.log_handler)
+
+ # Switch to newly created main scene
+ bpy.context.window.scene = bpy.data.scenes[self.gltf_importer.blender_scene]
+
+ return {'FINISHED'}
+
+
+def menu_func_import(self, context):
+ self.layout.operator(ImportglTF2.bl_idname, text=ImportglTF2.bl_label)
+
+
+classes = (
+ GLTF2ExportSettings,
+ ExportGLTF2_GLTF,
+ ExportGLTF2_GLB,
+ ExportGLTF2_AddonPreferences,
+ ImportglTF2
+)
+
+
+def register():
+ for c in classes:
+ bpy.utils.register_class(c)
+ # bpy.utils.register_module(__name__)
+
+ # add to the export / import menu
+ bpy.types.TOPBAR_MT_file_export.append(menu_func_export_gltf)
+ bpy.types.TOPBAR_MT_file_export.append(menu_func_export_glb)
+ bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
+
+
+def unregister():
+ for c in classes:
+ bpy.utils.unregister_class(c)
+ # bpy.utils.unregister_module(__name__)
+
+ # remove from the export / import menu
+ bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_gltf)
+ bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_glb)
+ bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_conversion.py b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py
new file mode 100755
index 00000000..95fa292d
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py
@@ -0,0 +1,42 @@
+# 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.
+
+from mathutils import Matrix, Quaternion
+
+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
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_data_path.py b/io_scene_gltf2/blender/com/gltf2_blender_data_path.py
new file mode 100755
index 00000000..c5ce4025
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_data_path.py
@@ -0,0 +1,28 @@
+# 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.
+
+
+def get_target_property_name(data_path: str) -> str:
+ """Retrieve target property."""
+ return data_path.rsplit('.', 1)[-1]
+
+
+def get_target_object_path(data_path: str) -> str:
+ """Retrieve target object data path without property"""
+ path_split = data_path.rsplit('.', 1)
+ self_targeting = len(path_split) < 2
+ if self_targeting:
+ return ""
+ return path_split[0]
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_image.py b/io_scene_gltf2/blender/com/gltf2_blender_image.py
new file mode 100755
index 00000000..7564070d
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_image.py
@@ -0,0 +1,32 @@
+# 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.
+
+#
+# Imports
+#
+
+from ...io.com.gltf2_io_image import create_img_from_pixels
+
+
+def create_img_from_blender_image(blender_image):
+ """
+ Create a new image object using the given blender image.
+
+ Returns the created image object.
+ """
+ if blender_image is None:
+ return None
+
+ return create_img_from_pixels(blender_image.size[0], blender_image.size[1], blender_image.pixels[:])
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_image_util.py b/io_scene_gltf2/blender/com/gltf2_blender_image_util.py
new file mode 100755
index 00000000..e2563a52
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_image_util.py
@@ -0,0 +1,121 @@
+# 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.
+
+import os
+import shutil
+import bpy
+import zlib
+import struct
+from io_scene_gltf2.blender.exp import gltf2_blender_get
+
+
+def create_image_file(context, blender_image, dst_path, file_format):
+ """Create JPEG or PNG file from a given Blender image."""
+ # Check, if source image exists e.g. does not exist if image is packed.
+ file_exists = 1
+ try:
+ src_path = bpy.path.abspath(blender_image.filepath, library=blender_image.library)
+ file = open(src_path)
+ except IOError:
+ file_exists = 0
+ else:
+ file.close()
+
+ if file_exists == 0:
+ # Image does not exist on disk ...
+ blender_image.filepath = dst_path
+ # ... so save it.
+ blender_image.save()
+
+ elif file_format == blender_image.file_format:
+ # Copy source image to destination, keeping original format.
+
+ src_path = bpy.path.abspath(blender_image.filepath, library=blender_image.library)
+
+ # Required for comapre.
+ src_path = src_path.replace('\\', '/')
+ dst_path = dst_path.replace('\\', '/')
+
+ # Check that source and destination path are not the same using os.path.abspath
+ # because bpy.path.abspath seems to not always return an absolute path
+ if os.path.abspath(dst_path) != os.path.abspath(src_path):
+ shutil.copyfile(src_path, dst_path)
+
+ else:
+ # Render a new image to destination, converting to target format.
+
+ # TODO: Reusing the existing scene means settings like exposure are applied on export,
+ # which we don't want, but I'm not sure how to create a new Scene object through the
+ # Python API. See: https://github.com/KhronosGroup/glTF-Blender-Exporter/issues/184.
+
+ tmp_file_format = context.scene.render.image_settings.file_format
+ tmp_color_depth = context.scene.render.image_settings.color_depth
+
+ context.scene.render.image_settings.file_format = file_format
+ context.scene.render.image_settings.color_depth = '8'
+ blender_image.save_render(dst_path, context.scene)
+
+ context.scene.render.image_settings.file_format = tmp_file_format
+ context.scene.render.image_settings.color_depth = tmp_color_depth
+
+
+def create_image_data(context, export_settings, blender_image, file_format):
+ """Create JPEG or PNG byte array from a given Blender image."""
+ if blender_image is None:
+ return None
+
+ if file_format == 'PNG':
+ return _create_png_data(blender_image)
+ else:
+ return _create_jpg_data(context, export_settings, blender_image)
+
+
+def _create_jpg_data(context, export_settings, blender_image):
+ """Create a JPEG byte array from a given Blender image."""
+ uri = gltf2_blender_get.get_image_uri(export_settings, blender_image)
+ path = export_settings['gltf_filedirectory'] + uri
+
+ create_image_file(context, blender_image, path, 'JPEG')
+
+ jpg_data = open(path, 'rb').read()
+ os.remove(path)
+
+ return jpg_data
+
+
+def _create_png_data(blender_image):
+ """Create a PNG byte array from a given Blender image."""
+ width, height = blender_image.size
+
+ buf = bytearray([int(channel * 255.0) for channel in blender_image.pixels])
+
+ #
+ # Taken from 'blender-thumbnailer.py' in Blender.
+ #
+
+ # reverse the vertical line order and add null bytes at the start
+ width_byte_4 = width * 4
+ raw_data = b"".join(
+ b'\x00' + buf[span:span + width_byte_4] for span in range((height - 1) * width * 4, -1, - width_byte_4))
+
+ def png_pack(png_tag, data):
+ chunk_head = png_tag + data
+ return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
+
+ return b"".join([
+ b'\x89PNG\r\n\x1a\n',
+ png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
+ png_pack(b'IDAT', zlib.compress(raw_data, 9)),
+ png_pack(b'IEND', b'')])
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_json.py b/io_scene_gltf2/blender/com/gltf2_blender_json.py
new file mode 100755
index 00000000..fbf833c1
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_json.py
@@ -0,0 +1,38 @@
+# 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.
+
+import json
+import bpy
+
+
+class BlenderJSONEncoder(json.JSONEncoder):
+ """Blender JSON Encoder."""
+
+ def default(self, obj):
+ if isinstance(obj, bpy.types.ID):
+ return dict(
+ name=obj.name,
+ type=obj.__class__.__name__
+ )
+ return super(BlenderJSONEncoder, self).default(obj)
+
+
+def is_json_convertible(data):
+ """Test, if a data set can be expressed as JSON."""
+ try:
+ json.dumps(data, cls=BlenderJSONEncoder)
+ return True
+ except:
+ return False
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py b/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py
new file mode 100755
index 00000000..05f35954
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py
@@ -0,0 +1,59 @@
+# 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.
+
+
+def get_output_node(node_tree):
+ """Retrive output node."""
+ output = [node for node in node_tree.nodes if node.type == 'OUTPUT_MATERIAL'][0]
+ return output
+
+
+def get_output_surface_input(node_tree):
+ """Retrieve surface input of output node."""
+ output_node = get_output_node(node_tree)
+ return output_node.inputs['Surface']
+
+
+def get_diffuse_texture(node_tree):
+ """Retrieve diffuse texture node."""
+ for node in node_tree.nodes:
+ print(node.name)
+ if node.label == 'BASE COLOR':
+ return node
+
+ return None
+
+
+def get_preoutput_node_output(node_tree):
+ """Retrieve node just before output node."""
+ output_node = get_output_node(node_tree)
+ preoutput_node = output_node.inputs['Surface'].links[0].from_node
+
+ # Pre output node is Principled BSDF or any BSDF => BSDF
+ if 'BSDF' in preoutput_node.type:
+ return preoutput_node.outputs['BSDF']
+ elif 'SHADER' in preoutput_node.type:
+ return preoutput_node.outputs['Shader']
+ else:
+ print(preoutput_node.type)
+
+
+def get_base_color_node(node_tree):
+ """Returns the last node of the diffuse block."""
+ for node in node_tree.nodes:
+ if node.label == 'BASE COLOR':
+ return node
+
+ return None
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_math.py b/io_scene_gltf2/blender/com/gltf2_blender_math.py
new file mode 100755
index 00000000..26eb6396
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_math.py
@@ -0,0 +1,159 @@
+# 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.
+
+import bpy
+import typing
+import math
+from mathutils import Matrix, Vector, Quaternion, Euler
+
+from io_scene_gltf2.blender.com.gltf2_blender_data_path import get_target_property_name
+
+
+def multiply(a, b):
+ """Multiplication."""
+ return a @ b
+
+
+def list_to_mathutils(values: typing.List[float], data_path: str) -> typing.Union[Vector, Quaternion, Euler]:
+ """Transform a list to blender py object."""
+ target = get_target_property_name(data_path)
+
+ if target == 'location':
+ return Vector(values)
+ elif target == 'rotation_axis_angle':
+ angle = values[0]
+ axis = values[1:]
+ return Quaternion(axis, math.radians(angle))
+ elif target == 'rotation_euler':
+ return Euler(values).to_quaternion()
+ elif target == 'rotation_quaternion':
+ return Quaternion(values)
+ elif target == 'scale':
+ return Vector(values)
+ elif target == 'value':
+ return values
+
+ return values
+
+
+def mathutils_to_gltf(x: typing.Union[Vector, Quaternion]) -> typing.List[float]:
+ """Transform a py object to glTF list."""
+ if isinstance(x, Vector):
+ return list(x)
+ if isinstance(x, Quaternion):
+ # Blender has w-first quaternion notation
+ return [x[1], x[2], x[3], x[0]]
+ else:
+ return list(x)
+
+
+def to_yup() -> Matrix:
+ """Transform to Yup."""
+ return Matrix(
+ ((1.0, 0.0, 0.0, 0.0),
+ (0.0, 0.0, 1.0, 0.0),
+ (0.0, -1.0, 0.0, 0.0),
+ (0.0, 0.0, 0.0, 1.0))
+ )
+
+
+to_zup = to_yup
+
+
+def swizzle_yup(v: typing.Union[Vector, Quaternion], data_path: str) -> typing.Union[Vector, Quaternion]:
+ """Manage Yup."""
+ target = get_target_property_name(data_path)
+ swizzle_func = {
+ "location": swizzle_yup_location,
+ "rotation_axis_angle": swizzle_yup_rotation,
+ "rotation_euler": swizzle_yup_rotation,
+ "rotation_quaternion": swizzle_yup_rotation,
+ "scale": swizzle_yup_scale,
+ "value": swizzle_yup_value
+ }.get(target)
+
+ if swizzle_func is None:
+ raise RuntimeError("Cannot transform values at {}".format(data_path))
+
+ return swizzle_func(v)
+
+
+def swizzle_yup_location(loc: Vector) -> Vector:
+ """Manage Yup location."""
+ return Vector((loc[0], loc[2], -loc[1]))
+
+
+def swizzle_yup_rotation(rot: Quaternion) -> Quaternion:
+ """Manage Yup rotation."""
+ return Quaternion((rot[0], rot[1], rot[3], -rot[2]))
+
+
+def swizzle_yup_scale(scale: Vector) -> Vector:
+ """Manage Yup scale."""
+ return Vector((scale[0], scale[2], scale[1]))
+
+
+def swizzle_yup_value(value: typing.Any) -> typing.Any:
+ """Manage Yup value."""
+ return value
+
+
+def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Matrix = Matrix.Identity(4)) -> typing \
+ .Union[Vector, Quaternion]:
+ """Manage transformations."""
+ target = get_target_property_name(data_path)
+ transform_func = {
+ "location": transform_location,
+ "rotation_axis_angle": transform_rotation,
+ "rotation_euler": transform_rotation,
+ "rotation_quaternion": transform_rotation,
+ "scale": transform_scale,
+ "value": transform_value
+ }.get(target)
+
+ if transform_func is None:
+ raise RuntimeError("Cannot transform values at {}".format(data_path))
+
+ return transform_func(v, transform)
+
+
+def transform_location(location: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector:
+ """Transform location."""
+ m = Matrix.Translation(location)
+ m = multiply(transform, m)
+ return m.to_translation()
+
+
+def transform_rotation(rotation: Quaternion, transform: Matrix = Matrix.Identity(4)) -> Quaternion:
+ """Transform rotation."""
+ m = rotation.to_matrix().to_4x4()
+ m = multiply(transform, m)
+ return m.to_quaternion()
+
+
+def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector:
+ """Transform scale."""
+ m = Matrix.Identity(4)
+ m[0][0] = scale.x
+ m[1][1] = scale.y
+ m[2][2] = scale.z
+ m = multiply(transform, m)
+
+ return m.to_scale()
+
+
+def transform_value(value: Vector, _: Matrix = Matrix.Identity(4)) -> Vector:
+ """Transform value."""
+ return value
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_animate.py b/io_scene_gltf2/blender/exp/gltf2_blender_animate.py
new file mode 100755
index 00000000..e4b11487
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_animate.py
@@ -0,0 +1,638 @@
+# 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.
+
+#
+# Imports
+#
+
+import bpy
+from . import gltf2_blender_export_keys
+from . import gltf2_blender_extract
+from mathutils import Matrix, Quaternion, Euler
+
+
+#
+# Globals
+#
+
+JOINT_NODE = 'JOINT'
+
+NEEDS_CONVERSION = 'CONVERSION_NEEDED'
+CUBIC_INTERPOLATION = 'CUBICSPLINE'
+LINEAR_INTERPOLATION = 'LINEAR'
+STEP_INTERPOLATION = 'STEP'
+BEZIER_INTERPOLATION = 'BEZIER'
+CONSTANT_INTERPOLATION = 'CONSTANT'
+
+
+#
+# Functions
+#
+
+def animate_get_interpolation(export_settings, blender_fcurve_list):
+ """
+ Retrieve the glTF interpolation, depending on a fcurve list.
+
+ Blender allows mixing and more variations of interpolations.
+ In such a case, a conversion is needed.
+ """
+ if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]:
+ return NEEDS_CONVERSION
+
+ #
+
+ interpolation = None
+
+ keyframe_count = None
+
+ for blender_fcurve in blender_fcurve_list:
+ if blender_fcurve is None:
+ continue
+
+ #
+
+ current_keyframe_count = len(blender_fcurve.keyframe_points)
+
+ if keyframe_count is None:
+ keyframe_count = current_keyframe_count
+
+ if current_keyframe_count > 0 > blender_fcurve.keyframe_points[0].co[0]:
+ return NEEDS_CONVERSION
+
+ if keyframe_count != current_keyframe_count:
+ return NEEDS_CONVERSION
+
+ #
+
+ for blender_keyframe in blender_fcurve.keyframe_points:
+ is_bezier = blender_keyframe.interpolation == BEZIER_INTERPOLATION
+ is_linear = blender_keyframe.interpolation == LINEAR_INTERPOLATION
+ is_constant = blender_keyframe.interpolation == CONSTANT_INTERPOLATION
+
+ if interpolation is None:
+ if is_bezier:
+ interpolation = CUBIC_INTERPOLATION
+ elif is_linear:
+ interpolation = LINEAR_INTERPOLATION
+ elif is_constant:
+ interpolation = STEP_INTERPOLATION
+ else:
+ interpolation = NEEDS_CONVERSION
+ return interpolation
+ else:
+ if is_bezier and interpolation != CUBIC_INTERPOLATION:
+ interpolation = NEEDS_CONVERSION
+ return interpolation
+ elif is_linear and interpolation != LINEAR_INTERPOLATION:
+ interpolation = NEEDS_CONVERSION
+ return interpolation
+ elif is_constant and interpolation != STEP_INTERPOLATION:
+ interpolation = NEEDS_CONVERSION
+ return interpolation
+ elif not is_bezier and not is_linear and not is_constant:
+ interpolation = NEEDS_CONVERSION
+ return interpolation
+
+ if interpolation is None:
+ interpolation = NEEDS_CONVERSION
+
+ return interpolation
+
+
+def animate_convert_rotation_axis_angle(axis_angle):
+ """Convert an axis angle to a quaternion rotation."""
+ q = Quaternion((axis_angle[1], axis_angle[2], axis_angle[3]), axis_angle[0])
+
+ return [q.x, q.y, q.z, q.w]
+
+
+def animate_convert_rotation_euler(euler, rotation_mode):
+ """Convert an euler angle to a quaternion rotation."""
+ rotation = Euler((euler[0], euler[1], euler[2]), rotation_mode).to_quaternion()
+
+ return [rotation.x, rotation.y, rotation.z, rotation.w]
+
+
+def animate_convert_keys(key_list):
+ """Convert Blender key frames to glTF time keys depending on the applied frames per second."""
+ times = []
+
+ for key in key_list:
+ times.append(key / bpy.context.scene.render.fps)
+
+ return times
+
+
+def animate_gather_keys(export_settings, fcurve_list, interpolation):
+ """
+ Merge and sort several key frames to one set.
+
+ If an interpolation conversion is needed, the sample key frames are created as well.
+ """
+ keys = []
+
+ frame_start = bpy.context.scene.frame_start
+ frame_end = bpy.context.scene.frame_end
+
+ if interpolation == NEEDS_CONVERSION:
+ start = None
+ end = None
+
+ for blender_fcurve in fcurve_list:
+ if blender_fcurve is None:
+ continue
+
+ if start is None:
+ start = blender_fcurve.range()[0]
+ else:
+ start = min(start, blender_fcurve.range()[0])
+
+ if end is None:
+ end = blender_fcurve.range()[1]
+ else:
+ end = max(end, blender_fcurve.range()[1])
+
+ #
+
+ add_epsilon_keyframe = False
+ for blender_keyframe in blender_fcurve.keyframe_points:
+ if add_epsilon_keyframe:
+ key = blender_keyframe.co[0] - 0.001
+
+ if key not in keys:
+ keys.append(key)
+
+ add_epsilon_keyframe = False
+
+ if blender_keyframe.interpolation == CONSTANT_INTERPOLATION:
+ add_epsilon_keyframe = True
+
+ if add_epsilon_keyframe:
+ key = end - 0.001
+
+ if key not in keys:
+ keys.append(key)
+
+ key = start
+ while key <= end:
+ if not export_settings[gltf2_blender_export_keys.FRAME_RANGE] or (frame_start <= key <= frame_end):
+ keys.append(key)
+ key += export_settings[gltf2_blender_export_keys.FRAME_STEP]
+
+ keys.sort()
+
+ else:
+ for blender_fcurve in fcurve_list:
+ if blender_fcurve is None:
+ continue
+
+ for blender_keyframe in blender_fcurve.keyframe_points:
+ key = blender_keyframe.co[0]
+ if not export_settings[gltf2_blender_export_keys.FRAME_RANGE] or (frame_start <= key <= frame_end):
+ if key not in keys:
+ keys.append(key)
+
+ keys.sort()
+
+ return keys
+
+
+def animate_location(export_settings, location, interpolation, node_type, node_name, action_name, matrix_correction,
+ matrix_basis):
+ """Calculate/gather the key value pairs for location transformations."""
+ joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+ if not joint_cache.get(node_name):
+ joint_cache[node_name] = {}
+
+ keys = animate_gather_keys(export_settings, location, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+ result_in_tangent = {}
+ result_out_tangent = {}
+
+ keyframe_index = 0
+ for timeIndex, time in enumerate(times):
+ translation = [0.0, 0.0, 0.0]
+ in_tangent = [0.0, 0.0, 0.0]
+ out_tangent = [0.0, 0.0, 0.0]
+
+ if node_type == JOINT_NODE:
+ if joint_cache[node_name].get(keys[keyframe_index]):
+ translation, tmp_rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+ else:
+ bpy.context.scene.frame_set(keys[keyframe_index])
+
+ matrix = matrix_correction * matrix_basis
+
+ translation, tmp_rotation, tmp_scale = matrix.decompose()
+
+ joint_cache[node_name][keys[keyframe_index]] = [translation, tmp_rotation, tmp_scale]
+ else:
+ channel_index = 0
+ for blender_fcurve in location:
+
+ if blender_fcurve is not None:
+
+ if interpolation == CUBIC_INTERPOLATION:
+ blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+ translation[channel_index] = blender_key_frame.co[1]
+
+ if timeIndex == 0:
+ in_tangent_value = 0.0
+ else:
+ factor = 3.0 / (time - times[timeIndex - 1])
+ in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+ if timeIndex == len(times) - 1:
+ out_tangent_value = 0.0
+ else:
+ factor = 3.0 / (times[timeIndex + 1] - time)
+ out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+ in_tangent[channel_index] = in_tangent_value
+ out_tangent[channel_index] = out_tangent_value
+ else:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ translation[channel_index] = value
+
+ channel_index += 1
+
+ # handle parent inverse
+ matrix = Matrix.Translation(translation)
+ matrix = matrix_correction * matrix
+ translation = matrix.to_translation()
+
+ translation = gltf2_blender_extract.convert_swizzle_location(translation, export_settings)
+ in_tangent = gltf2_blender_extract.convert_swizzle_location(in_tangent, export_settings)
+ out_tangent = gltf2_blender_extract.convert_swizzle_location(out_tangent, export_settings)
+
+ result[time] = translation
+ result_in_tangent[time] = in_tangent
+ result_out_tangent[time] = out_tangent
+
+ keyframe_index += 1
+
+ return result, result_in_tangent, result_out_tangent
+
+
+def animate_rotation_axis_angle(export_settings, rotation_axis_angle, interpolation, node_type, node_name, action_name,
+ matrix_correction, matrix_basis):
+ """Calculate/gather the key value pairs for axis angle transformations."""
+ joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+ if not joint_cache.get(node_name):
+ joint_cache[node_name] = {}
+
+ keys = animate_gather_keys(export_settings, rotation_axis_angle, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+
+ keyframe_index = 0
+ for time in times:
+ axis_angle_rotation = [1.0, 0.0, 0.0, 0.0]
+
+ if node_type == JOINT_NODE:
+ if joint_cache[node_name].get(keys[keyframe_index]):
+ tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+ else:
+ bpy.context.scene.frame_set(keys[keyframe_index])
+
+ matrix = matrix_correction * matrix_basis
+
+ tmp_location, rotation, tmp_scale = matrix.decompose()
+
+ joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale]
+ else:
+ channel_index = 0
+ for blender_fcurve in rotation_axis_angle:
+ if blender_fcurve is not None:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ axis_angle_rotation[channel_index] = value
+
+ channel_index += 1
+
+ rotation = animate_convert_rotation_axis_angle(axis_angle_rotation)
+
+ # handle parent inverse
+ rotation = Quaternion((rotation[3], rotation[0], rotation[1], rotation[2]))
+ matrix = rotation.to_matrix().to_4x4()
+ matrix = matrix_correction * matrix
+ rotation = matrix.to_quaternion()
+
+ # Bring back to internal Quaternion notation.
+ rotation = gltf2_blender_extract.convert_swizzle_rotation(
+ [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings)
+
+ # Bring back to glTF Quaternion notation.
+ rotation = [rotation[1], rotation[2], rotation[3], rotation[0]]
+
+ result[time] = rotation
+
+ keyframe_index += 1
+
+ return result
+
+
+def animate_rotation_euler(export_settings, rotation_euler, rotation_mode, interpolation, node_type, node_name,
+ action_name, matrix_correction, matrix_basis):
+ """Calculate/gather the key value pairs for euler angle transformations."""
+ joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+ if not joint_cache.get(node_name):
+ joint_cache[node_name] = {}
+
+ keys = animate_gather_keys(export_settings, rotation_euler, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+
+ keyframe_index = 0
+ for time in times:
+ euler_rotation = [0.0, 0.0, 0.0]
+
+ if node_type == JOINT_NODE:
+ if joint_cache[node_name].get(keys[keyframe_index]):
+ tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+ else:
+ bpy.context.scene.frame_set(keys[keyframe_index])
+
+ matrix = matrix_correction * matrix_basis
+
+ tmp_location, rotation, tmp_scale = matrix.decompose()
+
+ joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale]
+ else:
+ channel_index = 0
+ for blender_fcurve in rotation_euler:
+ if blender_fcurve is not None:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ euler_rotation[channel_index] = value
+
+ channel_index += 1
+
+ rotation = animate_convert_rotation_euler(euler_rotation, rotation_mode)
+
+ # handle parent inverse
+ rotation = Quaternion((rotation[3], rotation[0], rotation[1], rotation[2]))
+ matrix = rotation.to_matrix().to_4x4()
+ matrix = matrix_correction * matrix
+ rotation = matrix.to_quaternion()
+
+ # Bring back to internal Quaternion notation.
+ rotation = gltf2_blender_extract.convert_swizzle_rotation(
+ [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings)
+
+ # Bring back to glTF Quaternion notation.
+ rotation = [rotation[1], rotation[2], rotation[3], rotation[0]]
+
+ result[time] = rotation
+
+ keyframe_index += 1
+
+ return result
+
+
+def animate_rotation_quaternion(export_settings, rotation_quaternion, interpolation, node_type, node_name, action_name,
+ matrix_correction, matrix_basis):
+ """Calculate/gather the key value pairs for quaternion transformations."""
+ joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+ if not joint_cache.get(node_name):
+ joint_cache[node_name] = {}
+
+ keys = animate_gather_keys(export_settings, rotation_quaternion, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+ result_in_tangent = {}
+ result_out_tangent = {}
+
+ keyframe_index = 0
+ for timeIndex, time in enumerate(times):
+ rotation = [1.0, 0.0, 0.0, 0.0]
+ in_tangent = [1.0, 0.0, 0.0, 0.0]
+ out_tangent = [1.0, 0.0, 0.0, 0.0]
+
+ if node_type == JOINT_NODE:
+ if joint_cache[node_name].get(keys[keyframe_index]):
+ tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+ else:
+ bpy.context.scene.frame_set(keys[keyframe_index])
+
+ matrix = matrix_correction * matrix_basis
+
+ tmp_location, rotation, tmp_scale = matrix.decompose()
+
+ joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale]
+ else:
+ channel_index = 0
+ for blender_fcurve in rotation_quaternion:
+
+ if blender_fcurve is not None:
+ if interpolation == CUBIC_INTERPOLATION:
+ blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+ rotation[channel_index] = blender_key_frame.co[1]
+
+ if timeIndex == 0:
+ in_tangent_value = 0.0
+ else:
+ factor = 3.0 / (time - times[timeIndex - 1])
+ in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+ if timeIndex == len(times) - 1:
+ out_tangent_value = 0.0
+ else:
+ factor = 3.0 / (times[timeIndex + 1] - time)
+ out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+ in_tangent[channel_index] = in_tangent_value
+ out_tangent[channel_index] = out_tangent_value
+ else:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ rotation[channel_index] = value
+
+ channel_index += 1
+
+ rotation = Quaternion((rotation[0], rotation[1], rotation[2], rotation[3]))
+ in_tangent = gltf2_blender_extract.convert_swizzle_rotation(in_tangent, export_settings)
+ out_tangent = gltf2_blender_extract.convert_swizzle_rotation(out_tangent, export_settings)
+
+ # handle parent inverse
+ matrix = rotation.to_matrix().to_4x4()
+ matrix = matrix_correction * matrix
+ rotation = matrix.to_quaternion()
+
+ # Bring back to internal Quaternion notation.
+ rotation = gltf2_blender_extract.convert_swizzle_rotation(
+ [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings)
+
+ # Bring to glTF Quaternion notation.
+ rotation = [rotation[1], rotation[2], rotation[3], rotation[0]]
+ in_tangent = [in_tangent[1], in_tangent[2], in_tangent[3], in_tangent[0]]
+ out_tangent = [out_tangent[1], out_tangent[2], out_tangent[3], out_tangent[0]]
+
+ result[time] = rotation
+ result_in_tangent[time] = in_tangent
+ result_out_tangent[time] = out_tangent
+
+ keyframe_index += 1
+
+ return result, result_in_tangent, result_out_tangent
+
+
+def animate_scale(export_settings, scale, interpolation, node_type, node_name, action_name, matrix_correction,
+ matrix_basis):
+ """Calculate/gather the key value pairs for scale transformations."""
+ joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+ if not joint_cache.get(node_name):
+ joint_cache[node_name] = {}
+
+ keys = animate_gather_keys(export_settings, scale, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+ result_in_tangent = {}
+ result_out_tangent = {}
+
+ keyframe_index = 0
+ for timeIndex, time in enumerate(times):
+ scale_data = [1.0, 1.0, 1.0]
+ in_tangent = [0.0, 0.0, 0.0]
+ out_tangent = [0.0, 0.0, 0.0]
+
+ if node_type == JOINT_NODE:
+ if joint_cache[node_name].get(keys[keyframe_index]):
+ tmp_location, tmp_rotation, scale_data = joint_cache[node_name][keys[keyframe_index]]
+ else:
+ bpy.context.scene.frame_set(keys[keyframe_index])
+
+ matrix = matrix_correction * matrix_basis
+
+ tmp_location, tmp_rotation, scale_data = matrix.decompose()
+
+ joint_cache[node_name][keys[keyframe_index]] = [tmp_location, tmp_rotation, scale_data]
+ else:
+ channel_index = 0
+ for blender_fcurve in scale:
+
+ if blender_fcurve is not None:
+ if interpolation == CUBIC_INTERPOLATION:
+ blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+ scale_data[channel_index] = blender_key_frame.co[1]
+
+ if timeIndex == 0:
+ in_tangent_value = 0.0
+ else:
+ factor = 3.0 / (time - times[timeIndex - 1])
+ in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+ if timeIndex == len(times) - 1:
+ out_tangent_value = 0.0
+ else:
+ factor = 3.0 / (times[timeIndex + 1] - time)
+ out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+ in_tangent[channel_index] = in_tangent_value
+ out_tangent[channel_index] = out_tangent_value
+ else:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ scale_data[channel_index] = value
+
+ channel_index += 1
+
+ scale_data = gltf2_blender_extract.convert_swizzle_scale(scale_data, export_settings)
+ in_tangent = gltf2_blender_extract.convert_swizzle_scale(in_tangent, export_settings)
+ out_tangent = gltf2_blender_extract.convert_swizzle_scale(out_tangent, export_settings)
+
+ # handle parent inverse
+ matrix = Matrix()
+ matrix[0][0] = scale_data.x
+ matrix[1][1] = scale_data.y
+ matrix[2][2] = scale_data.z
+ matrix = matrix_correction * matrix
+ scale_data = matrix.to_scale()
+
+ result[time] = scale_data
+ result_in_tangent[time] = in_tangent
+ result_out_tangent[time] = out_tangent
+
+ keyframe_index += 1
+
+ return result, result_in_tangent, result_out_tangent
+
+
+def animate_value(export_settings, value_parameter, interpolation,
+ node_type, node_name, matrix_correction, matrix_basis):
+ """Calculate/gather the key value pairs for scalar anaimations."""
+ keys = animate_gather_keys(export_settings, value_parameter, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+ result_in_tangent = {}
+ result_out_tangent = {}
+
+ keyframe_index = 0
+ for timeIndex, time in enumerate(times):
+ value_data = []
+ in_tangent = []
+ out_tangent = []
+
+ for blender_fcurve in value_parameter:
+
+ if blender_fcurve is not None:
+ if interpolation == CUBIC_INTERPOLATION:
+ blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+ value_data.append(blender_key_frame.co[1])
+
+ if timeIndex == 0:
+ in_tangent_value = 0.0
+ else:
+ factor = 3.0 / (time - times[timeIndex - 1])
+ in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+ if timeIndex == len(times) - 1:
+ out_tangent_value = 0.0
+ else:
+ factor = 3.0 / (times[timeIndex + 1] - time)
+ out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+ in_tangent.append(in_tangent_value)
+ out_tangent.append(out_tangent_value)
+ else:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ value_data.append(value)
+
+ result[time] = value_data
+ result_in_tangent[time] = in_tangent
+ result_out_tangent[time] = out_tangent
+
+ keyframe_index += 1
+
+ return result, result_in_tangent, result_out_tangent
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export.py b/io_scene_gltf2/blender/exp/gltf2_blender_export.py
new file mode 100755
index 00000000..ae2db26b
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_export.py
@@ -0,0 +1,100 @@
+# 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.
+
+import bpy
+import sys
+import traceback
+
+from . import gltf2_blender_export_keys
+from . import gltf2_blender_gather
+from .gltf2_blender_gltf2_exporter import GlTF2Exporter
+from ..com import gltf2_blender_json
+from ...io.exp import gltf2_io_export
+from ...io.com.gltf2_io_debug import print_console, print_newline
+
+
+def save(operator,
+ context,
+ export_settings):
+ """Start the glTF 2.0 export and saves to content either to a .gltf or .glb file."""
+ print_console('INFO', 'Starting glTF 2.0 export')
+ context.window_manager.progress_begin(0, 100)
+ context.window_manager.progress_update(0)
+
+ if not export_settings[gltf2_blender_export_keys.COPYRIGHT]:
+ export_settings[gltf2_blender_export_keys.COPYRIGHT] = None
+
+ scenes, animations = gltf2_blender_gather.gather_gltf2(export_settings)
+ exporter = GlTF2Exporter(copyright=export_settings[gltf2_blender_export_keys.COPYRIGHT])
+ for scene in scenes:
+ exporter.add_scene(scene)
+ for animation in animations:
+ exporter.add_animation(animation)
+
+ buffer = bytes()
+ if export_settings[gltf2_blender_export_keys.FORMAT] == 'ASCII':
+ # .gltf
+ if export_settings[gltf2_blender_export_keys.EMBED_BUFFERS]:
+ exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY])
+ else:
+ exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY],
+ export_settings[gltf2_blender_export_keys.BINARY_FILENAME])
+ else:
+ # .glb
+ buffer = exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY], is_glb=True)
+ exporter.finalize_images(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY])
+ glTF = exporter.glTF
+
+ #
+
+ # TODO: move to custom JSON encoder
+ def dict_strip(obj):
+ o = obj
+ if isinstance(obj, dict):
+ o = {}
+ for k, v in obj.items():
+ if v is None:
+ continue
+ elif isinstance(v, list) and len(v) == 0:
+ continue
+ o[k] = dict_strip(v)
+ elif isinstance(obj, list):
+ o = []
+ for v in obj:
+ o.append(dict_strip(v))
+ elif isinstance(obj, float):
+ # force floats to int, if they are integers (prevent INTEGER_WRITTEN_AS_FLOAT validator warnings)
+ if int(obj) == obj:
+ return int(obj)
+ return o
+
+ try:
+ gltf2_io_export.save_gltf(dict_strip(glTF.to_dict()), export_settings, gltf2_blender_json.BlenderJSONEncoder,
+ buffer)
+ except AssertionError as e:
+ _, _, tb = sys.exc_info()
+ traceback.print_tb(tb) # Fixed format
+ tb_info = traceback.extract_tb(tb)
+ for tbi in tb_info:
+ filename, line, func, text = tbi
+ print_console('ERROR', 'An error occurred on line {} in statement {}'.format(line, text))
+ print_console('ERROR', str(e))
+ raise e
+
+ print_console('INFO', 'Finished glTF 2.0 export')
+ context.window_manager.progress_end()
+ print_newline()
+
+ return {'FINISHED'}
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py
new file mode 100755
index 00000000..9e47645a
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py
@@ -0,0 +1,66 @@
+# 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.
+
+FILTERED_VERTEX_GROUPS = 'filtered_vertex_groups'
+FILTERED_MESHES = 'filtered_meshes'
+FILTERED_IMAGES = 'filtered_images'
+FILTERED_IMAGES_USE_ALPHA = 'filtered_images_use_alpha'
+FILTERED_MERGED_IMAGES = 'filtered_merged_images'
+FILTERED_TEXTURES = 'filtered_textures'
+FILTERED_MATERIALS = 'filtered_materials'
+FILTERED_LIGHTS = 'filtered_lights'
+TEMPORARY_MESHES = 'temporary_meshes'
+FILTERED_OBJECTS = 'filtered_objects'
+FILTERED_CAMERAS = 'filtered_cameras'
+
+APPLY = 'gltf_apply'
+LAYERS = 'gltf_layers'
+SELECTED = 'gltf_selected'
+SKINS = 'gltf_skins'
+DISPLACEMENT = 'gltf_displacement'
+FORCE_SAMPLING = 'gltf_force_sampling'
+FRAME_RANGE = 'gltf_frame_range'
+FRAME_STEP = 'gltf_frame_step'
+JOINT_CACHE = 'gltf_joint_cache'
+COPYRIGHT = 'gltf_copyright'
+FORMAT = 'gltf_format'
+FILE_DIRECTORY = 'gltf_filedirectory'
+BINARY_FILENAME = 'gltf_binaryfilename'
+YUP = 'gltf_yup'
+MORPH = 'gltf_morph'
+INDICES = 'gltf_indices'
+CAMERA_INFINITE = 'gltf_camera_infinite'
+BAKE_SKINS = 'gltf_bake_skins'
+TEX_COORDS = 'gltf_texcoords'
+COLORS = 'gltf_colors'
+NORMALS = 'gltf_normals'
+TANGENTS = 'gltf_tangents'
+FORCE_INDICES = 'gltf_force_indices'
+MORPH_TANGENT = 'gltf_morph_tangent'
+MORPH_NORMAL = 'gltf_morph_normal'
+MOVE_KEYFRAMES = 'gltf_move_keyframes'
+MATERIALS = 'gltf_materials'
+EXTRAS = 'gltf_extras'
+CAMERAS = 'gltf_cameras'
+LIGHTS = 'gltf_lights'
+ANIMATIONS = 'gltf_animations'
+EMBED_IMAGES = 'gltf_embed_images'
+BINARY = 'gltf_binary'
+EMBED_BUFFERS = 'gltf_embed_buffers'
+TEXTURE_TRANSFORM = 'gltf_texture_transform'
+USE_NO_COLOR = 'gltf_use_no_color'
+
+METALLIC_ROUGHNESS_IMAGE = "metallic_roughness_image"
+GROUP_INDEX = 'group_index'
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
new file mode 100755
index 00000000..a5220129
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
@@ -0,0 +1,1117 @@
+# 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.
+
+#
+# Imports
+#
+
+from mathutils import Vector, Quaternion
+from mathutils.geometry import tessellate_polygon
+
+from . import gltf2_blender_export_keys
+from ...io.com.gltf2_io_debug import print_console
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
+
+#
+# Globals
+#
+
+INDICES_ID = 'indices'
+MATERIAL_ID = 'material'
+ATTRIBUTES_ID = 'attributes'
+
+COLOR_PREFIX = 'COLOR_'
+MORPH_TANGENT_PREFIX = 'MORPH_TANGENT_'
+MORPH_NORMAL_PREFIX = 'MORPH_NORMAL_'
+MORPH_POSITION_PREFIX = 'MORPH_POSITION_'
+TEXCOORD_PREFIX = 'TEXCOORD_'
+WEIGHTS_PREFIX = 'WEIGHTS_'
+JOINTS_PREFIX = 'JOINTS_'
+
+TANGENT_ATTRIBUTE = 'TANGENT'
+NORMAL_ATTRIBUTE = 'NORMAL'
+POSITION_ATTRIBUTE = 'POSITION'
+
+GLTF_MAX_COLORS = 2
+
+
+#
+# Classes
+#
+
+class ShapeKey:
+ def __init__(self, shape_key, vertex_normals, polygon_normals):
+ self.shape_key = shape_key
+ self.vertex_normals = vertex_normals
+ self.polygon_normals = polygon_normals
+
+
+#
+# Functions
+#
+
+def convert_swizzle_location(loc, export_settings):
+ """Convert a location from Blender coordinate system to glTF coordinate system."""
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ return Vector((loc[0], loc[2], -loc[1]))
+ else:
+ return Vector((loc[0], loc[1], loc[2]))
+
+
+def convert_swizzle_tangent(tan, export_settings):
+ """Convert a tangent from Blender coordinate system to glTF coordinate system."""
+ if tan[0] == 0.0 and tan[1] == 0.0 and tan[2] == 0.0:
+ print_console('WARNING', 'Tangent has zero length.')
+
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ return Vector((tan[0], tan[2], -tan[1], 1.0))
+ else:
+ return Vector((tan[0], tan[1], tan[2], 1.0))
+
+
+def convert_swizzle_rotation(rot, export_settings):
+ """
+ Convert a quaternion rotation from Blender coordinate system to glTF coordinate system.
+
+ 'w' is still at first position.
+ """
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ return Quaternion((rot[0], rot[1], rot[3], -rot[2]))
+ else:
+ return Quaternion((rot[0], rot[1], rot[2], rot[3]))
+
+
+def convert_swizzle_scale(scale, export_settings):
+ """Convert a scale from Blender coordinate system to glTF coordinate system."""
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ return Vector((scale[0], scale[2], scale[1]))
+ else:
+ return Vector((scale[0], scale[1], scale[2]))
+
+
+def decompose_transition(matrix, context, export_settings):
+ translation, rotation, scale = matrix.decompose()
+ """Decompose a matrix depending if it is associated to a joint or node."""
+ if context == 'NODE':
+ translation = convert_swizzle_location(translation, export_settings)
+ rotation = convert_swizzle_rotation(rotation, export_settings)
+ scale = convert_swizzle_scale(scale, export_settings)
+
+ # Put w at the end.
+ rotation = Quaternion((rotation[1], rotation[2], rotation[3], rotation[0]))
+
+ return translation, rotation, scale
+
+
+def color_srgb_to_scene_linear(c):
+ """
+ Convert from sRGB to scene linear color space.
+
+ Source: Cycles addon implementation, node_color.h.
+ """
+ if c < 0.04045:
+ return 0.0 if c < 0.0 else c * (1.0 / 12.92)
+ else:
+ return pow((c + 0.055) * (1.0 / 1.055), 2.4)
+
+
+def extract_primitive_floor(a, indices, use_tangents):
+ """Shift indices, that the first one starts with 0. It is assumed, that the indices are packed."""
+ attributes = {
+ POSITION_ATTRIBUTE: [],
+ NORMAL_ATTRIBUTE: []
+ }
+
+ if use_tangents:
+ attributes[TANGENT_ATTRIBUTE] = []
+
+ result_primitive = {
+ MATERIAL_ID: a[MATERIAL_ID],
+ INDICES_ID: [],
+ ATTRIBUTES_ID: attributes
+ }
+
+ source_attributes = a[ATTRIBUTES_ID]
+
+ #
+
+ tex_coord_index = 0
+ process_tex_coord = True
+ while process_tex_coord:
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+
+ if source_attributes.get(tex_coord_id) is not None:
+ attributes[tex_coord_id] = []
+ tex_coord_index += 1
+ else:
+ process_tex_coord = False
+
+ tex_coord_max = tex_coord_index
+
+ #
+
+ color_index = 0
+ process_color = True
+ while process_color:
+ color_id = COLOR_PREFIX + str(color_index)
+
+ if source_attributes.get(color_id) is not None:
+ attributes[color_id] = []
+ color_index += 1
+ else:
+ process_color = False
+
+ color_max = color_index
+
+ #
+
+ bone_index = 0
+ process_bone = True
+ while process_bone:
+ joint_id = JOINTS_PREFIX + str(bone_index)
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+
+ if source_attributes.get(joint_id) is not None:
+ attributes[joint_id] = []
+ attributes[weight_id] = []
+ bone_index += 1
+ else:
+ process_bone = False
+
+ bone_max = bone_index
+
+ #
+
+ morph_index = 0
+ process_morph = True
+ while process_morph:
+ morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+ morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+ morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+
+ if source_attributes.get(morph_position_id) is not None:
+ attributes[morph_position_id] = []
+ attributes[morph_normal_id] = []
+ if use_tangents:
+ attributes[morph_tangent_id] = []
+ morph_index += 1
+ else:
+ process_morph = False
+
+ morph_max = morph_index
+
+ #
+
+ min_index = min(indices)
+ max_index = max(indices)
+
+ for old_index in indices:
+ result_primitive[INDICES_ID].append(old_index - min_index)
+
+ for old_index in range(min_index, max_index + 1):
+ for vi in range(0, 3):
+ attributes[POSITION_ATTRIBUTE].append(source_attributes[POSITION_ATTRIBUTE][old_index * 3 + vi])
+ attributes[NORMAL_ATTRIBUTE].append(source_attributes[NORMAL_ATTRIBUTE][old_index * 3 + vi])
+
+ if use_tangents:
+ for vi in range(0, 4):
+ attributes[TANGENT_ATTRIBUTE].append(source_attributes[TANGENT_ATTRIBUTE][old_index * 4 + vi])
+
+ for tex_coord_index in range(0, tex_coord_max):
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+ for vi in range(0, 2):
+ attributes[tex_coord_id].append(source_attributes[tex_coord_id][old_index * 2 + vi])
+
+ for color_index in range(0, color_max):
+ color_id = COLOR_PREFIX + str(color_index)
+ for vi in range(0, 4):
+ attributes[color_id].append(source_attributes[color_id][old_index * 4 + vi])
+
+ for bone_index in range(0, bone_max):
+ joint_id = JOINTS_PREFIX + str(bone_index)
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+ for vi in range(0, 4):
+ attributes[joint_id].append(source_attributes[joint_id][old_index * 4 + vi])
+ attributes[weight_id].append(source_attributes[weight_id][old_index * 4 + vi])
+
+ for morph_index in range(0, morph_max):
+ morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+ morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+ morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+ for vi in range(0, 3):
+ attributes[morph_position_id].append(source_attributes[morph_position_id][old_index * 3 + vi])
+ attributes[morph_normal_id].append(source_attributes[morph_normal_id][old_index * 3 + vi])
+ if use_tangents:
+ for vi in range(0, 4):
+ attributes[morph_tangent_id].append(source_attributes[morph_tangent_id][old_index * 4 + vi])
+
+ return result_primitive
+
+
+def extract_primitive_pack(a, indices, use_tangents):
+ """Pack indices, that the first one starts with 0. Current indices can have gaps."""
+ attributes = {
+ POSITION_ATTRIBUTE: [],
+ NORMAL_ATTRIBUTE: []
+ }
+
+ if use_tangents:
+ attributes[TANGENT_ATTRIBUTE] = []
+
+ result_primitive = {
+ MATERIAL_ID: a[MATERIAL_ID],
+ INDICES_ID: [],
+ ATTRIBUTES_ID: attributes
+ }
+
+ source_attributes = a[ATTRIBUTES_ID]
+
+ #
+
+ tex_coord_index = 0
+ process_tex_coord = True
+ while process_tex_coord:
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+
+ if source_attributes.get(tex_coord_id) is not None:
+ attributes[tex_coord_id] = []
+ tex_coord_index += 1
+ else:
+ process_tex_coord = False
+
+ tex_coord_max = tex_coord_index
+
+ #
+
+ color_index = 0
+ process_color = True
+ while process_color:
+ color_id = COLOR_PREFIX + str(color_index)
+
+ if source_attributes.get(color_id) is not None:
+ attributes[color_id] = []
+ color_index += 1
+ else:
+ process_color = False
+
+ color_max = color_index
+
+ #
+
+ bone_index = 0
+ process_bone = True
+ while process_bone:
+ joint_id = JOINTS_PREFIX + str(bone_index)
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+
+ if source_attributes.get(joint_id) is not None:
+ attributes[joint_id] = []
+ attributes[weight_id] = []
+ bone_index += 1
+ else:
+ process_bone = False
+
+ bone_max = bone_index
+
+ #
+
+ morph_index = 0
+ process_morph = True
+ while process_morph:
+ morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+ morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+ morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+
+ if source_attributes.get(morph_position_id) is not None:
+ attributes[morph_position_id] = []
+ attributes[morph_normal_id] = []
+ if use_tangents:
+ attributes[morph_tangent_id] = []
+ morph_index += 1
+ else:
+ process_morph = False
+
+ morph_max = morph_index
+
+ #
+
+ old_to_new_indices = {}
+ new_to_old_indices = {}
+
+ new_index = 0
+ for old_index in indices:
+ if old_to_new_indices.get(old_index) is None:
+ old_to_new_indices[old_index] = new_index
+ new_to_old_indices[new_index] = old_index
+ new_index += 1
+
+ result_primitive[INDICES_ID].append(old_to_new_indices[old_index])
+
+ end_new_index = new_index
+
+ for new_index in range(0, end_new_index):
+ old_index = new_to_old_indices[new_index]
+
+ for vi in range(0, 3):
+ attributes[POSITION_ATTRIBUTE].append(source_attributes[POSITION_ATTRIBUTE][old_index * 3 + vi])
+ attributes[NORMAL_ATTRIBUTE].append(source_attributes[NORMAL_ATTRIBUTE][old_index * 3 + vi])
+
+ if use_tangents:
+ for vi in range(0, 4):
+ attributes[TANGENT_ATTRIBUTE].append(source_attributes[TANGENT_ATTRIBUTE][old_index * 4 + vi])
+
+ for tex_coord_index in range(0, tex_coord_max):
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+ for vi in range(0, 2):
+ attributes[tex_coord_id].append(source_attributes[tex_coord_id][old_index * 2 + vi])
+
+ for color_index in range(0, color_max):
+ color_id = COLOR_PREFIX + str(color_index)
+ for vi in range(0, 4):
+ attributes[color_id].append(source_attributes[color_id][old_index * 4 + vi])
+
+ for bone_index in range(0, bone_max):
+ joint_id = JOINTS_PREFIX + str(bone_index)
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+ for vi in range(0, 4):
+ attributes[joint_id].append(source_attributes[joint_id][old_index * 4 + vi])
+ attributes[weight_id].append(source_attributes[weight_id][old_index * 4 + vi])
+
+ for morph_index in range(0, morph_max):
+ morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+ morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+ morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+ for vi in range(0, 3):
+ attributes[morph_position_id].append(source_attributes[morph_position_id][old_index * 3 + vi])
+ attributes[morph_normal_id].append(source_attributes[morph_normal_id][old_index * 3 + vi])
+ if use_tangents:
+ for vi in range(0, 4):
+ attributes[morph_tangent_id].append(source_attributes[morph_tangent_id][old_index * 4 + vi])
+
+ return result_primitive
+
+
+def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, export_settings):
+ """
+ Extract primitives from a mesh. Polygons are triangulated and sorted by material.
+
+ Furthermore, primitives are split up, if the indices range is exceeded.
+ Finally, triangles are also split up/duplicated, if face normals are used instead of vertex normals.
+ """
+ print_console('INFO', 'Extracting primitive')
+
+ use_tangents = False
+ if blender_mesh.uv_layers.active and len(blender_mesh.uv_layers) > 0:
+ try:
+ blender_mesh.calc_tangents()
+ use_tangents = True
+ except Exception:
+ print_console('WARNING', 'Could not calculate tangents. Please try to triangulate the mesh first.')
+
+ #
+
+ material_map = {}
+
+ #
+ # Gathering position, normal and tex_coords.
+ #
+ no_material_attributes = {
+ POSITION_ATTRIBUTE: [],
+ NORMAL_ATTRIBUTE: []
+ }
+
+ if use_tangents:
+ no_material_attributes[TANGENT_ATTRIBUTE] = []
+
+ #
+ # Directory of materials with its primitive.
+ #
+ no_material_primitives = {
+ MATERIAL_ID: '',
+ INDICES_ID: [],
+ ATTRIBUTES_ID: no_material_attributes
+ }
+
+ material_name_to_primitives = {'': no_material_primitives}
+
+ #
+
+ vertex_index_to_new_indices = {}
+
+ material_map[''] = vertex_index_to_new_indices
+
+ #
+ # Create primitive for each material.
+ #
+ for blender_material in blender_mesh.materials:
+ if blender_material is None:
+ continue
+
+ attributes = {
+ POSITION_ATTRIBUTE: [],
+ NORMAL_ATTRIBUTE: []
+ }
+
+ if use_tangents:
+ attributes[TANGENT_ATTRIBUTE] = []
+
+ primitive = {
+ MATERIAL_ID: blender_material.name,
+ INDICES_ID: [],
+ ATTRIBUTES_ID: attributes
+ }
+
+ material_name_to_primitives[blender_material.name] = primitive
+
+ #
+
+ vertex_index_to_new_indices = {}
+
+ material_map[blender_material.name] = vertex_index_to_new_indices
+
+ tex_coord_max = 0
+ if blender_mesh.uv_layers.active:
+ tex_coord_max = len(blender_mesh.uv_layers)
+
+ #
+
+ vertex_colors = {}
+
+ color_index = 0
+ for vertex_color in blender_mesh.vertex_colors:
+ vertex_color_name = COLOR_PREFIX + str(color_index)
+ vertex_colors[vertex_color_name] = vertex_color
+
+ color_index += 1
+ if color_index >= GLTF_MAX_COLORS:
+ break
+ color_max = color_index
+
+ #
+
+ bone_max = 0
+ for blender_polygon in blender_mesh.polygons:
+ for loop_index in blender_polygon.loop_indices:
+ vertex_index = blender_mesh.loops[loop_index].vertex_index
+ bones_count = len(blender_mesh.vertices[vertex_index].groups)
+ if bones_count > 0:
+ if bones_count % 4 == 0:
+ bones_count -= 1
+ bone_max = max(bone_max, bones_count // 4 + 1)
+
+ #
+
+ morph_max = 0
+
+ blender_shape_keys = []
+
+ if blender_mesh.shape_keys is not None:
+ morph_max = len(blender_mesh.shape_keys.key_blocks) - 1
+
+ for blender_shape_key in blender_mesh.shape_keys.key_blocks:
+ if blender_shape_key != blender_shape_key.relative_key:
+ blender_shape_keys.append(ShapeKey(
+ blender_shape_key,
+ blender_shape_key.normals_vertex_get(), # calculate vertex normals for this shape key
+ blender_shape_key.normals_polygon_get())) # calculate polygon normals for this shape key
+
+ #
+ # Convert polygon to primitive indices and eliminate invalid ones. Assign to material.
+ #
+ for blender_polygon in blender_mesh.polygons:
+ export_color = True
+
+ #
+
+ if blender_polygon.material_index < 0 or blender_polygon.material_index >= len(blender_mesh.materials) or \
+ blender_mesh.materials[blender_polygon.material_index] is None:
+ primitive = material_name_to_primitives['']
+ vertex_index_to_new_indices = material_map['']
+ else:
+ primitive = material_name_to_primitives[blender_mesh.materials[blender_polygon.material_index].name]
+ vertex_index_to_new_indices = material_map[blender_mesh.materials[blender_polygon.material_index].name]
+ #
+
+ attributes = primitive[ATTRIBUTES_ID]
+
+ face_normal = blender_polygon.normal
+ face_tangent = Vector((0.0, 0.0, 0.0))
+ face_bitangent = Vector((0.0, 0.0, 0.0))
+ if use_tangents:
+ for loop_index in blender_polygon.loop_indices:
+ temp_vertex = blender_mesh.loops[loop_index]
+ face_tangent += temp_vertex.tangent
+ face_bitangent += temp_vertex.bitangent
+
+ face_tangent.normalize()
+ face_bitangent.normalize()
+
+ #
+
+ indices = primitive[INDICES_ID]
+
+ loop_index_list = []
+
+ if len(blender_polygon.loop_indices) == 3:
+ loop_index_list.extend(blender_polygon.loop_indices)
+ elif len(blender_polygon.loop_indices) > 3:
+ # Triangulation of polygon. Using internal function, as non-convex polygons could exist.
+ polyline = []
+
+ for loop_index in blender_polygon.loop_indices:
+ vertex_index = blender_mesh.loops[loop_index].vertex_index
+ v = blender_mesh.vertices[vertex_index].co
+ polyline.append(Vector((v[0], v[1], v[2])))
+
+ triangles = tessellate_polygon((polyline,))
+
+ for triangle in triangles:
+ loop_index_list.append(blender_polygon.loop_indices[triangle[0]])
+ loop_index_list.append(blender_polygon.loop_indices[triangle[2]])
+ loop_index_list.append(blender_polygon.loop_indices[triangle[1]])
+ else:
+ continue
+
+ for loop_index in loop_index_list:
+ vertex_index = blender_mesh.loops[loop_index].vertex_index
+
+ if vertex_index_to_new_indices.get(vertex_index) is None:
+ vertex_index_to_new_indices[vertex_index] = []
+
+ #
+
+ v = None
+ n = None
+ t = None
+ b = None
+ uvs = []
+ colors = []
+ joints = []
+ weights = []
+
+ target_positions = []
+ target_normals = []
+ target_tangents = []
+
+ vertex = blender_mesh.vertices[vertex_index]
+
+ v = convert_swizzle_location(vertex.co, export_settings)
+ if blender_polygon.use_smooth:
+ n = convert_swizzle_location(vertex.normal, export_settings)
+ if use_tangents:
+ t = convert_swizzle_tangent(blender_mesh.loops[loop_index].tangent, export_settings)
+ b = convert_swizzle_location(blender_mesh.loops[loop_index].bitangent, export_settings)
+ else:
+ n = convert_swizzle_location(face_normal, export_settings)
+ if use_tangents:
+ t = convert_swizzle_tangent(face_tangent, export_settings)
+ b = convert_swizzle_location(face_bitangent, export_settings)
+
+ if use_tangents:
+ tv = Vector((t[0], t[1], t[2]))
+ bv = Vector((b[0], b[1], b[2]))
+ nv = Vector((n[0], n[1], n[2]))
+
+ if (nv.cross(tv)).dot(bv) < 0.0:
+ t[3] = -1.0
+
+ if blender_mesh.uv_layers.active:
+ for tex_coord_index in range(0, tex_coord_max):
+ uv = blender_mesh.uv_layers[tex_coord_index].data[loop_index].uv
+ uvs.append([uv.x, 1.0 - uv.y])
+
+ #
+
+ if color_max > 0 and export_color:
+ for color_index in range(0, color_max):
+ color_name = COLOR_PREFIX + str(color_index)
+ color = vertex_colors[color_name].data[loop_index].color
+ colors.append([
+ color_srgb_to_scene_linear(color[0]),
+ color_srgb_to_scene_linear(color[1]),
+ color_srgb_to_scene_linear(color[2]),
+ 1.0
+ ])
+
+ #
+
+ bone_count = 0
+
+ if vertex.groups is not None and len(vertex.groups) > 0 and export_settings[gltf2_blender_export_keys.SKINS]:
+ joint = []
+ weight = []
+ for group_element in vertex.groups:
+
+ if len(joint) == 4:
+ bone_count += 1
+ joints.append(joint)
+ weights.append(weight)
+ joint = []
+ weight = []
+
+ #
+
+ vertex_group_index = group_element.group
+ vertex_group_name = blender_vertex_groups[vertex_group_index].name
+
+ #
+
+ joint_index = 0
+ modifiers_dict = {m.type: m for m in modifiers}
+ if "ARMATURE" in modifiers_dict:
+ armature = modifiers_dict["ARMATURE"].object
+ skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings)
+ for index, j in enumerate(skin.joints):
+ if j.name == vertex_group_name:
+ joint_index = index
+
+ joint_weight = group_element.weight
+
+ #
+ joint.append(joint_index)
+ weight.append(joint_weight)
+
+ if len(joint) > 0:
+ bone_count += 1
+
+ for fill in range(0, 4 - len(joint)):
+ joint.append(0)
+ weight.append(0.0)
+
+ joints.append(joint)
+ weights.append(weight)
+
+ for fill in range(0, bone_max - bone_count):
+ joints.append([0, 0, 0, 0])
+ weights.append([0.0, 0.0, 0.0, 0.0])
+
+ #
+
+ if morph_max > 0 and export_settings[gltf2_blender_export_keys.MORPH]:
+ for morph_index in range(0, morph_max):
+ blender_shape_key = blender_shape_keys[morph_index]
+
+ v_morph = convert_swizzle_location(blender_shape_key.shape_key.data[vertex_index].co,
+ export_settings)
+
+ # Store delta.
+ v_morph -= v
+
+ target_positions.append(v_morph)
+
+ #
+
+ n_morph = None
+
+ if blender_polygon.use_smooth:
+ temp_normals = blender_shape_key.vertex_normals
+ n_morph = (temp_normals[vertex_index * 3 + 0], temp_normals[vertex_index * 3 + 1],
+ temp_normals[vertex_index * 3 + 2])
+ else:
+ temp_normals = blender_shape_key.polygon_normals
+ n_morph = (
+ temp_normals[blender_polygon.index * 3 + 0], temp_normals[blender_polygon.index * 3 + 1],
+ temp_normals[blender_polygon.index * 3 + 2])
+
+ n_morph = convert_swizzle_location(n_morph, export_settings)
+
+ # Store delta.
+ n_morph -= n
+
+ target_normals.append(n_morph)
+
+ #
+
+ if use_tangents:
+ rotation = n_morph.rotation_difference(n)
+
+ t_morph = Vector((t[0], t[1], t[2]))
+
+ t_morph.rotate(rotation)
+
+ target_tangents.append(t_morph)
+
+ #
+ #
+
+ create = True
+
+ for current_new_index in vertex_index_to_new_indices[vertex_index]:
+ found = True
+
+ for i in range(0, 3):
+ if attributes[POSITION_ATTRIBUTE][current_new_index * 3 + i] != v[i]:
+ found = False
+ break
+
+ if attributes[NORMAL_ATTRIBUTE][current_new_index * 3 + i] != n[i]:
+ found = False
+ break
+
+ if use_tangents:
+ for i in range(0, 4):
+ if attributes[TANGENT_ATTRIBUTE][current_new_index * 4 + i] != t[i]:
+ found = False
+ break
+
+ if not found:
+ continue
+
+ for tex_coord_index in range(0, tex_coord_max):
+ uv = uvs[tex_coord_index]
+
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+ for i in range(0, 2):
+ if attributes[tex_coord_id][current_new_index * 2 + i] != uv[i]:
+ found = False
+ break
+
+ if export_color:
+ for color_index in range(0, color_max):
+ color = colors[color_index]
+
+ color_id = COLOR_PREFIX + str(color_index)
+ for i in range(0, 3):
+ # Alpha is always 1.0 - see above.
+ current_color = attributes[color_id][current_new_index * 4 + i]
+ if color_srgb_to_scene_linear(current_color) != color[i]:
+ found = False
+ break
+
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ for bone_index in range(0, bone_max):
+ joint = joints[bone_index]
+ weight = weights[bone_index]
+
+ joint_id = JOINTS_PREFIX + str(bone_index)
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+ for i in range(0, 4):
+ if attributes[joint_id][current_new_index * 4 + i] != joint[i]:
+ found = False
+ break
+ if attributes[weight_id][current_new_index * 4 + i] != weight[i]:
+ found = False
+ break
+
+ if export_settings[gltf2_blender_export_keys.MORPH]:
+ for morph_index in range(0, morph_max):
+ target_position = target_positions[morph_index]
+ target_normal = target_normals[morph_index]
+ if use_tangents:
+ target_tangent = target_tangents[morph_index]
+
+ target_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+ target_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+ target_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+ for i in range(0, 3):
+ if attributes[target_position_id][current_new_index * 3 + i] != target_position[i]:
+ found = False
+ break
+ if attributes[target_normal_id][current_new_index * 3 + i] != target_normal[i]:
+ found = False
+ break
+ if use_tangents:
+ if attributes[target_tangent_id][current_new_index * 3 + i] != target_tangent[i]:
+ found = False
+ break
+
+ if found:
+ indices.append(current_new_index)
+
+ create = False
+ break
+
+ if not create:
+ continue
+
+ new_index = 0
+
+ if primitive.get('max_index') is not None:
+ new_index = primitive['max_index'] + 1
+
+ primitive['max_index'] = new_index
+
+ vertex_index_to_new_indices[vertex_index].append(new_index)
+
+ #
+ #
+
+ indices.append(new_index)
+
+ #
+
+ attributes[POSITION_ATTRIBUTE].extend(v)
+ attributes[NORMAL_ATTRIBUTE].extend(n)
+ if use_tangents:
+ attributes[TANGENT_ATTRIBUTE].extend(t)
+
+ if blender_mesh.uv_layers.active:
+ for tex_coord_index in range(0, tex_coord_max):
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+
+ if attributes.get(tex_coord_id) is None:
+ attributes[tex_coord_id] = []
+
+ attributes[tex_coord_id].extend(uvs[tex_coord_index])
+
+ if export_color:
+ for color_index in range(0, color_max):
+ color_id = COLOR_PREFIX + str(color_index)
+
+ if attributes.get(color_id) is None:
+ attributes[color_id] = []
+
+ attributes[color_id].extend(colors[color_index])
+
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ for bone_index in range(0, bone_max):
+ joint_id = JOINTS_PREFIX + str(bone_index)
+
+ if attributes.get(joint_id) is None:
+ attributes[joint_id] = []
+
+ attributes[joint_id].extend(joints[bone_index])
+
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+
+ if attributes.get(weight_id) is None:
+ attributes[weight_id] = []
+
+ attributes[weight_id].extend(weights[bone_index])
+
+ if export_settings[gltf2_blender_export_keys.MORPH]:
+ for morph_index in range(0, morph_max):
+ target_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+
+ if attributes.get(target_position_id) is None:
+ attributes[target_position_id] = []
+
+ attributes[target_position_id].extend(target_positions[morph_index])
+
+ target_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+
+ if attributes.get(target_normal_id) is None:
+ attributes[target_normal_id] = []
+
+ attributes[target_normal_id].extend(target_normals[morph_index])
+
+ if use_tangents:
+ target_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+
+ if attributes.get(target_tangent_id) is None:
+ attributes[target_tangent_id] = []
+
+ attributes[target_tangent_id].extend(target_tangents[morph_index])
+
+ #
+ # Add primitive plus split them if needed.
+ #
+
+ result_primitives = []
+
+ for material_name, primitive in material_name_to_primitives.items():
+ export_color = True
+
+ #
+
+ indices = primitive[INDICES_ID]
+
+ if len(indices) == 0:
+ continue
+
+ position = primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE]
+ normal = primitive[ATTRIBUTES_ID][NORMAL_ATTRIBUTE]
+ if use_tangents:
+ tangent = primitive[ATTRIBUTES_ID][TANGENT_ATTRIBUTE]
+ tex_coords = []
+ for tex_coord_index in range(0, tex_coord_max):
+ tex_coords.append(primitive[ATTRIBUTES_ID][TEXCOORD_PREFIX + str(tex_coord_index)])
+ colors = []
+ if export_color:
+ for color_index in range(0, color_max):
+ tex_coords.append(primitive[ATTRIBUTES_ID][COLOR_PREFIX + str(color_index)])
+ joints = []
+ weights = []
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ for bone_index in range(0, bone_max):
+ joints.append(primitive[ATTRIBUTES_ID][JOINTS_PREFIX + str(bone_index)])
+ weights.append(primitive[ATTRIBUTES_ID][WEIGHTS_PREFIX + str(bone_index)])
+
+ target_positions = []
+ target_normals = []
+ target_tangents = []
+ if export_settings[gltf2_blender_export_keys.MORPH]:
+ for morph_index in range(0, morph_max):
+ target_positions.append(primitive[ATTRIBUTES_ID][MORPH_POSITION_PREFIX + str(morph_index)])
+ target_normals.append(primitive[ATTRIBUTES_ID][MORPH_NORMAL_PREFIX + str(morph_index)])
+ if use_tangents:
+ target_tangents.append(primitive[ATTRIBUTES_ID][MORPH_TANGENT_PREFIX + str(morph_index)])
+
+ #
+
+ count = len(indices)
+
+ if count == 0:
+ continue
+
+ max_index = max(indices)
+
+ #
+
+ range_indices = 65536
+ if export_settings[gltf2_blender_export_keys.INDICES] == 'UNSIGNED_BYTE':
+ range_indices = 256
+ elif export_settings[gltf2_blender_export_keys.INDICES] == 'UNSIGNED_INT':
+ range_indices = 4294967296
+
+ #
+
+ if max_index >= range_indices:
+ #
+ # Spliting result_primitives.
+ #
+
+ # At start, all indicees are pending.
+ pending_attributes = {
+ POSITION_ATTRIBUTE: [],
+ NORMAL_ATTRIBUTE: []
+ }
+
+ if use_tangents:
+ pending_attributes[TANGENT_ATTRIBUTE] = []
+
+ pending_primitive = {
+ MATERIAL_ID: material_name,
+ INDICES_ID: [],
+ ATTRIBUTES_ID: pending_attributes
+ }
+
+ pending_primitive[INDICES_ID].extend(indices)
+
+ pending_attributes[POSITION_ATTRIBUTE].extend(position)
+ pending_attributes[NORMAL_ATTRIBUTE].extend(normal)
+ if use_tangents:
+ pending_attributes[TANGENT_ATTRIBUTE].extend(tangent)
+ tex_coord_index = 0
+ for tex_coord in tex_coords:
+ pending_attributes[TEXCOORD_PREFIX + str(tex_coord_index)] = tex_coord
+ tex_coord_index += 1
+ if export_color:
+ color_index = 0
+ for color in colors:
+ pending_attributes[COLOR_PREFIX + str(color_index)] = color
+ color_index += 1
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ joint_index = 0
+ for joint in joints:
+ pending_attributes[JOINTS_PREFIX + str(joint_index)] = joint
+ joint_index += 1
+ weight_index = 0
+ for weight in weights:
+ pending_attributes[WEIGHTS_PREFIX + str(weight_index)] = weight
+ weight_index += 1
+ if export_settings[gltf2_blender_export_keys.MORPH]:
+ morph_index = 0
+ for target_position in target_positions:
+ pending_attributes[MORPH_POSITION_PREFIX + str(morph_index)] = target_position
+ morph_index += 1
+ morph_index = 0
+ for target_normal in target_normals:
+ pending_attributes[MORPH_NORMAL_PREFIX + str(morph_index)] = target_normal
+ morph_index += 1
+ if use_tangents:
+ morph_index = 0
+ for target_tangent in target_tangents:
+ pending_attributes[MORPH_TANGENT_PREFIX + str(morph_index)] = target_tangent
+ morph_index += 1
+
+ pending_indices = pending_primitive[INDICES_ID]
+
+ # Continue until all are processed.
+ while len(pending_indices) > 0:
+
+ process_indices = pending_primitive[INDICES_ID]
+
+ pending_indices = []
+
+ #
+ #
+
+ all_local_indices = []
+
+ for i in range(0, (max(process_indices) // range_indices) + 1):
+ all_local_indices.append([])
+
+ #
+ #
+
+ # For all faces ...
+ for face_index in range(0, len(process_indices), 3):
+
+ written = False
+
+ face_min_index = min(process_indices[face_index + 0], process_indices[face_index + 1],
+ process_indices[face_index + 2])
+ face_max_index = max(process_indices[face_index + 0], process_indices[face_index + 1],
+ process_indices[face_index + 2])
+
+ # ... check if it can be but in a range of maximum indices.
+ for i in range(0, (max(process_indices) // range_indices) + 1):
+ offset = i * range_indices
+
+ # Yes, so store the primitive with its indices.
+ if face_min_index >= offset and face_max_index < offset + range_indices:
+ all_local_indices[i].extend(
+ [process_indices[face_index + 0], process_indices[face_index + 1],
+ process_indices[face_index + 2]])
+
+ written = True
+ break
+
+ # If not written, the triangel face has indices from different ranges.
+ if not written:
+ pending_indices.extend([process_indices[face_index + 0], process_indices[face_index + 1],
+ process_indices[face_index + 2]])
+
+ # Only add result_primitives, which do have indices in it.
+ for local_indices in all_local_indices:
+ if len(local_indices) > 0:
+ current_primitive = extract_primitive_floor(pending_primitive, local_indices, use_tangents)
+
+ result_primitives.append(current_primitive)
+
+ print_console('DEBUG', 'Adding primitive with splitting. Indices: ' + str(
+ len(current_primitive[INDICES_ID])) + ' Vertices: ' + str(
+ len(current_primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE]) // 3))
+
+ # Process primitive faces having indices in several ranges.
+ if len(pending_indices) > 0:
+ pending_primitive = extract_primitive_pack(pending_primitive, pending_indices, use_tangents)
+
+ print_console('DEBUG', 'Creating temporary primitive for splitting')
+
+ else:
+ #
+ # No splitting needed.
+ #
+ result_primitives.append(primitive)
+
+ print_console('DEBUG', 'Adding primitive without splitting. Indices: ' + str(
+ len(primitive[INDICES_ID])) + ' Vertices: ' + str(
+ len(primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE]) // 3))
+
+ print_console('INFO', 'Primitives created: ' + str(len(result_primitives)))
+
+ return result_primitives
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_filter.py b/io_scene_gltf2/blender/exp/gltf2_blender_filter.py
new file mode 100755
index 00000000..ed3ee055
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_filter.py
@@ -0,0 +1,455 @@
+# 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.
+
+#
+# Imports
+#
+
+import bpy
+from . import gltf2_blender_export_keys
+from . import gltf2_blender_get
+from ...io.com.gltf2_io_debug import print_console
+from ..com.gltf2_blender_image import create_img_from_blender_image
+from ...io.com import gltf2_io_image
+
+#
+# Globals
+#
+
+PREVIEW = 'PREVIEW'
+GLOSSINESS = 'glTF Specular Glossiness'
+ROUGHNESS = 'glTF Metallic Roughness'
+
+
+#
+# Functions
+#
+
+def filter_merge_image(export_settings, blender_image):
+ metallic_channel = gltf2_blender_get.get_image_material_usage_to_socket(blender_image, "Metallic")
+ roughness_channel = gltf2_blender_get.get_image_material_usage_to_socket(blender_image, "Roughness")
+
+ if metallic_channel < 0 and roughness_channel < 0:
+ return False
+
+ output = export_settings[gltf2_blender_export_keys.METALLIC_ROUGHNESS_IMAGE]
+ if export_settings.get(export_keys.METALLIC_ROUGHNESS_IMAGE) is None:
+ width = blender_image.image.size[0]
+ height = blender_image.image.size[1]
+ output = gltf2_io_image.create_img(width, height, r=1.0, g=1.0, b=1.0, a=1.0)
+
+ source = create_img_from_blender_image(blender_image.image)
+
+ if metallic_channel >= 0:
+ gltf2_io_image.copy_img_channel(output, dst_channel=2, src_image=source, src_channel=metallic_channel)
+ output.name = blender_image.image.name + output.name
+ if roughness_channel >= 0:
+ gltf2_io_image.copy_img_channel(output, dst_channel=1, src_image=source, src_channel=roughness_channel)
+ if metallic_channel < 0:
+ output.name = output.name + blender_image.image.name
+ return True
+
+
+def filter_used_materials():
+ """Gather and return all unfiltered, valid Blender materials."""
+ materials = []
+
+ for blender_material in bpy.data.materials:
+ if blender_material.node_tree and blender_material.use_nodes:
+ for currentNode in blender_material.node_tree.nodes:
+ if isinstance(currentNode, bpy.types.ShaderNodeGroup):
+ if currentNode.node_tree.name.startswith(ROUGHNESS):
+ materials.append(blender_material)
+ elif currentNode.node_tree.name.startswith(GLOSSINESS):
+ materials.append(blender_material)
+ elif isinstance(currentNode, bpy.types.ShaderNodeBsdfPrincipled):
+ materials.append(blender_material)
+ else:
+ materials.append(blender_material)
+
+ return materials
+
+
+def filter_apply(export_settings):
+ """
+ Gathers and filters the objects and assets to export.
+
+ Also filters out invalid, deleted and not exportable elements.
+ """
+ filtered_objects = []
+ implicit_filtered_objects = []
+
+ for blender_object in bpy.data.objects:
+
+ if blender_object.users == 0:
+ continue
+
+ if export_settings[gltf2_blender_export_keys.SELECTED] and not blender_object.select:
+ continue
+
+ if not export_settings[gltf2_blender_export_keys.LAYERS] and not blender_object.layers[0]:
+ continue
+
+ filtered_objects.append(blender_object)
+
+ if export_settings[gltf2_blender_export_keys.SELECTED] or not export_settings[gltf2_blender_export_keys.LAYERS]:
+ current_parent = blender_object.parent
+ while current_parent:
+ if current_parent not in implicit_filtered_objects:
+ implicit_filtered_objects.append(current_parent)
+
+ current_parent = current_parent.parent
+
+ export_settings[gltf2_blender_export_keys.FILTERED_OBJECTS] = filtered_objects
+
+ # Meshes
+
+ filtered_meshes = {}
+ filtered_vertex_groups = {}
+ temporary_meshes = []
+
+ for blender_mesh in bpy.data.meshes:
+
+ if blender_mesh.users == 0:
+ continue
+
+ current_blender_mesh = blender_mesh
+
+ current_blender_object = None
+
+ skip = True
+
+ for blender_object in filtered_objects:
+
+ current_blender_object = blender_object
+
+ if current_blender_object.type != 'MESH':
+ continue
+
+ if current_blender_object.data == current_blender_mesh:
+
+ skip = False
+
+ use_auto_smooth = current_blender_mesh.use_auto_smooth
+
+ if use_auto_smooth:
+
+ if current_blender_mesh.shape_keys is None:
+ current_blender_object = current_blender_object.copy()
+ else:
+ use_auto_smooth = False
+
+ print_console('WARNING',
+ 'Auto smooth and shape keys cannot be exported in parallel. '
+ 'Falling back to non auto smooth.')
+
+ if export_settings[gltf2_blender_export_keys.APPLY] or use_auto_smooth:
+ # TODO: maybe add to new exporter
+ if not export_settings[gltf2_blender_export_keys.APPLY]:
+ current_blender_object.modifiers.clear()
+
+ if use_auto_smooth:
+ blender_modifier = current_blender_object.modifiers.new('Temporary_Auto_Smooth', 'EDGE_SPLIT')
+
+ blender_modifier.split_angle = current_blender_mesh.auto_smooth_angle
+ blender_modifier.use_edge_angle = not current_blender_mesh.has_custom_normals
+
+ current_blender_mesh = current_blender_object.to_mesh(bpy.context.scene, True, PREVIEW)
+ temporary_meshes.append(current_blender_mesh)
+
+ break
+
+ if skip:
+ continue
+
+ filtered_meshes[blender_mesh.name] = current_blender_mesh
+ filtered_vertex_groups[blender_mesh.name] = current_blender_object.vertex_groups
+
+ # Curves
+
+ for blender_curve in bpy.data.curves:
+
+ if blender_curve.users == 0:
+ continue
+
+ current_blender_curve = blender_curve
+
+ current_blender_mesh = None
+
+ current_blender_object = None
+
+ skip = True
+
+ for blender_object in filtered_objects:
+
+ current_blender_object = blender_object
+
+ if current_blender_object.type not in ('CURVE', 'FONT'):
+ continue
+
+ if current_blender_object.data == current_blender_curve:
+
+ skip = False
+
+ current_blender_object = current_blender_object.copy()
+
+ if not export_settings[gltf2_blender_export_keys.APPLY]:
+ current_blender_object.modifiers.clear()
+
+ current_blender_mesh = current_blender_object.to_mesh(bpy.context.scene, True, PREVIEW)
+ temporary_meshes.append(current_blender_mesh)
+
+ break
+
+ if skip:
+ continue
+
+ filtered_meshes[blender_curve.name] = current_blender_mesh
+ filtered_vertex_groups[blender_curve.name] = current_blender_object.vertex_groups
+
+ #
+
+ export_settings[gltf2_blender_export_keys.FILTERED_MESHES] = filtered_meshes
+ export_settings[gltf2_blender_export_keys.FILTERED_VERTEX_GROUPS] = filtered_vertex_groups
+ export_settings[gltf2_blender_export_keys.TEMPORARY_MESHES] = temporary_meshes
+
+ #
+
+ filtered_materials = []
+
+ for blender_material in filter_used_materials():
+
+ if blender_material.users == 0:
+ continue
+
+ for mesh_name, blender_mesh in filtered_meshes.items():
+ for compare_blender_material in blender_mesh.materials:
+ if compare_blender_material == blender_material and blender_material not in filtered_materials:
+ filtered_materials.append(blender_material)
+
+ #
+
+ for blender_object in filtered_objects:
+ if blender_object.material_slots:
+ for blender_material_slot in blender_object.material_slots:
+ if blender_material_slot.link == 'DATA':
+ continue
+
+ if blender_material_slot.material not in filtered_materials:
+ filtered_materials.append(blender_material_slot.material)
+
+ export_settings[gltf2_blender_export_keys.FILTERED_MATERIALS] = filtered_materials
+
+ #
+
+ filtered_textures = []
+ filtered_merged_textures = []
+
+ temp_filtered_texture_names = []
+
+ for blender_material in filtered_materials:
+ if blender_material.node_tree and blender_material.use_nodes:
+
+ per_material_textures = []
+
+ for blender_node in blender_material.node_tree.nodes:
+
+ if is_valid_node(blender_node) and blender_node not in filtered_textures:
+ add_node = False
+ add_merged_node = False
+ for blender_socket in blender_node.outputs:
+ if blender_socket.is_linked:
+ for blender_link in blender_socket.links:
+ if isinstance(blender_link.to_node, bpy.types.ShaderNodeGroup):
+ is_roughness = blender_link.to_node.node_tree.name.startswith(ROUGHNESS)
+ is_glossiness = blender_link.to_node.node_tree.name.startswith(GLOSSINESS)
+ if is_roughness or is_glossiness:
+ add_node = True
+ break
+ elif isinstance(blender_link.to_node, bpy.types.ShaderNodeBsdfPrincipled):
+ add_node = True
+ break
+ elif isinstance(blender_link.to_node, bpy.types.ShaderNodeNormalMap):
+ add_node = True
+ break
+ elif isinstance(blender_link.to_node, bpy.types.ShaderNodeSeparateRGB):
+ add_merged_node = True
+ break
+
+ if add_node or add_merged_node:
+ break
+
+ if add_node:
+ filtered_textures.append(blender_node)
+ # TODO: Add displacement texture, as not stored in node tree.
+
+ if add_merged_node:
+ if len(per_material_textures) == 0:
+ filtered_merged_textures.append(per_material_textures)
+
+ per_material_textures.append(blender_node)
+
+ else:
+
+ for blender_texture_slot in blender_material.texture_slots:
+
+ if is_valid_texture_slot(blender_texture_slot) and \
+ blender_texture_slot not in filtered_textures and \
+ blender_texture_slot.name not in temp_filtered_texture_names:
+ accept = False
+
+ if blender_texture_slot.use_map_color_diffuse:
+ accept = True
+
+ if blender_texture_slot.use_map_ambient:
+ accept = True
+ if blender_texture_slot.use_map_emit:
+ accept = True
+ if blender_texture_slot.use_map_normal:
+ accept = True
+
+ if export_settings[gltf2_blender_export_keys.DISPLACEMENT]:
+ if blender_texture_slot.use_map_displacement:
+ accept = True
+
+ if accept:
+ filtered_textures.append(blender_texture_slot)
+ temp_filtered_texture_names.append(blender_texture_slot.name)
+
+ export_settings[gltf2_blender_export_keys.FILTERED_TEXTURES] = filtered_textures
+
+ #
+
+ filtered_images = []
+ filtered_merged_images = []
+ filtered_images_use_alpha = {}
+
+ for blender_texture in filtered_textures:
+
+ if isinstance(blender_texture, bpy.types.ShaderNodeTexImage):
+ if is_valid_image(blender_texture.image) and blender_texture.image not in filtered_images:
+ filtered_images.append(blender_texture.image)
+ alpha_socket = blender_texture.outputs.get('Alpha')
+ if alpha_socket is not None and alpha_socket.is_linked:
+ filtered_images_use_alpha[blender_texture.image.name] = True
+
+ else:
+ if is_valid_image(blender_texture.texture.image) and blender_texture.texture.image not in filtered_images:
+ filtered_images.append(blender_texture.texture.image)
+ if blender_texture.use_map_alpha:
+ filtered_images_use_alpha[blender_texture.texture.image.name] = True
+
+ #
+
+ for per_material_textures in filtered_merged_textures:
+
+ export_settings[gltf2_blender_export_keys.METALLIC_ROUGHNESS_IMAGE] = None
+
+ for blender_texture in per_material_textures:
+
+ if isinstance(blender_texture, bpy.types.ShaderNodeTexImage):
+ if is_valid_image(blender_texture.image) and blender_texture.image not in filtered_images:
+ filter_merge_image(export_settings, blender_texture)
+
+ img = export_settings.get(export_keys.METALLIC_ROUGHNESS_IMAGE)
+ if img is not None:
+ filtered_merged_images.append(img)
+ export_settings[gltf2_blender_export_keys.FILTERED_TEXTURES].append(img)
+
+ export_settings[gltf2_blender_export_keys.FILTERED_MERGED_IMAGES] = filtered_merged_images
+ export_settings[gltf2_blender_export_keys.FILTERED_IMAGES] = filtered_images
+ export_settings[gltf2_blender_export_keys.FILTERED_IMAGES_USE_ALPHA] = filtered_images_use_alpha
+
+ #
+
+ filtered_cameras = []
+
+ for blender_camera in bpy.data.cameras:
+
+ if blender_camera.users == 0:
+ continue
+
+ if export_settings[gltf2_blender_export_keys.SELECTED]:
+ if blender_camera not in filtered_objects:
+ continue
+
+ filtered_cameras.append(blender_camera)
+
+ export_settings[gltf2_blender_export_keys.FILTERED_CAMERAS] = filtered_cameras
+
+ #
+ #
+
+ filtered_lights = []
+
+ for blender_light in bpy.data.lamps:
+
+ if blender_light.users == 0:
+ continue
+
+ if export_settings[gltf2_blender_export_keys.SELECTED]:
+ if blender_light not in filtered_objects:
+ continue
+
+ if blender_light.type == 'HEMI':
+ continue
+
+ filtered_lights.append(blender_light)
+
+ export_settings[gltf2_blender_export_keys.FILTERED_LIGHTS] = filtered_lights
+
+ #
+ #
+
+ for implicit_object in implicit_filtered_objects:
+ if implicit_object not in filtered_objects:
+ filtered_objects.append(implicit_object)
+
+ #
+ #
+ #
+
+ group_index = {}
+
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ for blender_object in filtered_objects:
+ if blender_object.type != 'ARMATURE' or len(blender_object.pose.bones) == 0:
+ continue
+ for blender_bone in blender_object.pose.bones:
+ group_index[blender_bone.name] = len(group_index)
+
+ export_settings[gltf2_blender_export_keys.GROUP_INDEX] = group_index
+
+
+def is_valid_node(blender_node):
+ return isinstance(blender_node, bpy.types.ShaderNodeTexImage) and is_valid_image(blender_node.image)
+
+
+def is_valid_image(image):
+ return image is not None and \
+ image.users != 0 and \
+ image.size[0] > 0 and \
+ image.size[1] > 0
+
+
+def is_valid_texture_slot(blender_texture_slot):
+ return blender_texture_slot is not None and \
+ blender_texture_slot.texture and \
+ blender_texture_slot.texture.users != 0 and \
+ blender_texture_slot.texture.type == 'IMAGE' and \
+ blender_texture_slot.texture.image is not None and \
+ blender_texture_slot.texture.image.users != 0 and \
+ blender_texture_slot.texture.image.size[0] > 0 and \
+ blender_texture_slot.texture.image.size[1] > 0
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py
new file mode 100755
index 00000000..6f4f3b1e
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py
@@ -0,0 +1,63 @@
+# 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.
+
+import bpy
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animations
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+
+def gather_gltf2(export_settings):
+ """
+ Gather glTF properties from the current state of blender.
+
+ :return: list of scene graphs to be added to the glTF export
+ """
+ scenes = []
+ animations = [] # unfortunately animations in gltf2 are just as 'root' as scenes.
+ for blender_scene in bpy.data.scenes:
+ scenes.append(__gather_scene(blender_scene, export_settings))
+ animations += __gather_animations(blender_scene, export_settings)
+
+ return scenes, animations
+
+
+@cached
+def __gather_scene(blender_scene, export_settings):
+ scene = gltf2_io.Scene(
+ extensions=None,
+ extras=None,
+ name=blender_scene.name,
+ nodes=[]
+ )
+
+ for blender_object in blender_scene.objects:
+ if blender_object.parent is None:
+ node = gltf2_blender_gather_nodes.gather_node(blender_object, export_settings)
+ if node is not None:
+ scene.nodes.append(node)
+
+ # TODO: lights
+
+ return scene
+
+
+def __gather_animations(blender_scene, export_settings):
+ animations = []
+ for blender_object in blender_scene.objects:
+ animations += gltf2_blender_gather_animations.gather_animations(blender_object, export_settings)
+ return animations
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py
new file mode 100755
index 00000000..fbe18323
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py
@@ -0,0 +1,82 @@
+# 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.
+
+
+import bpy
+import typing
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints
+
+
+@cached
+def gather_animation_channel_target(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.AnimationChannelTarget:
+ return gltf2_io.AnimationChannelTarget(
+ extensions=__gather_extensions(channels, blender_object, export_settings),
+ extras=__gather_extras(channels, blender_object, export_settings),
+ node=__gather_node(channels, blender_object, export_settings),
+ path=__gather_path(channels, blender_object, export_settings)
+ )
+
+
+def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_node(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.Node:
+ if blender_object.type == "ARMATURE":
+ # TODO: get joint from fcurve data_path and gather_joint
+ blender_bone = blender_object.path_resolve(channels[0].data_path.rsplit('.', 1)[0])
+ if isinstance(blender_bone, bpy.types.PoseBone):
+ return gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings)
+
+ return gltf2_blender_gather_nodes.gather_node(blender_object, export_settings)
+
+
+def __gather_path(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> str:
+ target = channels[0].data_path.split('.')[-1]
+ path = {
+ "location": "translation",
+ "rotation_axis_angle": "rotation",
+ "rotation_euler": "rotation",
+ "rotation_quaternion": "rotation",
+ "scale": "scale",
+ "value": "weights"
+ }.get(target)
+
+ if target is None:
+ raise RuntimeError("Cannot export an animation with {} target".format(target))
+
+ return path
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py
new file mode 100755
index 00000000..808c970d
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py
@@ -0,0 +1,131 @@
+# 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.
+
+import bpy
+import typing
+
+from ..com.gltf2_blender_data_path import get_target_object_path, get_target_property_name
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.com import gltf2_io_debug
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_samplers
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channel_target
+
+
+@cached
+def gather_animation_channels(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.List[gltf2_io.AnimationChannel]:
+ channels = []
+
+ for channel_group in __get_channel_groups(blender_action, blender_object):
+ channel = __gather_animation_channel(channel_group, blender_object, export_settings)
+ if channel is not None:
+ channels.append(channel)
+
+ return channels
+
+
+def __gather_animation_channel(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Union[gltf2_io.AnimationChannel, None]:
+ if not __filter_animation_channel(channels, blender_object, export_settings):
+ return None
+
+ return gltf2_io.AnimationChannel(
+ extensions=__gather_extensions(channels, blender_object, export_settings),
+ extras=__gather_extras(channels, blender_object, export_settings),
+ sampler=__gather_sampler(channels, blender_object, export_settings),
+ target=__gather_target(channels, blender_object, export_settings)
+ )
+
+
+def __filter_animation_channel(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> bool:
+ return True
+
+
+def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_sampler(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.AnimationSampler:
+ return gltf2_blender_gather_animation_samplers.gather_animation_sampler(
+ channels,
+ blender_object,
+ export_settings
+ )
+
+
+def __gather_target(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.AnimationChannelTarget:
+ return gltf2_blender_gather_animation_channel_target.gather_animation_channel_target(
+ channels, blender_object, export_settings)
+
+
+def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object):
+ targets = {}
+ for fcurve in blender_action.fcurves:
+ target_property = get_target_property_name(fcurve.data_path)
+ object_path = get_target_object_path(fcurve.data_path)
+
+ # find the object affected by this action
+ if not object_path:
+ target = blender_object
+ else:
+ try:
+ target = blender_object.path_resolve(object_path)
+ except ValueError:
+ # if the object is a mesh and the action target path can not be resolved, we know that this is a morph
+ # animation.
+ if blender_object.type == "MESH":
+ # if you need the specific shape key for some reason, this is it:
+ # shape_key = blender_object.data.shape_keys.path_resolve(object_path)
+ target = blender_object.data.shape_keys
+ else:
+ gltf2_io_debug.print_console("WARNING", "Can not export animations with target {}".format(object_path))
+ continue
+
+ # group channels by target object and affected property of the target
+ target_properties = targets.get(target, {})
+ channels = target_properties.get(target_property, [])
+ channels.append(fcurve)
+ target_properties[target_property] = channels
+ targets[target] = target_properties
+
+ groups = []
+ for p in targets.values():
+ groups += list(p.values())
+
+ return map(tuple, groups)
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py
new file mode 100755
index 00000000..7562d8c2
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py
@@ -0,0 +1,198 @@
+# 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.
+
+import bpy
+import mathutils
+import typing
+
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.com import gltf2_blender_math
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.io.com import gltf2_io_debug
+
+
+class Keyframe:
+ def __init__(self, channels: typing.Tuple[bpy.types.FCurve], time: float):
+ self.seconds = time / bpy.context.scene.render.fps
+ self.__target = channels[0].data_path.split('.')[-1]
+ self.__indices = [c.array_index for c in channels]
+
+ # Data holders for virtual properties
+ self.__value = None
+ self.__in_tangent = None
+ self.__out_tangent = None
+
+ def __get_target_len(self):
+ length = {
+ "location": 3,
+ "rotation_axis_angle": 4,
+ "rotation_euler": 3,
+ "rotation_quaternion": 4,
+ "scale": 3,
+ "value": 1
+ }.get(self.__target)
+
+ if length is None:
+ raise RuntimeError("Unknown target type {}".format(self.__target))
+
+ return length
+
+ def __set_indexed(self, value):
+ # 'value' targets don't use keyframe.array_index
+ if self.__target == "value":
+ return value
+ # Sometimes blender animations only reference a subset of components of a data target. Keyframe should always
+ # contain a complete Vector/ Quaternion --> use the array_index value of the keyframe to set components in such
+ # structures
+ result = [0.0] * self.__get_target_len()
+ for i, v in zip(self.__indices, value):
+ result[i] = v
+ result = gltf2_blender_math.list_to_mathutils(result, self.__target)
+ return result
+
+ @property
+ def value(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]:
+ return self.__value
+
+ @value.setter
+ def value(self, value: typing.List[float]):
+ self.__value = self.__set_indexed(value)
+
+ @property
+ def in_tangent(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]:
+ return self.__in_tangent
+
+ @in_tangent.setter
+ def in_tangent(self, value: typing.List[float]):
+ self.__in_tangent = self.__set_indexed(value)
+
+ @property
+ def out_tangent(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]:
+ return self.__in_tangent
+
+ @out_tangent.setter
+ def out_tangent(self, value: typing.List[float]):
+ self.__out_tangent = self.__set_indexed(value)
+
+
+# cache for performance reasons
+@cached
+def gather_keyframes(channels: typing.Tuple[bpy.types.FCurve], export_settings) \
+ -> typing.List[Keyframe]:
+ """Convert the blender action groups' fcurves to keyframes for use in glTF."""
+ # Find the start and end of the whole action group
+ ranges = [channel.range() for channel in channels]
+
+ start = min([channel.range()[0] for channel in channels])
+ end = max([channel.range()[1] for channel in channels])
+
+ keyframes = []
+ if needs_baking(channels, export_settings):
+ # Bake the animation, by evaluating it at a high frequency
+ # TODO: maybe baking can also be done with FCurve.convert_to_samples
+ time = start
+ # TODO: make user controllable
+ step = 1.0 / bpy.context.scene.render.fps
+ while time <= end:
+ key = Keyframe(channels, time)
+ key.value = [c.evaluate(time) for c in channels]
+ keyframes.append(key)
+ time += step
+ else:
+ # Just use the keyframes as they are specified in blender
+ times = [keyframe.co[0] for keyframe in channels[0].keyframe_points]
+ for i, time in enumerate(times):
+ key = Keyframe(channels, time)
+ # key.value = [c.keyframe_points[i].co[0] for c in action_group.channels]
+ key.value = [c.evaluate(time) for c in channels]
+
+ # compute tangents for cubic spline interpolation
+ if channels[0].keyframe_points[0].interpolation == "BEZIER":
+ # Construct the in tangent
+ if time == times[0]:
+ # start in-tangent has zero length
+ key.in_tangent = [0.0 for _ in channels]
+ else:
+ # otherwise construct an in tangent from the keyframes control points
+
+ key.in_tangent = [
+ 3.0 * (c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1]
+ ) / (time - times[i - 1])
+ for c in channels
+ ]
+ # Construct the out tangent
+ if time == times[-1]:
+ # end out-tangent has zero length
+ key.out_tangent = [0.0 for _ in channels]
+ else:
+ # otherwise construct an out tangent from the keyframes control points
+ key.out_tangent = [
+ 3.0 * (c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1]
+ ) / (times[i + 1] - time)
+ for c in channels
+ ]
+ keyframes.append(key)
+
+ return keyframes
+
+
+def needs_baking(channels: typing.Tuple[bpy.types.FCurve],
+ export_settings
+ ) -> bool:
+ """
+ Check if baking is needed.
+
+ Some blender animations need to be baked as they can not directly be expressed in glTF.
+ """
+ def all_equal(lst):
+ return lst[1:] == lst[:-1]
+
+
+ if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]:
+ return True
+
+ interpolation = channels[0].keyframe_points[0].interpolation
+ if interpolation not in ["BEZIER", "LINEAR", "CONSTANT"]:
+ gltf2_io_debug.print_console("WARNING",
+ "Baking animation because of an unsupported interpolation method: {}".format(
+ interpolation)
+ )
+ return True
+
+ if any(any(k.interpolation != interpolation for k in c.keyframe_points) for c in channels):
+ # There are different interpolation methods in one action group
+ gltf2_io_debug.print_console("WARNING",
+ "Baking animation because there are different "
+ "interpolation methods in one channel"
+ )
+ return True
+
+ if not all_equal([len(c.keyframe_points) for c in channels]):
+ gltf2_io_debug.print_console("WARNING",
+ "Baking animation because the number of keyframes is not "
+ "equal for all channel tracks")
+ return True
+
+ if len(channels[0].keyframe_points) <= 1:
+ # we need to bake to 'STEP', as at least two keyframes are required to interpolate
+ return True
+
+ if not all(all_equal(key_times) for key_times in zip([[k.co[0] for k in c.keyframe_points] for c in channels])):
+ # The channels have differently located keyframes
+ gltf2_io_debug.print_console("WARNING",
+ "Baking animation because of differently located keyframes in one channel")
+ return True
+
+ return False
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py
new file mode 100755
index 00000000..c94f1528
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py
@@ -0,0 +1,166 @@
+# 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.
+
+
+import bpy
+import mathutils
+import typing
+import math
+
+from . import gltf2_blender_export_keys
+from mathutils import Matrix
+from io_scene_gltf2.blender.com.gltf2_blender_data_path import get_target_property_name, get_target_object_path
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.blender.com import gltf2_blender_math
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_sampler_keyframes
+
+
+@cached
+def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.AnimationSampler:
+ return gltf2_io.AnimationSampler(
+ extensions=__gather_extensions(channels, blender_object, export_settings),
+ extras=__gather_extras(channels, blender_object, export_settings),
+ input=__gather_input(channels, blender_object, export_settings),
+ interpolation=__gather_interpolation(channels, blender_object, export_settings),
+ output=__gather_output(channels, blender_object, export_settings)
+ )
+
+
+def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_input(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.Accessor:
+ """Gather the key time codes."""
+ keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(channels, export_settings)
+ times = [k.seconds for k in keyframes]
+
+ return gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(times, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(times),
+ extensions=None,
+ extras=None,
+ max=[max(times)],
+ min=[min(times)],
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Scalar
+ )
+
+
+def __gather_interpolation(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> str:
+ if gltf2_blender_gather_animation_sampler_keyframes.needs_baking(channels, export_settings):
+ return 'STEP'
+
+ blender_keyframe = channels[0].keyframe_points[0]
+
+ # Select the interpolation method. Any unsupported method will fallback to STEP
+ return {
+ "BEZIER": "CUBICSPLINE",
+ "LINEAR": "LINEAR",
+ "CONSTANT": "STEP"
+ }[blender_keyframe.interpolation]
+
+
+def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.Accessor:
+ """Gather the data of the keyframes."""
+ keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(channels, export_settings)
+
+ target_datapath = channels[0].data_path
+
+ transform = Matrix.Identity(4)
+
+ if blender_object.type == "ARMATURE":
+ bone = blender_object.path_resolve(get_target_object_path(target_datapath))
+ if isinstance(bone, bpy.types.PoseBone):
+ transform = bone.bone.matrix_local
+ if bone.parent is not None:
+ parent_transform = bone.parent.bone.matrix_local
+ transform = gltf2_blender_math.multiply(parent_transform.inverted(), transform)
+ # if not export_settings[gltf2_blender_export_keys.YUP]:
+ # transform = gltf2_blender_math.multiply(gltf2_blender_math.to_zup(), transform)
+ else:
+ # only apply the y-up conversion to root bones, as child bones already are in the y-up space
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ transform = gltf2_blender_math.multiply(gltf2_blender_math.to_yup(), transform)
+
+ values = []
+ for keyframe in keyframes:
+ # Transform the data and extract
+ value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform)
+ if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
+ value = gltf2_blender_math.swizzle_yup(value, target_datapath)
+ keyframe_value = gltf2_blender_math.mathutils_to_gltf(value)
+ if keyframe.in_tangent is not None:
+ in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform)
+ if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
+ in_tangent = gltf2_blender_math.swizzle_yup(in_tangent, target_datapath)
+ keyframe_value = gltf2_blender_math.mathutils_to_gltf(in_tangent) + keyframe_value
+ if keyframe.out_tangent is not None:
+ out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform)
+ if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
+ out_tangent = gltf2_blender_math.swizzle_yup(out_tangent, target_datapath)
+ keyframe_value = keyframe_value + gltf2_blender_math.mathutils_to_gltf(out_tangent)
+ values += keyframe_value
+
+ component_type = gltf2_io_constants.ComponentType.Float
+ if get_target_property_name(target_datapath) == "value":
+ # channels with 'weight' targets must have scalar accessors
+ data_type = gltf2_io_constants.DataType.Scalar
+ else:
+ data_type = gltf2_io_constants.DataType.vec_type_from_num(len(keyframes[0].value))
+
+ return gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(values, component_type),
+ byte_offset=None,
+ component_type=component_type,
+ count=len(values) // gltf2_io_constants.DataType.num_elements(data_type),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=data_type
+ )
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py
new file mode 100755
index 00000000..3740fefa
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py
@@ -0,0 +1,169 @@
+# 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.
+
+import bpy
+import typing
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channels
+
+
+def gather_animations(blender_object: bpy.types.Object, export_settings) -> typing.List[gltf2_io.Animation]:
+ """
+ Gather all animations which contribute to the objects property.
+
+ :param blender_object: The blender object which is animated
+ :param export_settings:
+ :return: A list of glTF2 animations
+ """
+ animations = []
+
+ # Collect all 'actions' affecting this object. There is a direct mapping between blender actions and glTF animations
+ blender_actions = __get_blender_actions(blender_object)
+
+ # Export all collected actions.
+ for blender_action in blender_actions:
+ animation = __gather_animation(blender_action, blender_object, export_settings)
+ if animation is not None:
+ animations.append(animation)
+
+ return animations
+
+
+def __gather_animation(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Optional[gltf2_io.Animation]:
+ if not __filter_animation(blender_action, blender_object, export_settings):
+ return None
+
+ animation = gltf2_io.Animation(
+ channels=__gather_channels(blender_action, blender_object, export_settings),
+ extensions=__gather_extensions(blender_action, blender_object, export_settings),
+ extras=__gather_extras(blender_action, blender_object, export_settings),
+ name=__gather_name(blender_action, blender_object, export_settings),
+ samplers=__gather_samplers(blender_action, blender_object, export_settings)
+ )
+
+ # To allow reuse of samplers in one animation,
+ __link_samplers(animation, export_settings)
+
+ if not animation.channels:
+ return None
+
+ return animation
+
+
+def __filter_animation(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> bool:
+ if blender_action.users == 0:
+ return False
+
+ return True
+
+
+def __gather_channels(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.List[gltf2_io.AnimationChannel]:
+ return gltf2_blender_gather_animation_channels.gather_animation_channels(
+ blender_action, blender_object, export_settings)
+
+
+def __gather_extensions(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_extras(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_name(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Optional[str]:
+ return blender_action.name
+
+
+def __gather_samplers(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.List[gltf2_io.AnimationSampler]:
+ # We need to gather the samplers after gathering all channels --> populate this list in __link_samplers
+ return []
+
+
+def __link_samplers(animation: gltf2_io.Animation, export_settings):
+ """
+ Move animation samplers to their own list and store their indices at their previous locations.
+
+ After gathering, samplers are stored in the channels properties of the animation and need to be moved
+ to their own list while storing an index into this list at the position where they previously were.
+ This behaviour is similar to that of the glTFExporter that traverses all nodes
+ :param animation:
+ :param export_settings:
+ :return:
+ """
+ # TODO: move this to some util module and update gltf2 exporter also
+ T = typing.TypeVar('T')
+
+ def __append_unique_and_get_index(l: typing.List[T], item: T):
+ if item in l:
+ return l.index(item)
+ else:
+ index = len(l)
+ l.append(item)
+ return index
+
+ for i, channel in enumerate(animation.channels):
+ animation.channels[i].sampler = __append_unique_and_get_index(animation.samplers, channel.sampler)
+
+
+def __get_blender_actions(blender_object: bpy.types.Object
+ ) -> typing.List[bpy.types.Action]:
+ blender_actions = []
+
+ if blender_object.animation_data is not None:
+ # Collect active action.
+ if blender_object.animation_data.action is not None:
+ blender_actions.append(blender_object.animation_data.action)
+
+ # Collect associated strips from NLA tracks.
+ for track in blender_object.animation_data.nla_tracks:
+ # Multi-strip tracks do not export correctly yet (they need to be baked),
+ # so skip them for now and only write single-strip tracks.
+ if track.strips is None or len(track.strips) != 1:
+ continue
+ for strip in track.strips:
+ blender_actions.append(strip.action)
+
+ if blender_object.type == "MESH"\
+ and blender_object.data is not None \
+ and blender_object.data.shape_keys is not None \
+ and blender_object.data.shape_keys.animation_data is not None:
+ blender_actions.append(blender_object.data.shape_keys.animation_data.action)
+
+ # Remove duplicate actions.
+ blender_actions = list(set(blender_actions))
+
+ return blender_actions
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py
new file mode 100755
index 00000000..5b00a98b
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py
@@ -0,0 +1,60 @@
+# 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.
+
+import functools
+
+
+def cached(func):
+ """
+ Decorate the cache gather functions results.
+
+ The gather function is only executed if its result isn't in the cache yet
+ :param func: the function to be decorated. It will have a static __cache member afterwards
+ :return:
+ """
+ @functools.wraps(func)
+ def wrapper_cached(*args, **kwargs):
+ assert len(args) >= 2 and 0 <= len(kwargs) <= 1, "Wrong signature for cached function"
+ cache_key_args = args
+ # make a shallow copy of the keyword arguments so that 'export_settings' can be removed
+ cache_key_kwargs = dict(kwargs)
+ if kwargs.get("export_settings"):
+ export_settings = kwargs["export_settings"]
+ # 'export_settings' should not be cached
+ del cache_key_kwargs["export_settings"]
+ else:
+ export_settings = args[-1]
+ cache_key_args = args[:-1]
+
+ # we make a tuple from the function arguments so that they can be used as a key to the cache
+ cache_key = tuple(cache_key_args + tuple(cache_key_kwargs.values()))
+
+ # invalidate cache if export settings have changed
+ if not hasattr(func, "__export_settings") or export_settings != func.__export_settings:
+ func.__cache = {}
+ func.__export_settings = export_settings
+ # use or fill cache
+ if cache_key in func.__cache:
+ return func.__cache[cache_key]
+ else:
+ result = func(*args)
+ func.__cache[cache_key] = result
+ return result
+ return wrapper_cached
+
+
+# TODO: replace "cached" with "unique" in all cases where the caching is functional and not only for performance reasons
+call_or_fetch = cached
+unique = cached
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py
new file mode 100755
index 00000000..b09092ca
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py
@@ -0,0 +1,124 @@
+# 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.
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+
+import bpy
+import math
+
+
+@cached
+def gather_camera(blender_object, export_settings):
+ if not __filter_camera(blender_object, export_settings):
+ return None
+
+ return gltf2_io.Camera(
+ extensions=__gather_extensions(blender_object, export_settings),
+ extras=__gather_extras(blender_object, export_settings),
+ name=__gather_name(blender_object, export_settings),
+ orthographic=__gather_orthographic(blender_object, export_settings),
+ perspective=__gather_perspective(blender_object, export_settings),
+ type=__gather_type(blender_object, export_settings)
+ )
+
+
+def __filter_camera(blender_object, export_settings):
+ if blender_object.type != 'CAMERA':
+ return False
+ if not __gather_type(blender_object, export_settings):
+ return False
+
+ return True
+
+
+def __gather_extensions(blender_object, export_settings):
+ return None
+
+
+def __gather_extras(blender_object, export_settings):
+ return None
+
+
+def __gather_name(blender_object, export_settings):
+ return blender_object.data.name
+
+
+def __gather_orthographic(blender_object, export_settings):
+ if __gather_type(blender_object, export_settings) == "orthographic":
+ orthographic = gltf2_io.CameraOrthographic(
+ extensions=None,
+ extras=None,
+ xmag=None,
+ ymag=None,
+ zfar=None,
+ znear=None
+ )
+ blender_camera = blender_object.data
+
+ orthographic.xmag = blender_camera.ortho_scale
+ orthographic.ymag = blender_camera.ortho_scale
+
+ orthographic.znear = blender_camera.clip_start
+ orthographic.zfar = blender_camera.clip_end
+
+ return orthographic
+ return None
+
+
+def __gather_perspective(blender_object, export_settings):
+ if __gather_type(blender_object, export_settings) == "perspective":
+ perspective = gltf2_io.CameraPerspective(
+ aspect_ratio=None,
+ extensions=None,
+ extras=None,
+ yfov=None,
+ zfar=None,
+ znear=None
+ )
+ blender_camera = blender_object.data
+
+ width = bpy.context.scene.render.pixel_aspect_x * bpy.context.scene.render.resolution_x
+ height = bpy.context.scene.render.pixel_aspect_y * bpy.context.scene.render.resolution_y
+ perspective.aspectRatio = width / height
+
+ if width >= height:
+ if blender_camera.sensor_fit != 'VERTICAL':
+ perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio)
+ else:
+ perspective.yfov = blender_camera.angle
+ else:
+ if blender_camera.sensor_fit != 'HORIZONTAL':
+ perspective.yfov = blender_camera.angle
+ else:
+ perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio)
+
+ perspective.znear = blender_camera.clip_start
+
+ if not export_settings[gltf2_blender_export_keys.CAMERA_INFINITE]:
+ perspective.zfar = blender_camera.clip_end
+
+ return perspective
+ return None
+
+
+def __gather_type(blender_object, export_settings):
+ camera = blender_object.data
+ if camera.type == 'PERSP':
+ return "perspective"
+ elif camera.type == 'ORTHO':
+ return "orthographic"
+ return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py
new file mode 100755
index 00000000..4941ffe2
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py
@@ -0,0 +1,166 @@
+# 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.
+
+import bpy
+import typing
+import os
+import numpy as np
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.exp import gltf2_io_image_data
+
+
+def gather_image(
+ blender_shader_sockets_or_texture_slots: typing.Union[typing.Tuple[bpy.types.NodeSocket],
+ typing.Tuple[bpy.types.Texture]],
+ export_settings):
+ if not __filter_image(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+ image = gltf2_io.Image(
+ buffer_view=__gather_buffer_view(blender_shader_sockets_or_texture_slots, export_settings),
+ extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+ extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+ mime_type=__gather_mime_type(blender_shader_sockets_or_texture_slots, export_settings),
+ name=__gather_name(blender_shader_sockets_or_texture_slots, export_settings),
+ uri=__gather_uri(blender_shader_sockets_or_texture_slots, export_settings)
+ )
+ return image
+
+
+def __filter_image(sockets_or_slots, export_settings):
+ if not sockets_or_slots:
+ return False
+ return True
+
+
+def __gather_buffer_view(sockets_or_slots, export_settings):
+ if export_settings[gltf2_blender_export_keys.FORMAT] != 'ASCII':
+
+ image = __get_image_data(sockets_or_slots)
+ return gltf2_io_binary_data.BinaryData(
+ data=image.to_image_data(__gather_mime_type(sockets_or_slots, export_settings)))
+ return None
+
+
+def __gather_extensions(sockets_or_slots, export_settings):
+ return None
+
+
+def __gather_extras(sockets_or_slots, export_settings):
+ return None
+
+
+def __gather_mime_type(sockets_or_slots, export_settings):
+ return 'image/png'
+ # return 'image/jpeg'
+
+
+def __gather_name(sockets_or_slots, export_settings):
+ if __is_socket(sockets_or_slots):
+ node = __get_tex_from_socket(sockets_or_slots[0])
+ if node is not None:
+ return node.shader_node.image.name
+ elif isinstance(sockets_or_slots[0], bpy.types.MaterialTextureSlot):
+ return sockets_or_slots[0].name
+ return None
+
+
+def __gather_uri(sockets_or_slots, export_settings):
+ if export_settings[gltf2_blender_export_keys.FORMAT] == 'ASCII':
+ # as usual we just store the data in place instead of already resolving the references
+ return __get_image_data(sockets_or_slots)
+ return None
+
+
+def __is_socket(sockets_or_slots):
+ return isinstance(sockets_or_slots[0], bpy.types.NodeSocket)
+
+
+def __is_slot(sockets_or_slots):
+ return isinstance(sockets_or_slots[0], bpy.types.MaterialTextureSlot)
+
+
+def __get_image_data(sockets_or_slots):
+ # For shared ressources, such as images, we just store the portion of data that is needed in the glTF property
+ # in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary
+ # ressources.
+ def split_pixels_by_channels(image: bpy.types.Image) -> typing.Iterable[typing.Iterable[float]]:
+ pixels = np.array(image.pixels)
+ pixels = pixels.reshape((pixels.shape[0] // image.channels, image.channels))
+ channels = np.split(pixels, pixels.shape[1], axis=1)
+ return channels
+
+ if __is_socket(sockets_or_slots):
+ results = [__get_tex_from_socket(socket) for socket in sockets_or_slots]
+ image = None
+ for result, socket in zip(results, sockets_or_slots):
+ # rudimentarily try follow the node tree to find the correct image data.
+ channel = None
+ for elem in result.path:
+ if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateRGB):
+ channel = {
+ 'R': 0,
+ 'G': 1,
+ 'B': 2
+ }[elem.from_socket.name]
+
+ if channel is not None:
+ pixels = [split_pixels_by_channels(result.shader_node.image)[channel]]
+ else:
+ pixels = split_pixels_by_channels(result.shader_node.image)
+
+ file_name = os.path.splitext(result.shader_node.image.name)[0]
+
+ image_data = gltf2_io_image_data.ImageData(
+ file_name,
+ result.shader_node.image.size[0],
+ result.shader_node.image.size[1],
+ pixels)
+
+ if image is None:
+ image = image_data
+ else:
+ image.add_to_image(image_data)
+
+ return image
+ elif __is_slot(sockets_or_slots):
+ texture = __get_tex_from_slot(sockets_or_slots[0])
+ pixels = texture.image.pixels
+
+ image_data = gltf2_io_image_data.ImageData(
+ texture.name,
+ texture.image.size[0],
+ texture.image.size[1],
+ pixels)
+ return image_data
+ else:
+ # Texture slots
+ raise NotImplementedError()
+
+
+def __get_tex_from_socket(blender_shader_socket: bpy.types.NodeSocket):
+ result = gltf2_blender_search_node_tree.from_socket(
+ blender_shader_socket,
+ gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+ if not result:
+ return None
+ return result[0]
+
+
+def __get_tex_from_slot(blender_texture_slot):
+ return blender_texture_slot.texture
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py
new file mode 100755
index 00000000..38e47031
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py
@@ -0,0 +1,80 @@
+# 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.
+
+import mathutils
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.com import gltf2_io_debug
+from io_scene_gltf2.blender.exp import gltf2_blender_extract
+from io_scene_gltf2.blender.com import gltf2_blender_math
+
+
+@cached
+def gather_joint(blender_bone, export_settings):
+ """
+ Generate a glTF2 node from a blender bone, as joints in glTF2 are simply nodes.
+
+ :param blender_bone: a blender PoseBone
+ :param export_settings: the settings for this export
+ :return: a glTF2 node (acting as a joint)
+ """
+ axis_basis_change = mathutils.Matrix.Identity(4)
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ axis_basis_change = mathutils.Matrix(
+ ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))
+
+ # extract bone transform
+ if blender_bone.parent is None:
+ correction_matrix_local = gltf2_blender_math.multiply(axis_basis_change, blender_bone.bone.matrix_local)
+ else:
+ correction_matrix_local = gltf2_blender_math.multiply(
+ blender_bone.parent.bone.matrix_local.inverted(), blender_bone.bone.matrix_local)
+ matrix_basis = blender_bone.matrix_basis
+ if export_settings[gltf2_blender_export_keys.BAKE_SKINS]:
+ gltf2_io_debug.print_console("WARNING", "glTF bake skins not supported")
+ # matrix_basis = blender_object.convert_space(blender_bone, blender_bone.matrix, from_space='POSE',
+ # to_space='LOCAL')
+ trans, rot, sca = gltf2_blender_extract.decompose_transition(
+ gltf2_blender_math.multiply(correction_matrix_local, matrix_basis), 'JOINT', export_settings)
+ translation, rotation, scale = (None, None, None)
+ if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
+ translation = [trans[0], trans[1], trans[2]]
+ if rot[0] != 0.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 1.0:
+ rotation = [rot[0], rot[1], rot[2], rot[3]]
+ if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0:
+ scale = [sca[0], sca[1], sca[2]]
+
+ # traverse into children
+ children = []
+ for bone in blender_bone.children:
+ children.append(gather_joint(bone, export_settings))
+
+ # finally add to the joints array containing all the joints in the hierarchy
+ return gltf2_io.Node(
+ camera=None,
+ children=children,
+ extensions=None,
+ extras=None,
+ matrix=None,
+ mesh=None,
+ name=blender_bone.name,
+ rotation=rotation,
+ scale=scale,
+ skin=None,
+ translation=translation,
+ weights=None
+ )
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py
new file mode 100755
index 00000000..0d314681
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py
@@ -0,0 +1,113 @@
+# 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.
+
+import bpy
+import typing
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+
+
+@cached
+def gather_material_normal_texture_info_class(blender_shader_sockets_or_texture_slots: typing.Union[
+ typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]],
+ export_settings):
+ if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+ texture_info = gltf2_io.MaterialNormalTextureInfoClass(
+ extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+ extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+ scale=__gather_scale(blender_shader_sockets_or_texture_slots, export_settings),
+ index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings),
+ tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings)
+ )
+
+ return texture_info
+
+
+def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ if not blender_shader_sockets_or_texture_slots:
+ return False
+ if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]):
+ return False
+ if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+ if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]):
+ # sockets do not lead to a texture --> discard
+ return False
+ return True
+
+
+def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_scale(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_index(blender_shader_sockets_or_texture_slots, export_settings):
+ # We just put the actual shader into the 'index' member
+ return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings)
+
+
+def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings):
+ if __is_socket(blender_shader_sockets_or_texture_slots):
+ blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node
+ if len(blender_shader_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = blender_shader_node.inputs['Vector'].links[0].from_node
+
+ if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+ if len(input_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = input_node.inputs['Vector'].links[0].from_node
+
+ if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+ return 0
+
+ if input_node.uv_map == '':
+ return 0
+
+ # Try to gather map index.
+ for blender_mesh in bpy.data.meshes:
+ texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+ if texCoordIndex >= 0:
+ return texCoordIndex
+
+ return 0
+ else:
+ raise NotImplementedError()
+
+
+def __is_socket(sockets_or_slots):
+ return isinstance(sockets_or_slots[0], bpy.types.NodeSocket)
+
+
+def __get_tex_from_socket(socket):
+ result = gltf2_blender_search_node_tree.from_socket(
+ socket,
+ gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+ if not result:
+ return None
+ return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py
new file mode 100755
index 00000000..af219318
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py
@@ -0,0 +1,113 @@
+# 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.
+
+import bpy
+import typing
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+
+
+@cached
+def gather_material_occlusion_texture_info_class(blender_shader_sockets_or_texture_slots: typing.Union[
+ typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]],
+ export_settings):
+ if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+ texture_info = gltf2_io.MaterialOcclusionTextureInfoClass(
+ extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+ extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+ strength=__gather_scale(blender_shader_sockets_or_texture_slots, export_settings),
+ index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings),
+ tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings)
+ )
+
+ return texture_info
+
+
+def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ if not blender_shader_sockets_or_texture_slots:
+ return False
+ if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]):
+ return False
+ if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+ if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]):
+ # sockets do not lead to a texture --> discard
+ return False
+ return True
+
+
+def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_scale(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_index(blender_shader_sockets_or_texture_slots, export_settings):
+ # We just put the actual shader into the 'index' member
+ return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings)
+
+
+def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings):
+ if __is_socket(blender_shader_sockets_or_texture_slots):
+ blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node
+ if len(blender_shader_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = blender_shader_node.inputs['Vector'].links[0].from_node
+
+ if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+ if len(input_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = input_node.inputs['Vector'].links[0].from_node
+
+ if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+ return 0
+
+ if input_node.uv_map == '':
+ return 0
+
+ # Try to gather map index.
+ for blender_mesh in bpy.data.meshes:
+ texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+ if texCoordIndex >= 0:
+ return texCoordIndex
+
+ return 0
+ else:
+ raise NotImplementedError()
+
+
+def __is_socket(sockets_or_slots):
+ return isinstance(sockets_or_slots[0], bpy.types.NodeSocket)
+
+
+def __get_tex_from_socket(socket):
+ result = gltf2_blender_search_node_tree.from_socket(
+ socket,
+ gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+ if not result:
+ return None
+ return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py
new file mode 100755
index 00000000..d801eca7
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py
@@ -0,0 +1,138 @@
+# 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.
+
+import bpy
+
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_material_normal_texture_info_class
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_material_occlusion_texture_info_class
+
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_pbr_metallic_roughness
+from io_scene_gltf2.blender.exp import gltf2_blender_get
+
+
+@cached
+def gather_material(blender_material, export_settings):
+ """
+ Gather the material used by the blender primitive.
+
+ :param blender_material: the blender material used in the glTF primitive
+ :param export_settings:
+ :return: a glTF material
+ """
+ if not __filter_material(blender_material, export_settings):
+ return None
+
+ material = gltf2_io.Material(
+ alpha_cutoff=__gather_alpha_cutoff(blender_material, export_settings),
+ alpha_mode=__gather_alpha_mode(blender_material, export_settings),
+ double_sided=__gather_double_sided(blender_material, export_settings),
+ emissive_factor=__gather_emmissive_factor(blender_material, export_settings),
+ emissive_texture=__gather_emissive_texture(blender_material, export_settings),
+ extensions=__gather_extensions(blender_material, export_settings),
+ extras=__gather_extras(blender_material, export_settings),
+ name=__gather_name(blender_material, export_settings),
+ normal_texture=__gather_normal_texture(blender_material, export_settings),
+ occlusion_texture=__gather_occlusion_texture(blender_material, export_settings),
+ pbr_metallic_roughness=__gather_pbr_metallic_roughness(blender_material, export_settings)
+ )
+
+ return material
+ # material = blender_primitive['material']
+ #
+ # if get_material_requires_texcoords(glTF, material) and not export_settings['gltf_texcoords']:
+ # material = -1
+ #
+ # if get_material_requires_normals(glTF, material) and not export_settings['gltf_normals']:
+ # material = -1
+ #
+ # # Meshes/primitives without material are allowed.
+ # if material >= 0:
+ # primitive.material = material
+ # else:
+ # print_console('WARNING', 'Material ' + internal_primitive[
+ # 'material'] + ' not found. Please assign glTF 2.0 material or enable Blinn-Phong material in export.')
+
+
+def __filter_material(blender_material, export_settings):
+ # if not blender_material.use_nodes:
+ # return False
+ # if not blender_material.node_tree:
+ # return False
+ return True
+
+
+def __gather_alpha_cutoff(blender_material, export_settings):
+ return None
+
+
+def __gather_alpha_mode(blender_material, export_settings):
+ return None
+
+
+def __gather_double_sided(blender_material, export_settings):
+ return None
+
+
+def __gather_emmissive_factor(blender_material, export_settings):
+ emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Emissive")
+ if isinstance(emissive, bpy.types.NodeSocket):
+ return emissive.default_value
+ return None
+
+
+def __gather_emissive_texture(blender_material, export_settings):
+ emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Emissive")
+ return gltf2_blender_gather_texture_info.gather_texture_info((emissive,), export_settings)
+
+
+def __gather_extensions(blender_material, export_settings):
+ extensions = {}
+
+
+ # TODO specular glossiness extension
+
+ return extensions if extensions else None
+
+
+def __gather_extras(blender_material, export_setttings):
+ return None
+
+
+def __gather_name(blender_material, export_settings):
+
+ return None
+
+
+def __gather_normal_texture(blender_material, export_settings):
+ normal = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Normal")
+ return gltf2_blender_gather_material_normal_texture_info_class.gather_material_normal_texture_info_class(
+ (normal,),
+ export_settings)
+
+
+def __gather_occlusion_texture(blender_material, export_settings):
+ emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Occlusion")
+ return gltf2_blender_gather_material_occlusion_texture_info_class.gather_material_occlusion_texture_info_class(
+ (emissive,),
+ export_settings)
+
+
+def __gather_pbr_metallic_roughness(blender_material, export_settings):
+ return gltf2_blender_gather_materials_pbr_metallic_roughness.gather_material_pbr_metallic_roughness(
+ blender_material,
+ export_settings)
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py
new file mode 100755
index 00000000..7a567bc3
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py
@@ -0,0 +1,93 @@
+# 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.
+
+import bpy
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info
+from io_scene_gltf2.blender.exp import gltf2_blender_get
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+
+@cached
+def gather_material_pbr_metallic_roughness(blender_material, export_settings):
+ if not __filter_pbr_material(blender_material, export_settings):
+ return None
+
+ material = gltf2_io.MaterialPBRMetallicRoughness(
+ base_color_factor=__gather_base_color_factor(blender_material, export_settings),
+ base_color_texture=__gather_base_color_texture(blender_material, export_settings),
+ extensions=__gather_extensions(blender_material, export_settings),
+ extras=__gather_extras(blender_material, export_settings),
+ metallic_factor=__gather_metallic_factor(blender_material, export_settings),
+ metallic_roughness_texture=__gather_metallic_roughness_texture(blender_material, export_settings),
+ roughness_factor=__gather_roughness_factor(blender_material, export_settings)
+ )
+
+ return material
+
+
+def __filter_pbr_material(blender_material, export_settings):
+ return True
+
+
+def __gather_base_color_factor(blender_material, export_settings):
+ base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color")
+ if base_color_socket is None:
+ base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor")
+ if isinstance(base_color_socket, bpy.types.NodeSocket):
+ return list(base_color_socket.default_value)
+ return None
+
+def __gather_base_color_texture(blender_material, export_settings):
+ base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color")
+ if base_color_socket is None:
+ base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor")
+ return gltf2_blender_gather_texture_info.gather_texture_info((base_color_socket,), export_settings)
+
+
+def __gather_extensions(blender_material, export_settings):
+ return None
+
+
+def __gather_extras(blender_material, export_settings):
+ return None
+
+
+def __gather_metallic_factor(blender_material, export_settings):
+ metallic_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Metallic")
+ if isinstance(metallic_socket, bpy.types.NodeSocket):
+ return metallic_socket.default_value
+ return None
+
+
+def __gather_metallic_roughness_texture(blender_material, export_settings):
+ metallic_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Metallic")
+ roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Roughness")
+
+ if metallic_socket is None and roughness_socket is None:
+ metallic_roughness = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "MetallicRoughness")
+ texture_input = (metallic_roughness,)
+ else:
+ texture_input = (metallic_socket, roughness_socket)
+
+ return gltf2_blender_gather_texture_info.gather_texture_info(texture_input, export_settings)
+
+
+def __gather_roughness_factor(blender_material, export_settings):
+ roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Roughness")
+ if isinstance(roughness_socket, bpy.types.NodeSocket):
+ return roughness_socket.default_value
+ return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py
new file mode 100755
index 00000000..57903287
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py
@@ -0,0 +1,90 @@
+# 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.
+
+import bpy
+from typing import Optional, Dict, List, Any
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitives
+
+
+@cached
+def gather_mesh(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> Optional[gltf2_io.Mesh]:
+ if not __filter_mesh(blender_mesh, vertex_groups, modifiers, export_settings):
+ return None
+
+ mesh = gltf2_io.Mesh(
+ extensions=__gather_extensions(blender_mesh, vertex_groups, modifiers, export_settings),
+ extras=__gather_extras(blender_mesh, vertex_groups, modifiers, export_settings),
+ name=__gather_name(blender_mesh, vertex_groups, modifiers, export_settings),
+ primitives=__gather_primitives(blender_mesh, vertex_groups, modifiers, export_settings),
+ weights=__gather_weights(blender_mesh, vertex_groups, modifiers, export_settings)
+ )
+
+ return mesh
+
+
+def __filter_mesh(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> bool:
+ if blender_mesh.users == 0:
+ return False
+ return True
+
+
+def __gather_extensions(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> Any:
+ return None
+
+
+def __gather_extras(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> Optional[Dict[Any, Any]]:
+ return None
+
+
+def __gather_name(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> str:
+ return blender_mesh.name
+
+
+def __gather_primitives(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> List[gltf2_io.MeshPrimitive]:
+ return gltf2_blender_gather_primitives.gather_primitives(blender_mesh, vertex_groups, modifiers, export_settings)
+
+
+def __gather_weights(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> Optional[List[float]]:
+ return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py
new file mode 100755
index 00000000..dc7192d8
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py
@@ -0,0 +1,148 @@
+# 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.
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_cameras
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_mesh
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints
+from io_scene_gltf2.blender.exp import gltf2_blender_extract
+from io_scene_gltf2.io.com import gltf2_io
+
+
+@cached
+def gather_node(blender_object, export_settings):
+ if not __filter_node(blender_object, export_settings):
+ return None
+
+ node = gltf2_io.Node(
+ camera=__gather_camera(blender_object, export_settings),
+ children=__gather_children(blender_object, export_settings),
+ extensions=__gather_extensions(blender_object, export_settings),
+ extras=__gather_extras(blender_object, export_settings),
+ matrix=__gather_matrix(blender_object, export_settings),
+ mesh=__gather_mesh(blender_object, export_settings),
+ name=__gather_name(blender_object, export_settings),
+ rotation=None,
+ scale=None,
+ skin=__gather_skin(blender_object, export_settings),
+ translation=None,
+ weights=__gather_weights(blender_object, export_settings)
+ )
+ node.translation, node.rotation, node.scale = __gather_trans_rot_scale(blender_object, export_settings)
+
+ return node
+
+
+def __filter_node(blender_object, export_settings):
+ if blender_object.users == 0:
+ return False
+ if export_settings[gltf2_blender_export_keys.SELECTED] and not blender_object.select:
+ return False
+ if not export_settings[gltf2_blender_export_keys.LAYERS] and not blender_object.layers[0]:
+ return False
+ if blender_object.dupli_group is not None and not blender_object.dupli_group.layers[0]:
+ return False
+
+ return True
+
+
+def __gather_camera(blender_object, export_settings):
+ return gltf2_blender_gather_cameras.gather_camera(blender_object, export_settings)
+
+
+def __gather_children(blender_object, export_settings):
+ children = []
+ # standard children
+ for child_object in blender_object.children:
+ node = gather_node(child_object, export_settings)
+ if node is not None:
+ children.append(node)
+ # blender dupli objects
+ if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group:
+ for dupli_object in blender_object.dupli_group.objects:
+ node = gather_node(dupli_object, export_settings)
+ if node is not None:
+ children.append(node)
+
+ # blender bones
+ if blender_object.type == "ARMATURE":
+ for blender_bone in blender_object.pose.bones:
+ if not blender_bone.parent:
+ children.append(gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings))
+
+ return children
+
+
+def __gather_extensions(blender_object, export_settings):
+ return None
+
+
+def __gather_extras(blender_object, export_settings):
+ return None
+
+
+def __gather_matrix(blender_object, export_settings):
+ # return blender_object.matrix_local
+ return []
+
+
+def __gather_mesh(blender_object, export_settings):
+ if blender_object.type == "MESH":
+ # If not using vertex group, they are irrelevant for caching --> ensure that they do not trigger a cache miss
+ vertex_groups = blender_object.vertex_groups
+ modifiers = blender_object.modifiers
+ if len(vertex_groups) == 0:
+ vertex_groups = None
+ if len(modifiers) == 0:
+ modifiers = None
+
+ return gltf2_blender_gather_mesh.gather_mesh(blender_object.data, vertex_groups, modifiers, export_settings)
+ else:
+ return None
+
+
+def __gather_name(blender_object, export_settings):
+ if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group:
+ return "Duplication_Offset_" + blender_object.name
+ return blender_object.name
+
+
+def __gather_trans_rot_scale(blender_object, export_settings):
+ trans, rot, sca = gltf2_blender_extract.decompose_transition(blender_object.matrix_local, 'NODE', export_settings)
+ if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group:
+ trans = -gltf2_blender_extract.convert_swizzle_location(
+ blender_object.dupli_group.dupli_offset, export_settings)
+ translation, rotation, scale = (None, None, None)
+ if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
+ translation = [trans[0], trans[1], trans[2]]
+ if rot[0] != 0.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 1.0:
+ rotation = [rot[0], rot[1], rot[2], rot[3]]
+ if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0:
+ scale = [sca[0], sca[1], sca[2]]
+ return translation, rotation, scale
+
+
+def __gather_skin(blender_object, export_settings):
+ modifiers = {m.type: m for m in blender_object.modifiers}
+
+ if "ARMATURE" in modifiers:
+ # Skins and meshes must be in the same glTF node, which is different from how blender handles armatures
+ return gltf2_blender_gather_skins.gather_skin(modifiers["ARMATURE"].object, export_settings)
+
+
+def __gather_weights(blender_object, export_settings):
+ return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py
new file mode 100755
index 00000000..bd1294cd
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py
@@ -0,0 +1,217 @@
+# 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.
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.io.com import gltf2_io_debug
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.blender.exp import gltf2_blender_utils
+
+
+def gather_primitive_attributes(blender_primitive, export_settings):
+ """
+ Gathers the attributes, such as POSITION, NORMAL, TANGENT from a blender primitive.
+
+ :return: a dictionary of attributes
+ """
+ attributes = {}
+ attributes.update(__gather_position(blender_primitive, export_settings))
+ attributes.update(__gather_normal(blender_primitive, export_settings))
+ attributes.update(__gather_tangent(blender_primitive, export_settings))
+ attributes.update(__gather_texcoord(blender_primitive, export_settings))
+ attributes.update(__gather_colors(blender_primitive, export_settings))
+ attributes.update(__gather_skins(blender_primitive, export_settings))
+ return attributes
+
+
+def __gather_position(blender_primitive, export_settings):
+ position = blender_primitive["attributes"]["POSITION"]
+ componentType = gltf2_io_constants.ComponentType.Float
+ return {
+ "POSITION": gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(position, componentType),
+ byte_offset=None,
+ component_type=componentType,
+ count=len(position) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec3),
+ extensions=None,
+ extras=None,
+ max=gltf2_blender_utils.max_components(position, gltf2_io_constants.DataType.Vec3),
+ min=gltf2_blender_utils.min_components(position, gltf2_io_constants.DataType.Vec3),
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec3
+ )
+ }
+
+
+def __gather_normal(blender_primitive, export_settings):
+ if export_settings[gltf2_blender_export_keys.NORMALS]:
+ normal = blender_primitive["attributes"]['NORMAL']
+ return {
+ "NORMAL": gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(normal, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(normal) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec3),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec3
+ )
+ }
+ return {}
+
+
+def __gather_tangent(blender_primitive, export_settings):
+ if export_settings[gltf2_blender_export_keys.TANGENTS]:
+ if blender_primitive["attributes"].get('TANGENT') is not None:
+ tangent = blender_primitive["attributes"]['TANGENT']
+ return {
+ "TANGENT": gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+ tangent, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(tangent) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec4
+ )
+ }
+
+ return {}
+
+
+def __gather_texcoord(blender_primitive, export_settings):
+ attributes = {}
+ if export_settings[gltf2_blender_export_keys.TEX_COORDS]:
+ tex_coord_index = 0
+ tex_coord_id = 'TEXCOORD_' + str(tex_coord_index)
+ while blender_primitive["attributes"].get(tex_coord_id) is not None:
+ tex_coord = blender_primitive["attributes"][tex_coord_id]
+ attributes[tex_coord_id] = gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+ tex_coord, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(tex_coord) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec2),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec2
+ )
+ tex_coord_index += 1
+ tex_coord_id = 'TEXCOORD_' + str(tex_coord_index)
+ return attributes
+
+
+def __gather_colors(blender_primitive, export_settings):
+ attributes = {}
+ if export_settings[gltf2_blender_export_keys.COLORS]:
+ color_index = 0
+ color_id = 'COLOR_' + str(color_index)
+ while blender_primitive["attributes"].get(color_id) is not None:
+ internal_color = blender_primitive["attributes"][color_id]
+ attributes[color_id] = gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+ internal_color, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(internal_color) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec4
+ )
+ color_index += 1
+ color_id = 'COLOR_' + str(color_index)
+ return attributes
+
+
+def __gather_skins(blender_primitive, export_settings):
+ attributes = {}
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ bone_index = 0
+ joint_id = 'JOINTS_' + str(bone_index)
+ weight_id = 'WEIGHTS_' + str(bone_index)
+ while blender_primitive["attributes"].get(joint_id) and blender_primitive["attributes"].get(weight_id):
+ if bone_index >= 4:
+ gltf2_io_debug.print_console("WARNING", "There are more than 4 joint vertex influences."
+ "Consider to apply blenders Limit Total function.")
+ # TODO: add option to stop after 4
+ # break
+
+ # joints
+ internal_joint = blender_primitive["attributes"][joint_id]
+ joint = gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+ internal_joint, gltf2_io_constants.ComponentType.UnsignedShort),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.UnsignedShort,
+ count=len(internal_joint) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec4
+ )
+ attributes[joint_id] = joint
+
+ # weights
+ internal_weight = blender_primitive["attributes"][weight_id]
+ weight = gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+ internal_weight, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(internal_weight) // gltf2_io_constants.DataType.num_elements(
+ gltf2_io_constants.DataType.Vec4),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec4
+ )
+ attributes[weight_id] = weight
+
+ bone_index += 1
+ joint_id = 'JOINTS_' + str(bone_index)
+ weight_id = 'WEIGHTS_' + str(bone_index)
+ return attributes
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py
new file mode 100755
index 00000000..5b3e607b
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py
@@ -0,0 +1,200 @@
+# 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.
+
+import bpy
+from typing import List, Optional
+
+from .gltf2_blender_export_keys import INDICES, FORCE_INDICES, NORMALS, MORPH_NORMAL, TANGENTS, MORPH_TANGENT, MORPH
+
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_extract
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes
+from io_scene_gltf2.blender.exp import gltf2_blender_utils
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.io.com.gltf2_io_debug import print_console
+
+
+@cached
+def gather_primitives(
+ blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> List[gltf2_io.MeshPrimitive]:
+ """
+ Extract the mesh primitives from a blender object
+
+ :return: a list of glTF2 primitives
+ """
+ primitives = []
+ blender_primitives = gltf2_blender_extract.extract_primitives(
+ None, blender_mesh, vertex_groups, modifiers, export_settings)
+
+ for internal_primitive in blender_primitives:
+
+ primitive = gltf2_io.MeshPrimitive(
+ attributes=__gather_attributes(internal_primitive, blender_mesh, modifiers, export_settings),
+ extensions=None,
+ extras=None,
+ indices=__gather_indices(internal_primitive, blender_mesh, modifiers, export_settings),
+ material=__gather_materials(internal_primitive, blender_mesh, modifiers, export_settings),
+ mode=None,
+ targets=__gather_targets(internal_primitive, blender_mesh, modifiers, export_settings)
+ )
+ primitives.append(primitive)
+
+ return primitives
+
+
+def __gather_materials(blender_primitive, blender_mesh, modifiers, export_settings):
+ if not blender_primitive['material']:
+ # TODO: fix 'extract_promitives' so that the value of 'material' is None and not empty string
+ return None
+ material = bpy.data.materials[blender_primitive['material']]
+ return gltf2_blender_gather_materials.gather_material(material, export_settings)
+
+
+def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings):
+ indices = blender_primitive['indices']
+
+ max_index = max(indices)
+ if max_index < (1 << 8):
+ component_type = gltf2_io_constants.ComponentType.UnsignedByte
+ elif max_index < (1 << 16):
+ component_type = gltf2_io_constants.ComponentType.UnsignedShort
+ elif max_index < (1 << 32):
+ component_type = gltf2_io_constants.ComponentType.UnsignedInt
+ else:
+ print_console('ERROR', 'Invalid max_index: ' + str(max_index))
+ return None
+
+ if export_settings[FORCE_INDICES]:
+ component_type = gltf2_io_constants.ComponentType.from_legacy_define(export_settings[INDICES])
+
+ element_type = gltf2_io_constants.DataType.Scalar
+ binary_data = gltf2_io_binary_data.BinaryData.from_list(indices, component_type)
+ return gltf2_io.Accessor(
+ buffer_view=binary_data,
+ byte_offset=None,
+ component_type=component_type,
+ count=len(indices) // gltf2_io_constants.DataType.num_elements(element_type),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=element_type
+ )
+
+
+def __gather_attributes(blender_primitive, blender_mesh, modifiers, export_settings):
+ return gltf2_blender_gather_primitive_attributes.gather_primitive_attributes(blender_primitive, export_settings)
+
+
+def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings):
+ if export_settings[MORPH]:
+ targets = []
+ if blender_mesh.shape_keys is not None:
+ morph_index = 0
+ for blender_shape_key in blender_mesh.shape_keys.key_blocks:
+ if blender_shape_key != blender_shape_key.relative_key:
+
+ target_position_id = 'MORPH_POSITION_' + str(morph_index)
+ target_normal_id = 'MORPH_NORMAL_' + str(morph_index)
+ target_tangent_id = 'MORPH_TANGENT_' + str(morph_index)
+
+ if blender_primitive["attributes"].get(target_position_id):
+ target = {}
+ internal_target_position = blender_primitive["attributes"][target_position_id]
+ binary_data = gltf2_io_binary_data.BinaryData.from_list(
+ internal_target_position,
+ gltf2_io_constants.ComponentType.Float
+ )
+ target["POSITION"] = gltf2_io.Accessor(
+ buffer_view=binary_data,
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(internal_target_position) // gltf2_io_constants.DataType.num_elements(
+ gltf2_io_constants.DataType.Vec3),
+ extensions=None,
+ extras=None,
+ max=gltf2_blender_utils.max_components(
+ internal_target_position, gltf2_io_constants.DataType.Vec3),
+ min=gltf2_blender_utils.min_components(
+ internal_target_position, gltf2_io_constants.DataType.Vec3),
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec3
+ )
+
+ if export_settings[NORMALS] \
+ and export_settings[MORPH_NORMAL] \
+ and blender_primitive["attributes"].get(target_normal_id):
+
+ internal_target_normal = blender_primitive["attributes"][target_normal_id]
+ binary_data = gltf2_io_binary_data.BinaryData.from_list(
+ internal_target_normal,
+ gltf2_io_constants.ComponentType.Float,
+ )
+ target['NORMAL'] = gltf2_io.Accessor(
+ buffer_view=binary_data,
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(internal_target_normal) // gltf2_io_constants.DataType.num_elements(
+ gltf2_io_constants.DataType.Vec3),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec3
+ )
+
+ if export_settings[TANGENTS] \
+ and export_settings[MORPH_TANGENT] \
+ and blender_primitive["attributes"].get(target_tangent_id):
+ internal_target_tangent = blender_primitive["attributes"][target_tangent_id]
+ binary_data = gltf2_io_binary_data.BinaryData.from_list(
+ internal_target_tangent,
+ gltf2_io_constants.ComponentType.Float,
+ )
+ target['TANGENT'] = gltf2_io.Accessor(
+ buffer_view=binary_data,
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(internal_target_tangent) // gltf2_io_constants.DataType.num_elements(
+ gltf2_io_constants.DataType.Vec3),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec3
+ )
+ targets.append(target)
+ morph_index += 1
+ return targets
+ return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py
new file mode 100755
index 00000000..840c98f4
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py
@@ -0,0 +1,98 @@
+# 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.
+
+import bpy
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+
+@cached
+def gather_sampler(blender_shader_node: bpy.types.Node, export_settings):
+ if not __filter_sampler(blender_shader_node, export_settings):
+ return None
+
+ return gltf2_io.Sampler(
+ extensions=__gather_extensions(blender_shader_node, export_settings),
+ extras=__gather_extras(blender_shader_node, export_settings),
+ mag_filter=__gather_mag_filter(blender_shader_node, export_settings),
+ min_filter=__gather_min_filter(blender_shader_node, export_settings),
+ name=__gather_name(blender_shader_node, export_settings),
+ wrap_s=__gather_wrap_s(blender_shader_node, export_settings),
+ wrap_t=__gather_wrap_t(blender_shader_node, export_settings)
+ )
+
+
+def __filter_sampler(blender_shader_node, export_settings):
+ if not blender_shader_node.interpolation == 'Closest' and not blender_shader_node.extension == 'CLIP':
+ return False
+ return True
+
+
+def __gather_extensions(blender_shader_node, export_settings):
+ return None
+
+
+def __gather_extras(blender_shader_node, export_settings):
+ return None
+
+
+def __gather_mag_filter(blender_shader_node, export_settings):
+ if blender_shader_node.interpolation == 'Closest':
+ return 9728 # NEAREST
+ return 9729 # LINEAR
+
+
+def __gather_min_filter(blender_shader_node, export_settings):
+ if blender_shader_node.interpolation == 'Closest':
+ return 9984 # NEAREST_MIPMAP_NEAREST
+ return 9986 # NEAREST_MIPMAP_LINEAR
+
+
+def __gather_name(blender_shader_node, export_settings):
+ return None
+
+
+def __gather_wrap_s(blender_shader_node, export_settings):
+ if blender_shader_node.extension == 'CLIP':
+ return 33071
+ return None
+
+
+def __gather_wrap_t(blender_shader_node, export_settings):
+ if blender_shader_node.extension == 'CLIP':
+ return 33071
+ return None
+
+
+@cached
+def gather_sampler_from_texture_slot(blender_texture: bpy.types.TextureSlot, export_settings):
+ magFilter = 9729
+ wrap = 10497
+ if blender_texture.texture.extension == 'CLIP':
+ wrap = 33071
+
+ minFilter = 9986
+ if magFilter == 9728:
+ minFilter = 9984
+
+ return gltf2_io.Sampler(
+ extensions=None,
+ extras=None,
+ mag_filter=magFilter,
+ min_filter=minFilter,
+ name=None,
+ wrap_s=wrap,
+ wrap_t=wrap
+ )
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py
new file mode 100755
index 00000000..84703414
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py
@@ -0,0 +1,150 @@
+# 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.
+
+import mathutils
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints
+from io_scene_gltf2.blender.com import gltf2_blender_math
+
+
+@cached
+def gather_skin(blender_object, export_settings):
+ """
+ Gather armatures, bones etc into a glTF2 skin object.
+
+ :param blender_object: the object which may contain a skin
+ :param export_settings:
+ :return: a glTF2 skin object
+ """
+ if not __filter_skin(blender_object, export_settings):
+ return None
+
+ return gltf2_io.Skin(
+ extensions=__gather_extensions(blender_object, export_settings),
+ extras=__gather_extras(blender_object, export_settings),
+ inverse_bind_matrices=__gather_inverse_bind_matrices(blender_object, export_settings),
+ joints=__gather_joints(blender_object, export_settings),
+ name=__gather_name(blender_object, export_settings),
+ skeleton=__gather_skeleton(blender_object, export_settings)
+ )
+
+
+def __filter_skin(blender_object, export_settings):
+ if not export_settings[gltf2_blender_export_keys.SKINS]:
+ return False
+ if blender_object.type != 'ARMATURE' or len(blender_object.pose.bones) == 0:
+ return False
+
+ return True
+
+
+def __gather_extensions(blender_object, export_settings):
+ return None
+
+
+def __gather_extras(blender_object, export_settings):
+ return None
+
+
+def __gather_inverse_bind_matrices(blender_object, export_settings):
+ inverse_matrices = []
+
+ axis_basis_change = mathutils.Matrix.Identity(4)
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ axis_basis_change = mathutils.Matrix(
+ ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))
+
+ # # artificial torso, as needed by glTF
+ # inverse_bind_matrix = blender_object.matrix_world.inverted() * axis_basis_change.inverted()
+ # for column in range(0, 4):
+ # for row in range(0, 4):
+ # inverse_matrices.append(inverse_bind_matrix[row][column])
+
+ #
+ for blender_bone in blender_object.pose.bones:
+ inverse_bind_matrix = gltf2_blender_math.multiply(axis_basis_change, blender_bone.bone.matrix_local)
+ bind_shape_matrix = gltf2_blender_math.multiply(gltf2_blender_math.multiply(
+ axis_basis_change, blender_object.matrix_world.inverted()), axis_basis_change.inverted())
+
+ inverse_bind_matrix = gltf2_blender_math.multiply(inverse_bind_matrix.inverted(), bind_shape_matrix)
+ for column in range(0, 4):
+ for row in range(0, 4):
+ inverse_matrices.append(inverse_bind_matrix[row][column])
+
+ binary_data = gltf2_io_binary_data.BinaryData.from_list(inverse_matrices, gltf2_io_constants.ComponentType.Float)
+ return gltf2_io.Accessor(
+ buffer_view=binary_data,
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(inverse_matrices) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Mat4),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Mat4
+ )
+
+
+def __gather_joints(blender_object, export_settings):
+ # # the skeletal hierarchy groups below a 'root' joint
+ # # TODO: add transform?
+ # torso = gltf2_io.Node(
+ # camera=None,
+ # children=[],
+ # extensions={},
+ # extras=None,
+ # matrix=[],
+ # mesh=None,
+ # name="Skeleton_" + blender_object.name,
+ # rotation=None,
+ # scale=None,
+ # skin=None,
+ # translation=None,
+ # weights=None
+ # )
+
+ root_joints = []
+ # build the hierarchy of nodes out of the bones
+ for blender_bone in blender_object.pose.bones:
+ if not blender_bone.parent:
+ root_joints.append(gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings))
+
+ # joints is a flat list containing all nodes belonging to the skin
+ joints = []
+
+ def __collect_joints(node):
+ joints.append(node)
+ for child in node.children:
+ __collect_joints(child)
+ for joint in root_joints:
+ __collect_joints(joint)
+
+ return joints
+
+
+def __gather_name(blender_object, export_settings):
+ return blender_object.name
+
+
+def __gather_skeleton(blender_object, export_settings):
+ # In the future support the result of https://github.com/KhronosGroup/glTF/pull/1195
+ return None # gltf2_blender_gather_nodes.gather_node(blender_object, export_settings)
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py
new file mode 100755
index 00000000..93db33f9
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py
@@ -0,0 +1,100 @@
+# 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.
+
+import typing
+import bpy
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_sampler
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_image
+from io_scene_gltf2.io.com import gltf2_io_debug
+
+
+@cached
+def gather_texture(
+ blender_shader_sockets_or_texture_slots: typing.Union[
+ typing.Tuple[bpy.types.NodeSocket], typing.Tuple[typing.Any]],
+ export_settings):
+ """
+ Gather texture sampling information and image channels from a blender shader textu re attached to a shader socket.
+
+ :param blender_shader_sockets: The sockets of the material which should contribute to the texture
+ :param export_settings: configuration of the export
+ :return: a glTF 2.0 texture with sampler and source embedded (will be converted to references by the exporter)
+ """
+ # TODO: extend to texture slots
+ if not __filter_texture(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+ return gltf2_io.Texture(
+ extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+ extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+ name=__gather_name(blender_shader_sockets_or_texture_slots, export_settings),
+ sampler=__gather_sampler(blender_shader_sockets_or_texture_slots, export_settings),
+ source=__gather_source(blender_shader_sockets_or_texture_slots, export_settings)
+ )
+
+
+def __filter_texture(blender_shader_sockets_or_texture_slots, export_settings):
+ return True
+
+
+def __gather_extensions(blender_shader_sockets, export_settings):
+ return None
+
+
+def __gather_extras(blender_shader_sockets, export_settings):
+ return None
+
+
+def __gather_name(blender_shader_sockets, export_settings):
+ return None
+
+
+def __gather_sampler(blender_shader_sockets_or_texture_slots, export_settings):
+ if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+ shader_nodes = [__get_tex_from_socket(socket).shader_node for socket in blender_shader_sockets_or_texture_slots]
+ if len(shader_nodes) > 1:
+ gltf2_io_debug.print_console("WARNING",
+ "More than one shader node tex image used for a texture. "
+ "The resulting glTF sampler will behave like the first shader node tex image.")
+ return gltf2_blender_gather_sampler.gather_sampler(
+ shader_nodes[0],
+ export_settings)
+ elif isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.MaterialTextureSlot):
+ return gltf2_blender_gather_sampler.gather_sampler_from_texture_slot(
+ blender_shader_sockets_or_texture_slots[0],
+ export_settings
+ )
+ else:
+ # TODO: implement texture slot sampler
+ raise NotImplementedError()
+
+
+def __gather_source(blender_shader_sockets_or_texture_slots, export_settings):
+ return gltf2_blender_gather_image.gather_image(blender_shader_sockets_or_texture_slots, export_settings)
+
+# Helpers
+
+
+def __get_tex_from_socket(socket):
+ result = gltf2_blender_search_node_tree.from_socket(
+ socket,
+ gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+ if not result:
+ return None
+ return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py
new file mode 100755
index 00000000..149a2a84
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py
@@ -0,0 +1,107 @@
+# 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.
+
+import bpy
+import typing
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+
+
+@cached
+def gather_texture_info(blender_shader_sockets_or_texture_slots: typing.Union[
+ typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]],
+ export_settings):
+ if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+ texture_info = gltf2_io.TextureInfo(
+ extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+ extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+ index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings),
+ tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings)
+ )
+
+ return texture_info
+
+
+def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ if not blender_shader_sockets_or_texture_slots:
+ return False
+ if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]):
+ return False
+ if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+ if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]):
+ # sockets do not lead to a texture --> discard
+ return False
+ return True
+
+
+def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_index(blender_shader_sockets_or_texture_slots, export_settings):
+ # We just put the actual shader into the 'index' member
+ return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings)
+
+
+def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings):
+ if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+ blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node
+ if len(blender_shader_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = blender_shader_node.inputs['Vector'].links[0].from_node
+
+ if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+ if len(input_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = input_node.inputs['Vector'].links[0].from_node
+
+ if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+ return 0
+
+ if input_node.uv_map == '':
+ return 0
+
+ # Try to gather map index.
+ for blender_mesh in bpy.data.meshes:
+ texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+ if texCoordIndex >= 0:
+ return texCoordIndex
+
+ return 0
+ elif isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.MaterialTextureSlot):
+ # TODO: implement for texture slots
+ return 0
+ else:
+ raise NotImplementedError()
+
+
+def __get_tex_from_socket(socket):
+ result = gltf2_blender_search_node_tree.from_socket(
+ socket,
+ gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+ if not result:
+ return None
+ return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py b/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py
new file mode 100755
index 00000000..c26c494a
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py
@@ -0,0 +1,64 @@
+# 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.
+
+
+import bpy
+from io_scene_gltf2.blender.com import gltf2_blender_json
+
+
+def generate_extras(blender_element):
+ """Filter and create a custom property, which is stored in the glTF extra field."""
+ if not blender_element:
+ return None
+
+ extras = {}
+
+ # Custom properties, which are in most cases present and should not be exported.
+ black_list = ['cycles', 'cycles_visibility', 'cycles_curves', '_RNA_UI']
+
+ count = 0
+ for custom_property in blender_element.keys():
+ if custom_property in black_list:
+ continue
+
+ value = blender_element[custom_property]
+
+ add_value = False
+
+ if isinstance(value, bpy.types.ID):
+ add_value = True
+
+ if isinstance(value, str):
+ add_value = True
+
+ if isinstance(value, (int, float)):
+ add_value = True
+
+ if hasattr(value, "to_list"):
+ value = value.to_list()
+ add_value = True
+
+ if hasattr(value, "to_dict"):
+ value = value.to_dict()
+ add_value = gltf2_blender_json.is_json_convertible(value)
+
+ if add_value:
+ extras[custom_property] = value
+ count += 1
+
+ if count == 0:
+ return None
+
+ return extras
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_get.py b/io_scene_gltf2/blender/exp/gltf2_blender_get.py
new file mode 100755
index 00000000..27135224
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_get.py
@@ -0,0 +1,376 @@
+# 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.
+
+#
+# Imports
+#
+
+import bpy
+
+from . import gltf2_blender_export_keys
+from ...io.exp import gltf2_io_get
+from io_scene_gltf2.io.com import gltf2_io_debug
+#
+# Globals
+#
+
+#
+# Functions
+#
+
+
+def get_animation_target(action_group: bpy.types.ActionGroup):
+ return action_group.channels[0].data_path.split('.')[-1]
+
+
+def get_socket_or_texture_slot(blender_material: bpy.types.Material, name: str):
+ """
+ For a given material input name, retrieve the corresponding node tree socket or blender render texture slot.
+
+ :param blender_material: a blender material for which to get the socket/slot
+ :param name: the name of the socket/slot
+ :return: either a blender NodeSocket, if the material is a node tree or a blender Texture otherwise
+ """
+ if blender_material.node_tree and blender_material.use_nodes:
+ if name == "Emissive":
+ # Emissive is a special case as the input node in the 'Emission' shader node is named 'Color' and only the
+ # output is named 'Emission'
+ links = [link for link in blender_material.node_tree.links if link.from_socket.name == 'Emission']
+ if not links:
+ return None
+ return links[0].to_socket
+ i = [input for input in blender_material.node_tree.inputs]
+ o = [output for output in blender_material.node_tree.outputs]
+ nodes = [node for node in blender_material.node_tree.nodes]
+ nodes = filter(lambda n: isinstance(n, bpy.types.ShaderNodeBsdfPrincipled), nodes)
+ inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], [])
+ if not inputs:
+ return None
+ return inputs[0]
+
+
+
+ return None
+
+
+def find_shader_image_from_shader_socket(shader_socket, max_hops=10):
+ """Find any ShaderNodeTexImage in the path from the socket."""
+ if shader_socket is None:
+ return None
+
+ if max_hops <= 0:
+ return None
+
+ for link in shader_socket.links:
+ if isinstance(link.from_node, bpy.types.ShaderNodeTexImage):
+ return link.from_node
+
+ for socket in link.from_node.inputs.values():
+ image = find_shader_image_from_shader_socket(shader_socket=socket, max_hops=max_hops - 1)
+ if image is not None:
+ return image
+
+ return None
+
+
+def get_shader_add_to_shader_node(shader_node):
+
+ if shader_node is None:
+ return None
+
+ if len(shader_node.outputs['BSDF'].links) == 0:
+ return None
+
+ to_node = shader_node.outputs['BSDF'].links[0].to_node
+
+ if not isinstance(to_node, bpy.types.ShaderNodeAddShader):
+ return None
+
+ return to_node
+
+#
+
+
+def get_shader_emission_from_shader_add(shader_add):
+
+ if shader_add is None:
+ return None
+
+ if not isinstance(shader_add, bpy.types.ShaderNodeAddShader):
+ return None
+
+ from_node = None
+
+ for input in shader_add.inputs:
+
+ if len(input.links) == 0:
+ continue
+
+ from_node = input.links[0].from_node
+
+ if isinstance(from_node, bpy.types.ShaderNodeEmission):
+ break
+
+ return from_node
+
+
+def get_shader_mapping_from_shader_image(shader_image):
+
+ if shader_image is None:
+ return None
+
+ if not isinstance(shader_image, bpy.types.ShaderNodeTexImage):
+ return None
+
+ if shader_image.inputs.get('Vector') is None:
+ return None
+
+ if len(shader_image.inputs['Vector'].links) == 0:
+ return None
+
+ from_node = shader_image.inputs['Vector'].links[0].from_node
+
+ #
+
+ if not isinstance(from_node, bpy.types.ShaderNodeMapping):
+ return None
+
+ return from_node
+
+
+def get_image_material_usage_to_socket(shader_image, socket_name):
+ if shader_image is None:
+ return -1
+
+ if not isinstance(shader_image, bpy.types.ShaderNodeTexImage):
+ return -2
+
+ if shader_image.outputs.get('Color') is None:
+ return -3
+
+ if len(shader_image.outputs.get('Color').links) == 0:
+ return -4
+
+ for img_link in shader_image.outputs.get('Color').links:
+ separate_rgb = img_link.to_node
+
+ if not isinstance(separate_rgb, bpy.types.ShaderNodeSeparateRGB):
+ continue
+
+ for i, channel in enumerate("RGB"):
+ if separate_rgb.outputs.get(channel) is None:
+ continue
+ for link in separate_rgb.outputs.get(channel).links:
+ if socket_name == link.to_socket.name:
+ return i
+
+ return -6
+
+
+def get_emission_node_from_lamp_output_node(lamp_node):
+ if lamp_node is None:
+ return None
+
+ if not isinstance(lamp_node, bpy.types.ShaderNodeOutputLamp):
+ return None
+
+ if lamp_node.inputs.get('Surface') is None:
+ return None
+
+ if len(lamp_node.inputs.get('Surface').links) == 0:
+ return None
+
+ from_node = lamp_node.inputs.get('Surface').links[0].from_node
+ if isinstance(from_node, bpy.types.ShaderNodeEmission):
+ return from_node
+
+ return None
+
+
+def get_ligth_falloff_node_from_emission_node(emission_node, type):
+ if emission_node is None:
+ return None
+
+ if not isinstance(emission_node, bpy.types.ShaderNodeEmission):
+ return None
+
+ if emission_node.inputs.get('Strength') is None:
+ return None
+
+ if len(emission_node.inputs.get('Strength').links) == 0:
+ return None
+
+ from_node = emission_node.inputs.get('Strength').links[0].from_node
+ if not isinstance(from_node, bpy.types.ShaderNodeLightFalloff):
+ return None
+
+ if from_node.outputs.get(type) is None:
+ return None
+
+ if len(from_node.outputs.get(type).links) == 0:
+ return None
+
+ if emission_node != from_node.outputs.get(type).links[0].to_node:
+ return None
+
+ return from_node
+
+
+def get_shader_image_from_shader_node(name, shader_node):
+
+ if shader_node is None:
+ return None
+
+ if not isinstance(shader_node, bpy.types.ShaderNodeGroup) and \
+ not isinstance(shader_node, bpy.types.ShaderNodeBsdfPrincipled) and \
+ not isinstance(shader_node, bpy.types.ShaderNodeEmission):
+ return None
+
+ if shader_node.inputs.get(name) is None:
+ return None
+
+ if len(shader_node.inputs[name].links) == 0:
+ return None
+
+ from_node = shader_node.inputs[name].links[0].from_node
+
+ #
+
+ if isinstance(from_node, bpy.types.ShaderNodeNormalMap):
+
+ name = 'Color'
+
+ if len(from_node.inputs[name].links) == 0:
+ return None
+
+ from_node = from_node.inputs[name].links[0].from_node
+
+ #
+
+ if not isinstance(from_node, bpy.types.ShaderNodeTexImage):
+ return None
+
+ return from_node
+
+
+def get_texture_index_from_shader_node(export_settings, glTF, name, shader_node):
+ """Return the texture index in the glTF array."""
+ from_node = get_shader_image_from_shader_node(name, shader_node)
+
+ if from_node is None:
+ return -1
+
+ #
+
+ if from_node.image is None or from_node.image.size[0] == 0 or from_node.image.size[1] == 0:
+ return -1
+
+ return gltf2_io_get.get_texture_index(glTF, from_node.image.name)
+
+
+def get_texture_index_from_export_settings(export_settings, name):
+ """Return the texture index in the glTF array."""
+
+
+def get_texcoord_index_from_shader_node(glTF, name, shader_node):
+ """Return the texture coordinate index, if assigned and used."""
+ from_node = get_shader_image_from_shader_node(name, shader_node)
+
+ if from_node is None:
+ return 0
+
+ #
+
+ if len(from_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = from_node.inputs['Vector'].links[0].from_node
+
+ #
+
+ if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+ if len(input_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = input_node.inputs['Vector'].links[0].from_node
+
+ #
+
+ if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+ return 0
+
+ if input_node.uv_map == '':
+ return 0
+
+ #
+
+ # Try to gather map index.
+ for blender_mesh in bpy.data.meshes:
+ texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+ if texCoordIndex >= 0:
+ return texCoordIndex
+
+ return 0
+
+
+def get_image_uri(export_settings, blender_image):
+ """Return the final URI depending on a file path."""
+ file_format = get_image_format(export_settings, blender_image)
+ extension = '.jpg' if file_format == 'JPEG' else '.png'
+
+ return gltf2_io_get.get_image_name(blender_image.name) + extension
+
+
+def get_image_format(export_settings, blender_image):
+ """
+ Return the final output format of the given image.
+
+ Only PNG and JPEG are supported as outputs - all other formats must be converted.
+ """
+ if blender_image.file_format in ['PNG', 'JPEG']:
+ return blender_image.file_format
+
+ use_alpha = export_settings[gltf2_blender_export_keys.FILTERED_IMAGES_USE_ALPHA].get(blender_image.name)
+
+ return 'PNG' if use_alpha else 'JPEG'
+
+
+def get_node(data_path):
+ """Return Blender node on a given Blender data path."""
+ if data_path is None:
+ return None
+
+ index = data_path.find("[\"")
+ if (index == -1):
+ return None
+
+ node_name = data_path[(index + 2):]
+
+ index = node_name.find("\"")
+ if (index == -1):
+ return None
+
+ return node_name[:(index)]
+
+
+def get_data_path(data_path):
+ """Return Blender data path."""
+ index = data_path.rfind('.')
+
+ if index == -1:
+ return data_path
+
+ return data_path[(index + 1):]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py
new file mode 100755
index 00000000..14b62c23
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py
@@ -0,0 +1,263 @@
+# 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.
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.exp import gltf2_io_image_data
+from io_scene_gltf2.io.exp import gltf2_io_buffer
+
+
+class GlTF2Exporter:
+ """
+ The glTF exporter flattens a scene graph to a glTF serializable format.
+
+ Any child properties are replaced with references where necessary
+ """
+
+ def __init__(self, copyright=None):
+ self.__finalized = False
+
+ asset = gltf2_io.Asset(
+ copyright=copyright,
+ extensions=None,
+ extras=None,
+ generator='Khronos Blender glTF 2.0 I/O',
+ min_version=None,
+ version='2.0')
+
+ self.__gltf = gltf2_io.Gltf(
+ accessors=[],
+ animations=[],
+ asset=asset,
+ buffers=[],
+ buffer_views=[],
+ cameras=[],
+ extensions={},
+ extensions_required=[],
+ extensions_used=[],
+ extras=None,
+ images=[],
+ materials=[],
+ meshes=[],
+ nodes=[],
+ samplers=[],
+ scene=-1,
+ scenes=[],
+ skins=[],
+ textures=[]
+ )
+
+ self.__buffer = gltf2_io_buffer.Buffer()
+ self.__images = []
+
+ # mapping of all glTFChildOfRootProperty types to their corresponding root level arrays
+ self.__childOfRootPropertyTypeLookup = {
+ gltf2_io.Accessor: self.__gltf.accessors,
+ gltf2_io.Animation: self.__gltf.animations,
+ gltf2_io.Buffer: self.__gltf.buffers,
+ gltf2_io.BufferView: self.__gltf.buffer_views,
+ gltf2_io.Camera: self.__gltf.cameras,
+ gltf2_io.Image: self.__gltf.images,
+ gltf2_io.Material: self.__gltf.materials,
+ gltf2_io.Mesh: self.__gltf.meshes,
+ gltf2_io.Node: self.__gltf.nodes,
+ gltf2_io.Sampler: self.__gltf.samplers,
+ gltf2_io.Scene: self.__gltf.scenes,
+ gltf2_io.Skin: self.__gltf.skins,
+ gltf2_io.Texture: self.__gltf.textures
+ }
+
+ self.__propertyTypeLookup = [
+ gltf2_io.AccessorSparseIndices,
+ gltf2_io.AccessorSparse,
+ gltf2_io.AccessorSparseValues,
+ gltf2_io.AnimationChannel,
+ gltf2_io.AnimationChannelTarget,
+ gltf2_io.AnimationSampler,
+ gltf2_io.Asset,
+ gltf2_io.CameraOrthographic,
+ gltf2_io.CameraPerspective,
+ gltf2_io.MeshPrimitive,
+ gltf2_io.TextureInfo,
+ gltf2_io.MaterialPBRMetallicRoughness,
+ gltf2_io.MaterialNormalTextureInfoClass,
+ gltf2_io.MaterialOcclusionTextureInfoClass
+ ]
+
+ @property
+ def glTF(self):
+ if not self.__finalized:
+ raise RuntimeError("glTF requested, but buffers are not finalized yet")
+ return self.__gltf
+
+ def finalize_buffer(self, output_path=None, buffer_name=None, is_glb=False):
+ """
+ Finalize the glTF and write buffers.
+
+ :param buffer_path:
+ :return:
+ """
+ if self.__finalized:
+ raise RuntimeError("Tried to finalize buffers for finalized glTF file")
+
+ if is_glb:
+ uri = None
+ elif output_path and buffer_name:
+ with open(output_path + buffer_name, 'wb') as f:
+ f.write(self.__buffer.to_bytes())
+ uri = buffer_name
+ else:
+ uri = self.__buffer.to_embed_string()
+
+ buffer = gltf2_io.Buffer(
+ byte_length=self.__buffer.byte_length,
+ extensions=None,
+ extras=None,
+ name=None,
+ uri=uri
+ )
+ self.__gltf.buffers.append(buffer)
+
+ self.__finalized = True
+
+ if is_glb:
+ return self.__buffer.to_bytes()
+
+ def finalize_images(self, output_path):
+ """
+ Write all images.
+
+ Due to a current limitation the output_path must be the same as that of the glTF file
+ :param output_path:
+ :return:
+ """
+ for image in self.__images:
+ uri = output_path + image.name + ".png"
+ with open(uri, 'wb') as f:
+ f.write(image.to_png_data())
+
+ def add_scene(self, scene: gltf2_io.Scene, active: bool = True):
+ """
+ Add a scene to the glTF.
+
+ The scene should be built up with the generated glTF classes
+ :param scene: gltf2_io.Scene type. Root node of the scene graph
+ :param active: If true, sets the glTD.scene index to the added scene
+ :return: nothing
+ """
+ if self.__finalized:
+ raise RuntimeError("Tried to add scene to finalized glTF file")
+
+ # for node in scene.nodes:
+ # self.__traverse(node)
+ scene_num = self.__traverse(scene)
+ if active:
+ self.__gltf.scene = scene_num
+
+ def add_animation(self, animation: gltf2_io.Animation):
+ """
+ Add an animation to the glTF.
+
+ :param animation: glTF animation, with python style references (names)
+ :return: nothing
+ """
+ if self.__finalized:
+ raise RuntimeError("Tried to add animation to finalized glTF file")
+
+ self.__traverse(animation)
+
+ def __to_reference(self, property):
+ """
+ Append a child of root property to its respective list and return a reference into said list.
+
+ If the property is not child of root, the property itself is returned.
+ :param property: A property type object that should be converted to a reference
+ :return: a reference or the object itself if it is not child or root
+ """
+ gltf_list = self.__childOfRootPropertyTypeLookup.get(type(property), None)
+ if gltf_list is None:
+ # The object is not of a child of root --> don't convert to reference
+ return property
+
+ return self.__append_unique_and_get_index(gltf_list, property)
+
+ @staticmethod
+ def __append_unique_and_get_index(target: list, obj):
+ if obj in target:
+ return target.index(obj)
+ else:
+ index = len(target)
+ target.append(obj)
+ return index
+
+ def __add_image(self, image: gltf2_io_image_data.ImageData):
+ self.__images.append(image)
+ # TODO: we need to know the image url at this point already --> maybe add all options to the constructor of the
+ # exporter
+ # TODO: allow embedding of images (base64)
+ return image.name + ".png"
+
+ def __traverse(self, node):
+ """
+ Recursively traverse a scene graph consisting of gltf compatible elements.
+
+ The tree is traversed downwards until a primitive is reached. Then any ChildOfRoot property
+ is stored in the according list in the glTF and replaced with a index reference in the upper level.
+ """
+ def traverse_all_members(node):
+ for member_name in [a for a in dir(node) if not a.startswith('__') and not callable(getattr(node, a))]:
+ new_value = self.__traverse(getattr(node, member_name))
+ setattr(node, member_name, new_value) # usually this is the same as before
+
+ # TODO: maybe with extensions hooks we can find a more elegant solution
+ if member_name == "extensions" and new_value is not None:
+ for extension_name in new_value.keys():
+ self.__append_unique_and_get_index(self.__gltf.extensions_used, extension_name)
+ self.__append_unique_and_get_index(self.__gltf.extensions_required, extension_name)
+ return node
+
+ # traverse nodes of a child of root property type and add them to the glTF root
+ if type(node) in self.__childOfRootPropertyTypeLookup:
+ node = traverse_all_members(node)
+ idx = self.__to_reference(node)
+ # child of root properties are only present at root level --> replace with index in upper level
+ return idx
+
+ # traverse lists, such as children and replace them with indices
+ if isinstance(node, list):
+ for i in range(len(node)):
+ node[i] = self.__traverse(node[i])
+ return node
+
+ if isinstance(node, dict):
+ for key in node.keys():
+ node[key] = self.__traverse(node[key])
+ return node
+
+ # traverse into any other property
+ if type(node) in self.__propertyTypeLookup:
+ return traverse_all_members(node)
+
+ # binary data needs to be moved to a buffer and referenced with a buffer view
+ if isinstance(node, gltf2_io_binary_data.BinaryData):
+ buffer_view = self.__buffer.add_and_get_view(node)
+ return self.__to_reference(buffer_view)
+
+ # image data needs to be saved to file
+ if isinstance(node, gltf2_io_image_data.ImageData):
+ return self.__add_image(node)
+
+ # do nothing for any type that does not match a glTF schema (primitives)
+ return node
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py
new file mode 100755
index 00000000..a9dc86cf
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py
@@ -0,0 +1,97 @@
+# 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.
+
+#
+# Imports
+#
+
+import bpy
+import typing
+
+
+class Filter:
+ """Base class for all node tree filter operations."""
+
+ def __init__(self):
+ pass
+
+ def __call__(self, shader_node):
+ return True
+
+
+class FilterByName(Filter):
+ """
+ Filter the material node tree by name.
+
+ example usage:
+ find_from_socket(start_socket, ShaderNodeFilterByName("Normal"))
+ """
+
+ def __init__(self, name):
+ self.name = name
+ super(FilterByName, self).__init__()
+
+ def __call__(self, shader_node):
+ return shader_node.name == self.name
+
+
+class FilterByType(Filter):
+ """Filter the material node tree by type."""
+
+ def __init__(self, type):
+ self.type = type
+ super(FilterByType, self).__init__()
+
+ def __call__(self, shader_node):
+ return isinstance(shader_node, self.type)
+
+
+class NodeTreeSearchResult:
+ def __init__(self, shader_node: bpy.types.Node, path: typing.List[bpy.types.NodeLink]):
+ self.shader_node = shader_node
+ self.path = path
+
+
+# TODO: cache these searches
+def from_socket(start_socket: bpy.types.NodeSocket,
+ shader_node_filter: typing.Union[Filter, typing.Callable]) -> typing.List[NodeTreeSearchResult]:
+ """
+ Find shader nodes where the filter expression is true.
+
+ :param start_socket: the beginning of the traversal
+ :param shader_node_filter: should be a function(x: shader_node) -> bool
+ :return: a list of shader nodes for which filter is true
+ """
+ # hide implementation (especially the search path
+ def __search_from_socket(start_socket: bpy.types.NodeSocket,
+ shader_node_filter: typing.Union[Filter, typing.Callable],
+ search_path: typing.List[bpy.types.NodeLink]) -> typing.List[NodeTreeSearchResult]:
+ results = []
+
+ for link in start_socket.links:
+ # follow the link to a shader node
+ linked_node = link.from_node
+ # add the link to the current path
+ search_path.append(link)
+ # check if the node matches the filter
+ if shader_node_filter(linked_node):
+ results.append(NodeTreeSearchResult(linked_node, search_path))
+ # traverse into inputs of the node
+ for input_socket in linked_node.inputs:
+ results += __search_from_socket(input_socket, shader_node_filter, search_path)
+
+ return results
+
+ return __search_from_socket(start_socket, shader_node_filter, [])
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py b/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py
new file mode 100755
index 00000000..0fa7db6e
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py
@@ -0,0 +1,89 @@
+# 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.
+
+import bpy
+import typing
+
+
+class Filter:
+ """Base class for all node tree filter operations."""
+
+ def __call__(self, obj: bpy.types.Object):
+ return True
+
+
+class ByName(Filter):
+ """
+ Filter the objects by name.
+
+ example usage:
+ find_objects(FilterByName("Cube"))
+ """
+
+ def __init__(self, name):
+ self.name = name
+
+ def __call__(self, obj: bpy.types.Object):
+ return obj.name == self.name
+
+
+class ByDataType(Filter):
+ """Filter the scene objects by their data type."""
+
+ def __init__(self, data_type: str):
+ self.type = data_type
+
+ def __call__(self, obj: bpy.types.Object):
+ return obj.type == self.type
+
+
+class ByDataInstance(Filter):
+ """Filter the scene objects by a specific ID instance."""
+
+ def __init__(self, data_instance: bpy.types.ID):
+ self.data = data_instance
+
+ def __call__(self, obj: bpy.types.Object):
+ return self.data == obj.data
+
+
+def find_objects(object_filter: typing.Union[Filter, typing.Callable]):
+ """
+ Find objects in the scene where the filter expression is true.
+
+ :param object_filter: should be a function(x: object) -> bool
+ :return: a list of shader nodes for which filter is true
+ """
+ results = []
+ for obj in bpy.context.scene.objects:
+ if object_filter(obj):
+ results.append(obj)
+ return results
+
+
+def find_objects_from(obj: bpy.types.Object, object_filter: typing.Union[Filter, typing.Callable]):
+ """
+ Search for objects matching a filter function below a specified object.
+
+ :param obj: the starting point of the search
+ :param object_filter: a function(x: object) -> bool
+ :return: a list of objects which passed the filter
+ """
+ results = []
+ if object_filter(obj):
+ results.append(obj)
+ for child in obj.children:
+ results += find_objects_from(child, object_filter)
+ return results
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_utils.py b/io_scene_gltf2/blender/exp/gltf2_blender_utils.py
new file mode 100755
index 00000000..c3e0d6ee
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_utils.py
@@ -0,0 +1,68 @@
+# 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.
+
+import math
+from io_scene_gltf2.io.com import gltf2_io_constants
+
+
+# TODO: we could apply functional programming to these problems (currently we only have a single use case)
+
+def split_list_by_data_type(l: list, data_type: gltf2_io_constants.DataType):
+ """
+ Split a flat list of components by their data type.
+
+ E.g.: A list [0,1,2,3,4,5] of data type Vec3 would be split to [[0,1,2], [3,4,5]]
+ :param l: the flat list
+ :param data_type: the data type of the list
+ :return: a list of lists, where each element list contains the components of the data type
+ """
+ if not (len(l) % gltf2_io_constants.DataType.num_elements(data_type) == 0):
+ raise ValueError("List length does not match specified data type")
+ num_elements = gltf2_io_constants.DataType.num_elements(data_type)
+ return [l[i:i + num_elements] for i in range(0, len(l), num_elements)]
+
+
+def max_components(l: list, data_type: gltf2_io_constants.DataType) -> list:
+ """
+ Find the maximum components in a flat list.
+
+ This is required, for example, for the glTF2.0 accessor min and max properties
+ :param l: the flat list of components
+ :param data_type: the data type of the list (determines the length of the result)
+ :return: a list with length num_elements(data_type) containing the maximum per component along the list
+ """
+ components_lists = split_list_by_data_type(l, data_type)
+ result = [-math.inf] * gltf2_io_constants.DataType.num_elements(data_type)
+ for components in components_lists:
+ for i, c in enumerate(components):
+ result[i] = max(result[i], c)
+ return result
+
+
+def min_components(l: list, data_type: gltf2_io_constants.DataType) -> list:
+ """
+ Find the minimum components in a flat list.
+
+ This is required, for example, for the glTF2.0 accessor min and max properties
+ :param l: the flat list of components
+ :param data_type: the data type of the list (determines the length of the result)
+ :return: a list with length num_elements(data_type) containing the minimum per component along the list
+ """
+ components_lists = split_list_by_data_type(l, data_type)
+ result = [math.inf] * gltf2_io_constants.DataType.num_elements(data_type)
+ for components in components_lists:
+ for i, c in enumerate(components):
+ result[i] = min(result[i], c)
+ return result
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py
new file mode 100755
index 00000000..610da22e
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py
@@ -0,0 +1,327 @@
+# 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.
+
+import bpy
+from .gltf2_blender_texture import BlenderTextureInfo
+
+
+class BlenderKHR_materials_pbrSpecularGlossiness():
+ """Blender KHR_materials_pbrSpecularGlossiness extension."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf, pbrSG, mat_name, vertex_color):
+ """KHR_materials_pbrSpecularGlossiness creation."""
+ engine = bpy.context.scene.render.engine
+ if engine in ['CYCLES', 'BLENDER_EEVEE']:
+ BlenderKHR_materials_pbrSpecularGlossiness.create_nodetree(gltf, pbrSG, mat_name, vertex_color)
+
+ @staticmethod
+ def create_nodetree(gltf, pbrSG, mat_name, vertex_color):
+ """Node tree creation."""
+ material = bpy.data.materials[mat_name]
+ material.use_nodes = True
+ node_tree = material.node_tree
+
+ # delete all nodes except output
+ for node in list(node_tree.nodes):
+ if not node.type == 'OUTPUT_MATERIAL':
+ node_tree.nodes.remove(node)
+
+ output_node = node_tree.nodes[0]
+ output_node.location = 1000, 0
+
+ # create PBR node
+ diffuse = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
+ diffuse.location = 0, 0
+ glossy = node_tree.nodes.new('ShaderNodeBsdfGlossy')
+ glossy.location = 0, 100
+ mix = node_tree.nodes.new('ShaderNodeMixShader')
+ mix.location = 500, 0
+
+ glossy.inputs[1].default_value = 1 - pbrSG['glossinessFactor']
+
+ if pbrSG['diffuse_type'] == gltf.SIMPLE:
+ if not vertex_color:
+ # change input values
+ diffuse.inputs[0].default_value = pbrSG['diffuseFactor']
+
+ else:
+ # Create attribute node to get COLOR_0 data
+ attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+ attribute_node.attribute_name = 'COLOR_0'
+ attribute_node.location = -500, 0
+
+ # links
+ node_tree.links.new(diffuse.inputs[0], attribute_node.outputs[1])
+
+ elif pbrSG['diffuse_type'] == gltf.TEXTURE_FACTOR:
+
+ # TODO alpha ?
+ if vertex_color:
+ # TODO tree locations
+ # Create attribute / separate / math nodes
+ attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+ attribute_node.attribute_name = 'COLOR_0'
+
+ separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB')
+ math_vc_R = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_R.operation = 'MULTIPLY'
+
+ math_vc_G = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_G.operation = 'MULTIPLY'
+
+ math_vc_B = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_B.operation = 'MULTIPLY'
+
+ BlenderTextureInfo.create(gltf, pbrSG['diffuseTexture']['index'])
+
+ # create UV Map / Mapping / Texture nodes / separate & math and combine
+ text_node = node_tree.nodes.new('ShaderNodeTexImage')
+ text_node.image = \
+ bpy.data.images[
+ gltf.data.images[gltf.data.textures[pbrSG['diffuseTexture']['index']].source].blender_image_name
+ ]
+ text_node.location = -1000, 500
+
+ combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+ combine.location = -250, 500
+
+ math_R = node_tree.nodes.new('ShaderNodeMath')
+ math_R.location = -500, 750
+ math_R.operation = 'MULTIPLY'
+ math_R.inputs[1].default_value = pbrSG['diffuseFactor'][0]
+
+ math_G = node_tree.nodes.new('ShaderNodeMath')
+ math_G.location = -500, 500
+ math_G.operation = 'MULTIPLY'
+ math_G.inputs[1].default_value = pbrSG['diffuseFactor'][1]
+
+ math_B = node_tree.nodes.new('ShaderNodeMath')
+ math_B.location = -500, 250
+ math_B.operation = 'MULTIPLY'
+ math_B.inputs[1].default_value = pbrSG['diffuseFactor'][2]
+
+ separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+ separate.location = -750, 500
+
+ mapping = node_tree.nodes.new('ShaderNodeMapping')
+ mapping.location = -1500, 500
+
+ uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+ uvmap.location = -2000, 500
+ if 'texCoord' in pbrSG['diffuseTexture'].keys():
+ uvmap["gltf2_texcoord"] = pbrSG['diffuseTexture']['texCoord'] # Set custom flag to retrieve TexCoord
+ else:
+ uvmap["gltf2_texcoord"] = 0 # TODO: set in precompute instead of here?
+ # UV Map will be set after object/UVMap creation
+
+ # Create links
+ if vertex_color:
+ node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0])
+ node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0])
+ node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1])
+ node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2])
+ node_tree.links.new(math_vc_R.inputs[0], math_R.outputs[0])
+ node_tree.links.new(math_vc_G.inputs[0], math_G.outputs[0])
+ node_tree.links.new(math_vc_B.inputs[0], math_B.outputs[0])
+ node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0])
+ node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0])
+ node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0])
+
+ else:
+ node_tree.links.new(combine.inputs[0], math_R.outputs[0])
+ node_tree.links.new(combine.inputs[1], math_G.outputs[0])
+ node_tree.links.new(combine.inputs[2], math_B.outputs[0])
+
+ # Common for both mode (non vertex color / vertex color)
+ node_tree.links.new(math_R.inputs[0], separate.outputs[0])
+ node_tree.links.new(math_G.inputs[0], separate.outputs[1])
+ node_tree.links.new(math_B.inputs[0], separate.outputs[2])
+
+ node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+ node_tree.links.new(text_node.inputs[0], mapping.outputs[0])
+ node_tree.links.new(separate.inputs[0], text_node.outputs[0])
+
+ node_tree.links.new(diffuse.inputs[0], combine.outputs[0])
+
+ elif pbrSG['diffuse_type'] == gltf.TEXTURE:
+
+ BlenderTextureInfo.create(gltf, pbrSG['diffuseTexture']['index'])
+
+ # TODO alpha ?
+ if vertex_color:
+ # Create attribute / separate / math nodes
+ attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+ attribute_node.attribute_name = 'COLOR_0'
+ attribute_node.location = -2000, 250
+
+ separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB')
+ separate_vertex_color.location = -1500, 250
+
+ math_vc_R = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_R.operation = 'MULTIPLY'
+ math_vc_R.location = -1000, 750
+
+ math_vc_G = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_G.operation = 'MULTIPLY'
+ math_vc_G.location = -1000, 500
+
+ math_vc_B = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_B.operation = 'MULTIPLY'
+ math_vc_B.location = -1000, 250
+
+ combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+ combine.location = -500, 500
+
+ separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+ separate.location = -1500, 500
+
+ # create UV Map / Mapping / Texture nodes / separate & math and combine
+ text_node = node_tree.nodes.new('ShaderNodeTexImage')
+ text_node.image = bpy.data.images[
+ gltf.data.images[gltf.data.textures[pbrSG['diffuseTexture']['index']].source].blender_image_name
+ ]
+ if vertex_color:
+ text_node.location = -2000, 500
+ else:
+ text_node.location = -500, 500
+
+ mapping = node_tree.nodes.new('ShaderNodeMapping')
+ if vertex_color:
+ mapping.location = -2500, 500
+ else:
+ mapping.location = -1500, 500
+
+ uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+ if vertex_color:
+ uvmap.location = -3000, 500
+ else:
+ uvmap.location = -2000, 500
+ if 'texCoord' in pbrSG['diffuseTexture'].keys():
+ uvmap["gltf2_texcoord"] = pbrSG['diffuseTexture']['texCoord'] # Set custom flag to retrieve TexCoord
+ else:
+ uvmap["gltf2_texcoord"] = 0 # TODO: set in precompute instead of here?
+ # UV Map will be set after object/UVMap creation
+
+ # Create links
+ if vertex_color:
+ node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0])
+
+ node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0])
+ node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1])
+ node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2])
+
+ node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0])
+ node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0])
+ node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0])
+
+ node_tree.links.new(separate.inputs[0], text_node.outputs[0])
+
+ node_tree.links.new(principled.inputs[0], combine.outputs[0])
+
+ node_tree.links.new(math_vc_R.inputs[0], separate.outputs[0])
+ node_tree.links.new(math_vc_G.inputs[0], separate.outputs[1])
+ node_tree.links.new(math_vc_B.inputs[0], separate.outputs[2])
+
+ else:
+ node_tree.links.new(diffuse.inputs[0], text_node.outputs[0])
+
+ # Common for both mode (non vertex color / vertex color)
+
+ node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+ node_tree.links.new(text_node.inputs[0], mapping.outputs[0])
+
+ if pbrSG['specgloss_type'] == gltf.SIMPLE:
+
+ combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+ combine.inputs[0].default_value = pbrSG['specularFactor'][0]
+ combine.inputs[1].default_value = pbrSG['specularFactor'][1]
+ combine.inputs[2].default_value = pbrSG['specularFactor'][2]
+
+ # links
+ node_tree.links.new(glossy.inputs[0], combine.outputs[0])
+
+ elif pbrSG['specgloss_type'] == gltf.TEXTURE:
+ BlenderTextureInfo.create(gltf, pbrSG['specularGlossinessTexture']['index'])
+ spec_text = node_tree.nodes.new('ShaderNodeTexImage')
+ spec_text.image = bpy.data.images[
+ gltf.data.images[
+ gltf.data.textures[pbrSG['specularGlossinessTexture']['index']].source
+ ].blender_image_name
+ ]
+ spec_text.color_space = 'NONE'
+ spec_text.location = -500, 0
+
+ spec_mapping = node_tree.nodes.new('ShaderNodeMapping')
+ spec_mapping.location = -1000, 0
+
+ spec_uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+ spec_uvmap.location = -1500, 0
+ if 'texCoord' in pbrSG['specularGlossinessTexture'].keys():
+ # Set custom flag to retrieve TexCoord
+ spec_uvmap["gltf2_texcoord"] = pbrSG['specularGlossinessTexture']['texCoord']
+ else:
+ spec_uvmap["gltf2_texcoord"] = 0 # TODO: set in precompute instead of here?
+
+ # links
+ node_tree.links.new(glossy.inputs[0], spec_text.outputs[0])
+ node_tree.links.new(mix.inputs[0], spec_text.outputs[1])
+
+ node_tree.links.new(spec_mapping.inputs[0], spec_uvmap.outputs[0])
+ node_tree.links.new(spec_text.inputs[0], spec_mapping.outputs[0])
+
+ elif pbrSG['specgloss_type'] == gltf.TEXTURE_FACTOR:
+
+ BlenderTextureInfo.create(gltf, pbrSG['specularGlossinessTexture']['index'])
+
+ spec_text = node_tree.nodes.new('ShaderNodeTexImage')
+ spec_text.image = bpy.data.images[gltf.data.images[
+ gltf.data.textures[pbrSG['specularGlossinessTexture']['index']].source
+ ].blender_image_name]
+ spec_text.color_space = 'NONE'
+ spec_text.location = -1000, 0
+
+ spec_math = node_tree.nodes.new('ShaderNodeMath')
+ spec_math.operation = 'MULTIPLY'
+ spec_math.inputs[0].default_value = pbrSG['glossinessFactor']
+ spec_math.location = -250, 100
+
+ spec_mapping = node_tree.nodes.new('ShaderNodeMapping')
+ spec_mapping.location = -1000, 0
+
+ spec_uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+ spec_uvmap.location = -1500, 0
+ if 'texCoord' in pbrSG['specularGlossinessTexture'].keys():
+ # Set custom flag to retrieve TexCoord
+ spec_uvmap["gltf2_texcoord"] = pbrSG['specularGlossinessTexture']['texCoord']
+ else:
+ spec_uvmap["gltf2_texcoord"] = 0 # TODO: set in precompute instead of here?
+
+ # links
+
+ node_tree.links.new(spec_math.inputs[1], spec_text.outputs[0])
+ node_tree.links.new(mix.inputs[0], spec_text.outputs[1])
+ node_tree.links.new(glossy.inputs[1], spec_math.outputs[0])
+ node_tree.links.new(glossy.inputs[0], spec_text.outputs[0])
+
+ node_tree.links.new(spec_mapping.inputs[0], spec_uvmap.outputs[0])
+ node_tree.links.new(spec_text.inputs[0], spec_mapping.outputs[0])
+
+ # link node to output
+ node_tree.links.new(mix.inputs[2], diffuse.outputs[0])
+ node_tree.links.new(mix.inputs[1], glossy.outputs[0])
+ node_tree.links.new(output_node.inputs[0], mix.outputs[0])
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py
new file mode 100755
index 00000000..4180672a
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py
@@ -0,0 +1,35 @@
+# 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.
+
+from .gltf2_blender_animation_bone import BlenderBoneAnim
+from .gltf2_blender_animation_node import BlenderNodeAnim
+
+
+class BlenderAnimation():
+ """Dispatch Animation to bone or object animation."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def anim(gltf, anim_idx, node_idx):
+ """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)
+
+ if gltf.data.nodes[node_idx].children:
+ for child in gltf.data.nodes[node_idx].children:
+ BlenderAnimation.anim(gltf, anim_idx, child)
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py
new file mode 100755
index 00000000..6b670aea
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py
@@ -0,0 +1,188 @@
+# 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.
+
+import bpy
+from mathutils import Matrix
+
+from ..com.gltf2_blender_conversion import loc_gltf_to_blender, quaternion_gltf_to_blender, scale_to_matrix
+from ...io.imp.gltf2_io_binary import BinaryData
+
+
+class BlenderBoneAnim():
+ """Blender Bone Animation."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def set_interpolation(interpolation, kf):
+ """Set interpolation."""
+ if interpolation == "LINEAR":
+ kf.interpolation = 'LINEAR'
+ elif interpolation == "STEP":
+ kf.interpolation = 'CONSTANT'
+ elif interpolation == "CUBICSPLINE":
+ kf.interpolation = 'BEZIER'
+ else:
+ kf.interpolation = 'BEZIER'
+
+ @staticmethod
+ def parse_translation_channel(gltf, node, obj, bone, channel, animation):
+ """Manage Location animation."""
+ fps = bpy.context.scene.render.fps
+ blender_path = "location"
+
+ 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()
+
+ for idx, key in enumerate(keys):
+ if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+ # TODO manage tangent?
+ translation_keyframe = loc_gltf_to_blender(values[idx * 3 + 1])
+ else:
+ translation_keyframe = loc_gltf_to_blender(values[idx])
+ if not node.parent:
+ 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
+ final_trans = (parent_mat @ Matrix.Translation(translation_keyframe)).to_translation()
+ bone.location = inv_bind_matrix @ final_trans
+ bone.keyframe_insert(blender_path, frame=key[0] * fps, group="location")
+
+ for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "location"]:
+ for kf in fcurve.keyframe_points:
+ BlenderBoneAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+ @staticmethod
+ def parse_rotation_channel(gltf, node, obj, bone, channel, animation):
+ """Manage rotation animation."""
+ # Note: some operations lead to issue with quaternions. Converting to matrix and then back to quaternions breaks
+ # quaternion continuity
+ # (see antipodal quaternions). Blender interpolates between two antipodal quaternions, which causes glitches in
+ # animation.
+ # Converting to euler and then back to quaternion is a dirty fix preventing this issue in animation, until a
+ # better solution is found
+ # This fix is skipped when parent matrix is identity
+ fps = bpy.context.scene.render.fps
+ blender_path = "rotation_quaternion"
+
+ 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()
+
+ for idx, key in enumerate(keys):
+ if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+ # TODO manage tangent?
+ quat_keyframe = quaternion_gltf_to_blender(values[idx * 3 + 1])
+ else:
+ quat_keyframe = quaternion_gltf_to_blender(values[idx])
+ if not node.parent:
+ bone.rotation_quaternion = bind_rotation.inverted() @ quat_keyframe
+ 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_rot = (parent_mat @ quat_keyframe.to_matrix().to_4x4()).to_quaternion()
+ bone.rotation_quaternion = bind_rotation.rotation_difference(final_rot).to_euler().to_quaternion()
+ else:
+ bone.rotation_quaternion = \
+ bind_rotation.rotation_difference(quat_keyframe).to_euler().to_quaternion()
+
+ bone.keyframe_insert(blender_path, frame=key[0] * fps, group='rotation')
+
+ for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "rotation"]:
+ for kf in fcurve.keyframe_points:
+ BlenderBoneAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+ @staticmethod
+ def parse_scale_channel(gltf, node, obj, bone, channel, animation):
+ """Manage scaling animation."""
+ fps = bpy.context.scene.render.fps
+ blender_path = "scale"
+
+ 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())
+
+ for idx, key in enumerate(keys):
+ if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+ # TODO manage tangent?
+ scale_mat = scale_to_matrix(loc_gltf_to_blender(values[idx * 3 + 1]))
+ else:
+ scale_mat = scale_to_matrix(loc_gltf_to_blender(values[idx]))
+ if not node.parent:
+ bone.scale = (bind_scale.inverted() @ scale_mat).to_scale()
+ else:
+ if not gltf.data.nodes[node.parent].is_joint:
+ parent_mat = Matrix()
+ else:
+ parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix
+
+ bone.scale = (
+ bind_scale.inverted() @ scale_to_matrix(parent_mat.to_scale()) @ scale_mat
+ ).to_scale()
+
+ bone.keyframe_insert(blender_path, frame=key[0] * fps, group='scale')
+
+ for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "scale"]:
+ for kf in fcurve.keyframe_points:
+ BlenderBoneAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+ @staticmethod
+ def anim(gltf, anim_idx, node_idx):
+ """Manage animation."""
+ node = gltf.data.nodes[node_idx]
+ obj = bpy.data.objects[gltf.data.skins[node.skin_id].blender_armature_name]
+ bone = obj.pose.bones[node.blender_bone_name]
+
+ if anim_idx not in node.animations.keys():
+ return
+
+ animation = gltf.data.animations[anim_idx]
+
+ if animation.name:
+ name = animation.name + "_" + obj.name
+ else:
+ name = "Animation_" + str(anim_idx) + "_" + obj.name
+ if name not in bpy.data.actions:
+ action = bpy.data.actions.new(name)
+ else:
+ action = bpy.data.actions[name]
+ if not obj.animation_data:
+ obj.animation_data_create()
+ obj.animation_data.action = bpy.data.actions[action.name]
+
+ for channel_idx in node.animations[anim_idx]:
+ channel = animation.channels[channel_idx]
+
+ if channel.target.path == "translation":
+ BlenderBoneAnim.parse_translation_channel(gltf, node, obj, bone, channel, animation)
+
+ elif channel.target.path == "rotation":
+ BlenderBoneAnim.parse_rotation_channel(gltf, node, obj, bone, channel, animation)
+
+ elif channel.target.path == "scale":
+ BlenderBoneAnim.parse_scale_channel(gltf, node, 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
new file mode 100755
index 00000000..a5d4b40f
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py
@@ -0,0 +1,132 @@
+# 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.
+
+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 ...io.imp.gltf2_io_binary import BinaryData
+
+
+class BlenderNodeAnim():
+ """Blender Object Animation."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def set_interpolation(interpolation, kf):
+ """Manage interpolation."""
+ if interpolation == "LINEAR":
+ kf.interpolation = 'LINEAR'
+ elif interpolation == "STEP":
+ kf.interpolation = 'CONSTANT'
+ elif interpolation == "CUBICSPLINE":
+ kf.interpolation = 'BEZIER'
+ else:
+ kf.interpolation = 'BEZIER'
+
+ @staticmethod
+ def anim(gltf, anim_idx, node_idx):
+ """Manage animation."""
+ node = gltf.data.nodes[node_idx]
+ obj = bpy.data.objects[node.blender_object]
+ fps = bpy.context.scene.render.fps
+
+ if anim_idx not in node.animations.keys():
+ return
+
+ animation = gltf.data.animations[anim_idx]
+
+ if animation.name:
+ name = animation.name + "_" + obj.name
+ else:
+ name = "Animation_" + str(anim_idx) + "_" + obj.name
+ action = bpy.data.actions.new(name)
+ if not obj.animation_data:
+ obj.animation_data_create()
+ obj.animation_data.action = bpy.data.actions[action.name]
+
+ for channel_idx in node.animations[anim_idx]:
+ channel = animation.channels[channel_idx]
+
+ keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input)
+ values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output)
+
+ if channel.target.path in ['translation', 'rotation', 'scale']:
+
+ if channel.target.path == "translation":
+ blender_path = "location"
+ for idx, key in enumerate(keys):
+ if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+ # TODO manage tangent?
+ obj.location = Vector(loc_gltf_to_blender(list(values[idx * 3 + 1])))
+ else:
+ obj.location = Vector(loc_gltf_to_blender(list(values[idx])))
+ obj.keyframe_insert(blender_path, frame=key[0] * fps, group='location')
+
+ # Setting interpolation
+ for fcurve in [curve for curve in obj.animation_data.action.fcurves
+ if curve.group.name == "location"]:
+ for kf in fcurve.keyframe_points:
+ BlenderNodeAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+ elif channel.target.path == "rotation":
+ blender_path = "rotation_quaternion"
+ for idx, key in enumerate(keys):
+ if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+ # TODO manage tangent?
+ obj.rotation_quaternion = quaternion_gltf_to_blender(values[idx * 3 + 1])
+ else:
+ obj.rotation_quaternion = quaternion_gltf_to_blender(values[idx])
+ obj.keyframe_insert(blender_path, frame=key[0] * fps, group='rotation')
+
+ # Setting interpolation
+ for fcurve in [curve for curve in obj.animation_data.action.fcurves
+ if curve.group.name == "rotation"]:
+ for kf in fcurve.keyframe_points:
+ BlenderNodeAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+ elif channel.target.path == "scale":
+ blender_path = "scale"
+ for idx, key in enumerate(keys):
+ # TODO manage tangent?
+ if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+ obj.scale = Vector(scale_gltf_to_blender(list(values[idx * 3 + 1])))
+ else:
+ obj.scale = Vector(scale_gltf_to_blender(list(values[idx])))
+ obj.keyframe_insert(blender_path, frame=key[0] * fps, group='scale')
+
+ # Setting interpolation
+ for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "scale"]:
+ for kf in fcurve.keyframe_points:
+ BlenderNodeAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+ elif channel.target.path == 'weights':
+
+ # retrieve number of targets
+ nb_targets = 0
+ for prim in gltf.data.meshes[gltf.data.nodes[node_idx].mesh].primitives:
+ if prim.targets:
+ if len(prim.targets) > nb_targets:
+ nb_targets = len(prim.targets)
+
+ for idx, key in enumerate(keys):
+ for sk in range(nb_targets):
+ obj.data.shape_keys.key_blocks[sk + 1].value = values[idx * nb_targets + sk][0]
+ obj.data.shape_keys.key_blocks[sk + 1].keyframe_insert(
+ "value",
+ frame=key[0] * fps,
+ group='ShapeKeys'
+ )
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_camera.py b/io_scene_gltf2/blender/imp/gltf2_blender_camera.py
new file mode 100755
index 00000000..b5a10ac5
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_camera.py
@@ -0,0 +1,47 @@
+# 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.
+
+import bpy
+
+
+class BlenderCamera():
+ """Blender Camera."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf, camera_id):
+ """Camera creation."""
+ pycamera = gltf.data.cameras[camera_id]
+
+ if not pycamera.name:
+ pycamera.name = "Camera"
+
+ cam = bpy.data.cameras.new(pycamera.name)
+
+ # Blender create a perspective camera by default
+ if pycamera.type == "orthographic":
+ cam.type = "ORTHO"
+
+ # TODO: lot's of work for camera here...
+ if hasattr(pycamera, "znear"):
+ cam.clip_start = pycamera.znear
+
+ if hasattr(pycamera, "zfar"):
+ cam.clip_end = pycamera.zfar
+
+ obj = bpy.data.objects.new(pycamera.name, cam)
+ 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
new file mode 100755
index 00000000..9f8a3584
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py
@@ -0,0 +1,217 @@
+# 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.
+
+import bpy
+from .gltf2_blender_scene import BlenderScene
+from ...io.com.gltf2_io_trs import TRS
+
+
+class BlenderGlTF():
+ """Main glTF import class."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf):
+ """Create glTF main method."""
+ bpy.context.scene.render.engine = 'BLENDER_EEVEE'
+ BlenderGlTF.pre_compute(gltf)
+
+ for scene_idx, scene in enumerate(gltf.data.scenes):
+ BlenderScene.create(gltf, scene_idx)
+
+ # 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 alignement detection to avoid some bad cases
+
+ threshold = 0.001
+ for armobj in [obj for obj in bpy.data.objects if obj.type == "ARMATURE"]:
+ 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")
+
+ @staticmethod
+ def pre_compute(gltf):
+ """Pre compute, just before creation."""
+ # default scene used
+ gltf.blender_scene = None
+
+ # Blender material
+ if gltf.data.materials:
+ for material in gltf.data.materials:
+ material.blender_material = None
+
+ if material.pbr_metallic_roughness:
+ # Init
+ material.pbr_metallic_roughness.color_type = gltf.SIMPLE
+ material.pbr_metallic_roughness.vertex_color = False
+ material.pbr_metallic_roughness.metallic_type = gltf.SIMPLE
+
+ if material.pbr_metallic_roughness.base_color_texture:
+ material.pbr_metallic_roughness.color_type = gltf.TEXTURE
+
+ if material.pbr_metallic_roughness.metallic_roughness_texture:
+ material.pbr_metallic_roughness.metallic_type = gltf.TEXTURE
+
+ if material.pbr_metallic_roughness.base_color_factor:
+ if material.pbr_metallic_roughness.color_type == gltf.TEXTURE and \
+ material.pbr_metallic_roughness.base_color_factor != [1.0, 1.0, 1.0, 1.0]:
+ material.pbr_metallic_roughness.color_type = gltf.TEXTURE_FACTOR
+ else:
+ material.pbr_metallic_roughness.base_color_factor = [1.0, 1.0, 1.0, 1.0]
+
+ if material.pbr_metallic_roughness.metallic_factor is not None:
+ if material.pbr_metallic_roughness.metallic_type == gltf.TEXTURE \
+ and material.pbr_metallic_roughness.metallic_factor != 1.0:
+ material.pbr_metallic_roughness.metallic_type = gltf.TEXTURE_FACTOR
+ else:
+ material.pbr_metallic_roughness.metallic_factor = 1.0
+
+ if material.pbr_metallic_roughness.roughness_factor is not None:
+ if material.pbr_metallic_roughness.metallic_type == gltf.TEXTURE \
+ and material.pbr_metallic_roughness.roughness_factor != 1.0:
+ material.pbr_metallic_roughness.metallic_type = gltf.TEXTURE_FACTOR
+ else:
+ material.pbr_metallic_roughness.roughness_factor = 1.0
+
+ # pre compute material for KHR_materials_pbrSpecularGlossiness
+ if material.extensions is not None \
+ and 'KHR_materials_pbrSpecularGlossiness' in material.extensions.keys():
+ # Init
+ material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] = gltf.SIMPLE
+ material.extensions['KHR_materials_pbrSpecularGlossiness']['vertex_color'] = False
+ material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] = gltf.SIMPLE
+
+ if 'diffuseTexture' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys():
+ material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] = gltf.TEXTURE
+
+ if 'diffuseFactor' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys():
+ if material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] == gltf.TEXTURE \
+ and material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuseFactor'] != \
+ [1.0, 1.0, 1.0, 1.0]:
+ material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] = \
+ gltf.TEXTURE_FACTOR
+ else:
+ material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuseFactor'] = \
+ [1.0, 1.0, 1.0, 1.0]
+
+ if 'specularGlossinessTexture' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys():
+ material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] = gltf.TEXTURE
+
+ if 'specularFactor' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys():
+ if material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] == \
+ gltf.TEXTURE \
+ and material.extensions['KHR_materials_pbrSpecularGlossiness']['specularFactor'] != \
+ [1.0, 1.0, 1.0]:
+ material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] = \
+ gltf.TEXTURE_FACTOR
+ else:
+ material.extensions['KHR_materials_pbrSpecularGlossiness']['specularFactor'] = [1.0, 1.0, 1.0]
+
+ if 'glossinessFactor' not in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys():
+ material.extensions['KHR_materials_pbrSpecularGlossiness']['glossinessFactor'] = 1.0
+
+ for node_idx, node in enumerate(gltf.data.nodes):
+
+ # skin management
+ if node.skin is not None and node.mesh is not None:
+ if not hasattr(gltf.data.skins[node.skin], "node_ids"):
+ gltf.data.skins[node.skin].node_ids = []
+
+ gltf.data.skins[node.skin].node_ids.append(node_idx)
+
+ # 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):
+ node.animations = {}
+
+ for anim_idx, anim in enumerate(gltf.data.animations):
+ for channel_idx, channel in enumerate(anim.channels):
+ if anim_idx not in gltf.data.nodes[channel.target.node].animations.keys():
+ gltf.data.nodes[channel.target.node].animations[anim_idx] = []
+ gltf.data.nodes[channel.target.node].animations[anim_idx].append(channel_idx)
+
+ # Meshes
+ if gltf.data.meshes:
+ for mesh in gltf.data.meshes:
+ mesh.blender_name = None
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_image.py b/io_scene_gltf2/blender/imp/gltf2_blender_image.py
new file mode 100755
index 00000000..ca1eb626
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_image.py
@@ -0,0 +1,101 @@
+# 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.
+
+import bpy
+import os
+import tempfile
+from os.path import dirname, join, isfile, basename
+
+from ...io.imp.gltf2_io_binary import BinaryData
+
+
+# Note that Image is not a glTF2.0 object
+class BlenderImage():
+ """Manage Image."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def get_image_path(gltf, img_idx):
+ """Return image path."""
+ pyimage = gltf.data.images[img_idx]
+
+ image_name = "Image_" + str(img_idx)
+
+ if pyimage.uri:
+ sep = ';base64,'
+ if pyimage.uri[:5] == 'data:':
+ idx = pyimage.uri.find(sep)
+ if idx != -1:
+ return False, None, None
+
+ if isfile(join(dirname(gltf.filename), pyimage.uri)):
+ return True, join(dirname(gltf.filename), pyimage.uri), basename(join(dirname(gltf.filename), pyimage.uri))
+ else:
+ pyimage.gltf.log.error("Missing file (index " + str(img_idx) + "): " + pyimage.uri)
+ return False, None, None
+
+ if pyimage.buffer_view is None:
+ return False, None, None
+
+ return False, None, None
+
+ @staticmethod
+ def create(gltf, img_idx):
+ """Image creation."""
+ img = gltf.data.images[img_idx]
+
+ img.blender_image_name = None
+
+ if gltf.import_settings['import_pack_images'] is False:
+
+ # Images are not packed (if image is a real file)
+ real, path, img_name = BlenderImage.get_image_path(gltf, img_idx)
+
+ if real is True:
+
+ # Check if image is already loaded
+ for img_ in bpy.data.images:
+ if img_.filepath == path:
+ # Already loaded, not needed to reload it
+ img.blender_image_name = img_.name
+ return
+
+ blender_image = bpy.data.images.load(path)
+ blender_image.name = img_name
+ img.blender_image_name = blender_image.name
+ return
+
+ # Check if the file is already loaded (packed file)
+ file_creation_needed = True
+ for img_ in bpy.data.images:
+ if hasattr(img_, "gltf_index") and img_['gltf_index'] == img_idx:
+ file_creation_needed = False
+ img.blender_image_name = img_.name
+ break
+
+ if file_creation_needed is True:
+ # Create a temp image, pack, and delete image
+ tmp_image = tempfile.NamedTemporaryFile(delete=False)
+ img_data, img_name = BinaryData.get_image_data(gltf, img_idx)
+ tmp_image.write(img_data)
+ tmp_image.close()
+
+ blender_image = bpy.data.images.load(tmp_image.name)
+ blender_image.pack()
+ blender_image.name = img_name
+ img.blender_image_name = blender_image.name
+ blender_image['gltf_index'] = img_idx
+ os.remove(tmp_image.name)
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_map_emissive.py b/io_scene_gltf2/blender/imp/gltf2_blender_map_emissive.py
new file mode 100755
index 00000000..81cfd76e
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_map_emissive.py
@@ -0,0 +1,110 @@
+# 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.
+
+import bpy
+from .gltf2_blender_texture import BlenderTextureInfo
+from ..com.gltf2_blender_material_helpers import get_preoutput_node_output
+
+
+class BlenderEmissiveMap():
+ """Blender Emissive Map."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf, material_idx):
+ """Create emissive map."""
+ engine = bpy.context.scene.render.engine
+ if engine in ['CYCLES', 'BLENDER_EEVEE']:
+ BlenderEmissiveMap.create_nodetree(gltf, material_idx)
+
+ def create_nodetree(gltf, material_idx):
+ """Create node tree."""
+ pymaterial = gltf.data.materials[material_idx]
+
+ material = bpy.data.materials[pymaterial.blender_material]
+ node_tree = material.node_tree
+
+ BlenderTextureInfo.create(gltf, pymaterial.emissive_texture.index)
+
+ # check if there is some emssive_factor on material
+ if pymaterial.emissive_factor is None:
+ pymaterial.emissive_factor = [1.0, 1.0, 1.0]
+
+ # retrieve principled node and output node
+ principled = get_preoutput_node_output(node_tree)
+ output = [node for node in node_tree.nodes if node.type == 'OUTPUT_MATERIAL'][0]
+
+ # add nodes
+ emit = node_tree.nodes.new('ShaderNodeEmission')
+ emit.location = 0, 1000
+ if pymaterial.emissive_factor != [1.0, 1.0, 1.0]:
+ separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+ separate.location = -750, 1000
+ combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+ combine.location = -250, 1000
+ mapping = node_tree.nodes.new('ShaderNodeMapping')
+ mapping.location = -1500, 1000
+ uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+ uvmap.location = -2000, 1000
+ if pymaterial.emissive_texture.tex_coord is not None:
+ uvmap["gltf2_texcoord"] = pymaterial.emissive_texture.tex_coord # Set custom flag to retrieve TexCoord
+ else:
+ uvmap["gltf2_texcoord"] = 0 # TODO: set in precompute instead of here?
+
+ text = node_tree.nodes.new('ShaderNodeTexImage')
+ text.image = bpy.data.images[gltf.data.images[
+ gltf.data.textures[pymaterial.emissive_texture.index].source
+ ].blender_image_name]
+ text.label = 'EMISSIVE'
+ text.location = -1000, 1000
+ add = node_tree.nodes.new('ShaderNodeAddShader')
+ add.location = 500, 500
+
+ if pymaterial.emissive_factor != [1.0, 1.0, 1.0]:
+ math_R = node_tree.nodes.new('ShaderNodeMath')
+ math_R.location = -500, 1500
+ math_R.operation = 'MULTIPLY'
+ math_R.inputs[1].default_value = pymaterial.emissive_factor[0]
+
+ math_G = node_tree.nodes.new('ShaderNodeMath')
+ math_G.location = -500, 1250
+ math_G.operation = 'MULTIPLY'
+ math_G.inputs[1].default_value = pymaterial.emissive_factor[1]
+
+ math_B = node_tree.nodes.new('ShaderNodeMath')
+ math_B.location = -500, 1000
+ math_B.operation = 'MULTIPLY'
+ math_B.inputs[1].default_value = pymaterial.emissive_factor[2]
+
+ # create links
+ node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+ node_tree.links.new(text.inputs[0], mapping.outputs[0])
+ if pymaterial.emissive_factor != [1.0, 1.0, 1.0]:
+ node_tree.links.new(separate.inputs[0], text.outputs[0])
+ node_tree.links.new(math_R.inputs[0], separate.outputs[0])
+ node_tree.links.new(math_G.inputs[0], separate.outputs[1])
+ node_tree.links.new(math_B.inputs[0], separate.outputs[2])
+ node_tree.links.new(combine.inputs[0], math_R.outputs[0])
+ node_tree.links.new(combine.inputs[1], math_G.outputs[0])
+ node_tree.links.new(combine.inputs[2], math_B.outputs[0])
+ node_tree.links.new(emit.inputs[0], combine.outputs[0])
+ else:
+ node_tree.links.new(emit.inputs[0], text.outputs[0])
+
+ # following links will modify PBR node tree
+ node_tree.links.new(add.inputs[0], emit.outputs[0])
+ node_tree.links.new(add.inputs[1], principled)
+ node_tree.links.new(output.inputs[0], add.outputs[0])
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_map_normal.py b/io_scene_gltf2/blender/imp/gltf2_blender_map_normal.py
new file mode 100755
index 00000000..ab09f24c
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_map_normal.py
@@ -0,0 +1,89 @@
+# 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.
+
+import bpy
+from .gltf2_blender_texture import BlenderTextureInfo
+
+
+class BlenderNormalMap():
+ """Blender Normal map."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf, material_idx):
+ """Creation of Normal map."""
+ engine = bpy.context.scene.render.engine
+ if engine in ['CYCLES', 'BLENDER_EEVEE']:
+ BlenderNormalMap.create_nodetree(gltf, material_idx)
+
+ def create_nodetree(gltf, material_idx):
+ """Creation of Nodetree."""
+ pymaterial = gltf.data.materials[material_idx]
+
+ material = bpy.data.materials[pymaterial.blender_material]
+ node_tree = material.node_tree
+
+ BlenderTextureInfo.create(gltf, pymaterial.normal_texture.index)
+
+ # retrieve principled node and output node
+ principled = None
+ diffuse = None
+ glossy = None
+ if len([node for node in node_tree.nodes if node.type == "BSDF_PRINCIPLED"]) != 0:
+ principled = [node for node in node_tree.nodes if node.type == "BSDF_PRINCIPLED"][0]
+ else:
+ # No principled, we are probably coming from extension
+ diffuse = [node for node in node_tree.nodes if node.type == "BSDF_DIFFUSE"][0]
+ glossy = [node for node in node_tree.nodes if node.type == "BSDF_GLOSSY"][0]
+
+ # add nodes
+ mapping = node_tree.nodes.new('ShaderNodeMapping')
+ mapping.location = -1000, -500
+ uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+ uvmap.location = -1500, -500
+ if pymaterial.normal_texture.tex_coord is not None:
+ uvmap["gltf2_texcoord"] = pymaterial.normal_texture.tex_coord # Set custom flag to retrieve TexCoord
+ else:
+ uvmap["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here
+
+ text = node_tree.nodes.new('ShaderNodeTexImage')
+ text.image = bpy.data.images[gltf.data.images[
+ gltf.data.textures[pymaterial.normal_texture.index].source
+ ].blender_image_name]
+ text.label = 'NORMALMAP'
+ text.color_space = 'NONE'
+ text.location = -500, -500
+
+ normalmap_node = node_tree.nodes.new('ShaderNodeNormalMap')
+ normalmap_node.location = -250, -500
+ if pymaterial.normal_texture.tex_coord is not None:
+ # Set custom flag to retrieve TexCoord
+ normalmap_node["gltf2_texcoord"] = pymaterial.normal_texture.tex_coord
+ else:
+ normalmap_node["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here
+
+ # create links
+ node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+ node_tree.links.new(text.inputs[0], mapping.outputs[0])
+ node_tree.links.new(normalmap_node.inputs[1], text.outputs[0])
+
+ # following links will modify PBR node tree
+ if principled:
+ node_tree.links.new(principled.inputs[17], normalmap_node.outputs[0])
+ if diffuse:
+ node_tree.links.new(diffuse.inputs[2], normalmap_node.outputs[0])
+ if glossy:
+ node_tree.links.new(glossy.inputs[2], normalmap_node.outputs[0])
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_map_occlusion.py b/io_scene_gltf2/blender/imp/gltf2_blender_map_occlusion.py
new file mode 100755
index 00000000..5c41ce9e
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_map_occlusion.py
@@ -0,0 +1,41 @@
+# 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.
+
+import bpy
+from .gltf2_blender_texture import BlenderTextureInfo
+
+
+class BlenderOcclusionMap():
+ """Blender Occlusion map."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf, material_idx):
+ """Occlusion map creation."""
+ engine = bpy.context.scene.render.engine
+ if engine in ['CYCLES', 'BLENDER_EEVEE']:
+ BlenderOcclusionMap.create_nodetree(gltf, material_idx)
+
+ def create_nodetree(gltf, material_idx):
+ """Nodetree creation."""
+ pymaterial = gltf.data.materials[material_idx]
+
+ BlenderTextureInfo.create(gltf, pymaterial.occlusion_texture.index)
+
+ # Pack texture, but doesn't use it for now. Occlusion is calculated from Cycles.
+ bpy.data.images[gltf.data.images[gltf.data.textures[
+ pymaterial.occlusion_texture.index
+ ].source].blender_image_name].use_fake_user = True
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_material.py b/io_scene_gltf2/blender/imp/gltf2_blender_material.py
new file mode 100755
index 00000000..c9fa6927
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_material.py
@@ -0,0 +1,150 @@
+# 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.
+
+import bpy
+from .gltf2_blender_pbrMetallicRoughness import BlenderPbr
+from .gltf2_blender_KHR_materials_pbrSpecularGlossiness import BlenderKHR_materials_pbrSpecularGlossiness
+from .gltf2_blender_map_emissive import BlenderEmissiveMap
+from .gltf2_blender_map_normal import BlenderNormalMap
+from .gltf2_blender_map_occlusion import BlenderOcclusionMap
+from ..com.gltf2_blender_material_helpers import get_output_surface_input
+from ..com.gltf2_blender_material_helpers import get_preoutput_node_output
+from ..com.gltf2_blender_material_helpers import get_base_color_node
+from ...io.com.gltf2_io import MaterialPBRMetallicRoughness
+
+
+class BlenderMaterial():
+ """Blender Material."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf, material_idx, vertex_color):
+ """Material creation."""
+ pymaterial = gltf.data.materials[material_idx]
+
+ if pymaterial.name is not None:
+ name = pymaterial.name
+ else:
+ name = "Material_" + str(material_idx)
+
+ mat = bpy.data.materials.new(name)
+ pymaterial.blender_material = mat.name
+
+ if pymaterial.extensions is not None and 'KHR_materials_pbrSpecularGlossiness' in pymaterial.extensions.keys():
+ BlenderKHR_materials_pbrSpecularGlossiness.create(
+ gltf, pymaterial.extensions['KHR_materials_pbrSpecularGlossiness'], mat.name, vertex_color
+ )
+ else:
+ # create pbr material
+ if pymaterial.pbr_metallic_roughness is None:
+ # If no pbr material is set, we need to apply all default of pbr
+ pbr = {}
+ pbr["baseColorFactor"] = [1.0, 1.0, 1.0, 1.0]
+ pbr["metallicFactor"] = 1.0
+ pbr["roughnessFactor"] = 1.0
+ pymaterial.pbr_metallic_roughness = MaterialPBRMetallicRoughness.from_dict(pbr)
+ pymaterial.pbr_metallic_roughness.color_type = gltf.SIMPLE
+ pymaterial.pbr_metallic_roughness.metallic_type = gltf.SIMPLE
+
+ BlenderPbr.create(gltf, pymaterial.pbr_metallic_roughness, mat.name, vertex_color)
+
+ # add emission map if needed
+ if pymaterial.emissive_texture is not None:
+ BlenderEmissiveMap.create(gltf, material_idx)
+
+ # add normal map if needed
+ if pymaterial.normal_texture is not None:
+ BlenderNormalMap.create(gltf, material_idx)
+
+ # add occlusion map if needed
+ # will be pack, but not used
+ if pymaterial.occlusion_texture is not None:
+ BlenderOcclusionMap.create(gltf, material_idx)
+
+ if pymaterial.alpha_mode is not None and pymaterial.alpha_mode != 'OPAQUE':
+ BlenderMaterial.blender_alpha(gltf, material_idx)
+
+ @staticmethod
+ def set_uvmap(gltf, material_idx, prim, obj):
+ """Set UV Map."""
+ pymaterial = gltf.data.materials[material_idx]
+
+ node_tree = bpy.data.materials[pymaterial.blender_material].node_tree
+ uvmap_nodes = [node for node in node_tree.nodes if node.type in ['UVMAP', 'NORMAL_MAP']]
+ for uvmap_node in uvmap_nodes:
+ if uvmap_node["gltf2_texcoord"] in prim.blender_texcoord.keys():
+ uvmap_node.uv_map = prim.blender_texcoord[uvmap_node["gltf2_texcoord"]]
+
+ @staticmethod
+ def blender_alpha(gltf, material_idx):
+ """Set alpha."""
+ pymaterial = gltf.data.materials[material_idx]
+ material = bpy.data.materials[pymaterial.blender_material]
+
+ node_tree = material.node_tree
+ # Add nodes for basic transparency
+ # Add mix shader between output and Principled BSDF
+ trans = node_tree.nodes.new('ShaderNodeBsdfTransparent')
+ trans.location = 750, -500
+ mix = node_tree.nodes.new('ShaderNodeMixShader')
+ mix.location = 1000, 0
+
+ output_surface_input = get_output_surface_input(node_tree)
+ preoutput_node_output = get_preoutput_node_output(node_tree)
+
+ link = output_surface_input.links[0]
+ node_tree.links.remove(link)
+
+ # PBR => Mix input 1
+ node_tree.links.new(preoutput_node_output, mix.inputs[1])
+
+ # Trans => Mix input 2
+ node_tree.links.new(trans.outputs['BSDF'], mix.inputs[2])
+
+ # Mix => Output
+ node_tree.links.new(mix.outputs['Shader'], output_surface_input)
+
+ # alpha blend factor
+ add = node_tree.nodes.new('ShaderNodeMath')
+ add.operation = 'ADD'
+ add.location = 750, -250
+
+ diffuse_factor = 1.0
+ if pymaterial.extensions is not None and 'KHR_materials_pbrSpecularGlossiness' in pymaterial.extensions:
+ diffuse_factor = pymaterial.extensions['KHR_materials_pbrSpecularGlossiness']['diffuseFactor'][3]
+ elif pymaterial.pbr_metallic_roughness:
+ diffuse_factor = pymaterial.pbr_metallic_roughness.base_color_factor[3]
+
+ add.inputs[0].default_value = abs(1.0 - diffuse_factor)
+ add.inputs[1].default_value = 0.0
+ node_tree.links.new(add.outputs['Value'], mix.inputs[0])
+
+ # Take diffuse texture alpha into account if any
+ diffuse_texture = get_base_color_node(node_tree)
+ if diffuse_texture:
+ inverter = node_tree.nodes.new('ShaderNodeInvert')
+ inverter.location = 250, -250
+ inverter.inputs[1].default_value = (1.0, 1.0, 1.0, 1.0)
+ node_tree.links.new(diffuse_texture.outputs['Alpha'], inverter.inputs[0])
+
+ mult = node_tree.nodes.new('ShaderNodeMath')
+ mult.operation = 'MULTIPLY' if pymaterial.alpha_mode == 'BLEND' else 'GREATER_THAN'
+ mult.location = 500, -250
+ alpha_cutoff = 1.0 if pymaterial.alpha_mode == 'BLEND' else \
+ 1.0 - pymaterial.alpha_cutoff if pymaterial.alpha_cutoff is not None else 0.5
+ mult.inputs[1].default_value = alpha_cutoff
+ node_tree.links.new(inverter.outputs['Color'], mult.inputs[0])
+ node_tree.links.new(mult.outputs['Value'], add.inputs[0])
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py
new file mode 100755
index 00000000..88396455
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py
@@ -0,0 +1,158 @@
+# 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.
+
+import bpy
+import bmesh
+
+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():
+ """Blender Mesh."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf, mesh_idx, node_idx, parent):
+ """Mesh creation."""
+ pymesh = gltf.data.meshes[mesh_idx]
+
+ # Geometry
+ if pymesh.name:
+ mesh_name = pymesh.name
+ else:
+ mesh_name = "Mesh_" + str(mesh_idx)
+
+ mesh = bpy.data.meshes.new(mesh_name)
+ verts = []
+ edges = []
+ faces = []
+ for prim in pymesh.primitives:
+ verts, edges, faces = BlenderPrimitive.create(gltf, prim, verts, edges, faces)
+
+ mesh.from_pydata(verts, edges, faces)
+ mesh.validate()
+
+ pymesh.blender_name = mesh.name
+
+ return mesh
+
+ @staticmethod
+ def set_mesh(gltf, pymesh, mesh, obj):
+ """Set all data after mesh creation."""
+ # Normals
+ offset = 0
+ for prim in pymesh.primitives:
+ offset = BlenderPrimitive.set_normals(gltf, prim, mesh, offset)
+
+ mesh.update()
+
+ # manage UV
+ offset = 0
+ for prim in pymesh.primitives:
+ offset = BlenderPrimitive.set_UV(gltf, prim, obj, mesh, offset)
+
+ mesh.update()
+
+ # Object and UV are now created, we can set UVMap into material
+ for prim in pymesh.primitives:
+ BlenderPrimitive.set_UV_in_mat(gltf, prim, obj)
+
+ # Assign materials to mesh
+ offset = 0
+ cpt_index_mat = 0
+ bm = bmesh.new()
+ bm.from_mesh(obj.data)
+ bm.faces.ensure_lookup_table()
+ for prim in pymesh.primitives:
+ offset, cpt_index_mat = BlenderPrimitive.assign_material(gltf, prim, obj, bm, offset, cpt_index_mat)
+
+ bm.to_mesh(obj.data)
+ bm.free()
+
+ # Create shapekeys if needed
+ max_shape_to_create = 0
+ for prim in pymesh.primitives:
+ if prim.targets:
+ if len(prim.targets) > max_shape_to_create:
+ max_shape_to_create = len(prim.targets)
+
+ # Create basis shape key
+ if max_shape_to_create > 0:
+ obj.shape_key_add(name="Basis")
+
+ for i in range(max_shape_to_create):
+
+ obj.shape_key_add(name="target_" + str(i))
+
+ offset_idx = 0
+ for prim in pymesh.primitives:
+ if prim.targets is None:
+ continue
+ if i >= len(prim.targets):
+ continue
+
+ bm = bmesh.new()
+ bm.from_mesh(mesh)
+
+ shape_layer = bm.verts.layers.shape[i + 1]
+
+ pos = BinaryData.get_data_from_accessor(gltf, prim.targets[i]['POSITION'])
+
+ for vert in bm.verts:
+ if vert.index not in range(offset_idx, offset_idx + prim.vertices_length):
+ continue
+
+ shape = vert[shape_layer]
+
+ co = loc_gltf_to_blender(list(pos[vert.index - offset_idx]))
+ shape.x = obj.data.vertices[vert.index].co.x + co[0]
+ shape.y = obj.data.vertices[vert.index].co.y + co[1]
+ shape.z = obj.data.vertices[vert.index].co.z + co[2]
+
+ bm.to_mesh(obj.data)
+ bm.free()
+ offset_idx += prim.vertices_length
+
+ # set default weights for shape keys, and names
+ if pymesh.weights is not None:
+ for i in range(max_shape_to_create):
+ if i < len(pymesh.weights):
+ obj.data.shape_keys.key_blocks[i + 1].value = pymesh.weights[i]
+ if gltf.data.accessors[pymesh.primitives[0].targets[i]['POSITION']].name is not None:
+ obj.data.shape_keys.key_blocks[i + 1].name = \
+ gltf.data.accessors[pymesh.primitives[0].targets[i]['POSITION']].name
+
+ # Apply vertex color.
+ vertex_color = None
+ offset = 0
+ for prim in pymesh.primitives:
+ if 'COLOR_0' in prim.attributes.keys():
+ # Create vertex color, once only per object
+ if vertex_color is None:
+ vertex_color = obj.data.vertex_colors.new("COLOR_0")
+
+ color_data = BinaryData.get_data_from_accessor(gltf, prim.attributes['COLOR_0'])
+
+ for poly in mesh.polygons:
+ for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total):
+ vert_idx = mesh.loops[loop_idx].vertex_index
+ if vert_idx in range(offset, offset + prim.vertices_length):
+ cpt_idx = vert_idx - offset
+ vertex_color.data[loop_idx].color = color_data[cpt_idx][0:3]
+ # TODO : no alpha in vertex color
+ offset = offset + prim.vertices_length
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_node.py b/io_scene_gltf2/blender/imp/gltf2_blender_node.py
new file mode 100755
index 00000000..a82f1db7
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_node.py
@@ -0,0 +1,184 @@
+# 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.
+
+import bpy
+from .gltf2_blender_mesh import BlenderMesh
+from .gltf2_blender_camera import BlenderCamera
+from .gltf2_blender_skin import BlenderSkin
+from ..com.gltf2_blender_conversion import scale_to_matrix, matrix_gltf_to_blender
+
+
+class BlenderNode():
+ """Blender Node."""
+ def __new__(cls, *args, **kwargs):
+ 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
+
+ if pynode.mesh is not None:
+
+ if gltf.data.meshes[pynode.mesh].blender_name is not None:
+ # Mesh is already created, only create instance
+ mesh = bpy.data.meshes[gltf.data.meshes[pynode.mesh].blender_name]
+ else:
+ 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.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)
+
+ obj = bpy.data.objects.new(name, mesh)
+ obj.rotation_mode = 'QUATERNION'
+ 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 and pynode.skin is not None):
+ BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent)
+ pynode.blender_object = obj.name
+ BlenderNode.set_parent(gltf, pynode, obj, parent)
+
+ BlenderMesh.set_mesh(gltf, gltf.data.meshes[pynode.mesh], mesh, obj)
+
+ if pynode.children:
+ for child_idx in pynode.children:
+ BlenderNode.create(gltf, child_idx, node_idx)
+
+ return
+
+ 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)
+ BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent) # TODO default rotation of cameras ?
+ pynode.blender_object = obj.name
+ BlenderNode.set_parent(gltf, pynode, obj, parent)
+
+ 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
+
+ # No mesh, no camera. 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)
+ obj.rotation_mode = 'QUATERNION'
+ 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, pynode, obj, parent)
+
+ if pynode.children:
+ for child_idx in pynode.children:
+ BlenderNode.create(gltf, child_idx, node_idx)
+
+ @staticmethod
+ def set_parent(gltf, pynode, 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.scene.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):
+ """Set transforms."""
+ if parent is None:
+ obj.matrix_world = matrix_gltf_to_blender(pynode.transform)
+ 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)
+ return
+ else:
+ obj.matrix_world = matrix_gltf_to_blender(pynode.transform)
+ return
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py b/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py
new file mode 100755
index 00000000..cf1f48c1
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py
@@ -0,0 +1,347 @@
+# 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.
+
+import bpy
+from .gltf2_blender_texture import BlenderTextureInfo
+
+
+class BlenderPbr():
+ """Blender Pbr."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ def create(gltf, pypbr, mat_name, vertex_color):
+ """Pbr creation."""
+ engine = bpy.context.scene.render.engine
+ if engine in ['CYCLES', 'BLENDER_EEVEE']:
+ BlenderPbr.create_nodetree(gltf, pypbr, mat_name, vertex_color)
+
+ def create_nodetree(gltf, pypbr, mat_name, vertex_color):
+ """Nodetree creation."""
+ material = bpy.data.materials[mat_name]
+ material.use_nodes = True
+ node_tree = material.node_tree
+
+ # If there is no diffuse texture, but only a color, wihtout
+ # vertex_color, we set this color in viewport color
+ if pypbr.color_type == gltf.SIMPLE and not vertex_color:
+ material.diffuse_color = pypbr.base_color_factor[:3]
+
+ # delete all nodes except output
+ for node in list(node_tree.nodes):
+ if not node.type == 'OUTPUT_MATERIAL':
+ node_tree.nodes.remove(node)
+
+ output_node = node_tree.nodes[0]
+ output_node.location = 1250, 0
+
+ # create PBR node
+ principled = node_tree.nodes.new('ShaderNodeBsdfPrincipled')
+ principled.location = 0, 0
+
+ if pypbr.color_type == gltf.SIMPLE:
+
+ if not vertex_color:
+
+ # change input values
+ principled.inputs[0].default_value = pypbr.base_color_factor
+ # TODO : currently set metallic & specular in same way
+ principled.inputs[5].default_value = pypbr.metallic_factor
+ principled.inputs[7].default_value = pypbr.roughness_factor
+
+ else:
+ # Create attribute node to get COLOR_0 data
+ attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+ attribute_node.attribute_name = 'COLOR_0'
+ attribute_node.location = -500, 0
+
+ # TODO : currently set metallic & specular in same way
+ principled.inputs[5].default_value = pypbr.metallic_factor
+ principled.inputs[7].default_value = pypbr.roughness_factor
+
+ # links
+ rgb_node = node_tree.nodes.new('ShaderNodeMixRGB')
+ rgb_node.blend_type = 'MULTIPLY'
+ rgb_node.inputs['Fac'].default_value = 1.0
+ rgb_node.inputs['Color1'].default_value = pypbr.base_color_factor
+ node_tree.links.new(rgb_node.inputs['Color2'], attribute_node.outputs[0])
+ node_tree.links.new(principled.inputs[0], rgb_node.outputs[0])
+
+ elif pypbr.color_type == gltf.TEXTURE_FACTOR:
+
+ # TODO alpha ?
+ if vertex_color:
+ # TODO tree locations
+ # Create attribute / separate / math nodes
+ attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+ attribute_node.attribute_name = 'COLOR_0'
+
+ separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB')
+ math_vc_R = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_R.operation = 'MULTIPLY'
+
+ math_vc_G = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_G.operation = 'MULTIPLY'
+
+ math_vc_B = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_B.operation = 'MULTIPLY'
+
+ BlenderTextureInfo.create(gltf, pypbr.base_color_texture.index)
+
+ # create UV Map / Mapping / Texture nodes / separate & math and combine
+ text_node = node_tree.nodes.new('ShaderNodeTexImage')
+ text_node.image = bpy.data.images[gltf.data.images[
+ gltf.data.textures[pypbr.base_color_texture.index].source
+ ].blender_image_name]
+ text_node.label = 'BASE COLOR'
+ text_node.location = -1000, 500
+
+ combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+ combine.location = -250, 500
+
+ math_R = node_tree.nodes.new('ShaderNodeMath')
+ math_R.location = -500, 750
+ math_R.operation = 'MULTIPLY'
+ math_R.inputs[1].default_value = pypbr.base_color_factor[0]
+
+ math_G = node_tree.nodes.new('ShaderNodeMath')
+ math_G.location = -500, 500
+ math_G.operation = 'MULTIPLY'
+ math_G.inputs[1].default_value = pypbr.base_color_factor[1]
+
+ math_B = node_tree.nodes.new('ShaderNodeMath')
+ math_B.location = -500, 250
+ math_B.operation = 'MULTIPLY'
+ math_B.inputs[1].default_value = pypbr.base_color_factor[2]
+
+ separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+ separate.location = -750, 500
+
+ mapping = node_tree.nodes.new('ShaderNodeMapping')
+ mapping.location = -1500, 500
+
+ uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+ uvmap.location = -2000, 500
+ if pypbr.base_color_texture.tex_coord is not None:
+ uvmap["gltf2_texcoord"] = pypbr.base_color_texture.tex_coord # Set custom flag to retrieve TexCoord
+ else:
+ uvmap["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here
+ # UV Map will be set after object/UVMap creation
+
+ # Create links
+ if vertex_color:
+ node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0])
+ node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0])
+ node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1])
+ node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2])
+ node_tree.links.new(math_vc_R.inputs[0], math_R.outputs[0])
+ node_tree.links.new(math_vc_G.inputs[0], math_G.outputs[0])
+ node_tree.links.new(math_vc_B.inputs[0], math_B.outputs[0])
+ node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0])
+ node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0])
+ node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0])
+
+ else:
+ node_tree.links.new(combine.inputs[0], math_R.outputs[0])
+ node_tree.links.new(combine.inputs[1], math_G.outputs[0])
+ node_tree.links.new(combine.inputs[2], math_B.outputs[0])
+
+ # Common for both mode (non vertex color / vertex color)
+ node_tree.links.new(math_R.inputs[0], separate.outputs[0])
+ node_tree.links.new(math_G.inputs[0], separate.outputs[1])
+ node_tree.links.new(math_B.inputs[0], separate.outputs[2])
+
+ node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+ node_tree.links.new(text_node.inputs[0], mapping.outputs[0])
+ node_tree.links.new(separate.inputs[0], text_node.outputs[0])
+
+ node_tree.links.new(principled.inputs[0], combine.outputs[0])
+
+ elif pypbr.color_type == gltf.TEXTURE:
+
+ BlenderTextureInfo.create(gltf, pypbr.base_color_texture.index)
+
+ # TODO alpha ?
+ if vertex_color:
+ # Create attribute / separate / math nodes
+ attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+ attribute_node.attribute_name = 'COLOR_0'
+ attribute_node.location = -2000, 250
+
+ separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB')
+ separate_vertex_color.location = -1500, 250
+
+ math_vc_R = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_R.operation = 'MULTIPLY'
+ math_vc_R.location = -1000, 750
+
+ math_vc_G = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_G.operation = 'MULTIPLY'
+ math_vc_G.location = -1000, 500
+
+ math_vc_B = node_tree.nodes.new('ShaderNodeMath')
+ math_vc_B.operation = 'MULTIPLY'
+ math_vc_B.location = -1000, 250
+
+ combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+ combine.location = -500, 500
+
+ separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+ separate.location = -1500, 500
+
+ # create UV Map / Mapping / Texture nodes / separate & math and combine
+ text_node = node_tree.nodes.new('ShaderNodeTexImage')
+ text_node.image = bpy.data.images[gltf.data.images[
+ gltf.data.textures[pypbr.base_color_texture.index].source
+ ].blender_image_name]
+ text_node.label = 'BASE COLOR'
+ if vertex_color:
+ text_node.location = -2000, 500
+ else:
+ text_node.location = -500, 500
+
+ mapping = node_tree.nodes.new('ShaderNodeMapping')
+ if vertex_color:
+ mapping.location = -2500, 500
+ else:
+ mapping.location = -1500, 500
+
+ uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+ if vertex_color:
+ uvmap.location = -3000, 500
+ else:
+ uvmap.location = -2000, 500
+ if pypbr.base_color_texture.tex_coord is not None:
+ uvmap["gltf2_texcoord"] = pypbr.base_color_texture.tex_coord # Set custom flag to retrieve TexCoord
+ else:
+ uvmap["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here
+ # UV Map will be set after object/UVMap creation
+
+ # Create links
+ if vertex_color:
+ node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0])
+
+ node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0])
+ node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1])
+ node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2])
+
+ node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0])
+ node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0])
+ node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0])
+
+ node_tree.links.new(separate.inputs[0], text_node.outputs[0])
+
+ node_tree.links.new(principled.inputs[0], combine.outputs[0])
+
+ node_tree.links.new(math_vc_R.inputs[0], separate.outputs[0])
+ node_tree.links.new(math_vc_G.inputs[0], separate.outputs[1])
+ node_tree.links.new(math_vc_B.inputs[0], separate.outputs[2])
+
+ else:
+ node_tree.links.new(principled.inputs[0], text_node.outputs[0])
+
+ # Common for both mode (non vertex color / vertex color)
+
+ node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+ node_tree.links.new(text_node.inputs[0], mapping.outputs[0])
+
+ # Says metallic, but it means metallic & Roughness values
+ if pypbr.metallic_type == gltf.SIMPLE:
+ principled.inputs[4].default_value = pypbr.metallic_factor
+ principled.inputs[7].default_value = pypbr.roughness_factor
+
+ elif pypbr.metallic_type == gltf.TEXTURE:
+ BlenderTextureInfo.create(gltf, pypbr.metallic_roughness_texture.index)
+ metallic_text = node_tree.nodes.new('ShaderNodeTexImage')
+ metallic_text.image = bpy.data.images[gltf.data.images[
+ gltf.data.textures[pypbr.metallic_roughness_texture.index].source
+ ].blender_image_name]
+ metallic_text.color_space = 'NONE'
+ metallic_text.label = 'METALLIC ROUGHNESS'
+ metallic_text.location = -500, 0
+
+ metallic_separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+ metallic_separate.location = -250, 0
+
+ metallic_mapping = node_tree.nodes.new('ShaderNodeMapping')
+ metallic_mapping.location = -1000, 0
+
+ metallic_uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+ metallic_uvmap.location = -1500, 0
+ if pypbr.metallic_roughness_texture.tex_coord is not None:
+ # Set custom flag to retrieve TexCoord
+ metallic_uvmap["gltf2_texcoord"] = pypbr.metallic_roughness_texture.tex_coord
+ else:
+ metallic_uvmap["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here
+
+ # links
+ node_tree.links.new(metallic_separate.inputs[0], metallic_text.outputs[0])
+ node_tree.links.new(principled.inputs[4], metallic_separate.outputs[2]) # metallic
+ node_tree.links.new(principled.inputs[7], metallic_separate.outputs[1]) # Roughness
+
+ node_tree.links.new(metallic_mapping.inputs[0], metallic_uvmap.outputs[0])
+ node_tree.links.new(metallic_text.inputs[0], metallic_mapping.outputs[0])
+
+ elif pypbr.metallic_type == gltf.TEXTURE_FACTOR:
+
+ BlenderTextureInfo.create(gltf, pypbr.metallic_roughness_texture.index)
+ metallic_text = node_tree.nodes.new('ShaderNodeTexImage')
+ metallic_text.image = bpy.data.images[gltf.data.images[
+ gltf.data.textures[pypbr.metallic_roughness_texture.index].source
+ ].blender_image_name]
+ metallic_text.color_space = 'NONE'
+ metallic_text.label = 'METALLIC ROUGHNESS'
+ metallic_text.location = -1000, 0
+
+ metallic_separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+ metallic_separate.location = -500, 0
+
+ metallic_math = node_tree.nodes.new('ShaderNodeMath')
+ metallic_math.operation = 'MULTIPLY'
+ metallic_math.inputs[1].default_value = pypbr.metallic_factor
+ metallic_math.location = -250, 100
+
+ roughness_math = node_tree.nodes.new('ShaderNodeMath')
+ roughness_math.operation = 'MULTIPLY'
+ roughness_math.inputs[1].default_value = pypbr.roughness_factor
+ roughness_math.location = -250, -100
+
+ metallic_mapping = node_tree.nodes.new('ShaderNodeMapping')
+ metallic_mapping.location = -1000, 0
+
+ metallic_uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+ metallic_uvmap.location = -1500, 0
+ if pypbr.metallic_roughness_texture.tex_coord is not None:
+ # Set custom flag to retrieve TexCoord
+ metallic_uvmap["gltf2_texcoord"] = pypbr.metallic_roughness_texture.tex_coord
+ else:
+ metallic_uvmap["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here
+
+ # links
+ node_tree.links.new(metallic_separate.inputs[0], metallic_text.outputs[0])
+
+ # metallic
+ node_tree.links.new(metallic_math.inputs[0], metallic_separate.outputs[2])
+ node_tree.links.new(principled.inputs[4], metallic_math.outputs[0])
+
+ # roughness
+ node_tree.links.new(roughness_math.inputs[0], metallic_separate.outputs[1])
+ node_tree.links.new(principled.inputs[7], roughness_math.outputs[0])
+
+ node_tree.links.new(metallic_mapping.inputs[0], metallic_uvmap.outputs[0])
+ node_tree.links.new(metallic_text.inputs[0], metallic_mapping.outputs[0])
+
+ # link node to output
+ node_tree.links.new(output_node.inputs[0], principled.outputs[0])
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py b/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py
new file mode 100755
index 00000000..bc12f437
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py
@@ -0,0 +1,170 @@
+# 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.
+
+import bpy
+from mathutils import Vector
+
+from .gltf2_blender_material import BlenderMaterial
+from ..com.gltf2_blender_conversion import loc_gltf_to_blender
+from ...io.imp.gltf2_io_binary import BinaryData
+
+
+class BlenderPrimitive():
+ """Blender Primitive."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf, pyprimitive, verts, edges, faces):
+ """Primitive creation."""
+ pyprimitive.blender_texcoord = {}
+
+ # TODO mode of primitive 4 for now.
+ current_length = len(verts)
+ pos = BinaryData.get_data_from_accessor(gltf, pyprimitive.attributes['POSITION'])
+ if pyprimitive.indices is not None:
+ indices = BinaryData.get_data_from_accessor(gltf, pyprimitive.indices)
+ else:
+ indices = []
+ indices_ = range(0, len(pos))
+ for i in indices_:
+ indices.append((i,))
+
+ prim_verts = [loc_gltf_to_blender(vert) for vert in pos]
+ pyprimitive.vertices_length = len(prim_verts)
+ verts.extend(prim_verts)
+ prim_faces = []
+ for i in range(0, len(indices), 3):
+ vals = indices[i:i + 3]
+ new_vals = []
+ for y in vals:
+ new_vals.append(y[0] + current_length)
+ prim_faces.append(tuple(new_vals))
+ faces.extend(prim_faces)
+ pyprimitive.faces_length = len(prim_faces)
+
+ # manage material of primitive
+ if pyprimitive.material is not None:
+
+ # Create Blender material
+ # TODO, a same material can have difference COLOR_0 multiplicator
+ if gltf.data.materials[pyprimitive.material].blender_material is None:
+ vertex_color = None
+ if 'COLOR_0' in pyprimitive.attributes.keys():
+ vertex_color = pyprimitive.attributes['COLOR_0']
+ BlenderMaterial.create(gltf, pyprimitive.material, vertex_color)
+
+ return verts, edges, faces
+
+ def set_normals(gltf, pyprimitive, mesh, offset):
+ """Set Normal."""
+ if 'NORMAL' in pyprimitive.attributes.keys():
+ normal_data = BinaryData.get_data_from_accessor(gltf, pyprimitive.attributes['NORMAL'])
+ for poly in mesh.polygons:
+ if gltf.import_settings['import_shading'] == "NORMALS":
+ calc_norm_vertices = []
+ for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total):
+ vert_idx = mesh.loops[loop_idx].vertex_index
+ if vert_idx in range(offset, offset + pyprimitive.vertices_length):
+ cpt_vert = vert_idx - offset
+ mesh.vertices[vert_idx].normal = normal_data[cpt_vert]
+ calc_norm_vertices.append(vert_idx)
+
+ if len(calc_norm_vertices) == 3:
+ # Calcul normal
+ vert0 = mesh.vertices[calc_norm_vertices[0]].co
+ vert1 = mesh.vertices[calc_norm_vertices[1]].co
+ vert2 = mesh.vertices[calc_norm_vertices[2]].co
+ calc_normal = (vert1 - vert0).cross(vert2 - vert0).normalized()
+
+ # Compare normal to vertex normal
+ for i in calc_norm_vertices:
+ cpt_vert = vert_idx - offset
+ vec = Vector(
+ (normal_data[cpt_vert][0], normal_data[cpt_vert][1], normal_data[cpt_vert][2])
+ )
+ if not calc_normal.dot(vec) > 0.9999999:
+ poly.use_smooth = True
+ break
+ elif gltf.import_settings['import_shading'] == "FLAT":
+ poly.use_smooth = False
+ elif gltf.import_settings['import_shading'] == "SMOOTH":
+ poly.use_smooth = True
+ else:
+ pass # Should not happend
+
+ offset = offset + pyprimitive.vertices_length
+ return offset
+
+ def set_UV(gltf, pyprimitive, obj, mesh, offset):
+ """Set UV Map."""
+ for texcoord in [attr for attr in pyprimitive.attributes.keys() if attr[:9] == "TEXCOORD_"]:
+ if texcoord not in mesh.uv_layers:
+ mesh.uv_layers.new(name=texcoord)
+ pyprimitive.blender_texcoord[int(texcoord[9:])] = texcoord
+
+ texcoord_data = BinaryData.get_data_from_accessor(gltf, pyprimitive.attributes[texcoord])
+ for poly in mesh.polygons:
+ for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total):
+ vert_idx = mesh.loops[loop_idx].vertex_index
+ if vert_idx in range(offset, offset + pyprimitive.vertices_length):
+ obj.data.uv_layers[texcoord].data[loop_idx].uv = \
+ Vector((texcoord_data[vert_idx - offset][0], 1 - texcoord_data[vert_idx - offset][1]))
+
+ offset = offset + pyprimitive.vertices_length
+ return offset
+
+ def set_UV_in_mat(gltf, pyprimitive, obj):
+ """After nodetree creation, set UVMap in nodes."""
+ if pyprimitive.material is None:
+ return
+ if gltf.data.materials[pyprimitive.material].extensions \
+ and "KHR_materials_pbrSpecularGlossiness" in \
+ gltf.data.materials[pyprimitive.material].extensions.keys():
+ if pyprimitive.material is not None \
+ and gltf.data.materials[pyprimitive.material].extensions[
+ 'KHR_materials_pbrSpecularGlossiness'
+ ]['diffuse_type'] in [gltf.TEXTURE, gltf.TEXTURE_FACTOR]:
+ BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj)
+ else:
+ if pyprimitive.material is not None \
+ and gltf.data.materials[pyprimitive.material].extensions[
+ 'KHR_materials_pbrSpecularGlossiness'
+ ]['specgloss_type'] in [gltf.TEXTURE, gltf.TEXTURE_FACTOR]:
+ BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj)
+
+ else:
+ if pyprimitive.material is not None \
+ and gltf.data.materials[pyprimitive.material].pbr_metallic_roughness.color_type in \
+ [gltf.TEXTURE, gltf.TEXTURE_FACTOR]:
+ BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj)
+ else:
+ if pyprimitive.material is not None \
+ and gltf.data.materials[pyprimitive.material].pbr_metallic_roughness.metallic_type in \
+ [gltf.TEXTURE, gltf.TEXTURE_FACTOR]:
+ BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj)
+
+ def assign_material(gltf, pyprimitive, obj, bm, offset, cpt_index_mat):
+ """Assign material to faces of primitives."""
+ if pyprimitive.material is not None:
+ obj.data.materials.append(bpy.data.materials[gltf.data.materials[pyprimitive.material].blender_material])
+ for vert in bm.verts:
+ if vert.index in range(offset, offset + pyprimitive.vertices_length):
+ for loop in vert.link_loops:
+ face = loop.face.index
+ bm.faces[face].material_index = cpt_index_mat
+ cpt_index_mat += 1
+ offset = offset + pyprimitive.vertices_length
+ return offset, cpt_index_mat
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_scene.py b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py
new file mode 100755
index 00000000..c5e89659
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py
@@ -0,0 +1,94 @@
+# 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.
+
+import bpy
+from math import sqrt
+from mathutils import Quaternion
+from .gltf2_blender_node import BlenderNode
+from .gltf2_blender_skin import BlenderSkin
+from .gltf2_blender_animation import BlenderAnimation
+
+
+class BlenderScene():
+ """Blender Scene."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf, scene_idx):
+ """Scene creation."""
+ pyscene = gltf.data.scenes[scene_idx]
+
+ # 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.app.version < (2, 80, 0):
+ scene.render.engine = "CYCLES"
+ else:
+ scene.render.engine = "BLENDER_EEVEE"
+
+ gltf.blender_scene = scene.name
+ else:
+ gltf.blender_scene = pyscene.name
+
+ # 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))
+
+ bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj_rotation)
+
+ if pyscene.nodes is not None:
+ for node_idx in pyscene.nodes:
+ BlenderNode.create(gltf, node_idx, None) # None => No parent
+
+ # 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)
+
+ for skin_id, skin in enumerate(gltf.data.skins):
+ if hasattr(skin, "node_ids"):
+ BlenderSkin.assign_vertex_groups(gltf, skin_id)
+
+ for skin_id, skin in enumerate(gltf.data.skins):
+ if hasattr(skin, "node_ids"):
+ BlenderSkin.create_armature_modifiers(gltf, skin_id)
+
+ if gltf.data.animations:
+ for anim_idx, anim in enumerate(gltf.data.animations):
+ for node_idx in pyscene.nodes:
+ BlenderAnimation.anim(gltf, anim_idx, node_idx)
+
+ # Parent root node to rotation object
+ if pyscene.nodes is not None:
+ for node_idx in pyscene.nodes:
+ bpy.data.objects[gltf.data.nodes[node_idx].blender_object].parent = obj_rotation
+
+
+ for node_idx in pyscene.nodes:
+ for obj_ in bpy.context.scene.objects:
+ obj_.select_set(False)
+ 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')
+
+ bpy.context.scene.collection.objects.unlink(obj_rotation)
+ bpy.data.objects.remove(obj_rotation)
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_skin.py b/io_scene_gltf2/blender/imp/gltf2_blender_skin.py
new file mode 100755
index 00000000..db0e50f9
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_skin.py
@@ -0,0 +1,209 @@
+# 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.
+
+
+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
+
+
+class BlenderSkin():
+ """Blender Skinning / Armature."""
+ def __new__(cls, *args, **kwargs):
+ 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)
+ 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)
+ 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:
+ 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
+ obj.pose.bones[pynode.blender_bone_name].rotation_quaternion = \
+ (bind_rotation.to_matrix().to_4x4().inverted() @ parent_mat @
+ rotation.to_matrix().to_4x4()).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, 0.0)) # Needed to keep bone alive
+
+ # set bind and pose transforms
+ BlenderSkin.set_bone_transforms(gltf, skin_id, bone, node_id, parent)
+ bpy.ops.object.mode_set(mode="OBJECT")
+
+ @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 assign_vertex_groups(gltf, skin_id):
+ """Assign vertex groups to vertices."""
+ pyskin = gltf.data.skins[skin_id]
+ for node_id in pyskin.node_ids:
+ node = gltf.data.nodes[node_id]
+ obj = bpy.data.objects[node.blender_object]
+
+ offset = 0
+ for prim in gltf.data.meshes[node.mesh].primitives:
+ idx_already_done = {}
+
+ if 'JOINTS_0' in prim.attributes.keys() and 'WEIGHTS_0' in prim.attributes.keys():
+ joint_ = BinaryData.get_data_from_accessor(gltf, prim.attributes['JOINTS_0'])
+ weight_ = BinaryData.get_data_from_accessor(gltf, prim.attributes['WEIGHTS_0'])
+
+ for poly in obj.data.polygons:
+ for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total):
+ vert_idx = obj.data.loops[loop_idx].vertex_index
+
+ if vert_idx in idx_already_done.keys():
+ continue
+ idx_already_done[vert_idx] = True
+
+ if vert_idx in range(offset, offset + prim.vertices_length):
+
+ tab_index = vert_idx - offset
+ cpt = 0
+ for joint_idx in joint_[tab_index]:
+ weight_val = weight_[tab_index][cpt]
+ if weight_val != 0.0: # It can be a problem to assign weights of 0
+ # for bone index 0, if there is always 4 indices in joint_
+ # tuple
+ group = obj.vertex_groups[gltf.data.nodes[
+ pyskin.joints[joint_idx]
+ ].blender_bone_name]
+ group.add([vert_idx], weight_val, 'REPLACE')
+ cpt += 1
+ else:
+ gltf.log.error("No Skinning ?????") # TODO
+
+ offset = offset + prim.vertices_length
+
+ @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]
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_texture.py b/io_scene_gltf2/blender/imp/gltf2_blender_texture.py
new file mode 100755
index 00000000..c8983d9c
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_texture.py
@@ -0,0 +1,39 @@
+# 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.
+
+from .gltf2_blender_image import BlenderImage
+
+
+class BlenderTextureInfo():
+ """Blender Texture info."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf, pytextureinfo_idx):
+ """Create Texture info."""
+ BlenderTexture.create(gltf, pytextureinfo_idx)
+
+
+class BlenderTexture():
+ """Blender Texture."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def create(gltf, pytexture_idx):
+ """Create texture."""
+ pytexture = gltf.data.textures[pytexture_idx]
+ BlenderImage.create(gltf, pytexture.source)
+
diff --git a/io_scene_gltf2/io/__init__.py b/io_scene_gltf2/io/__init__.py
new file mode 100755
index 00000000..10973240
--- /dev/null
+++ b/io_scene_gltf2/io/__init__.py
@@ -0,0 +1,16 @@
+# 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.
+
+from .imp import *
+
diff --git a/io_scene_gltf2/io/com/gltf2_io.py b/io_scene_gltf2/io/com/gltf2_io.py
new file mode 100755
index 00000000..1332adf6
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io.py
@@ -0,0 +1,1200 @@
+# 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.
+
+# NOTE: Generated from latest glTF 2.0 JSON Scheme specs using quicktype (https://github.com/quicktype/quicktype)
+# command used:
+# quicktype --src glTF.schema.json --src-lang schema -t gltf --lang python --python-version 3.5
+
+# TODO: add __slots__ to all classes by extending the generator
+
+# TODO: REMOVE traceback import
+import sys
+import traceback
+
+from io_scene_gltf2.io.com import gltf2_io_debug
+
+
+def from_int(x):
+ assert isinstance(x, int) and not isinstance(x, bool)
+ return x
+
+
+def from_none(x):
+ assert x is None
+ return x
+
+
+def from_union(fs, x):
+ tracebacks = []
+ for f in fs:
+ try:
+ return f(x)
+ except AssertionError:
+ _, _, tb = sys.exc_info()
+ tracebacks.append(tb)
+ for tb in tracebacks:
+ traceback.print_tb(tb) # Fixed format
+ tb_info = traceback.extract_tb(tb)
+ for tbi in tb_info:
+ filename, line, func, text = tbi
+ gltf2_io_debug.print_console('ERROR', 'An error occurred on line {} in statement {}'.format(line, text))
+ assert False
+
+
+def from_dict(f, x):
+ assert isinstance(x, dict)
+ return {k: f(v) for (k, v) in x.items()}
+
+
+def to_class(c, x):
+ assert isinstance(x, c)
+ return x.to_dict()
+
+
+def from_list(f, x):
+ assert isinstance(x, list)
+ return [f(y) for y in x]
+
+
+def from_float(x):
+ assert isinstance(x, (float, int)) and not isinstance(x, bool)
+ return float(x)
+
+
+def from_str(x):
+ assert isinstance(x, str)
+ return x
+
+
+def from_bool(x):
+ assert isinstance(x, bool)
+ return x
+
+
+def to_float(x):
+ assert isinstance(x, float)
+ return x
+
+
+class AccessorSparseIndices:
+ """Index array of size `count` that points to those accessor attributes that deviate from
+ their initialization value. Indices must strictly increase.
+
+ Indices of those attributes that deviate from their initialization value.
+ """
+
+ def __init__(self, buffer_view, byte_offset, component_type, extensions, extras):
+ self.buffer_view = buffer_view
+ self.byte_offset = byte_offset
+ self.component_type = component_type
+ self.extensions = extensions
+ self.extras = extras
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ buffer_view = from_int(obj.get("bufferView"))
+ byte_offset = from_union([from_int, from_none], obj.get("byteOffset"))
+ component_type = from_int(obj.get("componentType"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ return AccessorSparseIndices(buffer_view, byte_offset, component_type, extensions, extras)
+
+ def to_dict(self):
+ result = {}
+ result["bufferView"] = from_int(self.buffer_view)
+ result["byteOffset"] = from_union([from_int, from_none], self.byte_offset)
+ result["componentType"] = from_int(self.component_type)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ return result
+
+
+class AccessorSparseValues:
+ """Array of size `count` times number of components, storing the displaced accessor
+ attributes pointed by `indices`. Substituted values must have the same `componentType`
+ and number of components as the base accessor.
+
+ Array of size `accessor.sparse.count` times number of components storing the displaced
+ accessor attributes pointed by `accessor.sparse.indices`.
+ """
+
+ def __init__(self, buffer_view, byte_offset, extensions, extras):
+ self.buffer_view = buffer_view
+ self.byte_offset = byte_offset
+ self.extensions = extensions
+ self.extras = extras
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ buffer_view = from_int(obj.get("bufferView"))
+ byte_offset = from_union([from_int, from_none], obj.get("byteOffset"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ return AccessorSparseValues(buffer_view, byte_offset, extensions, extras)
+
+ def to_dict(self):
+ result = {}
+ result["bufferView"] = from_int(self.buffer_view)
+ result["byteOffset"] = from_union([from_int, from_none], self.byte_offset)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ return result
+
+
+class AccessorSparse:
+ """Sparse storage of attributes that deviate from their initialization value."""
+
+ def __init__(self, count, extensions, extras, indices, values):
+ self.count = count
+ self.extensions = extensions
+ self.extras = extras
+ self.indices = indices
+ self.values = values
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ count = from_int(obj.get("count"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ indices = AccessorSparseIndices.from_dict(obj.get("indices"))
+ values = AccessorSparseValues.from_dict(obj.get("values"))
+ return AccessorSparse(count, extensions, extras, indices, values)
+
+ def to_dict(self):
+ result = {}
+ result["count"] = from_int(self.count)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["indices"] = to_class(AccessorSparseIndices, self.indices)
+ result["values"] = to_class(AccessorSparseValues, self.values)
+ return result
+
+
+class Accessor:
+ """A typed view into a bufferView. A bufferView contains raw binary data. An accessor
+ provides a typed view into a bufferView or a subset of a bufferView similar to how
+ WebGL's `vertexAttribPointer()` defines an attribute in a buffer.
+ """
+
+ def __init__(self, buffer_view, byte_offset, component_type, count, extensions, extras, max, min, name, normalized,
+ sparse, type):
+ self.buffer_view = buffer_view
+ self.byte_offset = byte_offset
+ self.component_type = component_type
+ self.count = count
+ self.extensions = extensions
+ self.extras = extras
+ self.max = max
+ self.min = min
+ self.name = name
+ self.normalized = normalized
+ self.sparse = sparse
+ self.type = type
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ buffer_view = from_union([from_int, from_none], obj.get("bufferView"))
+ byte_offset = from_union([from_int, from_none], obj.get("byteOffset"))
+ component_type = from_int(obj.get("componentType"))
+ count = from_int(obj.get("count"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ max = from_union([lambda x: from_list(from_float, x), from_none], obj.get("max"))
+ min = from_union([lambda x: from_list(from_float, x), from_none], obj.get("min"))
+ name = from_union([from_str, from_none], obj.get("name"))
+ normalized = from_union([from_bool, from_none], obj.get("normalized"))
+ sparse = from_union([AccessorSparse.from_dict, from_none], obj.get("sparse"))
+ type = from_str(obj.get("type"))
+ return Accessor(buffer_view, byte_offset, component_type, count, extensions, extras, max, min, name, normalized,
+ sparse, type)
+
+ def to_dict(self):
+ result = {}
+ result["bufferView"] = from_union([from_int, from_none], self.buffer_view)
+ result["byteOffset"] = from_union([from_int, from_none], self.byte_offset)
+ result["componentType"] = from_int(self.component_type)
+ result["count"] = from_int(self.count)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["max"] = from_union([lambda x: from_list(to_float, x), from_none], self.max)
+ result["min"] = from_union([lambda x: from_list(to_float, x), from_none], self.min)
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["normalized"] = from_union([from_bool, from_none], self.normalized)
+ result["sparse"] = from_union([lambda x: to_class(AccessorSparse, x), from_none], self.sparse)
+ result["type"] = from_str(self.type)
+ return result
+
+
+class AnimationChannelTarget:
+ """The index of the node and TRS property to target.
+
+ The index of the node and TRS property that an animation channel targets.
+ """
+
+ def __init__(self, extensions, extras, node, path):
+ self.extensions = extensions
+ self.extras = extras
+ self.node = node
+ self.path = path
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ node = from_union([from_int, from_none], obj.get("node"))
+ path = from_str(obj.get("path"))
+ return AnimationChannelTarget(extensions, extras, node, path)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["node"] = from_union([from_int, from_none], self.node)
+ result["path"] = from_str(self.path)
+ return result
+
+
+class AnimationChannel:
+ """Targets an animation's sampler at a node's property."""
+
+ def __init__(self, extensions, extras, sampler, target):
+ self.extensions = extensions
+ self.extras = extras
+ self.sampler = sampler
+ self.target = target
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ sampler = from_int(obj.get("sampler"))
+ target = AnimationChannelTarget.from_dict(obj.get("target"))
+ return AnimationChannel(extensions, extras, sampler, target)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["sampler"] = from_int(self.sampler)
+ result["target"] = to_class(AnimationChannelTarget, self.target)
+ return result
+
+
+class AnimationSampler:
+ """Combines input and output accessors with an interpolation algorithm to define a keyframe
+ graph (but not its target).
+ """
+
+ def __init__(self, extensions, extras, input, interpolation, output):
+ self.extensions = extensions
+ self.extras = extras
+ self.input = input
+ self.interpolation = interpolation
+ self.output = output
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ input = from_int(obj.get("input"))
+ interpolation = from_union([from_str, from_none], obj.get("interpolation"))
+ output = from_int(obj.get("output"))
+ return AnimationSampler(extensions, extras, input, interpolation, output)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["input"] = from_int(self.input)
+ result["interpolation"] = from_union([from_str, from_none], self.interpolation)
+ result["output"] = from_int(self.output)
+ return result
+
+
+class Animation:
+ """A keyframe animation."""
+
+ def __init__(self, channels, extensions, extras, name, samplers):
+ self.channels = channels
+ self.extensions = extensions
+ self.extras = extras
+ self.name = name
+ self.samplers = samplers
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ channels = from_list(AnimationChannel.from_dict, obj.get("channels"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ name = from_union([from_str, from_none], obj.get("name"))
+ samplers = from_list(AnimationSampler.from_dict, obj.get("samplers"))
+ return Animation(channels, extensions, extras, name, samplers)
+
+ def to_dict(self):
+ result = {}
+ result["channels"] = from_list(lambda x: to_class(AnimationChannel, x), self.channels)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["samplers"] = from_list(lambda x: to_class(AnimationSampler, x), self.samplers)
+ return result
+
+
+class Asset:
+ """Metadata about the glTF asset."""
+
+ def __init__(self, copyright, extensions, extras, generator, min_version, version):
+ self.copyright = copyright
+ self.extensions = extensions
+ self.extras = extras
+ self.generator = generator
+ self.min_version = min_version
+ self.version = version
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ copyright = from_union([from_str, from_none], obj.get("copyright"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ generator = from_union([from_str, from_none], obj.get("generator"))
+ min_version = from_union([from_str, from_none], obj.get("minVersion"))
+ version = from_str(obj.get("version"))
+ return Asset(copyright, extensions, extras, generator, min_version, version)
+
+ def to_dict(self):
+ result = {}
+ result["copyright"] = from_union([from_str, from_none], self.copyright)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["generator"] = from_union([from_str, from_none], self.generator)
+ result["minVersion"] = from_union([from_str, from_none], self.min_version)
+ result["version"] = from_str(self.version)
+ return result
+
+
+class BufferView:
+ """A view into a buffer generally representing a subset of the buffer."""
+
+ def __init__(self, buffer, byte_length, byte_offset, byte_stride, extensions, extras, name, target):
+ self.buffer = buffer
+ self.byte_length = byte_length
+ self.byte_offset = byte_offset
+ self.byte_stride = byte_stride
+ self.extensions = extensions
+ self.extras = extras
+ self.name = name
+ self.target = target
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ buffer = from_int(obj.get("buffer"))
+ byte_length = from_int(obj.get("byteLength"))
+ byte_offset = from_union([from_int, from_none], obj.get("byteOffset"))
+ byte_stride = from_union([from_int, from_none], obj.get("byteStride"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ name = from_union([from_str, from_none], obj.get("name"))
+ target = from_union([from_int, from_none], obj.get("target"))
+ return BufferView(buffer, byte_length, byte_offset, byte_stride, extensions, extras, name, target)
+
+ def to_dict(self):
+ result = {}
+ result["buffer"] = from_int(self.buffer)
+ result["byteLength"] = from_int(self.byte_length)
+ result["byteOffset"] = from_union([from_int, from_none], self.byte_offset)
+ result["byteStride"] = from_union([from_int, from_none], self.byte_stride)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["target"] = from_union([from_int, from_none], self.target)
+ return result
+
+
+class Buffer:
+ """A buffer points to binary geometry, animation, or skins."""
+
+ def __init__(self, byte_length, extensions, extras, name, uri):
+ self.byte_length = byte_length
+ self.extensions = extensions
+ self.extras = extras
+ self.name = name
+ self.uri = uri
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ byte_length = from_int(obj.get("byteLength"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ name = from_union([from_str, from_none], obj.get("name"))
+ uri = from_union([from_str, from_none], obj.get("uri"))
+ return Buffer(byte_length, extensions, extras, name, uri)
+
+ def to_dict(self):
+ result = {}
+ result["byteLength"] = from_int(self.byte_length)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["uri"] = from_union([from_str, from_none], self.uri)
+ return result
+
+
+class CameraOrthographic:
+ """An orthographic camera containing properties to create an orthographic projection matrix."""
+
+ def __init__(self, extensions, extras, xmag, ymag, zfar, znear):
+ self.extensions = extensions
+ self.extras = extras
+ self.xmag = xmag
+ self.ymag = ymag
+ self.zfar = zfar
+ self.znear = znear
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ xmag = from_float(obj.get("xmag"))
+ ymag = from_float(obj.get("ymag"))
+ zfar = from_float(obj.get("zfar"))
+ znear = from_float(obj.get("znear"))
+ return CameraOrthographic(extensions, extras, xmag, ymag, zfar, znear)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["xmag"] = to_float(self.xmag)
+ result["ymag"] = to_float(self.ymag)
+ result["zfar"] = to_float(self.zfar)
+ result["znear"] = to_float(self.znear)
+ return result
+
+
+class CameraPerspective:
+ """A perspective camera containing properties to create a perspective projection matrix."""
+
+ def __init__(self, aspect_ratio, extensions, extras, yfov, zfar, znear):
+ self.aspect_ratio = aspect_ratio
+ self.extensions = extensions
+ self.extras = extras
+ self.yfov = yfov
+ self.zfar = zfar
+ self.znear = znear
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ aspect_ratio = from_union([from_float, from_none], obj.get("aspectRatio"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ yfov = from_float(obj.get("yfov"))
+ zfar = from_union([from_float, from_none], obj.get("zfar"))
+ znear = from_float(obj.get("znear"))
+ return CameraPerspective(aspect_ratio, extensions, extras, yfov, zfar, znear)
+
+ def to_dict(self):
+ result = {}
+ result["aspectRatio"] = from_union([to_float, from_none], self.aspect_ratio)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["yfov"] = to_float(self.yfov)
+ result["zfar"] = from_union([to_float, from_none], self.zfar)
+ result["znear"] = to_float(self.znear)
+ return result
+
+
+class Camera:
+ """A camera's projection. A node can reference a camera to apply a transform to place the
+ camera in the scene.
+ """
+
+ def __init__(self, extensions, extras, name, orthographic, perspective, type):
+ self.extensions = extensions
+ self.extras = extras
+ self.name = name
+ self.orthographic = orthographic
+ self.perspective = perspective
+ self.type = type
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ name = from_union([from_str, from_none], obj.get("name"))
+ orthographic = from_union([CameraOrthographic.from_dict, from_none], obj.get("orthographic"))
+ perspective = from_union([CameraPerspective.from_dict, from_none], obj.get("perspective"))
+ type = from_str(obj.get("type"))
+ return Camera(extensions, extras, name, orthographic, perspective, type)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["orthographic"] = from_union([lambda x: to_class(CameraOrthographic, x), from_none], self.orthographic)
+ result["perspective"] = from_union([lambda x: to_class(CameraPerspective, x), from_none], self.perspective)
+ result["type"] = from_str(self.type)
+ return result
+
+
+class Image:
+ """Image data used to create a texture. Image can be referenced by URI or `bufferView`
+ index. `mimeType` is required in the latter case.
+ """
+
+ def __init__(self, buffer_view, extensions, extras, mime_type, name, uri):
+ self.buffer_view = buffer_view
+ self.extensions = extensions
+ self.extras = extras
+ self.mime_type = mime_type
+ self.name = name
+ self.uri = uri
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ buffer_view = from_union([from_int, from_none], obj.get("bufferView"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ mime_type = from_union([from_str, from_none], obj.get("mimeType"))
+ name = from_union([from_str, from_none], obj.get("name"))
+ uri = from_union([from_str, from_none], obj.get("uri"))
+ return Image(buffer_view, extensions, extras, mime_type, name, uri)
+
+ def to_dict(self):
+ result = {}
+ result["bufferView"] = from_union([from_int, from_none], self.buffer_view)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["mimeType"] = from_union([from_str, from_none], self.mime_type)
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["uri"] = from_union([from_str, from_none], self.uri)
+ return result
+
+
+class TextureInfo:
+ """The emissive map texture.
+
+ The base color texture.
+
+ The metallic-roughness texture.
+
+ Reference to a texture.
+ """
+
+ def __init__(self, extensions, extras, index, tex_coord):
+ self.extensions = extensions
+ self.extras = extras
+ self.index = index
+ self.tex_coord = tex_coord
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ index = from_int(obj.get("index"))
+ tex_coord = from_union([from_int, from_none], obj.get("texCoord"))
+ return TextureInfo(extensions, extras, index, tex_coord)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["index"] = from_int(self.index)
+ result["texCoord"] = from_union([from_int, from_none], self.tex_coord)
+ return result
+
+
+class MaterialNormalTextureInfoClass:
+ """The normal map texture.
+
+ Reference to a texture.
+ """
+
+ def __init__(self, extensions, extras, index, scale, tex_coord):
+ self.extensions = extensions
+ self.extras = extras
+ self.index = index
+ self.scale = scale
+ self.tex_coord = tex_coord
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ index = from_int(obj.get("index"))
+ scale = from_union([from_float, from_none], obj.get("scale"))
+ tex_coord = from_union([from_int, from_none], obj.get("texCoord"))
+ return MaterialNormalTextureInfoClass(extensions, extras, index, scale, tex_coord)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["index"] = from_int(self.index)
+ result["scale"] = from_union([to_float, from_none], self.scale)
+ result["texCoord"] = from_union([from_int, from_none], self.tex_coord)
+ return result
+
+
+class MaterialOcclusionTextureInfoClass:
+ """The occlusion map texture.
+
+ Reference to a texture.
+ """
+
+ def __init__(self, extensions, extras, index, strength, tex_coord):
+ self.extensions = extensions
+ self.extras = extras
+ self.index = index
+ self.strength = strength
+ self.tex_coord = tex_coord
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ index = from_int(obj.get("index"))
+ strength = from_union([from_float, from_none], obj.get("strength"))
+ tex_coord = from_union([from_int, from_none], obj.get("texCoord"))
+ return MaterialOcclusionTextureInfoClass(extensions, extras, index, strength, tex_coord)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["index"] = from_int(self.index)
+ result["strength"] = from_union([to_float, from_none], self.strength)
+ result["texCoord"] = from_union([from_int, from_none], self.tex_coord)
+ return result
+
+
+class MaterialPBRMetallicRoughness:
+ """A set of parameter values that are used to define the metallic-roughness material model
+ from Physically-Based Rendering (PBR) methodology. When not specified, all the default
+ values of `pbrMetallicRoughness` apply.
+
+ A set of parameter values that are used to define the metallic-roughness material model
+ from Physically-Based Rendering (PBR) methodology.
+ """
+
+ def __init__(self, base_color_factor, base_color_texture, extensions, extras, metallic_factor,
+ metallic_roughness_texture, roughness_factor):
+ self.base_color_factor = base_color_factor
+ self.base_color_texture = base_color_texture
+ self.extensions = extensions
+ self.extras = extras
+ self.metallic_factor = metallic_factor
+ self.metallic_roughness_texture = metallic_roughness_texture
+ self.roughness_factor = roughness_factor
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ base_color_factor = from_union([lambda x: from_list(from_float, x), from_none], obj.get("baseColorFactor"))
+ base_color_texture = from_union([TextureInfo.from_dict, from_none], obj.get("baseColorTexture"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ metallic_factor = from_union([from_float, from_none], obj.get("metallicFactor"))
+ metallic_roughness_texture = from_union([TextureInfo.from_dict, from_none], obj.get("metallicRoughnessTexture"))
+ roughness_factor = from_union([from_float, from_none], obj.get("roughnessFactor"))
+ return MaterialPBRMetallicRoughness(base_color_factor, base_color_texture, extensions, extras, metallic_factor,
+ metallic_roughness_texture, roughness_factor)
+
+ def to_dict(self):
+ result = {}
+ result["baseColorFactor"] = from_union([lambda x: from_list(to_float, x), from_none], self.base_color_factor)
+ result["baseColorTexture"] = from_union([lambda x: to_class(TextureInfo, x), from_none],
+ self.base_color_texture)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["metallicFactor"] = from_union([to_float, from_none], self.metallic_factor)
+ result["metallicRoughnessTexture"] = from_union([lambda x: to_class(TextureInfo, x), from_none],
+ self.metallic_roughness_texture)
+ result["roughnessFactor"] = from_union([to_float, from_none], self.roughness_factor)
+ return result
+
+
+class Material:
+ """The material appearance of a primitive."""
+
+ def __init__(self, alpha_cutoff, alpha_mode, double_sided, emissive_factor, emissive_texture, extensions, extras,
+ name, normal_texture, occlusion_texture, pbr_metallic_roughness):
+ self.alpha_cutoff = alpha_cutoff
+ self.alpha_mode = alpha_mode
+ self.double_sided = double_sided
+ self.emissive_factor = emissive_factor
+ self.emissive_texture = emissive_texture
+ self.extensions = extensions
+ self.extras = extras
+ self.name = name
+ self.normal_texture = normal_texture
+ self.occlusion_texture = occlusion_texture
+ self.pbr_metallic_roughness = pbr_metallic_roughness
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ alpha_cutoff = from_union([from_float, from_none], obj.get("alphaCutoff"))
+ alpha_mode = from_union([from_str, from_none], obj.get("alphaMode"))
+ double_sided = from_union([from_bool, from_none], obj.get("doubleSided"))
+ emissive_factor = from_union([lambda x: from_list(from_float, x), from_none], obj.get("emissiveFactor"))
+ emissive_texture = from_union([TextureInfo.from_dict, from_none], obj.get("emissiveTexture"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ name = from_union([from_str, from_none], obj.get("name"))
+ normal_texture = from_union([MaterialNormalTextureInfoClass.from_dict, from_none], obj.get("normalTexture"))
+ occlusion_texture = from_union([MaterialOcclusionTextureInfoClass.from_dict, from_none],
+ obj.get("occlusionTexture"))
+ pbr_metallic_roughness = from_union([MaterialPBRMetallicRoughness.from_dict, from_none],
+ obj.get("pbrMetallicRoughness"))
+ return Material(alpha_cutoff, alpha_mode, double_sided, emissive_factor, emissive_texture, extensions, extras,
+ name, normal_texture, occlusion_texture, pbr_metallic_roughness)
+
+ def to_dict(self):
+ result = {}
+ result["alphaCutoff"] = from_union([to_float, from_none], self.alpha_cutoff)
+ result["alphaMode"] = from_union([from_str, from_none], self.alpha_mode)
+ result["doubleSided"] = from_union([from_bool, from_none], self.double_sided)
+ result["emissiveFactor"] = from_union([lambda x: from_list(to_float, x), from_none], self.emissive_factor)
+ result["emissiveTexture"] = from_union([lambda x: to_class(TextureInfo, x), from_none], self.emissive_texture)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["normalTexture"] = from_union([lambda x: to_class(MaterialNormalTextureInfoClass, x), from_none],
+ self.normal_texture)
+ result["occlusionTexture"] = from_union([lambda x: to_class(MaterialOcclusionTextureInfoClass, x), from_none],
+ self.occlusion_texture)
+ result["pbrMetallicRoughness"] = from_union([lambda x: to_class(MaterialPBRMetallicRoughness, x), from_none],
+ self.pbr_metallic_roughness)
+ return result
+
+
+class MeshPrimitive:
+ """Geometry to be rendered with the given material."""
+
+ def __init__(self, attributes, extensions, extras, indices, material, mode, targets):
+ self.attributes = attributes
+ self.extensions = extensions
+ self.extras = extras
+ self.indices = indices
+ self.material = material
+ self.mode = mode
+ self.targets = targets
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ attributes = from_dict(from_int, obj.get("attributes"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ indices = from_union([from_int, from_none], obj.get("indices"))
+ material = from_union([from_int, from_none], obj.get("material"))
+ mode = from_union([from_int, from_none], obj.get("mode"))
+ targets = from_union([lambda x: from_list(lambda x: from_dict(from_int, x), x), from_none], obj.get("targets"))
+ return MeshPrimitive(attributes, extensions, extras, indices, material, mode, targets)
+
+ def to_dict(self):
+ result = {}
+ result["attributes"] = from_dict(from_int, self.attributes)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["indices"] = from_union([from_int, from_none], self.indices)
+ result["material"] = from_union([from_int, from_none], self.material)
+ result["mode"] = from_union([from_int, from_none], self.mode)
+ result["targets"] = from_union([lambda x: from_list(lambda x: from_dict(from_int, x), x), from_none],
+ self.targets)
+ return result
+
+
+class Mesh:
+ """A set of primitives to be rendered. A node can contain one mesh. A node's transform
+ places the mesh in the scene.
+ """
+
+ def __init__(self, extensions, extras, name, primitives, weights):
+ self.extensions = extensions
+ self.extras = extras
+ self.name = name
+ self.primitives = primitives
+ self.weights = weights
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ name = from_union([from_str, from_none], obj.get("name"))
+ primitives = from_list(MeshPrimitive.from_dict, obj.get("primitives"))
+ weights = from_union([lambda x: from_list(from_float, x), from_none], obj.get("weights"))
+ return Mesh(extensions, extras, name, primitives, weights)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["primitives"] = from_list(lambda x: to_class(MeshPrimitive, x), self.primitives)
+ result["weights"] = from_union([lambda x: from_list(to_float, x), from_none], self.weights)
+ return result
+
+
+class Node:
+ """A node in the node hierarchy. When the node contains `skin`, all `mesh.primitives` must
+ contain `JOINTS_0` and `WEIGHTS_0` attributes. A node can have either a `matrix` or any
+ combination of `translation`/`rotation`/`scale` (TRS) properties. TRS properties are
+ converted to matrices and postmultiplied in the `T * R * S` order to compose the
+ transformation matrix; first the scale is applied to the vertices, then the rotation, and
+ then the translation. If none are provided, the transform is the identity. When a node is
+ targeted for animation (referenced by an animation.channel.target), only TRS properties
+ may be present; `matrix` will not be present.
+ """
+
+ def __init__(self, camera, children, extensions, extras, matrix, mesh, name, rotation, scale, skin, translation,
+ weights):
+ self.camera = camera
+ self.children = children
+ self.extensions = extensions
+ self.extras = extras
+ self.matrix = matrix
+ self.mesh = mesh
+ self.name = name
+ self.rotation = rotation
+ self.scale = scale
+ self.skin = skin
+ self.translation = translation
+ self.weights = weights
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ camera = from_union([from_int, from_none], obj.get("camera"))
+ children = from_union([lambda x: from_list(from_int, x), from_none], obj.get("children"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ matrix = from_union([lambda x: from_list(from_float, x), from_none], obj.get("matrix"))
+ mesh = from_union([from_int, from_none], obj.get("mesh"))
+ name = from_union([from_str, from_none], obj.get("name"))
+ rotation = from_union([lambda x: from_list(from_float, x), from_none], obj.get("rotation"))
+ scale = from_union([lambda x: from_list(from_float, x), from_none], obj.get("scale"))
+ skin = from_union([from_int, from_none], obj.get("skin"))
+ translation = from_union([lambda x: from_list(from_float, x), from_none], obj.get("translation"))
+ weights = from_union([lambda x: from_list(from_float, x), from_none], obj.get("weights"))
+ return Node(camera, children, extensions, extras, matrix, mesh, name, rotation, scale, skin, translation,
+ weights)
+
+ def to_dict(self):
+ result = {}
+ result["camera"] = from_union([from_int, from_none], self.camera)
+ result["children"] = from_union([lambda x: from_list(from_int, x), from_none], self.children)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["matrix"] = from_union([lambda x: from_list(to_float, x), from_none], self.matrix)
+ result["mesh"] = from_union([from_int, from_none], self.mesh)
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["rotation"] = from_union([lambda x: from_list(to_float, x), from_none], self.rotation)
+ result["scale"] = from_union([lambda x: from_list(to_float, x), from_none], self.scale)
+ result["skin"] = from_union([from_int, from_none], self.skin)
+ result["translation"] = from_union([lambda x: from_list(to_float, x), from_none], self.translation)
+ result["weights"] = from_union([lambda x: from_list(to_float, x), from_none], self.weights)
+ return result
+
+
+class Sampler:
+ """Texture sampler properties for filtering and wrapping modes."""
+
+ def __init__(self, extensions, extras, mag_filter, min_filter, name, wrap_s, wrap_t):
+ self.extensions = extensions
+ self.extras = extras
+ self.mag_filter = mag_filter
+ self.min_filter = min_filter
+ self.name = name
+ self.wrap_s = wrap_s
+ self.wrap_t = wrap_t
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ mag_filter = from_union([from_int, from_none], obj.get("magFilter"))
+ min_filter = from_union([from_int, from_none], obj.get("minFilter"))
+ name = from_union([from_str, from_none], obj.get("name"))
+ wrap_s = from_union([from_int, from_none], obj.get("wrapS"))
+ wrap_t = from_union([from_int, from_none], obj.get("wrapT"))
+ return Sampler(extensions, extras, mag_filter, min_filter, name, wrap_s, wrap_t)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["magFilter"] = from_union([from_int, from_none], self.mag_filter)
+ result["minFilter"] = from_union([from_int, from_none], self.min_filter)
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["wrapS"] = from_union([from_int, from_none], self.wrap_s)
+ result["wrapT"] = from_union([from_int, from_none], self.wrap_t)
+ return result
+
+
+class Scene:
+ """The root nodes of a scene."""
+
+ def __init__(self, extensions, extras, name, nodes):
+ self.extensions = extensions
+ self.extras = extras
+ self.name = name
+ self.nodes = nodes
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ name = from_union([from_str, from_none], obj.get("name"))
+ nodes = from_union([lambda x: from_list(from_int, x), from_none], obj.get("nodes"))
+ return Scene(extensions, extras, name, nodes)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["nodes"] = from_union([lambda x: from_list(from_int, x), from_none], self.nodes)
+ return result
+
+
+class Skin:
+ """Joints and matrices defining a skin."""
+
+ def __init__(self, extensions, extras, inverse_bind_matrices, joints, name, skeleton):
+ self.extensions = extensions
+ self.extras = extras
+ self.inverse_bind_matrices = inverse_bind_matrices
+ self.joints = joints
+ self.name = name
+ self.skeleton = skeleton
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ inverse_bind_matrices = from_union([from_int, from_none], obj.get("inverseBindMatrices"))
+ joints = from_list(from_int, obj.get("joints"))
+ name = from_union([from_str, from_none], obj.get("name"))
+ skeleton = from_union([from_int, from_none], obj.get("skeleton"))
+ return Skin(extensions, extras, inverse_bind_matrices, joints, name, skeleton)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["inverseBindMatrices"] = from_union([from_int, from_none], self.inverse_bind_matrices)
+ result["joints"] = from_list(from_int, self.joints)
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["skeleton"] = from_union([from_int, from_none], self.skeleton)
+ return result
+
+
+class Texture:
+ """A texture and its sampler."""
+
+ def __init__(self, extensions, extras, name, sampler, source):
+ self.extensions = extensions
+ self.extras = extras
+ self.name = name
+ self.sampler = sampler
+ self.source = source
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extras = obj.get("extras")
+ name = from_union([from_str, from_none], obj.get("name"))
+ sampler = from_union([from_int, from_none], obj.get("sampler"))
+ source = from_int(obj.get("source"))
+ return Texture(extensions, extras, name, sampler, source)
+
+ def to_dict(self):
+ result = {}
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extras"] = self.extras
+ result["name"] = from_union([from_str, from_none], self.name)
+ result["sampler"] = from_union([from_int, from_none], self.sampler)
+ result["source"] = from_int(self.source)
+ return result
+
+
+class Gltf:
+ """The root object for a glTF asset."""
+
+ def __init__(self, accessors, animations, asset, buffers, buffer_views, cameras, extensions, extensions_required,
+ extensions_used, extras, images, materials, meshes, nodes, samplers, scene, scenes, skins, textures):
+ self.accessors = accessors
+ self.animations = animations
+ self.asset = asset
+ self.buffers = buffers
+ self.buffer_views = buffer_views
+ self.cameras = cameras
+ self.extensions = extensions
+ self.extensions_required = extensions_required
+ self.extensions_used = extensions_used
+ self.extras = extras
+ self.images = images
+ self.materials = materials
+ self.meshes = meshes
+ self.nodes = nodes
+ self.samplers = samplers
+ self.scene = scene
+ self.scenes = scenes
+ self.skins = skins
+ self.textures = textures
+
+ @staticmethod
+ def from_dict(obj):
+ assert isinstance(obj, dict)
+ accessors = from_union([lambda x: from_list(Accessor.from_dict, x), from_none], obj.get("accessors"))
+ animations = from_union([lambda x: from_list(Animation.from_dict, x), from_none], obj.get("animations"))
+ asset = Asset.from_dict(obj.get("asset"))
+ buffers = from_union([lambda x: from_list(Buffer.from_dict, x), from_none], obj.get("buffers"))
+ buffer_views = from_union([lambda x: from_list(BufferView.from_dict, x), from_none], obj.get("bufferViews"))
+ cameras = from_union([lambda x: from_list(Camera.from_dict, x), from_none], obj.get("cameras"))
+ extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ obj.get("extensions"))
+ extensions_required = from_union([lambda x: from_list(from_str, x), from_none], obj.get("extensionsRequired"))
+ extensions_used = from_union([lambda x: from_list(from_str, x), from_none], obj.get("extensionsUsed"))
+ extras = obj.get("extras")
+ images = from_union([lambda x: from_list(Image.from_dict, x), from_none], obj.get("images"))
+ materials = from_union([lambda x: from_list(Material.from_dict, x), from_none], obj.get("materials"))
+ meshes = from_union([lambda x: from_list(Mesh.from_dict, x), from_none], obj.get("meshes"))
+ nodes = from_union([lambda x: from_list(Node.from_dict, x), from_none], obj.get("nodes"))
+ samplers = from_union([lambda x: from_list(Sampler.from_dict, x), from_none], obj.get("samplers"))
+ scene = from_union([from_int, from_none], obj.get("scene"))
+ scenes = from_union([lambda x: from_list(Scene.from_dict, x), from_none], obj.get("scenes"))
+ skins = from_union([lambda x: from_list(Skin.from_dict, x), from_none], obj.get("skins"))
+ textures = from_union([lambda x: from_list(Texture.from_dict, x), from_none], obj.get("textures"))
+ return Gltf(accessors, animations, asset, buffers, buffer_views, cameras, extensions, extensions_required,
+ extensions_used, extras, images, materials, meshes, nodes, samplers, scene, scenes, skins, textures)
+
+ def to_dict(self):
+ result = {}
+ result["accessors"] = from_union([lambda x: from_list(lambda x: to_class(Accessor, x), x), from_none],
+ self.accessors)
+ result["animations"] = from_union([lambda x: from_list(lambda x: to_class(Animation, x), x), from_none],
+ self.animations)
+ result["asset"] = to_class(Asset, self.asset)
+ result["buffers"] = from_union([lambda x: from_list(lambda x: to_class(Buffer, x), x), from_none], self.buffers)
+ result["bufferViews"] = from_union([lambda x: from_list(lambda x: to_class(BufferView, x), x), from_none],
+ self.buffer_views)
+ result["cameras"] = from_union([lambda x: from_list(lambda x: to_class(Camera, x), x), from_none], self.cameras)
+ result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+ self.extensions)
+ result["extensionsRequired"] = from_union([lambda x: from_list(from_str, x), from_none],
+ self.extensions_required)
+ result["extensionsUsed"] = from_union([lambda x: from_list(from_str, x), from_none], self.extensions_used)
+ result["extras"] = self.extras
+ result["images"] = from_union([lambda x: from_list(lambda x: to_class(Image, x), x), from_none], self.images)
+ result["materials"] = from_union([lambda x: from_list(lambda x: to_class(Material, x), x), from_none],
+ self.materials)
+ result["meshes"] = from_union([lambda x: from_list(lambda x: to_class(Mesh, x), x), from_none], self.meshes)
+ result["nodes"] = from_union([lambda x: from_list(lambda x: to_class(Node, x), x), from_none], self.nodes)
+ result["samplers"] = from_union([lambda x: from_list(lambda x: to_class(Sampler, x), x), from_none],
+ self.samplers)
+ result["scene"] = from_union([from_int, from_none], self.scene)
+ result["scenes"] = from_union([lambda x: from_list(lambda x: to_class(Scene, x), x), from_none], self.scenes)
+ result["skins"] = from_union([lambda x: from_list(lambda x: to_class(Skin, x), x), from_none], self.skins)
+ result["textures"] = from_union([lambda x: from_list(lambda x: to_class(Texture, x), x), from_none],
+ self.textures)
+ return result
+
+
+def gltf_from_dict(s):
+ return Gltf.from_dict(s)
+
+
+def gltf_to_dict(x):
+ return to_class(Gltf, x)
+
diff --git a/io_scene_gltf2/io/com/gltf2_io_constants.py b/io_scene_gltf2/io/com/gltf2_io_constants.py
new file mode 100755
index 00000000..c97908cd
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io_constants.py
@@ -0,0 +1,132 @@
+# 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.
+
+from enum import IntEnum
+
+
+class ComponentType(IntEnum):
+ Byte = 5120
+ UnsignedByte = 5121
+ Short = 5122
+ UnsignedShort = 5123
+ UnsignedInt = 5125
+ Float = 5126
+
+ @classmethod
+ def to_type_code(cls, component_type):
+ return {
+ ComponentType.Byte: 'b',
+ ComponentType.UnsignedByte: 'B',
+ ComponentType.Short: 'h',
+ ComponentType.UnsignedShort: 'H',
+ ComponentType.UnsignedInt: 'I',
+ ComponentType.Float: 'f'
+ }[component_type]
+
+ @classmethod
+ def from_legacy_define(cls, type_define):
+ return {
+ GLTF_COMPONENT_TYPE_BYTE: ComponentType.Byte,
+ GLTF_COMPONENT_TYPE_UNSIGNED_BYTE: ComponentType.UnsignedByte,
+ GLTF_COMPONENT_TYPE_SHORT: ComponentType.Short,
+ GLTF_COMPONENT_TYPE_UNSIGNED_SHORT: ComponentType.UnsignedShort,
+ GLTF_COMPONENT_TYPE_UNSIGNED_INT: ComponentType.UnsignedInt,
+ GLTF_COMPONENT_TYPE_FLOAT: ComponentType.Float
+ }[type_define]
+
+ @classmethod
+ def get_size(cls, component_type):
+ return {
+ ComponentType.Byte: 1,
+ ComponentType.UnsignedByte: 1,
+ ComponentType.Short: 2,
+ ComponentType.UnsignedShort: 2,
+ ComponentType.UnsignedInt: 4,
+ ComponentType.Float: 4
+ }[component_type]
+
+
+class DataType:
+ Scalar = "SCALAR"
+ Vec2 = "VEC2"
+ Vec3 = "VEC3"
+ Vec4 = "VEC4"
+ Mat2 = "MAT2"
+ Mat3 = "MAT3"
+ Mat4 = "MAT4"
+
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("{} should not be instantiated".format(cls.__name__))
+
+ @classmethod
+ def num_elements(cls, data_type):
+ return {
+ DataType.Scalar: 1,
+ DataType.Vec2: 2,
+ DataType.Vec3: 3,
+ DataType.Vec4: 4,
+ DataType.Mat2: 4,
+ DataType.Mat3: 9,
+ DataType.Mat4: 16
+ }[data_type]
+
+ @classmethod
+ def vec_type_from_num(cls, num_elems):
+ if not (0 < num_elems < 5):
+ raise ValueError("No vector type with {} elements".format(num_elems))
+ return {
+ 1: DataType.Scalar,
+ 2: DataType.Vec2,
+ 3: DataType.Vec3,
+ 4: DataType.Vec4
+ }[num_elems]
+
+ @classmethod
+ def mat_type_from_num(cls, num_elems):
+ if not (4 <= num_elems <= 16):
+ raise ValueError("No matrix type with {} elements".format(num_elems))
+ return {
+ 4: DataType.Mat2,
+ 9: DataType.Mat3,
+ 16: DataType.Mat4
+ }[num_elems]
+
+
+#################
+# LEGACY DEFINES
+
+GLTF_VERSION = "2.0"
+
+#
+# Component Types
+#
+GLTF_COMPONENT_TYPE_BYTE = "BYTE"
+GLTF_COMPONENT_TYPE_UNSIGNED_BYTE = "UNSIGNED_BYTE"
+GLTF_COMPONENT_TYPE_SHORT = "SHORT"
+GLTF_COMPONENT_TYPE_UNSIGNED_SHORT = "UNSIGNED_SHORT"
+GLTF_COMPONENT_TYPE_UNSIGNED_INT = "UNSIGNED_INT"
+GLTF_COMPONENT_TYPE_FLOAT = "FLOAT"
+
+
+#
+# Data types
+#
+GLTF_DATA_TYPE_SCALAR = "SCALAR"
+GLTF_DATA_TYPE_VEC2 = "VEC2"
+GLTF_DATA_TYPE_VEC3 = "VEC3"
+GLTF_DATA_TYPE_VEC4 = "VEC4"
+GLTF_DATA_TYPE_MAT2 = "MAT2"
+GLTF_DATA_TYPE_MAT3 = "MAT3"
+GLTF_DATA_TYPE_MAT4 = "MAT4"
+
diff --git a/io_scene_gltf2/io/com/gltf2_io_debug.py b/io_scene_gltf2/io/com/gltf2_io_debug.py
new file mode 100755
index 00000000..a7df8fed
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io_debug.py
@@ -0,0 +1,138 @@
+# 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.
+
+#
+# Imports
+#
+
+import time
+import logging
+
+#
+# Globals
+#
+
+OUTPUT_LEVELS = ['ERROR', 'WARNING', 'INFO', 'PROFILE', 'DEBUG', 'VERBOSE']
+
+g_current_output_level = 'DEBUG'
+g_profile_started = False
+g_profile_start = 0.0
+g_profile_end = 0.0
+g_profile_delta = 0.0
+
+#
+# Functions
+#
+
+
+def set_output_level(level):
+ """Set an output debug level."""
+ global g_current_output_level
+
+ if OUTPUT_LEVELS.index(level) < 0:
+ return
+
+ g_current_output_level = level
+
+
+def print_console(level, output):
+ """Print to Blender console with a given header and output."""
+ global OUTPUT_LEVELS
+ global g_current_output_level
+
+ if OUTPUT_LEVELS.index(level) > OUTPUT_LEVELS.index(g_current_output_level):
+ return
+
+ print(level + ': ' + output)
+
+
+def print_newline():
+ """Print a new line to Blender console."""
+ print()
+
+
+def print_timestamp(label=None):
+ """Print a timestamp to Blender console."""
+ output = 'Timestamp: ' + str(time.time())
+
+ if label is not None:
+ output = output + ' (' + label + ')'
+
+ print_console('PROFILE', output)
+
+
+def profile_start():
+ """Start profiling by storing the current time."""
+ global g_profile_start
+ global g_profile_started
+
+ if g_profile_started:
+ print_console('ERROR', 'Profiling already started')
+ return
+
+ g_profile_started = True
+
+ g_profile_start = time.time()
+
+
+def profile_end(label=None):
+ """Stop profiling and printing out the delta time since profile start."""
+ global g_profile_end
+ global g_profile_delta
+ global g_profile_started
+
+ if not g_profile_started:
+ print_console('ERROR', 'Profiling not started')
+ return
+
+ g_profile_started = False
+
+ g_profile_end = time.time()
+ g_profile_delta = g_profile_end - g_profile_start
+
+ output = 'Delta time: ' + str(g_profile_delta)
+
+ if label is not None:
+ output = output + ' (' + label + ')'
+
+ print_console('PROFILE', output)
+
+
+# TODO: need to have a unique system for logging importer/exporter
+# TODO: this logger is used for importer, but in io and in blender part, but is written here in a _io_ file
+class Log:
+ def __init__(self, loglevel):
+ self.logger = logging.getLogger('glTFImporter')
+ self.hdlr = logging.StreamHandler()
+ formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
+ self.hdlr.setFormatter(formatter)
+ self.logger.addHandler(self.hdlr)
+ self.logger.setLevel(int(loglevel))
+
+ @staticmethod
+ def get_levels():
+ levels = [
+ (str(logging.CRITICAL), "Critical", "", logging.CRITICAL),
+ (str(logging.ERROR), "Error", "", logging.ERROR),
+ (str(logging.WARNING), "Warning", "", logging.WARNING),
+ (str(logging.INFO), "Info", "", logging.INFO),
+ (str(logging.NOTSET), "NotSet", "", logging.NOTSET)
+ ]
+
+ return levels
+
+ @staticmethod
+ def default():
+ return str(logging.ERROR)
+
diff --git a/io_scene_gltf2/io/com/gltf2_io_functional.py b/io_scene_gltf2/io/com/gltf2_io_functional.py
new file mode 100755
index 00000000..eb65112f
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io_functional.py
@@ -0,0 +1,41 @@
+# 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.
+
+import typing
+
+
+def chunks(lst: typing.Sequence[typing.Any], n: int) -> typing.List[typing.Any]:
+ """
+ Generator that yields successive n sized chunks of the list l
+ :param lst: the list to be split
+ :param n: the length of the chunks
+ :return: a sublist of at most length n
+ """
+ result = []
+ for i in range(0, len(lst), n):
+ result.append(lst[i:i + n])
+ return result
+
+
+def unzip(*args: typing.Iterable[typing.Any]) -> typing.Iterable[typing.Iterable[typing.Any]]:
+ """
+ Unzip the list. Inverse of the builtin zip
+ :param args: a list of lists or multiple list arguments
+ :return: a list of unzipped lists
+ """
+ if len(args) == 1:
+ args = args[0]
+
+ return zip(*args)
+
diff --git a/io_scene_gltf2/io/com/gltf2_io_image.py b/io_scene_gltf2/io/com/gltf2_io_image.py
new file mode 100755
index 00000000..af86daeb
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io_image.py
@@ -0,0 +1,154 @@
+# 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.
+
+#
+# Imports
+#
+
+import struct
+import zlib
+
+
+class Image:
+ """
+ Image object class to represent a 4-channel RGBA image.
+
+ Pixel values are expected to be floating point in the range of [0.0 to 1.0]
+ """
+
+ def __init__(self, width, height, pixels):
+ self.width = width
+ self.height = height
+ self.channels = 4
+ self.pixels = pixels
+ self.name = ""
+ self.file_format = "PNG"
+
+ def to_png_data(self):
+ buf = bytearray([int(channel * 255.0) for channel in self.pixels])
+
+ #
+ # Taken from 'blender-thumbnailer.py' in Blender.
+ #
+
+ # reverse the vertical line order and add null bytes at the start
+ width_byte_4 = self.width * 4
+ raw_data = b"".join(
+ b'\x00' + buf[span:span + width_byte_4] for span in range(
+ (self.height - 1) * self.width * 4, -1, - width_byte_4))
+
+ def png_pack(png_tag, data):
+ chunk_head = png_tag + data
+ return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
+
+ return b"".join([
+ b'\x89PNG\r\n\x1a\n',
+ png_pack(b'IHDR', struct.pack("!2I5B", self.width, self.height, 8, 6, 0, 0, 0)),
+ png_pack(b'IDAT', zlib.compress(raw_data, 9)),
+ png_pack(b'IEND', b'')])
+
+ def to_image_data(self, mime_type):
+ if mime_type == 'image/png':
+ return self.to_png_data()
+ raise ValueError("Unsupported image file type {}".format(mime_type))
+
+ def save_png(self, dst_path):
+ data = self.to_png_data()
+ with open(dst_path, 'wb') as f:
+ f.write(data)
+
+
+def create_img(width, height, r=0.0, g=0.0, b=0.0, a=1.0):
+ """
+ Create a new image object with 4 channels and initialize it with the given default values.
+
+ (if no arguments are given, these default to R=0, G=0, B=0, A=1.0)
+ Return the created image object.
+ """
+ return Image(width, height, [r, g, b, a] * (width * height))
+
+
+def create_img_from_pixels(width, height, pixels):
+ """
+ Create a new image object with 4 channels and initialize it using the given array of pixel data.
+
+ Return the created image object.
+ """
+ if pixels is None or len(pixels) != width * height * 4:
+ return None
+
+ return Image(width, height, pixels)
+
+
+def copy_img_channel(dst_image, dst_channel, src_image, src_channel):
+ """
+ Copy a single channel (identified by src_channel) from src_image to dst_image (overwriting dst_channel).
+
+ src_image and dst_image are expected to be image objects created using create_img.
+ Return True on success, False otherwise.
+ """
+ if dst_image is None or src_image is None:
+ return False
+
+ if dst_channel < 0 or dst_channel >= dst_image.channels or src_channel < 0 or src_channel >= src_image.channels:
+ return False
+
+ if src_image.width != dst_image.width or \
+ src_image.height != dst_image.height or \
+ src_image.channels != dst_image.channels:
+ return False
+
+ for i in range(0, len(dst_image.pixels), dst_image.channels):
+ dst_image.pixels[i + dst_channel] = src_image.pixels[i + src_channel]
+
+ return True
+
+
+def test_save_img(image, path):
+ """
+ Save the given image to a PNG file (specified by path).
+
+ Return True on success, False otherwise.
+ """
+ if image is None or image.channels != 4:
+ return False
+
+ width = image.width
+ height = image.height
+
+ buf = bytearray([int(channel * 255.0) for channel in image.pixels])
+
+ #
+ # Taken from 'blender-thumbnailer.py' in Blender.
+ #
+
+ # reverse the vertical line order and add null bytes at the start
+ width_byte_4 = width * 4
+ raw_data = b"".join(
+ b'\x00' + buf[span:span + width_byte_4] for span in range((height - 1) * width * 4, -1, - width_byte_4))
+
+ def png_pack(png_tag, data):
+ chunk_head = png_tag + data
+ return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
+
+ data = b"".join([
+ b'\x89PNG\r\n\x1a\n',
+ png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
+ png_pack(b'IDAT', zlib.compress(raw_data, 9)),
+ png_pack(b'IEND', b'')])
+
+ with open(path, 'wb') as f:
+ f.write(data)
+ return True
+
diff --git a/io_scene_gltf2/io/com/gltf2_io_trs.py b/io_scene_gltf2/io/com/gltf2_io_trs.py
new file mode 100755
index 00000000..59f30830
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io_trs.py
@@ -0,0 +1,68 @@
+# 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/exp/gltf2_io_binary_data.py b/io_scene_gltf2/io/exp/gltf2_io_binary_data.py
new file mode 100755
index 00000000..42f6d5d7
--- /dev/null
+++ b/io_scene_gltf2/io/exp/gltf2_io_binary_data.py
@@ -0,0 +1,36 @@
+# 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.
+
+import typing
+import array
+from io_scene_gltf2.io.com import gltf2_io_constants
+
+
+class BinaryData:
+ """Store for gltf binary data that can later be stored in a buffer."""
+
+ def __init__(self, data: bytes):
+ if not isinstance(data, bytes):
+ raise TypeError("Data is not a bytes array")
+ self.data = data
+
+ @classmethod
+ def from_list(cls, lst: typing.List[typing.Any], gltf_component_type: gltf2_io_constants.ComponentType):
+ format_char = gltf2_io_constants.ComponentType.to_type_code(gltf_component_type)
+ return BinaryData(array.array(format_char, lst).tobytes())
+
+ @property
+ def byte_length(self):
+ return len(self.data)
+
diff --git a/io_scene_gltf2/io/exp/gltf2_io_buffer.py b/io_scene_gltf2/io/exp/gltf2_io_buffer.py
new file mode 100755
index 00000000..694be11e
--- /dev/null
+++ b/io_scene_gltf2/io/exp/gltf2_io_buffer.py
@@ -0,0 +1,61 @@
+# 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.
+
+import base64
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+
+
+class Buffer:
+ """Class representing binary data for use in a glTF file as 'buffer' property."""
+
+ def __init__(self, buffer_index=0):
+ self.__data = b""
+ self.__buffer_index = buffer_index
+
+ def add_and_get_view(self, binary_data: gltf2_io_binary_data.BinaryData) -> gltf2_io.BufferView:
+ """Add binary data to the buffer. Return a glTF BufferView."""
+ offset = len(self.__data)
+ self.__data += binary_data.data
+
+ # offsets should be a multiple of 4 --> therefore add padding if necessary
+ padding = (4 - (binary_data.byte_length % 4)) % 4
+ self.__data += b"\x00" * padding
+
+ buffer_view = gltf2_io.BufferView(
+ buffer=self.__buffer_index,
+ byte_length=binary_data.byte_length,
+ byte_offset=offset,
+ byte_stride=None,
+ extensions=None,
+ extras=None,
+ name=None,
+ target=None
+ )
+ return buffer_view
+
+ @property
+ def byte_length(self):
+ return len(self.__data)
+
+ def to_bytes(self):
+ return self.__data
+
+ def to_embed_string(self):
+ return 'data:application/octet-stream;base64,' + base64.b64encode(self.__data).decode('ascii')
+
+ def clear(self):
+ self.__data = b""
+
diff --git a/io_scene_gltf2/io/exp/gltf2_io_export.py b/io_scene_gltf2/io/exp/gltf2_io_export.py
new file mode 100755
index 00000000..561b2ac1
--- /dev/null
+++ b/io_scene_gltf2/io/exp/gltf2_io_export.py
@@ -0,0 +1,97 @@
+# 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.
+
+#
+# Imports
+#
+
+import json
+import struct
+
+#
+# Globals
+#
+
+#
+# Functions
+#
+
+
+def save_gltf(glTF, export_settings, encoder, glb_buffer):
+ indent = None
+ separators = separators = (',', ':')
+
+ if export_settings['gltf_format'] == 'ASCII' and not export_settings['gltf_strip']:
+ indent = 4
+ # The comma is typically followed by a newline, so no trailing whitespace is needed on it.
+ separators = separators = (',', ' : ')
+
+ glTF_encoded = json.dumps(glTF, indent=indent, separators=separators, sort_keys=True, cls=encoder, allow_nan=False)
+
+ #
+
+ if export_settings['gltf_format'] == 'ASCII':
+ file = open(export_settings['gltf_filepath'], "w", encoding="utf8", newline="\n")
+ file.write(glTF_encoded)
+ file.write("\n")
+ file.close()
+
+ binary = export_settings['gltf_binary']
+ if len(binary) > 0 and not export_settings['gltf_embed_buffers']:
+ file = open(export_settings['gltf_filedirectory'] + export_settings['gltf_binaryfilename'], "wb")
+ file.write(binary)
+ file.close()
+
+ else:
+ file = open(export_settings['gltf_filepath'], "wb")
+
+ glTF_data = glTF_encoded.encode()
+ binary = glb_buffer
+
+ length_gtlf = len(glTF_data)
+ spaces_gltf = (4 - (length_gtlf & 3)) & 3
+ length_gtlf += spaces_gltf
+
+ length_bin = len(binary)
+ zeros_bin = (4 - (length_bin & 3)) & 3
+ length_bin += zeros_bin
+
+ length = 12 + 8 + length_gtlf
+ if length_bin > 0:
+ length += 8 + length_bin
+
+ # Header (Version 2)
+ file.write('glTF'.encode())
+ file.write(struct.pack("I", 2))
+ file.write(struct.pack("I", length))
+
+ # Chunk 0 (JSON)
+ file.write(struct.pack("I", length_gtlf))
+ file.write('JSON'.encode())
+ file.write(glTF_data)
+ for i in range(0, spaces_gltf):
+ file.write(' '.encode())
+
+ # Chunk 1 (BIN)
+ if length_bin > 0:
+ file.write(struct.pack("I", length_bin))
+ file.write('BIN\0'.encode())
+ file.write(binary)
+ for i in range(0, zeros_bin):
+ file.write('\0'.encode())
+
+ file.close()
+
+ return True
+
diff --git a/io_scene_gltf2/io/exp/gltf2_io_get.py b/io_scene_gltf2/io/exp/gltf2_io_get.py
new file mode 100755
index 00000000..35c65615
--- /dev/null
+++ b/io_scene_gltf2/io/exp/gltf2_io_get.py
@@ -0,0 +1,316 @@
+# 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.
+
+#
+# Imports
+#
+
+import os
+
+#
+# Globals
+#
+
+#
+# Functions
+#
+
+
+def get_material_requires_texcoords(glTF, index):
+ """Query function, if a material "needs" texture coordinates. This is the case, if a texture is present and used."""
+ if glTF.materials is None:
+ return False
+
+ materials = glTF.materials
+
+ if index < 0 or index >= len(materials):
+ return False
+
+ material = materials[index]
+
+ # General
+
+ if material.emissive_texture is not None:
+ return True
+
+ if material.normal_texture is not None:
+ return True
+
+ if material.occlusion_texture is not None:
+ return True
+
+ # Metallic roughness
+
+ if material.pbr_metallic_roughness is not None and \
+ material.pbr_metallic_roughness.base_color_texture is not None:
+ return True
+
+ if material.pbr_metallic_roughness is not None and \
+ material.pbr_metallic_roughness.metallic_roughness_texture is not None:
+ return True
+
+ return False
+
+
+def get_material_requires_normals(glTF, index):
+ """
+ Query function, if a material "needs" normals. This is the case, if a texture is present and used.
+
+ At point of writing, same function as for texture coordinates.
+ """
+ return get_material_requires_texcoords(glTF, index)
+
+
+def get_material_index(glTF, name):
+ """Return the material index in the glTF array."""
+ if name is None:
+ return -1
+
+ if glTF.materials is None:
+ return -1
+
+ index = 0
+ for material in glTF.materials:
+ if material.name == name:
+ return index
+
+ index += 1
+
+ return -1
+
+
+def get_mesh_index(glTF, name):
+ """Return the mesh index in the glTF array."""
+ if glTF.meshes is None:
+ return -1
+
+ index = 0
+ for mesh in glTF.meshes:
+ if mesh.name == name:
+ return index
+
+ index += 1
+
+ return -1
+
+
+def get_skin_index(glTF, name, index_offset):
+ """Return the skin index in the glTF array."""
+ if glTF.skins is None:
+ return -1
+
+ skeleton = get_node_index(glTF, name)
+
+ index = 0
+ for skin in glTF.skins:
+ if skin.skeleton == skeleton:
+ return index + index_offset
+
+ index += 1
+
+ return -1
+
+
+def get_camera_index(glTF, name):
+ """Return the camera index in the glTF array."""
+ if glTF.cameras is None:
+ return -1
+
+ index = 0
+ for camera in glTF.cameras:
+ if camera.name == name:
+ return index
+
+ index += 1
+
+ return -1
+
+
+def get_light_index(glTF, name):
+ """Return the light index in the glTF array."""
+ if glTF.extensions is None:
+ return -1
+
+ extensions = glTF.extensions
+
+ if extensions.get('KHR_lights_punctual') is None:
+ return -1
+
+ khr_lights_punctual = extensions['KHR_lights_punctual']
+
+ if khr_lights_punctual.get('lights') is None:
+ return -1
+
+ lights = khr_lights_punctual['lights']
+
+ index = 0
+ for light in lights:
+ if light['name'] == name:
+ return index
+
+ index += 1
+
+ return -1
+
+
+def get_node_index(glTF, name):
+ """Return the node index in the glTF array."""
+ if glTF.nodes is None:
+ return -1
+
+ index = 0
+ for node in glTF.nodes:
+ if node.name == name:
+ return index
+
+ index += 1
+
+ return -1
+
+
+def get_scene_index(glTF, name):
+ """Return the scene index in the glTF array."""
+ if glTF.scenes is None:
+ return -1
+
+ index = 0
+ for scene in glTF.scenes:
+ if scene.name == name:
+ return index
+
+ index += 1
+
+ return -1
+
+
+def get_texture_index(glTF, filename):
+ """Return the texture index in the glTF array by a given file path."""
+ if glTF.textures is None:
+ return -1
+
+ image_index = get_image_index(glTF, filename)
+
+ if image_index == -1:
+ return -1
+
+ for texture_index, texture in enumerate(glTF.textures):
+ if image_index == texture.source:
+ return texture_index
+
+ return -1
+
+
+def get_image_index(glTF, filename):
+ """Return the image index in the glTF array."""
+ if glTF.images is None:
+ return -1
+
+ image_name = get_image_name(filename)
+
+ for index, current_image in enumerate(glTF.images):
+ if image_name == current_image.name:
+ return index
+
+ return -1
+
+
+def get_image_name(filename):
+ """Return user-facing, extension-agnostic name for image."""
+ return os.path.splitext(filename)[0]
+
+
+def get_scalar(default_value, init_value=0.0):
+ """Return scalar with a given default/fallback value."""
+ return_value = init_value
+
+ if default_value is None:
+ return return_value
+
+ return_value = default_value
+
+ return return_value
+
+
+def get_vec2(default_value, init_value=[0.0, 0.0]):
+ """Return vec2 with a given default/fallback value."""
+ return_value = init_value
+
+ if default_value is None or len(default_value) < 2:
+ return return_value
+
+ index = 0
+ for number in default_value:
+ return_value[index] = number
+
+ index += 1
+ if index == 2:
+ return return_value
+
+ return return_value
+
+
+def get_vec3(default_value, init_value=[0.0, 0.0, 0.0]):
+ """Return vec3 with a given default/fallback value."""
+ return_value = init_value
+
+ if default_value is None or len(default_value) < 3:
+ return return_value
+
+ index = 0
+ for number in default_value:
+ return_value[index] = number
+
+ index += 1
+ if index == 3:
+ return return_value
+
+ return return_value
+
+
+def get_vec4(default_value, init_value=[0.0, 0.0, 0.0, 1.0]):
+ """Return vec4 with a given default/fallback value."""
+ return_value = init_value
+
+ if default_value is None or len(default_value) < 4:
+ return return_value
+
+ index = 0
+ for number in default_value:
+ return_value[index] = number
+
+ index += 1
+ if index == 4:
+ return return_value
+
+ return return_value
+
+
+def get_index(elements, name):
+ """Return index of a glTF element by a given name."""
+ if elements is None or name is None:
+ return -1
+
+ index = 0
+ for element in elements:
+ if isinstance(element, dict):
+ if element.get('name') == name:
+ return index
+ else:
+ if element.name == name:
+ return index
+
+ index += 1
+
+ return -1
+
diff --git a/io_scene_gltf2/io/exp/gltf2_io_image_data.py b/io_scene_gltf2/io/exp/gltf2_io_image_data.py
new file mode 100755
index 00000000..23a2843e
--- /dev/null
+++ b/io_scene_gltf2/io/exp/gltf2_io_image_data.py
@@ -0,0 +1,106 @@
+# 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.
+
+import typing
+import struct
+import zlib
+import numpy as np
+
+class ImageData:
+ """Contains channels of an image with raw pixel data."""
+ # TODO: refactor to only operate on numpy arrays
+ # FUTURE_WORK: as a method to allow the node graph to be better supported, we could model some of
+ # the node graph elements with numpy functions
+
+ def __init__(self, name: str, width: int, height: int, channels: typing.Optional[typing.List[np.ndarray]] = []):
+ if width <= 0 or height <= 0:
+ raise ValueError("Image data can not have zero width or height")
+ self.name = name
+ self.channels = channels
+ self.width = width
+ self.height = height
+
+ def add_to_image(self, image_data):
+ if self.width != image_data.width or self.height != image_data.height:
+ raise ValueError("Image dimensions do not match")
+ if len(self.channels) + len(image_data.channels) > 4:
+ raise ValueError("Can't append image: channels full")
+ self.name += image_data.name
+ self.channels += image_data.channels
+
+ @property
+ def r(self):
+ if len(self.channels) <= 0:
+ return None
+ return self.channels[0]
+
+ @property
+ def g(self):
+ if len(self.channels) <= 1:
+ return None
+ return self.channels[1]
+
+ @property
+ def b(self):
+ if len(self.channels) <= 2:
+ return None
+ return self.channels[2]
+
+ @property
+ def a(self):
+ if len(self.channels) <= 3:
+ return None
+ return self.channels[3]
+
+ def to_image_data(self, mime_type: str) -> bytes:
+ if mime_type == 'image/png':
+ return self.to_png_data()
+ raise ValueError("Unsupported image file type {}".format(mime_type))
+
+ def to_png_data(self) -> bytes:
+ channels = self.channels
+
+ # if there is no data, create a single pixel image
+ if not channels:
+ channels = np.zeros((1, 1))
+
+ # fill all channels of the png
+ for _ in range(4 - len(channels)):
+ channels.append(np.ones_like(channels[0]))
+
+ image = np.concatenate(self.channels, axis=1)
+ image = image.flatten()
+ image = (image * 255.0).astype(np.uint8)
+ buf = image.tobytes()
+
+ #
+ # Taken from 'blender-thumbnailer.py' in Blender.
+ #
+
+ # reverse the vertical line order and add null bytes at the start
+ width_byte_4 = self.width * 4
+ raw_data = b"".join(
+ b'\x00' + buf[span:span + width_byte_4] for span in range(
+ (self.height - 1) * self.width * 4, -1, - width_byte_4))
+
+ def png_pack(png_tag, data):
+ chunk_head = png_tag + data
+ return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
+
+ return b"".join([
+ b'\x89PNG\r\n\x1a\n',
+ png_pack(b'IHDR', struct.pack("!2I5B", self.width, self.height, 8, 6, 0, 0, 0)),
+ png_pack(b'IDAT', zlib.compress(raw_data, 9)),
+ png_pack(b'IEND', b'')])
+
diff --git a/io_scene_gltf2/io/imp/__init__.py b/io_scene_gltf2/io/imp/__init__.py
new file mode 100755
index 00000000..d3c53771
--- /dev/null
+++ b/io_scene_gltf2/io/imp/__init__.py
@@ -0,0 +1,16 @@
+# 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.
+
+"""IO imp package."""
+
diff --git a/io_scene_gltf2/io/imp/gltf2_io_binary.py b/io_scene_gltf2/io/imp/gltf2_io_binary.py
new file mode 100755
index 00000000..5f51d95d
--- /dev/null
+++ b/io_scene_gltf2/io/imp/gltf2_io_binary.py
@@ -0,0 +1,178 @@
+# 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.
+
+import struct
+import base64
+from os.path import dirname, join, isfile, basename
+
+
+class BinaryData():
+ """Binary reader."""
+ def __new__(cls, *args, **kwargs):
+ raise RuntimeError("%s should not be instantiated" % cls)
+
+ @staticmethod
+ def get_binary_from_accessor(gltf, accessor_idx):
+ """Get binary from accessor."""
+ accessor = gltf.data.accessors[accessor_idx]
+ bufferView = gltf.data.buffer_views[accessor.buffer_view] # TODO initialize with 0 when not present!
+ if bufferView.buffer in gltf.buffers.keys():
+ buffer = gltf.buffers[bufferView.buffer]
+ else:
+ # load buffer
+ gltf.load_buffer(bufferView.buffer)
+ buffer = gltf.buffers[bufferView.buffer]
+
+ accessor_offset = accessor.byte_offset
+ bufferview_offset = bufferView.byte_offset
+
+ if accessor_offset is None:
+ accessor_offset = 0
+ if bufferview_offset is None:
+ bufferview_offset = 0
+
+ return buffer[accessor_offset + bufferview_offset:accessor_offset + bufferview_offset + bufferView.byte_length]
+
+ @staticmethod
+ def get_data_from_accessor(gltf, accessor_idx):
+ """Get data from accessor."""
+ accessor = gltf.data.accessors[accessor_idx]
+
+ bufferView = gltf.data.buffer_views[accessor.buffer_view] # TODO initialize with 0 when not present!
+ buffer_data = BinaryData.get_binary_from_accessor(gltf, accessor_idx)
+
+ fmt_char = gltf.fmt_char_dict[accessor.component_type]
+ component_nb = gltf.component_nb_dict[accessor.type]
+ fmt = '<' + (fmt_char * component_nb)
+ stride_ = struct.calcsize(fmt)
+ # TODO data alignment stuff
+
+ if bufferView.byte_stride:
+ stride = bufferView.byte_stride
+ else:
+ stride = stride_
+
+ data = []
+ offset = 0
+ while len(data) < accessor.count:
+ element = struct.unpack_from(fmt, buffer_data, offset)
+ data.append(element)
+ offset += stride
+
+ if accessor.sparse:
+ sparse_indices_data = BinaryData.get_data_from_sparse(gltf, accessor.sparse, "indices")
+ sparse_values_values = BinaryData.get_data_from_sparse(
+ gltf,
+ accessor.sparse,
+ "values",
+ accessor.type,
+ accessor.component_type
+ )
+
+ # apply sparse
+ for cpt_idx, idx in enumerate(sparse_indices_data):
+ data[idx[0]] = sparse_values_values[cpt_idx]
+
+ # Normalization
+ if accessor.normalized:
+ for idx, tuple in enumerate(data):
+ new_tuple = ()
+ for i in tuple:
+ new_tuple += (float(i),)
+ data[idx] = new_tuple
+
+ return data
+
+ @staticmethod
+ def get_data_from_sparse(gltf, sparse, type_, type_val=None, comp_type=None):
+ """Get data from sparse."""
+ if type_ == "indices":
+ bufferView = gltf.data.buffer_views[sparse.indices.buffer_view]
+ offset = sparse.indices.byte_offset
+ component_nb = gltf.component_nb_dict['SCALAR']
+ fmt_char = gltf.fmt_char_dict[sparse.indices.component_type]
+ elif type_ == "values":
+ bufferView = gltf.data.buffer_views[sparse.values.buffer_view]
+ offset = sparse.values.byte_offset
+ component_nb = gltf.component_nb_dict[type_val]
+ fmt_char = gltf.fmt_char_dict[comp_type]
+
+ if bufferView.buffer in gltf.buffers.keys():
+ buffer = gltf.buffers[bufferView.buffer]
+ else:
+ # load buffer
+ gltf.load_buffer(bufferView.buffer)
+ buffer = gltf.buffers[bufferView.buffer]
+
+ bin_data = buffer[bufferView.byte_offset + offset:bufferView.byte_offset + offset + bufferView.byte_length]
+
+ fmt = '<' + (fmt_char * component_nb)
+ stride_ = struct.calcsize(fmt)
+ # TODO data alignment stuff ?
+
+ if bufferView.byte_stride:
+ stride = bufferView.byte_stride
+ else:
+ stride = stride_
+
+ data = []
+ offset = 0
+ while len(data) < sparse.count:
+ element = struct.unpack_from(fmt, bin_data, offset)
+ data.append(element)
+ offset += stride
+
+ return data
+
+ @staticmethod
+ def get_image_data(gltf, img_idx):
+ """Get data from image."""
+ pyimage = gltf.data.images[img_idx]
+
+ image_name = "Image_" + str(img_idx)
+
+ if pyimage.uri:
+ sep = ';base64,'
+ if pyimage.uri[:5] == 'data:':
+ idx = pyimage.uri.find(sep)
+ if idx != -1:
+ data = pyimage.uri[idx + len(sep):]
+ return base64.b64decode(data), image_name
+
+ if isfile(join(dirname(gltf.filename), pyimage.uri)):
+ with open(join(dirname(gltf.filename), pyimage.uri), 'rb') as f_:
+ return f_.read(), basename(join(dirname(gltf.filename), pyimage.uri))
+ else:
+ pyimage.gltf.log.error("Missing file (index " + str(img_idx) + "): " + pyimage.uri)
+ return None, None
+
+ if pyimage.buffer_view is None:
+ return None, None
+
+ bufferView = gltf.data.buffer_views[pyimage.buffer_view]
+
+ if bufferView.buffer in gltf.buffers.keys():
+ buffer = gltf.buffers[bufferView.buffer]
+ else:
+ # load buffer
+ gltf.load_buffer(bufferView.buffer)
+ buffer = gltf.buffers[bufferView.buffer]
+
+ bufferview_offset = bufferView.byte_offset
+
+ if bufferview_offset is None:
+ bufferview_offset = 0
+
+ return buffer[bufferview_offset:bufferview_offset + bufferView.byte_length], image_name
+
diff --git a/io_scene_gltf2/io/imp/gltf2_io_gltf.py b/io_scene_gltf2/io/imp/gltf2_io_gltf.py
new file mode 100755
index 00000000..1c9e67a2
--- /dev/null
+++ b/io_scene_gltf2/io/imp/gltf2_io_gltf.py
@@ -0,0 +1,199 @@
+# 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.
+
+from ..com.gltf2_io import gltf_from_dict
+from ..com.gltf2_io_debug import Log
+import logging
+import json
+import struct
+import base64
+from os.path import dirname, join, getsize, isfile
+
+
+class glTFImporter():
+ """glTF Importer class."""
+
+ def __init__(self, filename, import_settings):
+ """initialization."""
+ self.filename = filename
+ self.import_settings = import_settings
+ self.buffers = {}
+
+ if 'loglevel' not in self.import_settings.keys():
+ self.import_settings['loglevel'] = logging.ERROR
+
+ log = Log(import_settings['loglevel'])
+ self.log = log.logger
+ self.log_handler = log.hdlr
+
+ self.SIMPLE = 1
+ self.TEXTURE = 2
+ self.TEXTURE_FACTOR = 3
+
+ # TODO: move to a com place?
+ self.extensions_managed = [
+ 'KHR_materials_pbrSpecularGlossiness'
+ ]
+
+ # TODO : merge with io_constants
+ self.fmt_char_dict = {}
+ self.fmt_char_dict[5120] = 'b' # Byte
+ self.fmt_char_dict[5121] = 'B' # Unsigned Byte
+ self.fmt_char_dict[5122] = 'h' # Short
+ self.fmt_char_dict[5123] = 'H' # Unsigned Short
+ self.fmt_char_dict[5125] = 'I' # Unsigned Int
+ self.fmt_char_dict[5126] = 'f' # Float
+
+ self.component_nb_dict = {}
+ self.component_nb_dict['SCALAR'] = 1
+ self.component_nb_dict['VEC2'] = 2
+ self.component_nb_dict['VEC3'] = 3
+ self.component_nb_dict['VEC4'] = 4
+ self.component_nb_dict['MAT2'] = 4
+ self.component_nb_dict['MAT3'] = 9
+ self.component_nb_dict['MAT4'] = 16
+
+ @staticmethod
+ def bad_json_value(val):
+ """Bad Json value."""
+ raise ValueError('Json contains some unauthorized values')
+
+ def checks(self):
+ """Some checks."""
+ if self.data.asset.version != "2.0":
+ return False, "glTF version must be 2"
+
+ if self.data.extensions_required is not None:
+ for extension in self.data.extensions_required:
+ if extension not in self.data.extensions_used:
+ return False, "Extension required must be in Extension Used too"
+ if extension not in self.extensions_managed:
+ return False, "Extension " + extension + " is not available on this addon version"
+
+ if self.data.extensions_used is not None:
+ for extension in self.data.extensions_used:
+ if extension not in self.extensions_managed:
+ # Non blocking error #TODO log
+ pass
+
+ return True, None
+
+ def load_glb(self):
+ """Load binary glb."""
+ header = struct.unpack_from('<4sII', self.content)
+ self.format = header[0]
+ self.version = header[1]
+ self.file_size = header[2]
+
+ if self.format != b'glTF':
+ return False, "This file is not a glTF/glb file"
+
+ if self.version != 2:
+ return False, "glTF version doesn't match to 2"
+
+ if self.file_size != getsize(self.filename):
+ return False, "File size doesn't match"
+
+ offset = 12 # header size = 12
+
+ # TODO check json type for chunk 0, and BIN type for next ones
+
+ # json
+ type, len_, str_json, offset = self.load_chunk(offset)
+ if len_ != len(str_json):
+ return False, "Length of json part doesn't match"
+ try:
+ json_ = json.loads(str_json.decode('utf-8'), parse_constant=glTFImporter.bad_json_value)
+ self.data = gltf_from_dict(json_)
+ except ValueError as e:
+ return False, e.args[0]
+
+ # binary data
+ chunk_cpt = 0
+ while offset < len(self.content):
+ type, len_, data, offset = self.load_chunk(offset)
+ if len_ != len(data):
+ return False, "Length of bin buffer " + str(chunk_cpt) + " doesn't match"
+
+ self.buffers[chunk_cpt] = data
+ chunk_cpt += 1
+
+ self.content = None
+ return True, None
+
+ def load_chunk(self, offset):
+ """Load chunk."""
+ chunk_header = struct.unpack_from('<I4s', self.content, offset)
+ data_length = chunk_header[0]
+ data_type = chunk_header[1]
+ data = self.content[offset + 8: offset + 8 + data_length]
+
+ return data_type, data_length, data, offset + 8 + data_length
+
+ def read(self):
+ """Read file."""
+ # Check this is a file
+ if not isfile(self.filename):
+ return False, "Please select a file"
+
+ # Check if file is gltf or glb
+ with open(self.filename, 'rb') as f:
+ self.content = f.read()
+
+ self.is_glb_format = self.content[:4] == b'glTF'
+
+ # glTF file
+ if not self.is_glb_format:
+ self.content = None
+ with open(self.filename, 'r') as f:
+ content = f.read()
+ try:
+ self.data = gltf_from_dict(json.loads(content, parse_constant=glTFImporter.bad_json_value))
+ return True, None
+ except ValueError as e:
+ return False, e.args[0]
+
+ # glb file
+ else:
+ # Parsing glb file
+ success, txt = self.load_glb()
+ 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]
+
+ if buffer.uri:
+ sep = ';base64,'
+ if buffer.uri[:5] == 'data:':
+ idx = buffer.uri.find(sep)
+ if idx != -1:
+ data = buffer.uri[idx + len(sep):]
+ self.buffers[buffer_idx] = base64.b64decode(data)
+ return
+
+ with open(join(dirname(self.filename), buffer.uri), 'rb') as f_:
+ self.buffers[buffer_idx] = f_.read()
+