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

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Duroure <julien.duroure@gmail.com>2018-11-24 18:28:33 +0300
committerJulien Duroure <julien.duroure@gmail.com>2018-11-24 18:28:33 +0300
commitb1f2133fa2849da272e9d8404f371220226ddaf1 (patch)
tree25db56e0f2211bd1059fe0e04e78430a6156e021 /io_scene_gltf2/io
parent8959f1798cfc86924493347304118c61bd5c7f7a (diff)
Initial commit of glTF 2.0 importer/exporter
Official Khronos Group Blender glTF 2.0 importer and exporter. glTF specification: https://github.com/KhronosGroup/glTF The upstream repository can be found here: https://github.com/KhronosGroup/glTF-Blender-IO Reviewed By: Bastien, Campbell Differential Revision: https://developer.blender.org/D3929
Diffstat (limited to 'io_scene_gltf2/io')
-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
15 files changed, 2758 insertions, 0 deletions
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()
+