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/imp | |
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/imp')
-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 |
3 files changed, 393 insertions, 0 deletions
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() + |