diff options
author | Julien Duroure <julien.duroure@gmail.com> | 2018-11-24 18:28:33 +0300 |
---|---|---|
committer | Julien Duroure <julien.duroure@gmail.com> | 2018-11-24 18:28:33 +0300 |
commit | b1f2133fa2849da272e9d8404f371220226ddaf1 (patch) | |
tree | 25db56e0f2211bd1059fe0e04e78430a6156e021 /io_scene_gltf2/io | |
parent | 8959f1798cfc86924493347304118c61bd5c7f7a (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-x | io_scene_gltf2/io/__init__.py | 16 | ||||
-rwxr-xr-x | io_scene_gltf2/io/com/gltf2_io.py | 1200 | ||||
-rwxr-xr-x | io_scene_gltf2/io/com/gltf2_io_constants.py | 132 | ||||
-rwxr-xr-x | io_scene_gltf2/io/com/gltf2_io_debug.py | 138 | ||||
-rwxr-xr-x | io_scene_gltf2/io/com/gltf2_io_functional.py | 41 | ||||
-rwxr-xr-x | io_scene_gltf2/io/com/gltf2_io_image.py | 154 | ||||
-rwxr-xr-x | io_scene_gltf2/io/com/gltf2_io_trs.py | 68 | ||||
-rwxr-xr-x | io_scene_gltf2/io/exp/gltf2_io_binary_data.py | 36 | ||||
-rwxr-xr-x | io_scene_gltf2/io/exp/gltf2_io_buffer.py | 61 | ||||
-rwxr-xr-x | io_scene_gltf2/io/exp/gltf2_io_export.py | 97 | ||||
-rwxr-xr-x | io_scene_gltf2/io/exp/gltf2_io_get.py | 316 | ||||
-rwxr-xr-x | io_scene_gltf2/io/exp/gltf2_io_image_data.py | 106 | ||||
-rwxr-xr-x | io_scene_gltf2/io/imp/__init__.py | 16 | ||||
-rwxr-xr-x | io_scene_gltf2/io/imp/gltf2_io_binary.py | 178 | ||||
-rwxr-xr-x | io_scene_gltf2/io/imp/gltf2_io_gltf.py | 199 |
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() + |