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 | |
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
74 files changed, 12593 insertions, 0 deletions
diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py new file mode 100755 index 00000000..0e864cad --- /dev/null +++ b/io_scene_gltf2/__init__.py @@ -0,0 +1,579 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Imports +# + +import os +import bpy +from bpy_extras.io_utils import ImportHelper, ExportHelper +from bpy.types import Operator, AddonPreferences + +from .io.com.gltf2_io_debug import print_console, Log +from .io.imp.gltf2_io_gltf import glTFImporter +from .blender.imp.gltf2_blender_gltf import BlenderGlTF + +from bpy.props import (CollectionProperty, + StringProperty, + BoolProperty, + EnumProperty, + FloatProperty, + IntProperty) + +# +# Globals +# + +bl_info = { + 'name': 'glTF 2.0 format', + 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann & Moritz Becher', + "version": (0, 0, 1), + 'blender': (2, 80, 0), + 'location': 'File > Import-Export', + 'description': 'Import-Export as glTF 2.0', + 'warning': '', + 'wiki_url': '' + '', + 'support': 'COMMUNITY', + 'category': 'Import-Export'} + + +# +# Functions / Classes. +# + + +class GLTF2ExportSettings(bpy.types.Operator): + """Save the export settings on export (saved in .blend). +Toggle off to clear settings""" + bl_label = "Save Settings" + bl_idname = "scene.gltf2_export_settings_set" + + def execute(self, context): + operator = context.active_operator + operator.will_save_settings = not operator.will_save_settings + if not operator.will_save_settings: + # clear settings + context.scene.pop(operator.scene_key) + return {"FINISHED"} + + +class ExportGLTF2_Base: + + # TODO: refactor to avoid boilerplate + + export_copyright: StringProperty( + name='Copyright', + description='', + default='' + ) + + export_embed_buffers: BoolProperty( + name='Embed buffers', + description='', + default=False + ) + + export_embed_images: BoolProperty( + name='Embed images', + description='', + default=False + ) + + export_strip: BoolProperty( + name='Strip delimiters', + description='', + default=False + ) + + export_indices: EnumProperty( + name='Maximum indices', + items=(('UNSIGNED_BYTE', 'Unsigned Byte', ''), + ('UNSIGNED_SHORT', 'Unsigned Short', ''), + ('UNSIGNED_INT', 'Unsigned Integer', '')), + default='UNSIGNED_INT' + ) + + export_force_indices: BoolProperty( + name='Force maximum indices', + description='', + default=False + ) + + export_texcoords: BoolProperty( + name='Export texture coordinates', + description='', + default=True + ) + + export_normals: BoolProperty( + name='Export normals', + description='', + default=True + ) + + export_tangents: BoolProperty( + name='Export tangents', + description='', + default=True + ) + + export_materials: BoolProperty( + name='Export materials', + description='', + default=True + ) + + export_colors: BoolProperty( + name='Export colors', + description='', + default=True + ) + + export_cameras: BoolProperty( + name='Export cameras', + description='', + default=False + ) + + export_camera_infinite: BoolProperty( + name='Infinite perspective Camera', + description='', + default=False + ) + + export_selected: BoolProperty( + name='Export selected only', + description='', + default=False + ) + + export_layers: BoolProperty( + name='Export all layers', + description='', + default=True + ) + + export_extras: BoolProperty( + name='Export extras', + description='', + default=False + ) + + export_yup: BoolProperty( + name='Convert Z up to Y up', + description='', + default=True + ) + + export_apply: BoolProperty( + name='Apply modifiers', + description='', + default=False + ) + + export_animations: BoolProperty( + name='Export animations', + description='', + default=True + ) + + export_frame_range: BoolProperty( + name='Export within playback range', + description='', + default=True + ) + + export_frame_step: IntProperty( + name='Frame step size', + description='Step size (in frames) for animation export.', + default=1, + min=1, + max=120 + ) + + export_move_keyframes: BoolProperty( + name='Keyframes start with 0', + description='', + default=True + ) + + export_force_sampling: BoolProperty( + name='Force sample animations', + description='', + default=False + ) + + export_current_frame: BoolProperty( + name='Export current frame', + description='', + default=True + ) + + export_skins: BoolProperty( + name='Export skinning', + description='', + default=True + ) + + export_bake_skins: BoolProperty( + name='Bake skinning constraints', + description='', + default=False + ) + + export_morph: BoolProperty( + name='Export morphing', + description='', + default=True + ) + + export_morph_normal: BoolProperty( + name='Export morphing normals', + description='', + default=True + ) + + export_morph_tangent: BoolProperty( + name='Export morphing tangents', + description='', + default=True + ) + + export_lights: BoolProperty( + name='Export KHR_lights_punctual', + description='', + default=False + ) + + export_texture_transform: BoolProperty( + name='Export KHR_texture_transform', + description='', + default=False + ) + + export_displacement: BoolProperty( + name='Export KHR_materials_displacement', + description='', + default=False + ) + + will_save_settings: BoolProperty(default=False) + + # Custom scene property for saving settings + scene_key = "glTF2ExportSettings" + + # + + def invoke(self, context, event): + settings = context.scene.get(self.scene_key) + self.will_save_settings = False + if settings: + try: + for (k, v) in settings.items(): + setattr(self, k, v) + self.will_save_settings = True + + except AttributeError: + self.report({"ERROR"}, "Loading export settings failed. Removed corrupted settings") + del context.scene[self.scene_key] + + return ExportHelper.invoke(self, context, event) + + def save_settings(self, context): + # find all export_ props + all_props = self.properties + export_props = {x: all_props.get(x) for x in dir(all_props) + if x.startswith("export_") and all_props.get(x) is not None} + + context.scene[self.scene_key] = export_props + + def execute(self, context): + from .blender.exp import gltf2_blender_export + + if self.will_save_settings: + self.save_settings(context) + + # All custom export settings are stored in this container. + export_settings = {} + + export_settings['gltf_filepath'] = bpy.path.ensure_ext(self.filepath, self.filename_ext) + export_settings['gltf_filedirectory'] = os.path.dirname(export_settings['gltf_filepath']) + '/' + + export_settings['gltf_format'] = self.export_format + export_settings['gltf_copyright'] = self.export_copyright + export_settings['gltf_embed_buffers'] = self.export_embed_buffers + export_settings['gltf_embed_images'] = self.export_embed_images + export_settings['gltf_strip'] = self.export_strip + export_settings['gltf_indices'] = self.export_indices + export_settings['gltf_force_indices'] = self.export_force_indices + export_settings['gltf_texcoords'] = self.export_texcoords + export_settings['gltf_normals'] = self.export_normals + export_settings['gltf_tangents'] = self.export_tangents and self.export_normals + export_settings['gltf_materials'] = self.export_materials + export_settings['gltf_colors'] = self.export_colors + export_settings['gltf_cameras'] = self.export_cameras + if self.export_cameras: + export_settings['gltf_camera_infinite'] = self.export_camera_infinite + else: + export_settings['gltf_camera_infinite'] = False + export_settings['gltf_selected'] = self.export_selected + export_settings['gltf_layers'] = self.export_layers + export_settings['gltf_extras'] = self.export_extras + export_settings['gltf_yup'] = self.export_yup + export_settings['gltf_apply'] = self.export_apply + export_settings['gltf_animations'] = self.export_animations + if self.export_animations: + export_settings['gltf_current_frame'] = False + export_settings['gltf_frame_range'] = self.export_frame_range + export_settings['gltf_move_keyframes'] = self.export_move_keyframes + export_settings['gltf_force_sampling'] = self.export_force_sampling + else: + export_settings['gltf_current_frame'] = self.export_current_frame + export_settings['gltf_frame_range'] = False + export_settings['gltf_move_keyframes'] = False + export_settings['gltf_force_sampling'] = False + export_settings['gltf_skins'] = self.export_skins + if self.export_skins: + export_settings['gltf_bake_skins'] = self.export_bake_skins + else: + export_settings['gltf_bake_skins'] = False + export_settings['gltf_frame_step'] = self.export_frame_step + export_settings['gltf_morph'] = self.export_morph + if self.export_morph: + export_settings['gltf_morph_normal'] = self.export_morph_normal + else: + export_settings['gltf_morph_normal'] = False + if self.export_morph and self.export_morph_normal: + export_settings['gltf_morph_tangent'] = self.export_morph_tangent + else: + export_settings['gltf_morph_tangent'] = False + + export_settings['gltf_lights'] = self.export_lights + export_settings['gltf_texture_transform'] = self.export_texture_transform + export_settings['gltf_displacement'] = self.export_displacement + + export_settings['gltf_binary'] = bytearray() + export_settings['gltf_binaryfilename'] = os.path.splitext(os.path.basename(self.filepath))[0] + '.bin' + + return gltf2_blender_export.save(self, context, export_settings) + + def draw(self, context): + layout = self.layout + + # + + col = layout.box().column() + col.label(text='Embedding:') # , icon='PACKAGE') + col.prop(self, 'export_copyright') + if self.export_format == 'ASCII': + col.prop(self, 'export_embed_buffers') + col.prop(self, 'export_embed_images') + col.prop(self, 'export_strip') + + col = layout.box().column() + col.label(text='Nodes:') # , icon='OOPS') + col.prop(self, 'export_selected') + col.prop(self, 'export_layers') + col.prop(self, 'export_extras') + col.prop(self, 'export_yup') + + col = layout.box().column() + col.label(text='Meshes:') # , icon='MESH_DATA') + col.prop(self, 'export_apply') + col.prop(self, 'export_indices') + col.prop(self, 'export_force_indices') + + col = layout.box().column() + col.label(text='Attributes:') # , icon='SURFACE_DATA') + col.prop(self, 'export_texcoords') + col.prop(self, 'export_normals') + if self.export_normals: + col.prop(self, 'export_tangents') + col.prop(self, 'export_colors') + + col = layout.box().column() + col.label(text='Objects:') # , icon='OBJECT_DATA') + col.prop(self, 'export_cameras') + if self.export_cameras: + col.prop(self, 'export_camera_infinite') + + col = layout.box().column() + col.label(text='Materials:') # , icon='MATERIAL_DATA') + col.prop(self, 'export_materials') + col.prop(self, 'export_texture_transform') + + col = layout.box().column() + col.label(text='Animation:') # , icon='OUTLINER_DATA_POSE') + col.prop(self, 'export_animations') + if self.export_animations: + col.prop(self, 'export_frame_range') + col.prop(self, 'export_frame_step') + col.prop(self, 'export_move_keyframes') + col.prop(self, 'export_force_sampling') + else: + col.prop(self, 'export_current_frame') + col.prop(self, 'export_skins') + if self.export_skins: + col.prop(self, 'export_bake_skins') + col.prop(self, 'export_morph') + if self.export_morph: + col.prop(self, 'export_morph_normal') + if self.export_morph_normal: + col.prop(self, 'export_morph_tangent') + + addon_prefs = context.user_preferences.addons[__name__].preferences + if addon_prefs.experimental: + col = layout.box().column() + col.label(text='Experimental:') # , icon='RADIO') + col.prop(self, 'export_lights') + col.prop(self, 'export_displacement') + + row = layout.row() + row.operator( + GLTF2ExportSettings.bl_idname, + text=GLTF2ExportSettings.bl_label, + icon="%s" % "PINNED" if self.will_save_settings else "UNPINNED") + +# TODO: refactor operators to single operator for both cases + +class ExportGLTF2_GLTF(bpy.types.Operator, ExportGLTF2_Base, ExportHelper): + """Export scene as glTF 2.0 file""" + bl_idname = 'export_scene.gltf' + bl_label = 'Export glTF 2.0' + + filename_ext = '.gltf' + filter_glob: StringProperty(default='*.gltf', options={'HIDDEN'}) + + export_format = 'ASCII' + + +class ExportGLTF2_GLB(bpy.types.Operator, ExportGLTF2_Base, ExportHelper): + """Export scene as glTF 2.0 file""" + bl_idname = 'export_scene.glb' + bl_label = 'Export glTF 2.0 binary' + + filename_ext = '.glb' + filter_glob: StringProperty(default='*.glb', options={'HIDDEN'}) + + export_format = 'BINARY' + + +def menu_func_export_gltf(self, context): + self.layout.operator(ExportGLTF2_GLTF.bl_idname, text='glTF 2.0 (.gltf)') + + +def menu_func_export_glb(self, context): + self.layout.operator(ExportGLTF2_GLB.bl_idname, text='glTF 2.0 (.glb)') + + +class ExportGLTF2_AddonPreferences(AddonPreferences): + bl_idname = __name__ + + experimental: BoolProperty(name='Enable experimental glTF export settings', default=False) + + def draw(self, context): + layout = self.layout + layout.prop(self, "experimental") + + +class ImportglTF2(Operator, ImportHelper): + bl_idname = 'import_scene.gltf' + bl_label = "glTF 2.0 (.gltf/.glb)" + + filter_glob: StringProperty(default="*.gltf;*.glb", options={'HIDDEN'}) + + loglevel: EnumProperty(items=Log.get_levels(), name="Log Level", default=Log.default()) + + import_pack_images: BoolProperty( + name='Pack images', + description='', + default=True + ) + + import_shading: EnumProperty( + name="Shading", + items=(("NORMALS", "Use Normal Data", ""), + ("FLAT", "Flat Shading", ""), + ("SMOOTH", "Smooth Shading", "")), + default="NORMALS") + + def draw(self, context): + layout = self.layout + + layout.prop(self, 'loglevel') + layout.prop(self, 'import_pack_images') + layout.prop(self, 'import_shading') + + def execute(self, context): + return self.import_gltf2(context) + + def import_gltf2(self, context): + import_settings = self.as_keywords() + + self.gltf_importer = glTFImporter(self.filepath, import_settings) + success, txt = self.gltf_importer.read() + if not success: + self.report({'ERROR'}, txt) + return {'CANCELLED'} + success, txt = self.gltf_importer.checks() + if not success: + self.report({'ERROR'}, txt) + return {'CANCELLED'} + self.gltf_importer.log.critical("Data are loaded, start creating Blender stuff") + BlenderGlTF.create(self.gltf_importer) + self.gltf_importer.log.critical("glTF import is now finished") + self.gltf_importer.log.removeHandler(self.gltf_importer.log_handler) + + # Switch to newly created main scene + bpy.context.window.scene = bpy.data.scenes[self.gltf_importer.blender_scene] + + return {'FINISHED'} + + +def menu_func_import(self, context): + self.layout.operator(ImportglTF2.bl_idname, text=ImportglTF2.bl_label) + + +classes = ( + GLTF2ExportSettings, + ExportGLTF2_GLTF, + ExportGLTF2_GLB, + ExportGLTF2_AddonPreferences, + ImportglTF2 +) + + +def register(): + for c in classes: + bpy.utils.register_class(c) + # bpy.utils.register_module(__name__) + + # add to the export / import menu + bpy.types.TOPBAR_MT_file_export.append(menu_func_export_gltf) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export_glb) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + + +def unregister(): + for c in classes: + bpy.utils.unregister_class(c) + # bpy.utils.unregister_module(__name__) + + # remove from the export / import menu + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_gltf) + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_glb) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + diff --git a/io_scene_gltf2/blender/com/gltf2_blender_conversion.py b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py new file mode 100755 index 00000000..95fa292d --- /dev/null +++ b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py @@ -0,0 +1,42 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from mathutils import Matrix, Quaternion + +def matrix_gltf_to_blender(mat_input): + """Matrix from glTF format to Blender format.""" + mat = Matrix([mat_input[0:4], mat_input[4:8], mat_input[8:12], mat_input[12:16]]) + mat.transpose() + return mat + +def loc_gltf_to_blender(loc): + """Location.""" + return loc + +def scale_gltf_to_blender(scale): + """Scaling.""" + return scale + +def quaternion_gltf_to_blender(q): + """Quaternion from glTF to Blender.""" + return Quaternion([q[3], q[0], q[1], q[2]]) + +def scale_to_matrix(scale): + """Scale to matrix.""" + mat = Matrix() + for i in range(3): + mat[i][i] = scale[i] + + return mat + diff --git a/io_scene_gltf2/blender/com/gltf2_blender_data_path.py b/io_scene_gltf2/blender/com/gltf2_blender_data_path.py new file mode 100755 index 00000000..c5ce4025 --- /dev/null +++ b/io_scene_gltf2/blender/com/gltf2_blender_data_path.py @@ -0,0 +1,28 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def get_target_property_name(data_path: str) -> str: + """Retrieve target property.""" + return data_path.rsplit('.', 1)[-1] + + +def get_target_object_path(data_path: str) -> str: + """Retrieve target object data path without property""" + path_split = data_path.rsplit('.', 1) + self_targeting = len(path_split) < 2 + if self_targeting: + return "" + return path_split[0] + diff --git a/io_scene_gltf2/blender/com/gltf2_blender_image.py b/io_scene_gltf2/blender/com/gltf2_blender_image.py new file mode 100755 index 00000000..7564070d --- /dev/null +++ b/io_scene_gltf2/blender/com/gltf2_blender_image.py @@ -0,0 +1,32 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Imports +# + +from ...io.com.gltf2_io_image import create_img_from_pixels + + +def create_img_from_blender_image(blender_image): + """ + Create a new image object using the given blender image. + + Returns the created image object. + """ + if blender_image is None: + return None + + return create_img_from_pixels(blender_image.size[0], blender_image.size[1], blender_image.pixels[:]) + diff --git a/io_scene_gltf2/blender/com/gltf2_blender_image_util.py b/io_scene_gltf2/blender/com/gltf2_blender_image_util.py new file mode 100755 index 00000000..e2563a52 --- /dev/null +++ b/io_scene_gltf2/blender/com/gltf2_blender_image_util.py @@ -0,0 +1,121 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import shutil +import bpy +import zlib +import struct +from io_scene_gltf2.blender.exp import gltf2_blender_get + + +def create_image_file(context, blender_image, dst_path, file_format): + """Create JPEG or PNG file from a given Blender image.""" + # Check, if source image exists e.g. does not exist if image is packed. + file_exists = 1 + try: + src_path = bpy.path.abspath(blender_image.filepath, library=blender_image.library) + file = open(src_path) + except IOError: + file_exists = 0 + else: + file.close() + + if file_exists == 0: + # Image does not exist on disk ... + blender_image.filepath = dst_path + # ... so save it. + blender_image.save() + + elif file_format == blender_image.file_format: + # Copy source image to destination, keeping original format. + + src_path = bpy.path.abspath(blender_image.filepath, library=blender_image.library) + + # Required for comapre. + src_path = src_path.replace('\\', '/') + dst_path = dst_path.replace('\\', '/') + + # Check that source and destination path are not the same using os.path.abspath + # because bpy.path.abspath seems to not always return an absolute path + if os.path.abspath(dst_path) != os.path.abspath(src_path): + shutil.copyfile(src_path, dst_path) + + else: + # Render a new image to destination, converting to target format. + + # TODO: Reusing the existing scene means settings like exposure are applied on export, + # which we don't want, but I'm not sure how to create a new Scene object through the + # Python API. See: https://github.com/KhronosGroup/glTF-Blender-Exporter/issues/184. + + tmp_file_format = context.scene.render.image_settings.file_format + tmp_color_depth = context.scene.render.image_settings.color_depth + + context.scene.render.image_settings.file_format = file_format + context.scene.render.image_settings.color_depth = '8' + blender_image.save_render(dst_path, context.scene) + + context.scene.render.image_settings.file_format = tmp_file_format + context.scene.render.image_settings.color_depth = tmp_color_depth + + +def create_image_data(context, export_settings, blender_image, file_format): + """Create JPEG or PNG byte array from a given Blender image.""" + if blender_image is None: + return None + + if file_format == 'PNG': + return _create_png_data(blender_image) + else: + return _create_jpg_data(context, export_settings, blender_image) + + +def _create_jpg_data(context, export_settings, blender_image): + """Create a JPEG byte array from a given Blender image.""" + uri = gltf2_blender_get.get_image_uri(export_settings, blender_image) + path = export_settings['gltf_filedirectory'] + uri + + create_image_file(context, blender_image, path, 'JPEG') + + jpg_data = open(path, 'rb').read() + os.remove(path) + + return jpg_data + + +def _create_png_data(blender_image): + """Create a PNG byte array from a given Blender image.""" + width, height = blender_image.size + + buf = bytearray([int(channel * 255.0) for channel in blender_image.pixels]) + + # + # Taken from 'blender-thumbnailer.py' in Blender. + # + + # reverse the vertical line order and add null bytes at the start + width_byte_4 = width * 4 + raw_data = b"".join( + b'\x00' + buf[span:span + width_byte_4] for span in range((height - 1) * width * 4, -1, - width_byte_4)) + + def png_pack(png_tag, data): + chunk_head = png_tag + data + return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head)) + + return b"".join([ + b'\x89PNG\r\n\x1a\n', + png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)), + png_pack(b'IDAT', zlib.compress(raw_data, 9)), + png_pack(b'IEND', b'')]) + diff --git a/io_scene_gltf2/blender/com/gltf2_blender_json.py b/io_scene_gltf2/blender/com/gltf2_blender_json.py new file mode 100755 index 00000000..fbf833c1 --- /dev/null +++ b/io_scene_gltf2/blender/com/gltf2_blender_json.py @@ -0,0 +1,38 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import bpy + + +class BlenderJSONEncoder(json.JSONEncoder): + """Blender JSON Encoder.""" + + def default(self, obj): + if isinstance(obj, bpy.types.ID): + return dict( + name=obj.name, + type=obj.__class__.__name__ + ) + return super(BlenderJSONEncoder, self).default(obj) + + +def is_json_convertible(data): + """Test, if a data set can be expressed as JSON.""" + try: + json.dumps(data, cls=BlenderJSONEncoder) + return True + except: + return False + diff --git a/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py b/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py new file mode 100755 index 00000000..05f35954 --- /dev/null +++ b/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py @@ -0,0 +1,59 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def get_output_node(node_tree): + """Retrive output node.""" + output = [node for node in node_tree.nodes if node.type == 'OUTPUT_MATERIAL'][0] + return output + + +def get_output_surface_input(node_tree): + """Retrieve surface input of output node.""" + output_node = get_output_node(node_tree) + return output_node.inputs['Surface'] + + +def get_diffuse_texture(node_tree): + """Retrieve diffuse texture node.""" + for node in node_tree.nodes: + print(node.name) + if node.label == 'BASE COLOR': + return node + + return None + + +def get_preoutput_node_output(node_tree): + """Retrieve node just before output node.""" + output_node = get_output_node(node_tree) + preoutput_node = output_node.inputs['Surface'].links[0].from_node + + # Pre output node is Principled BSDF or any BSDF => BSDF + if 'BSDF' in preoutput_node.type: + return preoutput_node.outputs['BSDF'] + elif 'SHADER' in preoutput_node.type: + return preoutput_node.outputs['Shader'] + else: + print(preoutput_node.type) + + +def get_base_color_node(node_tree): + """Returns the last node of the diffuse block.""" + for node in node_tree.nodes: + if node.label == 'BASE COLOR': + return node + + return None + diff --git a/io_scene_gltf2/blender/com/gltf2_blender_math.py b/io_scene_gltf2/blender/com/gltf2_blender_math.py new file mode 100755 index 00000000..26eb6396 --- /dev/null +++ b/io_scene_gltf2/blender/com/gltf2_blender_math.py @@ -0,0 +1,159 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import typing +import math +from mathutils import Matrix, Vector, Quaternion, Euler + +from io_scene_gltf2.blender.com.gltf2_blender_data_path import get_target_property_name + + +def multiply(a, b): + """Multiplication.""" + return a @ b + + +def list_to_mathutils(values: typing.List[float], data_path: str) -> typing.Union[Vector, Quaternion, Euler]: + """Transform a list to blender py object.""" + target = get_target_property_name(data_path) + + if target == 'location': + return Vector(values) + elif target == 'rotation_axis_angle': + angle = values[0] + axis = values[1:] + return Quaternion(axis, math.radians(angle)) + elif target == 'rotation_euler': + return Euler(values).to_quaternion() + elif target == 'rotation_quaternion': + return Quaternion(values) + elif target == 'scale': + return Vector(values) + elif target == 'value': + return values + + return values + + +def mathutils_to_gltf(x: typing.Union[Vector, Quaternion]) -> typing.List[float]: + """Transform a py object to glTF list.""" + if isinstance(x, Vector): + return list(x) + if isinstance(x, Quaternion): + # Blender has w-first quaternion notation + return [x[1], x[2], x[3], x[0]] + else: + return list(x) + + +def to_yup() -> Matrix: + """Transform to Yup.""" + return Matrix( + ((1.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 1.0, 0.0), + (0.0, -1.0, 0.0, 0.0), + (0.0, 0.0, 0.0, 1.0)) + ) + + +to_zup = to_yup + + +def swizzle_yup(v: typing.Union[Vector, Quaternion], data_path: str) -> typing.Union[Vector, Quaternion]: + """Manage Yup.""" + target = get_target_property_name(data_path) + swizzle_func = { + "location": swizzle_yup_location, + "rotation_axis_angle": swizzle_yup_rotation, + "rotation_euler": swizzle_yup_rotation, + "rotation_quaternion": swizzle_yup_rotation, + "scale": swizzle_yup_scale, + "value": swizzle_yup_value + }.get(target) + + if swizzle_func is None: + raise RuntimeError("Cannot transform values at {}".format(data_path)) + + return swizzle_func(v) + + +def swizzle_yup_location(loc: Vector) -> Vector: + """Manage Yup location.""" + return Vector((loc[0], loc[2], -loc[1])) + + +def swizzle_yup_rotation(rot: Quaternion) -> Quaternion: + """Manage Yup rotation.""" + return Quaternion((rot[0], rot[1], rot[3], -rot[2])) + + +def swizzle_yup_scale(scale: Vector) -> Vector: + """Manage Yup scale.""" + return Vector((scale[0], scale[2], scale[1])) + + +def swizzle_yup_value(value: typing.Any) -> typing.Any: + """Manage Yup value.""" + return value + + +def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Matrix = Matrix.Identity(4)) -> typing \ + .Union[Vector, Quaternion]: + """Manage transformations.""" + target = get_target_property_name(data_path) + transform_func = { + "location": transform_location, + "rotation_axis_angle": transform_rotation, + "rotation_euler": transform_rotation, + "rotation_quaternion": transform_rotation, + "scale": transform_scale, + "value": transform_value + }.get(target) + + if transform_func is None: + raise RuntimeError("Cannot transform values at {}".format(data_path)) + + return transform_func(v, transform) + + +def transform_location(location: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector: + """Transform location.""" + m = Matrix.Translation(location) + m = multiply(transform, m) + return m.to_translation() + + +def transform_rotation(rotation: Quaternion, transform: Matrix = Matrix.Identity(4)) -> Quaternion: + """Transform rotation.""" + m = rotation.to_matrix().to_4x4() + m = multiply(transform, m) + return m.to_quaternion() + + +def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector: + """Transform scale.""" + m = Matrix.Identity(4) + m[0][0] = scale.x + m[1][1] = scale.y + m[2][2] = scale.z + m = multiply(transform, m) + + return m.to_scale() + + +def transform_value(value: Vector, _: Matrix = Matrix.Identity(4)) -> Vector: + """Transform value.""" + return value + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_animate.py b/io_scene_gltf2/blender/exp/gltf2_blender_animate.py new file mode 100755 index 00000000..e4b11487 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_animate.py @@ -0,0 +1,638 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Imports +# + +import bpy +from . import gltf2_blender_export_keys +from . import gltf2_blender_extract +from mathutils import Matrix, Quaternion, Euler + + +# +# Globals +# + +JOINT_NODE = 'JOINT' + +NEEDS_CONVERSION = 'CONVERSION_NEEDED' +CUBIC_INTERPOLATION = 'CUBICSPLINE' +LINEAR_INTERPOLATION = 'LINEAR' +STEP_INTERPOLATION = 'STEP' +BEZIER_INTERPOLATION = 'BEZIER' +CONSTANT_INTERPOLATION = 'CONSTANT' + + +# +# Functions +# + +def animate_get_interpolation(export_settings, blender_fcurve_list): + """ + Retrieve the glTF interpolation, depending on a fcurve list. + + Blender allows mixing and more variations of interpolations. + In such a case, a conversion is needed. + """ + if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]: + return NEEDS_CONVERSION + + # + + interpolation = None + + keyframe_count = None + + for blender_fcurve in blender_fcurve_list: + if blender_fcurve is None: + continue + + # + + current_keyframe_count = len(blender_fcurve.keyframe_points) + + if keyframe_count is None: + keyframe_count = current_keyframe_count + + if current_keyframe_count > 0 > blender_fcurve.keyframe_points[0].co[0]: + return NEEDS_CONVERSION + + if keyframe_count != current_keyframe_count: + return NEEDS_CONVERSION + + # + + for blender_keyframe in blender_fcurve.keyframe_points: + is_bezier = blender_keyframe.interpolation == BEZIER_INTERPOLATION + is_linear = blender_keyframe.interpolation == LINEAR_INTERPOLATION + is_constant = blender_keyframe.interpolation == CONSTANT_INTERPOLATION + + if interpolation is None: + if is_bezier: + interpolation = CUBIC_INTERPOLATION + elif is_linear: + interpolation = LINEAR_INTERPOLATION + elif is_constant: + interpolation = STEP_INTERPOLATION + else: + interpolation = NEEDS_CONVERSION + return interpolation + else: + if is_bezier and interpolation != CUBIC_INTERPOLATION: + interpolation = NEEDS_CONVERSION + return interpolation + elif is_linear and interpolation != LINEAR_INTERPOLATION: + interpolation = NEEDS_CONVERSION + return interpolation + elif is_constant and interpolation != STEP_INTERPOLATION: + interpolation = NEEDS_CONVERSION + return interpolation + elif not is_bezier and not is_linear and not is_constant: + interpolation = NEEDS_CONVERSION + return interpolation + + if interpolation is None: + interpolation = NEEDS_CONVERSION + + return interpolation + + +def animate_convert_rotation_axis_angle(axis_angle): + """Convert an axis angle to a quaternion rotation.""" + q = Quaternion((axis_angle[1], axis_angle[2], axis_angle[3]), axis_angle[0]) + + return [q.x, q.y, q.z, q.w] + + +def animate_convert_rotation_euler(euler, rotation_mode): + """Convert an euler angle to a quaternion rotation.""" + rotation = Euler((euler[0], euler[1], euler[2]), rotation_mode).to_quaternion() + + return [rotation.x, rotation.y, rotation.z, rotation.w] + + +def animate_convert_keys(key_list): + """Convert Blender key frames to glTF time keys depending on the applied frames per second.""" + times = [] + + for key in key_list: + times.append(key / bpy.context.scene.render.fps) + + return times + + +def animate_gather_keys(export_settings, fcurve_list, interpolation): + """ + Merge and sort several key frames to one set. + + If an interpolation conversion is needed, the sample key frames are created as well. + """ + keys = [] + + frame_start = bpy.context.scene.frame_start + frame_end = bpy.context.scene.frame_end + + if interpolation == NEEDS_CONVERSION: + start = None + end = None + + for blender_fcurve in fcurve_list: + if blender_fcurve is None: + continue + + if start is None: + start = blender_fcurve.range()[0] + else: + start = min(start, blender_fcurve.range()[0]) + + if end is None: + end = blender_fcurve.range()[1] + else: + end = max(end, blender_fcurve.range()[1]) + + # + + add_epsilon_keyframe = False + for blender_keyframe in blender_fcurve.keyframe_points: + if add_epsilon_keyframe: + key = blender_keyframe.co[0] - 0.001 + + if key not in keys: + keys.append(key) + + add_epsilon_keyframe = False + + if blender_keyframe.interpolation == CONSTANT_INTERPOLATION: + add_epsilon_keyframe = True + + if add_epsilon_keyframe: + key = end - 0.001 + + if key not in keys: + keys.append(key) + + key = start + while key <= end: + if not export_settings[gltf2_blender_export_keys.FRAME_RANGE] or (frame_start <= key <= frame_end): + keys.append(key) + key += export_settings[gltf2_blender_export_keys.FRAME_STEP] + + keys.sort() + + else: + for blender_fcurve in fcurve_list: + if blender_fcurve is None: + continue + + for blender_keyframe in blender_fcurve.keyframe_points: + key = blender_keyframe.co[0] + if not export_settings[gltf2_blender_export_keys.FRAME_RANGE] or (frame_start <= key <= frame_end): + if key not in keys: + keys.append(key) + + keys.sort() + + return keys + + +def animate_location(export_settings, location, interpolation, node_type, node_name, action_name, matrix_correction, + matrix_basis): + """Calculate/gather the key value pairs for location transformations.""" + joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name] + if not joint_cache.get(node_name): + joint_cache[node_name] = {} + + keys = animate_gather_keys(export_settings, location, interpolation) + + times = animate_convert_keys(keys) + + result = {} + result_in_tangent = {} + result_out_tangent = {} + + keyframe_index = 0 + for timeIndex, time in enumerate(times): + translation = [0.0, 0.0, 0.0] + in_tangent = [0.0, 0.0, 0.0] + out_tangent = [0.0, 0.0, 0.0] + + if node_type == JOINT_NODE: + if joint_cache[node_name].get(keys[keyframe_index]): + translation, tmp_rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]] + else: + bpy.context.scene.frame_set(keys[keyframe_index]) + + matrix = matrix_correction * matrix_basis + + translation, tmp_rotation, tmp_scale = matrix.decompose() + + joint_cache[node_name][keys[keyframe_index]] = [translation, tmp_rotation, tmp_scale] + else: + channel_index = 0 + for blender_fcurve in location: + + if blender_fcurve is not None: + + if interpolation == CUBIC_INTERPOLATION: + blender_key_frame = blender_fcurve.keyframe_points[keyframe_index] + + translation[channel_index] = blender_key_frame.co[1] + + if timeIndex == 0: + in_tangent_value = 0.0 + else: + factor = 3.0 / (time - times[timeIndex - 1]) + in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor + + if timeIndex == len(times) - 1: + out_tangent_value = 0.0 + else: + factor = 3.0 / (times[timeIndex + 1] - time) + out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor + + in_tangent[channel_index] = in_tangent_value + out_tangent[channel_index] = out_tangent_value + else: + value = blender_fcurve.evaluate(keys[keyframe_index]) + + translation[channel_index] = value + + channel_index += 1 + + # handle parent inverse + matrix = Matrix.Translation(translation) + matrix = matrix_correction * matrix + translation = matrix.to_translation() + + translation = gltf2_blender_extract.convert_swizzle_location(translation, export_settings) + in_tangent = gltf2_blender_extract.convert_swizzle_location(in_tangent, export_settings) + out_tangent = gltf2_blender_extract.convert_swizzle_location(out_tangent, export_settings) + + result[time] = translation + result_in_tangent[time] = in_tangent + result_out_tangent[time] = out_tangent + + keyframe_index += 1 + + return result, result_in_tangent, result_out_tangent + + +def animate_rotation_axis_angle(export_settings, rotation_axis_angle, interpolation, node_type, node_name, action_name, + matrix_correction, matrix_basis): + """Calculate/gather the key value pairs for axis angle transformations.""" + joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name] + if not joint_cache.get(node_name): + joint_cache[node_name] = {} + + keys = animate_gather_keys(export_settings, rotation_axis_angle, interpolation) + + times = animate_convert_keys(keys) + + result = {} + + keyframe_index = 0 + for time in times: + axis_angle_rotation = [1.0, 0.0, 0.0, 0.0] + + if node_type == JOINT_NODE: + if joint_cache[node_name].get(keys[keyframe_index]): + tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]] + else: + bpy.context.scene.frame_set(keys[keyframe_index]) + + matrix = matrix_correction * matrix_basis + + tmp_location, rotation, tmp_scale = matrix.decompose() + + joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale] + else: + channel_index = 0 + for blender_fcurve in rotation_axis_angle: + if blender_fcurve is not None: + value = blender_fcurve.evaluate(keys[keyframe_index]) + + axis_angle_rotation[channel_index] = value + + channel_index += 1 + + rotation = animate_convert_rotation_axis_angle(axis_angle_rotation) + + # handle parent inverse + rotation = Quaternion((rotation[3], rotation[0], rotation[1], rotation[2])) + matrix = rotation.to_matrix().to_4x4() + matrix = matrix_correction * matrix + rotation = matrix.to_quaternion() + + # Bring back to internal Quaternion notation. + rotation = gltf2_blender_extract.convert_swizzle_rotation( + [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings) + + # Bring back to glTF Quaternion notation. + rotation = [rotation[1], rotation[2], rotation[3], rotation[0]] + + result[time] = rotation + + keyframe_index += 1 + + return result + + +def animate_rotation_euler(export_settings, rotation_euler, rotation_mode, interpolation, node_type, node_name, + action_name, matrix_correction, matrix_basis): + """Calculate/gather the key value pairs for euler angle transformations.""" + joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name] + if not joint_cache.get(node_name): + joint_cache[node_name] = {} + + keys = animate_gather_keys(export_settings, rotation_euler, interpolation) + + times = animate_convert_keys(keys) + + result = {} + + keyframe_index = 0 + for time in times: + euler_rotation = [0.0, 0.0, 0.0] + + if node_type == JOINT_NODE: + if joint_cache[node_name].get(keys[keyframe_index]): + tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]] + else: + bpy.context.scene.frame_set(keys[keyframe_index]) + + matrix = matrix_correction * matrix_basis + + tmp_location, rotation, tmp_scale = matrix.decompose() + + joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale] + else: + channel_index = 0 + for blender_fcurve in rotation_euler: + if blender_fcurve is not None: + value = blender_fcurve.evaluate(keys[keyframe_index]) + + euler_rotation[channel_index] = value + + channel_index += 1 + + rotation = animate_convert_rotation_euler(euler_rotation, rotation_mode) + + # handle parent inverse + rotation = Quaternion((rotation[3], rotation[0], rotation[1], rotation[2])) + matrix = rotation.to_matrix().to_4x4() + matrix = matrix_correction * matrix + rotation = matrix.to_quaternion() + + # Bring back to internal Quaternion notation. + rotation = gltf2_blender_extract.convert_swizzle_rotation( + [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings) + + # Bring back to glTF Quaternion notation. + rotation = [rotation[1], rotation[2], rotation[3], rotation[0]] + + result[time] = rotation + + keyframe_index += 1 + + return result + + +def animate_rotation_quaternion(export_settings, rotation_quaternion, interpolation, node_type, node_name, action_name, + matrix_correction, matrix_basis): + """Calculate/gather the key value pairs for quaternion transformations.""" + joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name] + if not joint_cache.get(node_name): + joint_cache[node_name] = {} + + keys = animate_gather_keys(export_settings, rotation_quaternion, interpolation) + + times = animate_convert_keys(keys) + + result = {} + result_in_tangent = {} + result_out_tangent = {} + + keyframe_index = 0 + for timeIndex, time in enumerate(times): + rotation = [1.0, 0.0, 0.0, 0.0] + in_tangent = [1.0, 0.0, 0.0, 0.0] + out_tangent = [1.0, 0.0, 0.0, 0.0] + + if node_type == JOINT_NODE: + if joint_cache[node_name].get(keys[keyframe_index]): + tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]] + else: + bpy.context.scene.frame_set(keys[keyframe_index]) + + matrix = matrix_correction * matrix_basis + + tmp_location, rotation, tmp_scale = matrix.decompose() + + joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale] + else: + channel_index = 0 + for blender_fcurve in rotation_quaternion: + + if blender_fcurve is not None: + if interpolation == CUBIC_INTERPOLATION: + blender_key_frame = blender_fcurve.keyframe_points[keyframe_index] + + rotation[channel_index] = blender_key_frame.co[1] + + if timeIndex == 0: + in_tangent_value = 0.0 + else: + factor = 3.0 / (time - times[timeIndex - 1]) + in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor + + if timeIndex == len(times) - 1: + out_tangent_value = 0.0 + else: + factor = 3.0 / (times[timeIndex + 1] - time) + out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor + + in_tangent[channel_index] = in_tangent_value + out_tangent[channel_index] = out_tangent_value + else: + value = blender_fcurve.evaluate(keys[keyframe_index]) + + rotation[channel_index] = value + + channel_index += 1 + + rotation = Quaternion((rotation[0], rotation[1], rotation[2], rotation[3])) + in_tangent = gltf2_blender_extract.convert_swizzle_rotation(in_tangent, export_settings) + out_tangent = gltf2_blender_extract.convert_swizzle_rotation(out_tangent, export_settings) + + # handle parent inverse + matrix = rotation.to_matrix().to_4x4() + matrix = matrix_correction * matrix + rotation = matrix.to_quaternion() + + # Bring back to internal Quaternion notation. + rotation = gltf2_blender_extract.convert_swizzle_rotation( + [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings) + + # Bring to glTF Quaternion notation. + rotation = [rotation[1], rotation[2], rotation[3], rotation[0]] + in_tangent = [in_tangent[1], in_tangent[2], in_tangent[3], in_tangent[0]] + out_tangent = [out_tangent[1], out_tangent[2], out_tangent[3], out_tangent[0]] + + result[time] = rotation + result_in_tangent[time] = in_tangent + result_out_tangent[time] = out_tangent + + keyframe_index += 1 + + return result, result_in_tangent, result_out_tangent + + +def animate_scale(export_settings, scale, interpolation, node_type, node_name, action_name, matrix_correction, + matrix_basis): + """Calculate/gather the key value pairs for scale transformations.""" + joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name] + if not joint_cache.get(node_name): + joint_cache[node_name] = {} + + keys = animate_gather_keys(export_settings, scale, interpolation) + + times = animate_convert_keys(keys) + + result = {} + result_in_tangent = {} + result_out_tangent = {} + + keyframe_index = 0 + for timeIndex, time in enumerate(times): + scale_data = [1.0, 1.0, 1.0] + in_tangent = [0.0, 0.0, 0.0] + out_tangent = [0.0, 0.0, 0.0] + + if node_type == JOINT_NODE: + if joint_cache[node_name].get(keys[keyframe_index]): + tmp_location, tmp_rotation, scale_data = joint_cache[node_name][keys[keyframe_index]] + else: + bpy.context.scene.frame_set(keys[keyframe_index]) + + matrix = matrix_correction * matrix_basis + + tmp_location, tmp_rotation, scale_data = matrix.decompose() + + joint_cache[node_name][keys[keyframe_index]] = [tmp_location, tmp_rotation, scale_data] + else: + channel_index = 0 + for blender_fcurve in scale: + + if blender_fcurve is not None: + if interpolation == CUBIC_INTERPOLATION: + blender_key_frame = blender_fcurve.keyframe_points[keyframe_index] + + scale_data[channel_index] = blender_key_frame.co[1] + + if timeIndex == 0: + in_tangent_value = 0.0 + else: + factor = 3.0 / (time - times[timeIndex - 1]) + in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor + + if timeIndex == len(times) - 1: + out_tangent_value = 0.0 + else: + factor = 3.0 / (times[timeIndex + 1] - time) + out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor + + in_tangent[channel_index] = in_tangent_value + out_tangent[channel_index] = out_tangent_value + else: + value = blender_fcurve.evaluate(keys[keyframe_index]) + + scale_data[channel_index] = value + + channel_index += 1 + + scale_data = gltf2_blender_extract.convert_swizzle_scale(scale_data, export_settings) + in_tangent = gltf2_blender_extract.convert_swizzle_scale(in_tangent, export_settings) + out_tangent = gltf2_blender_extract.convert_swizzle_scale(out_tangent, export_settings) + + # handle parent inverse + matrix = Matrix() + matrix[0][0] = scale_data.x + matrix[1][1] = scale_data.y + matrix[2][2] = scale_data.z + matrix = matrix_correction * matrix + scale_data = matrix.to_scale() + + result[time] = scale_data + result_in_tangent[time] = in_tangent + result_out_tangent[time] = out_tangent + + keyframe_index += 1 + + return result, result_in_tangent, result_out_tangent + + +def animate_value(export_settings, value_parameter, interpolation, + node_type, node_name, matrix_correction, matrix_basis): + """Calculate/gather the key value pairs for scalar anaimations.""" + keys = animate_gather_keys(export_settings, value_parameter, interpolation) + + times = animate_convert_keys(keys) + + result = {} + result_in_tangent = {} + result_out_tangent = {} + + keyframe_index = 0 + for timeIndex, time in enumerate(times): + value_data = [] + in_tangent = [] + out_tangent = [] + + for blender_fcurve in value_parameter: + + if blender_fcurve is not None: + if interpolation == CUBIC_INTERPOLATION: + blender_key_frame = blender_fcurve.keyframe_points[keyframe_index] + + value_data.append(blender_key_frame.co[1]) + + if timeIndex == 0: + in_tangent_value = 0.0 + else: + factor = 3.0 / (time - times[timeIndex - 1]) + in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor + + if timeIndex == len(times) - 1: + out_tangent_value = 0.0 + else: + factor = 3.0 / (times[timeIndex + 1] - time) + out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor + + in_tangent.append(in_tangent_value) + out_tangent.append(out_tangent_value) + else: + value = blender_fcurve.evaluate(keys[keyframe_index]) + + value_data.append(value) + + result[time] = value_data + result_in_tangent[time] = in_tangent + result_out_tangent[time] = out_tangent + + keyframe_index += 1 + + return result, result_in_tangent, result_out_tangent + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export.py b/io_scene_gltf2/blender/exp/gltf2_blender_export.py new file mode 100755 index 00000000..ae2db26b --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_export.py @@ -0,0 +1,100 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import sys +import traceback + +from . import gltf2_blender_export_keys +from . import gltf2_blender_gather +from .gltf2_blender_gltf2_exporter import GlTF2Exporter +from ..com import gltf2_blender_json +from ...io.exp import gltf2_io_export +from ...io.com.gltf2_io_debug import print_console, print_newline + + +def save(operator, + context, + export_settings): + """Start the glTF 2.0 export and saves to content either to a .gltf or .glb file.""" + print_console('INFO', 'Starting glTF 2.0 export') + context.window_manager.progress_begin(0, 100) + context.window_manager.progress_update(0) + + if not export_settings[gltf2_blender_export_keys.COPYRIGHT]: + export_settings[gltf2_blender_export_keys.COPYRIGHT] = None + + scenes, animations = gltf2_blender_gather.gather_gltf2(export_settings) + exporter = GlTF2Exporter(copyright=export_settings[gltf2_blender_export_keys.COPYRIGHT]) + for scene in scenes: + exporter.add_scene(scene) + for animation in animations: + exporter.add_animation(animation) + + buffer = bytes() + if export_settings[gltf2_blender_export_keys.FORMAT] == 'ASCII': + # .gltf + if export_settings[gltf2_blender_export_keys.EMBED_BUFFERS]: + exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY]) + else: + exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY], + export_settings[gltf2_blender_export_keys.BINARY_FILENAME]) + else: + # .glb + buffer = exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY], is_glb=True) + exporter.finalize_images(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY]) + glTF = exporter.glTF + + # + + # TODO: move to custom JSON encoder + def dict_strip(obj): + o = obj + if isinstance(obj, dict): + o = {} + for k, v in obj.items(): + if v is None: + continue + elif isinstance(v, list) and len(v) == 0: + continue + o[k] = dict_strip(v) + elif isinstance(obj, list): + o = [] + for v in obj: + o.append(dict_strip(v)) + elif isinstance(obj, float): + # force floats to int, if they are integers (prevent INTEGER_WRITTEN_AS_FLOAT validator warnings) + if int(obj) == obj: + return int(obj) + return o + + try: + gltf2_io_export.save_gltf(dict_strip(glTF.to_dict()), export_settings, gltf2_blender_json.BlenderJSONEncoder, + buffer) + except AssertionError as e: + _, _, tb = sys.exc_info() + traceback.print_tb(tb) # Fixed format + tb_info = traceback.extract_tb(tb) + for tbi in tb_info: + filename, line, func, text = tbi + print_console('ERROR', 'An error occurred on line {} in statement {}'.format(line, text)) + print_console('ERROR', str(e)) + raise e + + print_console('INFO', 'Finished glTF 2.0 export') + context.window_manager.progress_end() + print_newline() + + return {'FINISHED'} + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py new file mode 100755 index 00000000..9e47645a --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py @@ -0,0 +1,66 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FILTERED_VERTEX_GROUPS = 'filtered_vertex_groups' +FILTERED_MESHES = 'filtered_meshes' +FILTERED_IMAGES = 'filtered_images' +FILTERED_IMAGES_USE_ALPHA = 'filtered_images_use_alpha' +FILTERED_MERGED_IMAGES = 'filtered_merged_images' +FILTERED_TEXTURES = 'filtered_textures' +FILTERED_MATERIALS = 'filtered_materials' +FILTERED_LIGHTS = 'filtered_lights' +TEMPORARY_MESHES = 'temporary_meshes' +FILTERED_OBJECTS = 'filtered_objects' +FILTERED_CAMERAS = 'filtered_cameras' + +APPLY = 'gltf_apply' +LAYERS = 'gltf_layers' +SELECTED = 'gltf_selected' +SKINS = 'gltf_skins' +DISPLACEMENT = 'gltf_displacement' +FORCE_SAMPLING = 'gltf_force_sampling' +FRAME_RANGE = 'gltf_frame_range' +FRAME_STEP = 'gltf_frame_step' +JOINT_CACHE = 'gltf_joint_cache' +COPYRIGHT = 'gltf_copyright' +FORMAT = 'gltf_format' +FILE_DIRECTORY = 'gltf_filedirectory' +BINARY_FILENAME = 'gltf_binaryfilename' +YUP = 'gltf_yup' +MORPH = 'gltf_morph' +INDICES = 'gltf_indices' +CAMERA_INFINITE = 'gltf_camera_infinite' +BAKE_SKINS = 'gltf_bake_skins' +TEX_COORDS = 'gltf_texcoords' +COLORS = 'gltf_colors' +NORMALS = 'gltf_normals' +TANGENTS = 'gltf_tangents' +FORCE_INDICES = 'gltf_force_indices' +MORPH_TANGENT = 'gltf_morph_tangent' +MORPH_NORMAL = 'gltf_morph_normal' +MOVE_KEYFRAMES = 'gltf_move_keyframes' +MATERIALS = 'gltf_materials' +EXTRAS = 'gltf_extras' +CAMERAS = 'gltf_cameras' +LIGHTS = 'gltf_lights' +ANIMATIONS = 'gltf_animations' +EMBED_IMAGES = 'gltf_embed_images' +BINARY = 'gltf_binary' +EMBED_BUFFERS = 'gltf_embed_buffers' +TEXTURE_TRANSFORM = 'gltf_texture_transform' +USE_NO_COLOR = 'gltf_use_no_color' + +METALLIC_ROUGHNESS_IMAGE = "metallic_roughness_image" +GROUP_INDEX = 'group_index' + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py new file mode 100755 index 00000000..a5220129 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -0,0 +1,1117 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Imports +# + +from mathutils import Vector, Quaternion +from mathutils.geometry import tessellate_polygon + +from . import gltf2_blender_export_keys +from ...io.com.gltf2_io_debug import print_console +from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins + +# +# Globals +# + +INDICES_ID = 'indices' +MATERIAL_ID = 'material' +ATTRIBUTES_ID = 'attributes' + +COLOR_PREFIX = 'COLOR_' +MORPH_TANGENT_PREFIX = 'MORPH_TANGENT_' +MORPH_NORMAL_PREFIX = 'MORPH_NORMAL_' +MORPH_POSITION_PREFIX = 'MORPH_POSITION_' +TEXCOORD_PREFIX = 'TEXCOORD_' +WEIGHTS_PREFIX = 'WEIGHTS_' +JOINTS_PREFIX = 'JOINTS_' + +TANGENT_ATTRIBUTE = 'TANGENT' +NORMAL_ATTRIBUTE = 'NORMAL' +POSITION_ATTRIBUTE = 'POSITION' + +GLTF_MAX_COLORS = 2 + + +# +# Classes +# + +class ShapeKey: + def __init__(self, shape_key, vertex_normals, polygon_normals): + self.shape_key = shape_key + self.vertex_normals = vertex_normals + self.polygon_normals = polygon_normals + + +# +# Functions +# + +def convert_swizzle_location(loc, export_settings): + """Convert a location from Blender coordinate system to glTF coordinate system.""" + if export_settings[gltf2_blender_export_keys.YUP]: + return Vector((loc[0], loc[2], -loc[1])) + else: + return Vector((loc[0], loc[1], loc[2])) + + +def convert_swizzle_tangent(tan, export_settings): + """Convert a tangent from Blender coordinate system to glTF coordinate system.""" + if tan[0] == 0.0 and tan[1] == 0.0 and tan[2] == 0.0: + print_console('WARNING', 'Tangent has zero length.') + + if export_settings[gltf2_blender_export_keys.YUP]: + return Vector((tan[0], tan[2], -tan[1], 1.0)) + else: + return Vector((tan[0], tan[1], tan[2], 1.0)) + + +def convert_swizzle_rotation(rot, export_settings): + """ + Convert a quaternion rotation from Blender coordinate system to glTF coordinate system. + + 'w' is still at first position. + """ + if export_settings[gltf2_blender_export_keys.YUP]: + return Quaternion((rot[0], rot[1], rot[3], -rot[2])) + else: + return Quaternion((rot[0], rot[1], rot[2], rot[3])) + + +def convert_swizzle_scale(scale, export_settings): + """Convert a scale from Blender coordinate system to glTF coordinate system.""" + if export_settings[gltf2_blender_export_keys.YUP]: + return Vector((scale[0], scale[2], scale[1])) + else: + return Vector((scale[0], scale[1], scale[2])) + + +def decompose_transition(matrix, context, export_settings): + translation, rotation, scale = matrix.decompose() + """Decompose a matrix depending if it is associated to a joint or node.""" + if context == 'NODE': + translation = convert_swizzle_location(translation, export_settings) + rotation = convert_swizzle_rotation(rotation, export_settings) + scale = convert_swizzle_scale(scale, export_settings) + + # Put w at the end. + rotation = Quaternion((rotation[1], rotation[2], rotation[3], rotation[0])) + + return translation, rotation, scale + + +def color_srgb_to_scene_linear(c): + """ + Convert from sRGB to scene linear color space. + + Source: Cycles addon implementation, node_color.h. + """ + if c < 0.04045: + return 0.0 if c < 0.0 else c * (1.0 / 12.92) + else: + return pow((c + 0.055) * (1.0 / 1.055), 2.4) + + +def extract_primitive_floor(a, indices, use_tangents): + """Shift indices, that the first one starts with 0. It is assumed, that the indices are packed.""" + attributes = { + POSITION_ATTRIBUTE: [], + NORMAL_ATTRIBUTE: [] + } + + if use_tangents: + attributes[TANGENT_ATTRIBUTE] = [] + + result_primitive = { + MATERIAL_ID: a[MATERIAL_ID], + INDICES_ID: [], + ATTRIBUTES_ID: attributes + } + + source_attributes = a[ATTRIBUTES_ID] + + # + + tex_coord_index = 0 + process_tex_coord = True + while process_tex_coord: + tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index) + + if source_attributes.get(tex_coord_id) is not None: + attributes[tex_coord_id] = [] + tex_coord_index += 1 + else: + process_tex_coord = False + + tex_coord_max = tex_coord_index + + # + + color_index = 0 + process_color = True + while process_color: + color_id = COLOR_PREFIX + str(color_index) + + if source_attributes.get(color_id) is not None: + attributes[color_id] = [] + color_index += 1 + else: + process_color = False + + color_max = color_index + + # + + bone_index = 0 + process_bone = True + while process_bone: + joint_id = JOINTS_PREFIX + str(bone_index) + weight_id = WEIGHTS_PREFIX + str(bone_index) + + if source_attributes.get(joint_id) is not None: + attributes[joint_id] = [] + attributes[weight_id] = [] + bone_index += 1 + else: + process_bone = False + + bone_max = bone_index + + # + + morph_index = 0 + process_morph = True + while process_morph: + morph_position_id = MORPH_POSITION_PREFIX + str(morph_index) + morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index) + morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index) + + if source_attributes.get(morph_position_id) is not None: + attributes[morph_position_id] = [] + attributes[morph_normal_id] = [] + if use_tangents: + attributes[morph_tangent_id] = [] + morph_index += 1 + else: + process_morph = False + + morph_max = morph_index + + # + + min_index = min(indices) + max_index = max(indices) + + for old_index in indices: + result_primitive[INDICES_ID].append(old_index - min_index) + + for old_index in range(min_index, max_index + 1): + for vi in range(0, 3): + attributes[POSITION_ATTRIBUTE].append(source_attributes[POSITION_ATTRIBUTE][old_index * 3 + vi]) + attributes[NORMAL_ATTRIBUTE].append(source_attributes[NORMAL_ATTRIBUTE][old_index * 3 + vi]) + + if use_tangents: + for vi in range(0, 4): + attributes[TANGENT_ATTRIBUTE].append(source_attributes[TANGENT_ATTRIBUTE][old_index * 4 + vi]) + + for tex_coord_index in range(0, tex_coord_max): + tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index) + for vi in range(0, 2): + attributes[tex_coord_id].append(source_attributes[tex_coord_id][old_index * 2 + vi]) + + for color_index in range(0, color_max): + color_id = COLOR_PREFIX + str(color_index) + for vi in range(0, 4): + attributes[color_id].append(source_attributes[color_id][old_index * 4 + vi]) + + for bone_index in range(0, bone_max): + joint_id = JOINTS_PREFIX + str(bone_index) + weight_id = WEIGHTS_PREFIX + str(bone_index) + for vi in range(0, 4): + attributes[joint_id].append(source_attributes[joint_id][old_index * 4 + vi]) + attributes[weight_id].append(source_attributes[weight_id][old_index * 4 + vi]) + + for morph_index in range(0, morph_max): + morph_position_id = MORPH_POSITION_PREFIX + str(morph_index) + morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index) + morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index) + for vi in range(0, 3): + attributes[morph_position_id].append(source_attributes[morph_position_id][old_index * 3 + vi]) + attributes[morph_normal_id].append(source_attributes[morph_normal_id][old_index * 3 + vi]) + if use_tangents: + for vi in range(0, 4): + attributes[morph_tangent_id].append(source_attributes[morph_tangent_id][old_index * 4 + vi]) + + return result_primitive + + +def extract_primitive_pack(a, indices, use_tangents): + """Pack indices, that the first one starts with 0. Current indices can have gaps.""" + attributes = { + POSITION_ATTRIBUTE: [], + NORMAL_ATTRIBUTE: [] + } + + if use_tangents: + attributes[TANGENT_ATTRIBUTE] = [] + + result_primitive = { + MATERIAL_ID: a[MATERIAL_ID], + INDICES_ID: [], + ATTRIBUTES_ID: attributes + } + + source_attributes = a[ATTRIBUTES_ID] + + # + + tex_coord_index = 0 + process_tex_coord = True + while process_tex_coord: + tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index) + + if source_attributes.get(tex_coord_id) is not None: + attributes[tex_coord_id] = [] + tex_coord_index += 1 + else: + process_tex_coord = False + + tex_coord_max = tex_coord_index + + # + + color_index = 0 + process_color = True + while process_color: + color_id = COLOR_PREFIX + str(color_index) + + if source_attributes.get(color_id) is not None: + attributes[color_id] = [] + color_index += 1 + else: + process_color = False + + color_max = color_index + + # + + bone_index = 0 + process_bone = True + while process_bone: + joint_id = JOINTS_PREFIX + str(bone_index) + weight_id = WEIGHTS_PREFIX + str(bone_index) + + if source_attributes.get(joint_id) is not None: + attributes[joint_id] = [] + attributes[weight_id] = [] + bone_index += 1 + else: + process_bone = False + + bone_max = bone_index + + # + + morph_index = 0 + process_morph = True + while process_morph: + morph_position_id = MORPH_POSITION_PREFIX + str(morph_index) + morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index) + morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index) + + if source_attributes.get(morph_position_id) is not None: + attributes[morph_position_id] = [] + attributes[morph_normal_id] = [] + if use_tangents: + attributes[morph_tangent_id] = [] + morph_index += 1 + else: + process_morph = False + + morph_max = morph_index + + # + + old_to_new_indices = {} + new_to_old_indices = {} + + new_index = 0 + for old_index in indices: + if old_to_new_indices.get(old_index) is None: + old_to_new_indices[old_index] = new_index + new_to_old_indices[new_index] = old_index + new_index += 1 + + result_primitive[INDICES_ID].append(old_to_new_indices[old_index]) + + end_new_index = new_index + + for new_index in range(0, end_new_index): + old_index = new_to_old_indices[new_index] + + for vi in range(0, 3): + attributes[POSITION_ATTRIBUTE].append(source_attributes[POSITION_ATTRIBUTE][old_index * 3 + vi]) + attributes[NORMAL_ATTRIBUTE].append(source_attributes[NORMAL_ATTRIBUTE][old_index * 3 + vi]) + + if use_tangents: + for vi in range(0, 4): + attributes[TANGENT_ATTRIBUTE].append(source_attributes[TANGENT_ATTRIBUTE][old_index * 4 + vi]) + + for tex_coord_index in range(0, tex_coord_max): + tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index) + for vi in range(0, 2): + attributes[tex_coord_id].append(source_attributes[tex_coord_id][old_index * 2 + vi]) + + for color_index in range(0, color_max): + color_id = COLOR_PREFIX + str(color_index) + for vi in range(0, 4): + attributes[color_id].append(source_attributes[color_id][old_index * 4 + vi]) + + for bone_index in range(0, bone_max): + joint_id = JOINTS_PREFIX + str(bone_index) + weight_id = WEIGHTS_PREFIX + str(bone_index) + for vi in range(0, 4): + attributes[joint_id].append(source_attributes[joint_id][old_index * 4 + vi]) + attributes[weight_id].append(source_attributes[weight_id][old_index * 4 + vi]) + + for morph_index in range(0, morph_max): + morph_position_id = MORPH_POSITION_PREFIX + str(morph_index) + morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index) + morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index) + for vi in range(0, 3): + attributes[morph_position_id].append(source_attributes[morph_position_id][old_index * 3 + vi]) + attributes[morph_normal_id].append(source_attributes[morph_normal_id][old_index * 3 + vi]) + if use_tangents: + for vi in range(0, 4): + attributes[morph_tangent_id].append(source_attributes[morph_tangent_id][old_index * 4 + vi]) + + return result_primitive + + +def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, export_settings): + """ + Extract primitives from a mesh. Polygons are triangulated and sorted by material. + + Furthermore, primitives are split up, if the indices range is exceeded. + Finally, triangles are also split up/duplicated, if face normals are used instead of vertex normals. + """ + print_console('INFO', 'Extracting primitive') + + use_tangents = False + if blender_mesh.uv_layers.active and len(blender_mesh.uv_layers) > 0: + try: + blender_mesh.calc_tangents() + use_tangents = True + except Exception: + print_console('WARNING', 'Could not calculate tangents. Please try to triangulate the mesh first.') + + # + + material_map = {} + + # + # Gathering position, normal and tex_coords. + # + no_material_attributes = { + POSITION_ATTRIBUTE: [], + NORMAL_ATTRIBUTE: [] + } + + if use_tangents: + no_material_attributes[TANGENT_ATTRIBUTE] = [] + + # + # Directory of materials with its primitive. + # + no_material_primitives = { + MATERIAL_ID: '', + INDICES_ID: [], + ATTRIBUTES_ID: no_material_attributes + } + + material_name_to_primitives = {'': no_material_primitives} + + # + + vertex_index_to_new_indices = {} + + material_map[''] = vertex_index_to_new_indices + + # + # Create primitive for each material. + # + for blender_material in blender_mesh.materials: + if blender_material is None: + continue + + attributes = { + POSITION_ATTRIBUTE: [], + NORMAL_ATTRIBUTE: [] + } + + if use_tangents: + attributes[TANGENT_ATTRIBUTE] = [] + + primitive = { + MATERIAL_ID: blender_material.name, + INDICES_ID: [], + ATTRIBUTES_ID: attributes + } + + material_name_to_primitives[blender_material.name] = primitive + + # + + vertex_index_to_new_indices = {} + + material_map[blender_material.name] = vertex_index_to_new_indices + + tex_coord_max = 0 + if blender_mesh.uv_layers.active: + tex_coord_max = len(blender_mesh.uv_layers) + + # + + vertex_colors = {} + + color_index = 0 + for vertex_color in blender_mesh.vertex_colors: + vertex_color_name = COLOR_PREFIX + str(color_index) + vertex_colors[vertex_color_name] = vertex_color + + color_index += 1 + if color_index >= GLTF_MAX_COLORS: + break + color_max = color_index + + # + + bone_max = 0 + for blender_polygon in blender_mesh.polygons: + for loop_index in blender_polygon.loop_indices: + vertex_index = blender_mesh.loops[loop_index].vertex_index + bones_count = len(blender_mesh.vertices[vertex_index].groups) + if bones_count > 0: + if bones_count % 4 == 0: + bones_count -= 1 + bone_max = max(bone_max, bones_count // 4 + 1) + + # + + morph_max = 0 + + blender_shape_keys = [] + + if blender_mesh.shape_keys is not None: + morph_max = len(blender_mesh.shape_keys.key_blocks) - 1 + + for blender_shape_key in blender_mesh.shape_keys.key_blocks: + if blender_shape_key != blender_shape_key.relative_key: + blender_shape_keys.append(ShapeKey( + blender_shape_key, + blender_shape_key.normals_vertex_get(), # calculate vertex normals for this shape key + blender_shape_key.normals_polygon_get())) # calculate polygon normals for this shape key + + # + # Convert polygon to primitive indices and eliminate invalid ones. Assign to material. + # + for blender_polygon in blender_mesh.polygons: + export_color = True + + # + + if blender_polygon.material_index < 0 or blender_polygon.material_index >= len(blender_mesh.materials) or \ + blender_mesh.materials[blender_polygon.material_index] is None: + primitive = material_name_to_primitives[''] + vertex_index_to_new_indices = material_map[''] + else: + primitive = material_name_to_primitives[blender_mesh.materials[blender_polygon.material_index].name] + vertex_index_to_new_indices = material_map[blender_mesh.materials[blender_polygon.material_index].name] + # + + attributes = primitive[ATTRIBUTES_ID] + + face_normal = blender_polygon.normal + face_tangent = Vector((0.0, 0.0, 0.0)) + face_bitangent = Vector((0.0, 0.0, 0.0)) + if use_tangents: + for loop_index in blender_polygon.loop_indices: + temp_vertex = blender_mesh.loops[loop_index] + face_tangent += temp_vertex.tangent + face_bitangent += temp_vertex.bitangent + + face_tangent.normalize() + face_bitangent.normalize() + + # + + indices = primitive[INDICES_ID] + + loop_index_list = [] + + if len(blender_polygon.loop_indices) == 3: + loop_index_list.extend(blender_polygon.loop_indices) + elif len(blender_polygon.loop_indices) > 3: + # Triangulation of polygon. Using internal function, as non-convex polygons could exist. + polyline = [] + + for loop_index in blender_polygon.loop_indices: + vertex_index = blender_mesh.loops[loop_index].vertex_index + v = blender_mesh.vertices[vertex_index].co + polyline.append(Vector((v[0], v[1], v[2]))) + + triangles = tessellate_polygon((polyline,)) + + for triangle in triangles: + loop_index_list.append(blender_polygon.loop_indices[triangle[0]]) + loop_index_list.append(blender_polygon.loop_indices[triangle[2]]) + loop_index_list.append(blender_polygon.loop_indices[triangle[1]]) + else: + continue + + for loop_index in loop_index_list: + vertex_index = blender_mesh.loops[loop_index].vertex_index + + if vertex_index_to_new_indices.get(vertex_index) is None: + vertex_index_to_new_indices[vertex_index] = [] + + # + + v = None + n = None + t = None + b = None + uvs = [] + colors = [] + joints = [] + weights = [] + + target_positions = [] + target_normals = [] + target_tangents = [] + + vertex = blender_mesh.vertices[vertex_index] + + v = convert_swizzle_location(vertex.co, export_settings) + if blender_polygon.use_smooth: + n = convert_swizzle_location(vertex.normal, export_settings) + if use_tangents: + t = convert_swizzle_tangent(blender_mesh.loops[loop_index].tangent, export_settings) + b = convert_swizzle_location(blender_mesh.loops[loop_index].bitangent, export_settings) + else: + n = convert_swizzle_location(face_normal, export_settings) + if use_tangents: + t = convert_swizzle_tangent(face_tangent, export_settings) + b = convert_swizzle_location(face_bitangent, export_settings) + + if use_tangents: + tv = Vector((t[0], t[1], t[2])) + bv = Vector((b[0], b[1], b[2])) + nv = Vector((n[0], n[1], n[2])) + + if (nv.cross(tv)).dot(bv) < 0.0: + t[3] = -1.0 + + if blender_mesh.uv_layers.active: + for tex_coord_index in range(0, tex_coord_max): + uv = blender_mesh.uv_layers[tex_coord_index].data[loop_index].uv + uvs.append([uv.x, 1.0 - uv.y]) + + # + + if color_max > 0 and export_color: + for color_index in range(0, color_max): + color_name = COLOR_PREFIX + str(color_index) + color = vertex_colors[color_name].data[loop_index].color + colors.append([ + color_srgb_to_scene_linear(color[0]), + color_srgb_to_scene_linear(color[1]), + color_srgb_to_scene_linear(color[2]), + 1.0 + ]) + + # + + bone_count = 0 + + if vertex.groups is not None and len(vertex.groups) > 0 and export_settings[gltf2_blender_export_keys.SKINS]: + joint = [] + weight = [] + for group_element in vertex.groups: + + if len(joint) == 4: + bone_count += 1 + joints.append(joint) + weights.append(weight) + joint = [] + weight = [] + + # + + vertex_group_index = group_element.group + vertex_group_name = blender_vertex_groups[vertex_group_index].name + + # + + joint_index = 0 + modifiers_dict = {m.type: m for m in modifiers} + if "ARMATURE" in modifiers_dict: + armature = modifiers_dict["ARMATURE"].object + skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings) + for index, j in enumerate(skin.joints): + if j.name == vertex_group_name: + joint_index = index + + joint_weight = group_element.weight + + # + joint.append(joint_index) + weight.append(joint_weight) + + if len(joint) > 0: + bone_count += 1 + + for fill in range(0, 4 - len(joint)): + joint.append(0) + weight.append(0.0) + + joints.append(joint) + weights.append(weight) + + for fill in range(0, bone_max - bone_count): + joints.append([0, 0, 0, 0]) + weights.append([0.0, 0.0, 0.0, 0.0]) + + # + + if morph_max > 0 and export_settings[gltf2_blender_export_keys.MORPH]: + for morph_index in range(0, morph_max): + blender_shape_key = blender_shape_keys[morph_index] + + v_morph = convert_swizzle_location(blender_shape_key.shape_key.data[vertex_index].co, + export_settings) + + # Store delta. + v_morph -= v + + target_positions.append(v_morph) + + # + + n_morph = None + + if blender_polygon.use_smooth: + temp_normals = blender_shape_key.vertex_normals + n_morph = (temp_normals[vertex_index * 3 + 0], temp_normals[vertex_index * 3 + 1], + temp_normals[vertex_index * 3 + 2]) + else: + temp_normals = blender_shape_key.polygon_normals + n_morph = ( + temp_normals[blender_polygon.index * 3 + 0], temp_normals[blender_polygon.index * 3 + 1], + temp_normals[blender_polygon.index * 3 + 2]) + + n_morph = convert_swizzle_location(n_morph, export_settings) + + # Store delta. + n_morph -= n + + target_normals.append(n_morph) + + # + + if use_tangents: + rotation = n_morph.rotation_difference(n) + + t_morph = Vector((t[0], t[1], t[2])) + + t_morph.rotate(rotation) + + target_tangents.append(t_morph) + + # + # + + create = True + + for current_new_index in vertex_index_to_new_indices[vertex_index]: + found = True + + for i in range(0, 3): + if attributes[POSITION_ATTRIBUTE][current_new_index * 3 + i] != v[i]: + found = False + break + + if attributes[NORMAL_ATTRIBUTE][current_new_index * 3 + i] != n[i]: + found = False + break + + if use_tangents: + for i in range(0, 4): + if attributes[TANGENT_ATTRIBUTE][current_new_index * 4 + i] != t[i]: + found = False + break + + if not found: + continue + + for tex_coord_index in range(0, tex_coord_max): + uv = uvs[tex_coord_index] + + tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index) + for i in range(0, 2): + if attributes[tex_coord_id][current_new_index * 2 + i] != uv[i]: + found = False + break + + if export_color: + for color_index in range(0, color_max): + color = colors[color_index] + + color_id = COLOR_PREFIX + str(color_index) + for i in range(0, 3): + # Alpha is always 1.0 - see above. + current_color = attributes[color_id][current_new_index * 4 + i] + if color_srgb_to_scene_linear(current_color) != color[i]: + found = False + break + + if export_settings[gltf2_blender_export_keys.SKINS]: + for bone_index in range(0, bone_max): + joint = joints[bone_index] + weight = weights[bone_index] + + joint_id = JOINTS_PREFIX + str(bone_index) + weight_id = WEIGHTS_PREFIX + str(bone_index) + for i in range(0, 4): + if attributes[joint_id][current_new_index * 4 + i] != joint[i]: + found = False + break + if attributes[weight_id][current_new_index * 4 + i] != weight[i]: + found = False + break + + if export_settings[gltf2_blender_export_keys.MORPH]: + for morph_index in range(0, morph_max): + target_position = target_positions[morph_index] + target_normal = target_normals[morph_index] + if use_tangents: + target_tangent = target_tangents[morph_index] + + target_position_id = MORPH_POSITION_PREFIX + str(morph_index) + target_normal_id = MORPH_NORMAL_PREFIX + str(morph_index) + target_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index) + for i in range(0, 3): + if attributes[target_position_id][current_new_index * 3 + i] != target_position[i]: + found = False + break + if attributes[target_normal_id][current_new_index * 3 + i] != target_normal[i]: + found = False + break + if use_tangents: + if attributes[target_tangent_id][current_new_index * 3 + i] != target_tangent[i]: + found = False + break + + if found: + indices.append(current_new_index) + + create = False + break + + if not create: + continue + + new_index = 0 + + if primitive.get('max_index') is not None: + new_index = primitive['max_index'] + 1 + + primitive['max_index'] = new_index + + vertex_index_to_new_indices[vertex_index].append(new_index) + + # + # + + indices.append(new_index) + + # + + attributes[POSITION_ATTRIBUTE].extend(v) + attributes[NORMAL_ATTRIBUTE].extend(n) + if use_tangents: + attributes[TANGENT_ATTRIBUTE].extend(t) + + if blender_mesh.uv_layers.active: + for tex_coord_index in range(0, tex_coord_max): + tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index) + + if attributes.get(tex_coord_id) is None: + attributes[tex_coord_id] = [] + + attributes[tex_coord_id].extend(uvs[tex_coord_index]) + + if export_color: + for color_index in range(0, color_max): + color_id = COLOR_PREFIX + str(color_index) + + if attributes.get(color_id) is None: + attributes[color_id] = [] + + attributes[color_id].extend(colors[color_index]) + + if export_settings[gltf2_blender_export_keys.SKINS]: + for bone_index in range(0, bone_max): + joint_id = JOINTS_PREFIX + str(bone_index) + + if attributes.get(joint_id) is None: + attributes[joint_id] = [] + + attributes[joint_id].extend(joints[bone_index]) + + weight_id = WEIGHTS_PREFIX + str(bone_index) + + if attributes.get(weight_id) is None: + attributes[weight_id] = [] + + attributes[weight_id].extend(weights[bone_index]) + + if export_settings[gltf2_blender_export_keys.MORPH]: + for morph_index in range(0, morph_max): + target_position_id = MORPH_POSITION_PREFIX + str(morph_index) + + if attributes.get(target_position_id) is None: + attributes[target_position_id] = [] + + attributes[target_position_id].extend(target_positions[morph_index]) + + target_normal_id = MORPH_NORMAL_PREFIX + str(morph_index) + + if attributes.get(target_normal_id) is None: + attributes[target_normal_id] = [] + + attributes[target_normal_id].extend(target_normals[morph_index]) + + if use_tangents: + target_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index) + + if attributes.get(target_tangent_id) is None: + attributes[target_tangent_id] = [] + + attributes[target_tangent_id].extend(target_tangents[morph_index]) + + # + # Add primitive plus split them if needed. + # + + result_primitives = [] + + for material_name, primitive in material_name_to_primitives.items(): + export_color = True + + # + + indices = primitive[INDICES_ID] + + if len(indices) == 0: + continue + + position = primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE] + normal = primitive[ATTRIBUTES_ID][NORMAL_ATTRIBUTE] + if use_tangents: + tangent = primitive[ATTRIBUTES_ID][TANGENT_ATTRIBUTE] + tex_coords = [] + for tex_coord_index in range(0, tex_coord_max): + tex_coords.append(primitive[ATTRIBUTES_ID][TEXCOORD_PREFIX + str(tex_coord_index)]) + colors = [] + if export_color: + for color_index in range(0, color_max): + tex_coords.append(primitive[ATTRIBUTES_ID][COLOR_PREFIX + str(color_index)]) + joints = [] + weights = [] + if export_settings[gltf2_blender_export_keys.SKINS]: + for bone_index in range(0, bone_max): + joints.append(primitive[ATTRIBUTES_ID][JOINTS_PREFIX + str(bone_index)]) + weights.append(primitive[ATTRIBUTES_ID][WEIGHTS_PREFIX + str(bone_index)]) + + target_positions = [] + target_normals = [] + target_tangents = [] + if export_settings[gltf2_blender_export_keys.MORPH]: + for morph_index in range(0, morph_max): + target_positions.append(primitive[ATTRIBUTES_ID][MORPH_POSITION_PREFIX + str(morph_index)]) + target_normals.append(primitive[ATTRIBUTES_ID][MORPH_NORMAL_PREFIX + str(morph_index)]) + if use_tangents: + target_tangents.append(primitive[ATTRIBUTES_ID][MORPH_TANGENT_PREFIX + str(morph_index)]) + + # + + count = len(indices) + + if count == 0: + continue + + max_index = max(indices) + + # + + range_indices = 65536 + if export_settings[gltf2_blender_export_keys.INDICES] == 'UNSIGNED_BYTE': + range_indices = 256 + elif export_settings[gltf2_blender_export_keys.INDICES] == 'UNSIGNED_INT': + range_indices = 4294967296 + + # + + if max_index >= range_indices: + # + # Spliting result_primitives. + # + + # At start, all indicees are pending. + pending_attributes = { + POSITION_ATTRIBUTE: [], + NORMAL_ATTRIBUTE: [] + } + + if use_tangents: + pending_attributes[TANGENT_ATTRIBUTE] = [] + + pending_primitive = { + MATERIAL_ID: material_name, + INDICES_ID: [], + ATTRIBUTES_ID: pending_attributes + } + + pending_primitive[INDICES_ID].extend(indices) + + pending_attributes[POSITION_ATTRIBUTE].extend(position) + pending_attributes[NORMAL_ATTRIBUTE].extend(normal) + if use_tangents: + pending_attributes[TANGENT_ATTRIBUTE].extend(tangent) + tex_coord_index = 0 + for tex_coord in tex_coords: + pending_attributes[TEXCOORD_PREFIX + str(tex_coord_index)] = tex_coord + tex_coord_index += 1 + if export_color: + color_index = 0 + for color in colors: + pending_attributes[COLOR_PREFIX + str(color_index)] = color + color_index += 1 + if export_settings[gltf2_blender_export_keys.SKINS]: + joint_index = 0 + for joint in joints: + pending_attributes[JOINTS_PREFIX + str(joint_index)] = joint + joint_index += 1 + weight_index = 0 + for weight in weights: + pending_attributes[WEIGHTS_PREFIX + str(weight_index)] = weight + weight_index += 1 + if export_settings[gltf2_blender_export_keys.MORPH]: + morph_index = 0 + for target_position in target_positions: + pending_attributes[MORPH_POSITION_PREFIX + str(morph_index)] = target_position + morph_index += 1 + morph_index = 0 + for target_normal in target_normals: + pending_attributes[MORPH_NORMAL_PREFIX + str(morph_index)] = target_normal + morph_index += 1 + if use_tangents: + morph_index = 0 + for target_tangent in target_tangents: + pending_attributes[MORPH_TANGENT_PREFIX + str(morph_index)] = target_tangent + morph_index += 1 + + pending_indices = pending_primitive[INDICES_ID] + + # Continue until all are processed. + while len(pending_indices) > 0: + + process_indices = pending_primitive[INDICES_ID] + + pending_indices = [] + + # + # + + all_local_indices = [] + + for i in range(0, (max(process_indices) // range_indices) + 1): + all_local_indices.append([]) + + # + # + + # For all faces ... + for face_index in range(0, len(process_indices), 3): + + written = False + + face_min_index = min(process_indices[face_index + 0], process_indices[face_index + 1], + process_indices[face_index + 2]) + face_max_index = max(process_indices[face_index + 0], process_indices[face_index + 1], + process_indices[face_index + 2]) + + # ... check if it can be but in a range of maximum indices. + for i in range(0, (max(process_indices) // range_indices) + 1): + offset = i * range_indices + + # Yes, so store the primitive with its indices. + if face_min_index >= offset and face_max_index < offset + range_indices: + all_local_indices[i].extend( + [process_indices[face_index + 0], process_indices[face_index + 1], + process_indices[face_index + 2]]) + + written = True + break + + # If not written, the triangel face has indices from different ranges. + if not written: + pending_indices.extend([process_indices[face_index + 0], process_indices[face_index + 1], + process_indices[face_index + 2]]) + + # Only add result_primitives, which do have indices in it. + for local_indices in all_local_indices: + if len(local_indices) > 0: + current_primitive = extract_primitive_floor(pending_primitive, local_indices, use_tangents) + + result_primitives.append(current_primitive) + + print_console('DEBUG', 'Adding primitive with splitting. Indices: ' + str( + len(current_primitive[INDICES_ID])) + ' Vertices: ' + str( + len(current_primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE]) // 3)) + + # Process primitive faces having indices in several ranges. + if len(pending_indices) > 0: + pending_primitive = extract_primitive_pack(pending_primitive, pending_indices, use_tangents) + + print_console('DEBUG', 'Creating temporary primitive for splitting') + + else: + # + # No splitting needed. + # + result_primitives.append(primitive) + + print_console('DEBUG', 'Adding primitive without splitting. Indices: ' + str( + len(primitive[INDICES_ID])) + ' Vertices: ' + str( + len(primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE]) // 3)) + + print_console('INFO', 'Primitives created: ' + str(len(result_primitives))) + + return result_primitives + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_filter.py b/io_scene_gltf2/blender/exp/gltf2_blender_filter.py new file mode 100755 index 00000000..ed3ee055 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_filter.py @@ -0,0 +1,455 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Imports +# + +import bpy +from . import gltf2_blender_export_keys +from . import gltf2_blender_get +from ...io.com.gltf2_io_debug import print_console +from ..com.gltf2_blender_image import create_img_from_blender_image +from ...io.com import gltf2_io_image + +# +# Globals +# + +PREVIEW = 'PREVIEW' +GLOSSINESS = 'glTF Specular Glossiness' +ROUGHNESS = 'glTF Metallic Roughness' + + +# +# Functions +# + +def filter_merge_image(export_settings, blender_image): + metallic_channel = gltf2_blender_get.get_image_material_usage_to_socket(blender_image, "Metallic") + roughness_channel = gltf2_blender_get.get_image_material_usage_to_socket(blender_image, "Roughness") + + if metallic_channel < 0 and roughness_channel < 0: + return False + + output = export_settings[gltf2_blender_export_keys.METALLIC_ROUGHNESS_IMAGE] + if export_settings.get(export_keys.METALLIC_ROUGHNESS_IMAGE) is None: + width = blender_image.image.size[0] + height = blender_image.image.size[1] + output = gltf2_io_image.create_img(width, height, r=1.0, g=1.0, b=1.0, a=1.0) + + source = create_img_from_blender_image(blender_image.image) + + if metallic_channel >= 0: + gltf2_io_image.copy_img_channel(output, dst_channel=2, src_image=source, src_channel=metallic_channel) + output.name = blender_image.image.name + output.name + if roughness_channel >= 0: + gltf2_io_image.copy_img_channel(output, dst_channel=1, src_image=source, src_channel=roughness_channel) + if metallic_channel < 0: + output.name = output.name + blender_image.image.name + return True + + +def filter_used_materials(): + """Gather and return all unfiltered, valid Blender materials.""" + materials = [] + + for blender_material in bpy.data.materials: + if blender_material.node_tree and blender_material.use_nodes: + for currentNode in blender_material.node_tree.nodes: + if isinstance(currentNode, bpy.types.ShaderNodeGroup): + if currentNode.node_tree.name.startswith(ROUGHNESS): + materials.append(blender_material) + elif currentNode.node_tree.name.startswith(GLOSSINESS): + materials.append(blender_material) + elif isinstance(currentNode, bpy.types.ShaderNodeBsdfPrincipled): + materials.append(blender_material) + else: + materials.append(blender_material) + + return materials + + +def filter_apply(export_settings): + """ + Gathers and filters the objects and assets to export. + + Also filters out invalid, deleted and not exportable elements. + """ + filtered_objects = [] + implicit_filtered_objects = [] + + for blender_object in bpy.data.objects: + + if blender_object.users == 0: + continue + + if export_settings[gltf2_blender_export_keys.SELECTED] and not blender_object.select: + continue + + if not export_settings[gltf2_blender_export_keys.LAYERS] and not blender_object.layers[0]: + continue + + filtered_objects.append(blender_object) + + if export_settings[gltf2_blender_export_keys.SELECTED] or not export_settings[gltf2_blender_export_keys.LAYERS]: + current_parent = blender_object.parent + while current_parent: + if current_parent not in implicit_filtered_objects: + implicit_filtered_objects.append(current_parent) + + current_parent = current_parent.parent + + export_settings[gltf2_blender_export_keys.FILTERED_OBJECTS] = filtered_objects + + # Meshes + + filtered_meshes = {} + filtered_vertex_groups = {} + temporary_meshes = [] + + for blender_mesh in bpy.data.meshes: + + if blender_mesh.users == 0: + continue + + current_blender_mesh = blender_mesh + + current_blender_object = None + + skip = True + + for blender_object in filtered_objects: + + current_blender_object = blender_object + + if current_blender_object.type != 'MESH': + continue + + if current_blender_object.data == current_blender_mesh: + + skip = False + + use_auto_smooth = current_blender_mesh.use_auto_smooth + + if use_auto_smooth: + + if current_blender_mesh.shape_keys is None: + current_blender_object = current_blender_object.copy() + else: + use_auto_smooth = False + + print_console('WARNING', + 'Auto smooth and shape keys cannot be exported in parallel. ' + 'Falling back to non auto smooth.') + + if export_settings[gltf2_blender_export_keys.APPLY] or use_auto_smooth: + # TODO: maybe add to new exporter + if not export_settings[gltf2_blender_export_keys.APPLY]: + current_blender_object.modifiers.clear() + + if use_auto_smooth: + blender_modifier = current_blender_object.modifiers.new('Temporary_Auto_Smooth', 'EDGE_SPLIT') + + blender_modifier.split_angle = current_blender_mesh.auto_smooth_angle + blender_modifier.use_edge_angle = not current_blender_mesh.has_custom_normals + + current_blender_mesh = current_blender_object.to_mesh(bpy.context.scene, True, PREVIEW) + temporary_meshes.append(current_blender_mesh) + + break + + if skip: + continue + + filtered_meshes[blender_mesh.name] = current_blender_mesh + filtered_vertex_groups[blender_mesh.name] = current_blender_object.vertex_groups + + # Curves + + for blender_curve in bpy.data.curves: + + if blender_curve.users == 0: + continue + + current_blender_curve = blender_curve + + current_blender_mesh = None + + current_blender_object = None + + skip = True + + for blender_object in filtered_objects: + + current_blender_object = blender_object + + if current_blender_object.type not in ('CURVE', 'FONT'): + continue + + if current_blender_object.data == current_blender_curve: + + skip = False + + current_blender_object = current_blender_object.copy() + + if not export_settings[gltf2_blender_export_keys.APPLY]: + current_blender_object.modifiers.clear() + + current_blender_mesh = current_blender_object.to_mesh(bpy.context.scene, True, PREVIEW) + temporary_meshes.append(current_blender_mesh) + + break + + if skip: + continue + + filtered_meshes[blender_curve.name] = current_blender_mesh + filtered_vertex_groups[blender_curve.name] = current_blender_object.vertex_groups + + # + + export_settings[gltf2_blender_export_keys.FILTERED_MESHES] = filtered_meshes + export_settings[gltf2_blender_export_keys.FILTERED_VERTEX_GROUPS] = filtered_vertex_groups + export_settings[gltf2_blender_export_keys.TEMPORARY_MESHES] = temporary_meshes + + # + + filtered_materials = [] + + for blender_material in filter_used_materials(): + + if blender_material.users == 0: + continue + + for mesh_name, blender_mesh in filtered_meshes.items(): + for compare_blender_material in blender_mesh.materials: + if compare_blender_material == blender_material and blender_material not in filtered_materials: + filtered_materials.append(blender_material) + + # + + for blender_object in filtered_objects: + if blender_object.material_slots: + for blender_material_slot in blender_object.material_slots: + if blender_material_slot.link == 'DATA': + continue + + if blender_material_slot.material not in filtered_materials: + filtered_materials.append(blender_material_slot.material) + + export_settings[gltf2_blender_export_keys.FILTERED_MATERIALS] = filtered_materials + + # + + filtered_textures = [] + filtered_merged_textures = [] + + temp_filtered_texture_names = [] + + for blender_material in filtered_materials: + if blender_material.node_tree and blender_material.use_nodes: + + per_material_textures = [] + + for blender_node in blender_material.node_tree.nodes: + + if is_valid_node(blender_node) and blender_node not in filtered_textures: + add_node = False + add_merged_node = False + for blender_socket in blender_node.outputs: + if blender_socket.is_linked: + for blender_link in blender_socket.links: + if isinstance(blender_link.to_node, bpy.types.ShaderNodeGroup): + is_roughness = blender_link.to_node.node_tree.name.startswith(ROUGHNESS) + is_glossiness = blender_link.to_node.node_tree.name.startswith(GLOSSINESS) + if is_roughness or is_glossiness: + add_node = True + break + elif isinstance(blender_link.to_node, bpy.types.ShaderNodeBsdfPrincipled): + add_node = True + break + elif isinstance(blender_link.to_node, bpy.types.ShaderNodeNormalMap): + add_node = True + break + elif isinstance(blender_link.to_node, bpy.types.ShaderNodeSeparateRGB): + add_merged_node = True + break + + if add_node or add_merged_node: + break + + if add_node: + filtered_textures.append(blender_node) + # TODO: Add displacement texture, as not stored in node tree. + + if add_merged_node: + if len(per_material_textures) == 0: + filtered_merged_textures.append(per_material_textures) + + per_material_textures.append(blender_node) + + else: + + for blender_texture_slot in blender_material.texture_slots: + + if is_valid_texture_slot(blender_texture_slot) and \ + blender_texture_slot not in filtered_textures and \ + blender_texture_slot.name not in temp_filtered_texture_names: + accept = False + + if blender_texture_slot.use_map_color_diffuse: + accept = True + + if blender_texture_slot.use_map_ambient: + accept = True + if blender_texture_slot.use_map_emit: + accept = True + if blender_texture_slot.use_map_normal: + accept = True + + if export_settings[gltf2_blender_export_keys.DISPLACEMENT]: + if blender_texture_slot.use_map_displacement: + accept = True + + if accept: + filtered_textures.append(blender_texture_slot) + temp_filtered_texture_names.append(blender_texture_slot.name) + + export_settings[gltf2_blender_export_keys.FILTERED_TEXTURES] = filtered_textures + + # + + filtered_images = [] + filtered_merged_images = [] + filtered_images_use_alpha = {} + + for blender_texture in filtered_textures: + + if isinstance(blender_texture, bpy.types.ShaderNodeTexImage): + if is_valid_image(blender_texture.image) and blender_texture.image not in filtered_images: + filtered_images.append(blender_texture.image) + alpha_socket = blender_texture.outputs.get('Alpha') + if alpha_socket is not None and alpha_socket.is_linked: + filtered_images_use_alpha[blender_texture.image.name] = True + + else: + if is_valid_image(blender_texture.texture.image) and blender_texture.texture.image not in filtered_images: + filtered_images.append(blender_texture.texture.image) + if blender_texture.use_map_alpha: + filtered_images_use_alpha[blender_texture.texture.image.name] = True + + # + + for per_material_textures in filtered_merged_textures: + + export_settings[gltf2_blender_export_keys.METALLIC_ROUGHNESS_IMAGE] = None + + for blender_texture in per_material_textures: + + if isinstance(blender_texture, bpy.types.ShaderNodeTexImage): + if is_valid_image(blender_texture.image) and blender_texture.image not in filtered_images: + filter_merge_image(export_settings, blender_texture) + + img = export_settings.get(export_keys.METALLIC_ROUGHNESS_IMAGE) + if img is not None: + filtered_merged_images.append(img) + export_settings[gltf2_blender_export_keys.FILTERED_TEXTURES].append(img) + + export_settings[gltf2_blender_export_keys.FILTERED_MERGED_IMAGES] = filtered_merged_images + export_settings[gltf2_blender_export_keys.FILTERED_IMAGES] = filtered_images + export_settings[gltf2_blender_export_keys.FILTERED_IMAGES_USE_ALPHA] = filtered_images_use_alpha + + # + + filtered_cameras = [] + + for blender_camera in bpy.data.cameras: + + if blender_camera.users == 0: + continue + + if export_settings[gltf2_blender_export_keys.SELECTED]: + if blender_camera not in filtered_objects: + continue + + filtered_cameras.append(blender_camera) + + export_settings[gltf2_blender_export_keys.FILTERED_CAMERAS] = filtered_cameras + + # + # + + filtered_lights = [] + + for blender_light in bpy.data.lamps: + + if blender_light.users == 0: + continue + + if export_settings[gltf2_blender_export_keys.SELECTED]: + if blender_light not in filtered_objects: + continue + + if blender_light.type == 'HEMI': + continue + + filtered_lights.append(blender_light) + + export_settings[gltf2_blender_export_keys.FILTERED_LIGHTS] = filtered_lights + + # + # + + for implicit_object in implicit_filtered_objects: + if implicit_object not in filtered_objects: + filtered_objects.append(implicit_object) + + # + # + # + + group_index = {} + + if export_settings[gltf2_blender_export_keys.SKINS]: + for blender_object in filtered_objects: + if blender_object.type != 'ARMATURE' or len(blender_object.pose.bones) == 0: + continue + for blender_bone in blender_object.pose.bones: + group_index[blender_bone.name] = len(group_index) + + export_settings[gltf2_blender_export_keys.GROUP_INDEX] = group_index + + +def is_valid_node(blender_node): + return isinstance(blender_node, bpy.types.ShaderNodeTexImage) and is_valid_image(blender_node.image) + + +def is_valid_image(image): + return image is not None and \ + image.users != 0 and \ + image.size[0] > 0 and \ + image.size[1] > 0 + + +def is_valid_texture_slot(blender_texture_slot): + return blender_texture_slot is not None and \ + blender_texture_slot.texture and \ + blender_texture_slot.texture.users != 0 and \ + blender_texture_slot.texture.type == 'IMAGE' and \ + blender_texture_slot.texture.image is not None and \ + blender_texture_slot.texture.image.users != 0 and \ + blender_texture_slot.texture.image.size[0] > 0 and \ + blender_texture_slot.texture.image.size[1] > 0 + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py new file mode 100755 index 00000000..6f4f3b1e --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py @@ -0,0 +1,63 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy + +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes +from io_scene_gltf2.blender.exp import gltf2_blender_gather_animations +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached + + +def gather_gltf2(export_settings): + """ + Gather glTF properties from the current state of blender. + + :return: list of scene graphs to be added to the glTF export + """ + scenes = [] + animations = [] # unfortunately animations in gltf2 are just as 'root' as scenes. + for blender_scene in bpy.data.scenes: + scenes.append(__gather_scene(blender_scene, export_settings)) + animations += __gather_animations(blender_scene, export_settings) + + return scenes, animations + + +@cached +def __gather_scene(blender_scene, export_settings): + scene = gltf2_io.Scene( + extensions=None, + extras=None, + name=blender_scene.name, + nodes=[] + ) + + for blender_object in blender_scene.objects: + if blender_object.parent is None: + node = gltf2_blender_gather_nodes.gather_node(blender_object, export_settings) + if node is not None: + scene.nodes.append(node) + + # TODO: lights + + return scene + + +def __gather_animations(blender_scene, export_settings): + animations = [] + for blender_object in blender_scene.objects: + animations += gltf2_blender_gather_animations.gather_animations(blender_object, export_settings) + return animations + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py new file mode 100755 index 00000000..fbe18323 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py @@ -0,0 +1,82 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import bpy +import typing +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes +from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints + + +@cached +def gather_animation_channel_target(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> gltf2_io.AnimationChannelTarget: + return gltf2_io.AnimationChannelTarget( + extensions=__gather_extensions(channels, blender_object, export_settings), + extras=__gather_extras(channels, blender_object, export_settings), + node=__gather_node(channels, blender_object, export_settings), + path=__gather_path(channels, blender_object, export_settings) + ) + + +def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> typing.Any: + return None + + +def __gather_extras(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> typing.Any: + return None + + +def __gather_node(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> gltf2_io.Node: + if blender_object.type == "ARMATURE": + # TODO: get joint from fcurve data_path and gather_joint + blender_bone = blender_object.path_resolve(channels[0].data_path.rsplit('.', 1)[0]) + if isinstance(blender_bone, bpy.types.PoseBone): + return gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings) + + return gltf2_blender_gather_nodes.gather_node(blender_object, export_settings) + + +def __gather_path(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> str: + target = channels[0].data_path.split('.')[-1] + path = { + "location": "translation", + "rotation_axis_angle": "rotation", + "rotation_euler": "rotation", + "rotation_quaternion": "rotation", + "scale": "scale", + "value": "weights" + }.get(target) + + if target is None: + raise RuntimeError("Cannot export an animation with {} target".format(target)) + + return path + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py new file mode 100755 index 00000000..808c970d --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py @@ -0,0 +1,131 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import typing + +from ..com.gltf2_blender_data_path import get_target_object_path, get_target_property_name +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.io.com import gltf2_io_debug +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_samplers +from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channel_target + + +@cached +def gather_animation_channels(blender_action: bpy.types.Action, + blender_object: bpy.types.Object, + export_settings + ) -> typing.List[gltf2_io.AnimationChannel]: + channels = [] + + for channel_group in __get_channel_groups(blender_action, blender_object): + channel = __gather_animation_channel(channel_group, blender_object, export_settings) + if channel is not None: + channels.append(channel) + + return channels + + +def __gather_animation_channel(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> typing.Union[gltf2_io.AnimationChannel, None]: + if not __filter_animation_channel(channels, blender_object, export_settings): + return None + + return gltf2_io.AnimationChannel( + extensions=__gather_extensions(channels, blender_object, export_settings), + extras=__gather_extras(channels, blender_object, export_settings), + sampler=__gather_sampler(channels, blender_object, export_settings), + target=__gather_target(channels, blender_object, export_settings) + ) + + +def __filter_animation_channel(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> bool: + return True + + +def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> typing.Any: + return None + + +def __gather_extras(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> typing.Any: + return None + + +def __gather_sampler(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> gltf2_io.AnimationSampler: + return gltf2_blender_gather_animation_samplers.gather_animation_sampler( + channels, + blender_object, + export_settings + ) + + +def __gather_target(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> gltf2_io.AnimationChannelTarget: + return gltf2_blender_gather_animation_channel_target.gather_animation_channel_target( + channels, blender_object, export_settings) + + +def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object): + targets = {} + for fcurve in blender_action.fcurves: + target_property = get_target_property_name(fcurve.data_path) + object_path = get_target_object_path(fcurve.data_path) + + # find the object affected by this action + if not object_path: + target = blender_object + else: + try: + target = blender_object.path_resolve(object_path) + except ValueError: + # if the object is a mesh and the action target path can not be resolved, we know that this is a morph + # animation. + if blender_object.type == "MESH": + # if you need the specific shape key for some reason, this is it: + # shape_key = blender_object.data.shape_keys.path_resolve(object_path) + target = blender_object.data.shape_keys + else: + gltf2_io_debug.print_console("WARNING", "Can not export animations with target {}".format(object_path)) + continue + + # group channels by target object and affected property of the target + target_properties = targets.get(target, {}) + channels = target_properties.get(target_property, []) + channels.append(fcurve) + target_properties[target_property] = channels + targets[target] = target_properties + + groups = [] + for p in targets.values(): + groups += list(p.values()) + + return map(tuple, groups) + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py new file mode 100755 index 00000000..7562d8c2 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py @@ -0,0 +1,198 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import mathutils +import typing + +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.blender.com import gltf2_blender_math +from . import gltf2_blender_export_keys +from io_scene_gltf2.io.com import gltf2_io_debug + + +class Keyframe: + def __init__(self, channels: typing.Tuple[bpy.types.FCurve], time: float): + self.seconds = time / bpy.context.scene.render.fps + self.__target = channels[0].data_path.split('.')[-1] + self.__indices = [c.array_index for c in channels] + + # Data holders for virtual properties + self.__value = None + self.__in_tangent = None + self.__out_tangent = None + + def __get_target_len(self): + length = { + "location": 3, + "rotation_axis_angle": 4, + "rotation_euler": 3, + "rotation_quaternion": 4, + "scale": 3, + "value": 1 + }.get(self.__target) + + if length is None: + raise RuntimeError("Unknown target type {}".format(self.__target)) + + return length + + def __set_indexed(self, value): + # 'value' targets don't use keyframe.array_index + if self.__target == "value": + return value + # Sometimes blender animations only reference a subset of components of a data target. Keyframe should always + # contain a complete Vector/ Quaternion --> use the array_index value of the keyframe to set components in such + # structures + result = [0.0] * self.__get_target_len() + for i, v in zip(self.__indices, value): + result[i] = v + result = gltf2_blender_math.list_to_mathutils(result, self.__target) + return result + + @property + def value(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]: + return self.__value + + @value.setter + def value(self, value: typing.List[float]): + self.__value = self.__set_indexed(value) + + @property + def in_tangent(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]: + return self.__in_tangent + + @in_tangent.setter + def in_tangent(self, value: typing.List[float]): + self.__in_tangent = self.__set_indexed(value) + + @property + def out_tangent(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]: + return self.__in_tangent + + @out_tangent.setter + def out_tangent(self, value: typing.List[float]): + self.__out_tangent = self.__set_indexed(value) + + +# cache for performance reasons +@cached +def gather_keyframes(channels: typing.Tuple[bpy.types.FCurve], export_settings) \ + -> typing.List[Keyframe]: + """Convert the blender action groups' fcurves to keyframes for use in glTF.""" + # Find the start and end of the whole action group + ranges = [channel.range() for channel in channels] + + start = min([channel.range()[0] for channel in channels]) + end = max([channel.range()[1] for channel in channels]) + + keyframes = [] + if needs_baking(channels, export_settings): + # Bake the animation, by evaluating it at a high frequency + # TODO: maybe baking can also be done with FCurve.convert_to_samples + time = start + # TODO: make user controllable + step = 1.0 / bpy.context.scene.render.fps + while time <= end: + key = Keyframe(channels, time) + key.value = [c.evaluate(time) for c in channels] + keyframes.append(key) + time += step + else: + # Just use the keyframes as they are specified in blender + times = [keyframe.co[0] for keyframe in channels[0].keyframe_points] + for i, time in enumerate(times): + key = Keyframe(channels, time) + # key.value = [c.keyframe_points[i].co[0] for c in action_group.channels] + key.value = [c.evaluate(time) for c in channels] + + # compute tangents for cubic spline interpolation + if channels[0].keyframe_points[0].interpolation == "BEZIER": + # Construct the in tangent + if time == times[0]: + # start in-tangent has zero length + key.in_tangent = [0.0 for _ in channels] + else: + # otherwise construct an in tangent from the keyframes control points + + key.in_tangent = [ + 3.0 * (c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1] + ) / (time - times[i - 1]) + for c in channels + ] + # Construct the out tangent + if time == times[-1]: + # end out-tangent has zero length + key.out_tangent = [0.0 for _ in channels] + else: + # otherwise construct an out tangent from the keyframes control points + key.out_tangent = [ + 3.0 * (c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1] + ) / (times[i + 1] - time) + for c in channels + ] + keyframes.append(key) + + return keyframes + + +def needs_baking(channels: typing.Tuple[bpy.types.FCurve], + export_settings + ) -> bool: + """ + Check if baking is needed. + + Some blender animations need to be baked as they can not directly be expressed in glTF. + """ + def all_equal(lst): + return lst[1:] == lst[:-1] + + + if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]: + return True + + interpolation = channels[0].keyframe_points[0].interpolation + if interpolation not in ["BEZIER", "LINEAR", "CONSTANT"]: + gltf2_io_debug.print_console("WARNING", + "Baking animation because of an unsupported interpolation method: {}".format( + interpolation) + ) + return True + + if any(any(k.interpolation != interpolation for k in c.keyframe_points) for c in channels): + # There are different interpolation methods in one action group + gltf2_io_debug.print_console("WARNING", + "Baking animation because there are different " + "interpolation methods in one channel" + ) + return True + + if not all_equal([len(c.keyframe_points) for c in channels]): + gltf2_io_debug.print_console("WARNING", + "Baking animation because the number of keyframes is not " + "equal for all channel tracks") + return True + + if len(channels[0].keyframe_points) <= 1: + # we need to bake to 'STEP', as at least two keyframes are required to interpolate + return True + + if not all(all_equal(key_times) for key_times in zip([[k.co[0] for k in c.keyframe_points] for c in channels])): + # The channels have differently located keyframes + gltf2_io_debug.print_console("WARNING", + "Baking animation because of differently located keyframes in one channel") + return True + + return False + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py new file mode 100755 index 00000000..c94f1528 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py @@ -0,0 +1,166 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import bpy +import mathutils +import typing +import math + +from . import gltf2_blender_export_keys +from mathutils import Matrix +from io_scene_gltf2.blender.com.gltf2_blender_data_path import get_target_property_name, get_target_object_path +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.exp import gltf2_io_binary_data +from io_scene_gltf2.io.com import gltf2_io_constants +from io_scene_gltf2.blender.com import gltf2_blender_math +from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_sampler_keyframes + + +@cached +def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> gltf2_io.AnimationSampler: + return gltf2_io.AnimationSampler( + extensions=__gather_extensions(channels, blender_object, export_settings), + extras=__gather_extras(channels, blender_object, export_settings), + input=__gather_input(channels, blender_object, export_settings), + interpolation=__gather_interpolation(channels, blender_object, export_settings), + output=__gather_output(channels, blender_object, export_settings) + ) + + +def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> typing.Any: + return None + + +def __gather_extras(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> typing.Any: + return None + + +def __gather_input(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> gltf2_io.Accessor: + """Gather the key time codes.""" + keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(channels, export_settings) + times = [k.seconds for k in keyframes] + + return gltf2_io.Accessor( + buffer_view=gltf2_io_binary_data.BinaryData.from_list(times, gltf2_io_constants.ComponentType.Float), + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(times), + extensions=None, + extras=None, + max=[max(times)], + min=[min(times)], + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Scalar + ) + + +def __gather_interpolation(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> str: + if gltf2_blender_gather_animation_sampler_keyframes.needs_baking(channels, export_settings): + return 'STEP' + + blender_keyframe = channels[0].keyframe_points[0] + + # Select the interpolation method. Any unsupported method will fallback to STEP + return { + "BEZIER": "CUBICSPLINE", + "LINEAR": "LINEAR", + "CONSTANT": "STEP" + }[blender_keyframe.interpolation] + + +def __gather_output(channels: typing.Tuple[bpy.types.FCurve], + blender_object: bpy.types.Object, + export_settings + ) -> gltf2_io.Accessor: + """Gather the data of the keyframes.""" + keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(channels, export_settings) + + target_datapath = channels[0].data_path + + transform = Matrix.Identity(4) + + if blender_object.type == "ARMATURE": + bone = blender_object.path_resolve(get_target_object_path(target_datapath)) + if isinstance(bone, bpy.types.PoseBone): + transform = bone.bone.matrix_local + if bone.parent is not None: + parent_transform = bone.parent.bone.matrix_local + transform = gltf2_blender_math.multiply(parent_transform.inverted(), transform) + # if not export_settings[gltf2_blender_export_keys.YUP]: + # transform = gltf2_blender_math.multiply(gltf2_blender_math.to_zup(), transform) + else: + # only apply the y-up conversion to root bones, as child bones already are in the y-up space + if export_settings[gltf2_blender_export_keys.YUP]: + transform = gltf2_blender_math.multiply(gltf2_blender_math.to_yup(), transform) + + values = [] + for keyframe in keyframes: + # Transform the data and extract + value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform) + if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE": + value = gltf2_blender_math.swizzle_yup(value, target_datapath) + keyframe_value = gltf2_blender_math.mathutils_to_gltf(value) + if keyframe.in_tangent is not None: + in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform) + if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE": + in_tangent = gltf2_blender_math.swizzle_yup(in_tangent, target_datapath) + keyframe_value = gltf2_blender_math.mathutils_to_gltf(in_tangent) + keyframe_value + if keyframe.out_tangent is not None: + out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform) + if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE": + out_tangent = gltf2_blender_math.swizzle_yup(out_tangent, target_datapath) + keyframe_value = keyframe_value + gltf2_blender_math.mathutils_to_gltf(out_tangent) + values += keyframe_value + + component_type = gltf2_io_constants.ComponentType.Float + if get_target_property_name(target_datapath) == "value": + # channels with 'weight' targets must have scalar accessors + data_type = gltf2_io_constants.DataType.Scalar + else: + data_type = gltf2_io_constants.DataType.vec_type_from_num(len(keyframes[0].value)) + + return gltf2_io.Accessor( + buffer_view=gltf2_io_binary_data.BinaryData.from_list(values, component_type), + byte_offset=None, + component_type=component_type, + count=len(values) // gltf2_io_constants.DataType.num_elements(data_type), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + type=data_type + ) + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py new file mode 100755 index 00000000..3740fefa --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py @@ -0,0 +1,169 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import typing + +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channels + + +def gather_animations(blender_object: bpy.types.Object, export_settings) -> typing.List[gltf2_io.Animation]: + """ + Gather all animations which contribute to the objects property. + + :param blender_object: The blender object which is animated + :param export_settings: + :return: A list of glTF2 animations + """ + animations = [] + + # Collect all 'actions' affecting this object. There is a direct mapping between blender actions and glTF animations + blender_actions = __get_blender_actions(blender_object) + + # Export all collected actions. + for blender_action in blender_actions: + animation = __gather_animation(blender_action, blender_object, export_settings) + if animation is not None: + animations.append(animation) + + return animations + + +def __gather_animation(blender_action: bpy.types.Action, + blender_object: bpy.types.Object, + export_settings + ) -> typing.Optional[gltf2_io.Animation]: + if not __filter_animation(blender_action, blender_object, export_settings): + return None + + animation = gltf2_io.Animation( + channels=__gather_channels(blender_action, blender_object, export_settings), + extensions=__gather_extensions(blender_action, blender_object, export_settings), + extras=__gather_extras(blender_action, blender_object, export_settings), + name=__gather_name(blender_action, blender_object, export_settings), + samplers=__gather_samplers(blender_action, blender_object, export_settings) + ) + + # To allow reuse of samplers in one animation, + __link_samplers(animation, export_settings) + + if not animation.channels: + return None + + return animation + + +def __filter_animation(blender_action: bpy.types.Action, + blender_object: bpy.types.Object, + export_settings + ) -> bool: + if blender_action.users == 0: + return False + + return True + + +def __gather_channels(blender_action: bpy.types.Action, + blender_object: bpy.types.Object, + export_settings + ) -> typing.List[gltf2_io.AnimationChannel]: + return gltf2_blender_gather_animation_channels.gather_animation_channels( + blender_action, blender_object, export_settings) + + +def __gather_extensions(blender_action: bpy.types.Action, + blender_object: bpy.types.Object, + export_settings + ) -> typing.Any: + return None + + +def __gather_extras(blender_action: bpy.types.Action, + blender_object: bpy.types.Object, + export_settings + ) -> typing.Any: + return None + + +def __gather_name(blender_action: bpy.types.Action, + blender_object: bpy.types.Object, + export_settings + ) -> typing.Optional[str]: + return blender_action.name + + +def __gather_samplers(blender_action: bpy.types.Action, + blender_object: bpy.types.Object, + export_settings + ) -> typing.List[gltf2_io.AnimationSampler]: + # We need to gather the samplers after gathering all channels --> populate this list in __link_samplers + return [] + + +def __link_samplers(animation: gltf2_io.Animation, export_settings): + """ + Move animation samplers to their own list and store their indices at their previous locations. + + After gathering, samplers are stored in the channels properties of the animation and need to be moved + to their own list while storing an index into this list at the position where they previously were. + This behaviour is similar to that of the glTFExporter that traverses all nodes + :param animation: + :param export_settings: + :return: + """ + # TODO: move this to some util module and update gltf2 exporter also + T = typing.TypeVar('T') + + def __append_unique_and_get_index(l: typing.List[T], item: T): + if item in l: + return l.index(item) + else: + index = len(l) + l.append(item) + return index + + for i, channel in enumerate(animation.channels): + animation.channels[i].sampler = __append_unique_and_get_index(animation.samplers, channel.sampler) + + +def __get_blender_actions(blender_object: bpy.types.Object + ) -> typing.List[bpy.types.Action]: + blender_actions = [] + + if blender_object.animation_data is not None: + # Collect active action. + if blender_object.animation_data.action is not None: + blender_actions.append(blender_object.animation_data.action) + + # Collect associated strips from NLA tracks. + for track in blender_object.animation_data.nla_tracks: + # Multi-strip tracks do not export correctly yet (they need to be baked), + # so skip them for now and only write single-strip tracks. + if track.strips is None or len(track.strips) != 1: + continue + for strip in track.strips: + blender_actions.append(strip.action) + + if blender_object.type == "MESH"\ + and blender_object.data is not None \ + and blender_object.data.shape_keys is not None \ + and blender_object.data.shape_keys.animation_data is not None: + blender_actions.append(blender_object.data.shape_keys.animation_data.action) + + # Remove duplicate actions. + blender_actions = list(set(blender_actions)) + + return blender_actions + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py new file mode 100755 index 00000000..5b00a98b --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py @@ -0,0 +1,60 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools + + +def cached(func): + """ + Decorate the cache gather functions results. + + The gather function is only executed if its result isn't in the cache yet + :param func: the function to be decorated. It will have a static __cache member afterwards + :return: + """ + @functools.wraps(func) + def wrapper_cached(*args, **kwargs): + assert len(args) >= 2 and 0 <= len(kwargs) <= 1, "Wrong signature for cached function" + cache_key_args = args + # make a shallow copy of the keyword arguments so that 'export_settings' can be removed + cache_key_kwargs = dict(kwargs) + if kwargs.get("export_settings"): + export_settings = kwargs["export_settings"] + # 'export_settings' should not be cached + del cache_key_kwargs["export_settings"] + else: + export_settings = args[-1] + cache_key_args = args[:-1] + + # we make a tuple from the function arguments so that they can be used as a key to the cache + cache_key = tuple(cache_key_args + tuple(cache_key_kwargs.values())) + + # invalidate cache if export settings have changed + if not hasattr(func, "__export_settings") or export_settings != func.__export_settings: + func.__cache = {} + func.__export_settings = export_settings + # use or fill cache + if cache_key in func.__cache: + return func.__cache[cache_key] + else: + result = func(*args) + func.__cache[cache_key] = result + return result + return wrapper_cached + + +# TODO: replace "cached" with "unique" in all cases where the caching is functional and not only for performance reasons +call_or_fetch = cached +unique = cached + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py new file mode 100755 index 00000000..b09092ca --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py @@ -0,0 +1,124 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import gltf2_blender_export_keys +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com import gltf2_io + +import bpy +import math + + +@cached +def gather_camera(blender_object, export_settings): + if not __filter_camera(blender_object, export_settings): + return None + + return gltf2_io.Camera( + extensions=__gather_extensions(blender_object, export_settings), + extras=__gather_extras(blender_object, export_settings), + name=__gather_name(blender_object, export_settings), + orthographic=__gather_orthographic(blender_object, export_settings), + perspective=__gather_perspective(blender_object, export_settings), + type=__gather_type(blender_object, export_settings) + ) + + +def __filter_camera(blender_object, export_settings): + if blender_object.type != 'CAMERA': + return False + if not __gather_type(blender_object, export_settings): + return False + + return True + + +def __gather_extensions(blender_object, export_settings): + return None + + +def __gather_extras(blender_object, export_settings): + return None + + +def __gather_name(blender_object, export_settings): + return blender_object.data.name + + +def __gather_orthographic(blender_object, export_settings): + if __gather_type(blender_object, export_settings) == "orthographic": + orthographic = gltf2_io.CameraOrthographic( + extensions=None, + extras=None, + xmag=None, + ymag=None, + zfar=None, + znear=None + ) + blender_camera = blender_object.data + + orthographic.xmag = blender_camera.ortho_scale + orthographic.ymag = blender_camera.ortho_scale + + orthographic.znear = blender_camera.clip_start + orthographic.zfar = blender_camera.clip_end + + return orthographic + return None + + +def __gather_perspective(blender_object, export_settings): + if __gather_type(blender_object, export_settings) == "perspective": + perspective = gltf2_io.CameraPerspective( + aspect_ratio=None, + extensions=None, + extras=None, + yfov=None, + zfar=None, + znear=None + ) + blender_camera = blender_object.data + + width = bpy.context.scene.render.pixel_aspect_x * bpy.context.scene.render.resolution_x + height = bpy.context.scene.render.pixel_aspect_y * bpy.context.scene.render.resolution_y + perspective.aspectRatio = width / height + + if width >= height: + if blender_camera.sensor_fit != 'VERTICAL': + perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio) + else: + perspective.yfov = blender_camera.angle + else: + if blender_camera.sensor_fit != 'HORIZONTAL': + perspective.yfov = blender_camera.angle + else: + perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio) + + perspective.znear = blender_camera.clip_start + + if not export_settings[gltf2_blender_export_keys.CAMERA_INFINITE]: + perspective.zfar = blender_camera.clip_end + + return perspective + return None + + +def __gather_type(blender_object, export_settings): + camera = blender_object.data + if camera.type == 'PERSP': + return "perspective" + elif camera.type == 'ORTHO': + return "orthographic" + return None + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py new file mode 100755 index 00000000..4941ffe2 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py @@ -0,0 +1,166 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import typing +import os +import numpy as np + +from . import gltf2_blender_export_keys +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree +from io_scene_gltf2.io.exp import gltf2_io_binary_data +from io_scene_gltf2.io.exp import gltf2_io_image_data + + +def gather_image( + blender_shader_sockets_or_texture_slots: typing.Union[typing.Tuple[bpy.types.NodeSocket], + typing.Tuple[bpy.types.Texture]], + export_settings): + if not __filter_image(blender_shader_sockets_or_texture_slots, export_settings): + return None + image = gltf2_io.Image( + buffer_view=__gather_buffer_view(blender_shader_sockets_or_texture_slots, export_settings), + extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings), + extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings), + mime_type=__gather_mime_type(blender_shader_sockets_or_texture_slots, export_settings), + name=__gather_name(blender_shader_sockets_or_texture_slots, export_settings), + uri=__gather_uri(blender_shader_sockets_or_texture_slots, export_settings) + ) + return image + + +def __filter_image(sockets_or_slots, export_settings): + if not sockets_or_slots: + return False + return True + + +def __gather_buffer_view(sockets_or_slots, export_settings): + if export_settings[gltf2_blender_export_keys.FORMAT] != 'ASCII': + + image = __get_image_data(sockets_or_slots) + return gltf2_io_binary_data.BinaryData( + data=image.to_image_data(__gather_mime_type(sockets_or_slots, export_settings))) + return None + + +def __gather_extensions(sockets_or_slots, export_settings): + return None + + +def __gather_extras(sockets_or_slots, export_settings): + return None + + +def __gather_mime_type(sockets_or_slots, export_settings): + return 'image/png' + # return 'image/jpeg' + + +def __gather_name(sockets_or_slots, export_settings): + if __is_socket(sockets_or_slots): + node = __get_tex_from_socket(sockets_or_slots[0]) + if node is not None: + return node.shader_node.image.name + elif isinstance(sockets_or_slots[0], bpy.types.MaterialTextureSlot): + return sockets_or_slots[0].name + return None + + +def __gather_uri(sockets_or_slots, export_settings): + if export_settings[gltf2_blender_export_keys.FORMAT] == 'ASCII': + # as usual we just store the data in place instead of already resolving the references + return __get_image_data(sockets_or_slots) + return None + + +def __is_socket(sockets_or_slots): + return isinstance(sockets_or_slots[0], bpy.types.NodeSocket) + + +def __is_slot(sockets_or_slots): + return isinstance(sockets_or_slots[0], bpy.types.MaterialTextureSlot) + + +def __get_image_data(sockets_or_slots): + # For shared ressources, such as images, we just store the portion of data that is needed in the glTF property + # in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary + # ressources. + def split_pixels_by_channels(image: bpy.types.Image) -> typing.Iterable[typing.Iterable[float]]: + pixels = np.array(image.pixels) + pixels = pixels.reshape((pixels.shape[0] // image.channels, image.channels)) + channels = np.split(pixels, pixels.shape[1], axis=1) + return channels + + if __is_socket(sockets_or_slots): + results = [__get_tex_from_socket(socket) for socket in sockets_or_slots] + image = None + for result, socket in zip(results, sockets_or_slots): + # rudimentarily try follow the node tree to find the correct image data. + channel = None + for elem in result.path: + if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateRGB): + channel = { + 'R': 0, + 'G': 1, + 'B': 2 + }[elem.from_socket.name] + + if channel is not None: + pixels = [split_pixels_by_channels(result.shader_node.image)[channel]] + else: + pixels = split_pixels_by_channels(result.shader_node.image) + + file_name = os.path.splitext(result.shader_node.image.name)[0] + + image_data = gltf2_io_image_data.ImageData( + file_name, + result.shader_node.image.size[0], + result.shader_node.image.size[1], + pixels) + + if image is None: + image = image_data + else: + image.add_to_image(image_data) + + return image + elif __is_slot(sockets_or_slots): + texture = __get_tex_from_slot(sockets_or_slots[0]) + pixels = texture.image.pixels + + image_data = gltf2_io_image_data.ImageData( + texture.name, + texture.image.size[0], + texture.image.size[1], + pixels) + return image_data + else: + # Texture slots + raise NotImplementedError() + + +def __get_tex_from_socket(blender_shader_socket: bpy.types.NodeSocket): + result = gltf2_blender_search_node_tree.from_socket( + blender_shader_socket, + gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) + if not result: + return None + return result[0] + + +def __get_tex_from_slot(blender_texture_slot): + return blender_texture_slot.texture + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py new file mode 100755 index 00000000..38e47031 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py @@ -0,0 +1,80 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mathutils + +from . import gltf2_blender_export_keys +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.io.com import gltf2_io_debug +from io_scene_gltf2.blender.exp import gltf2_blender_extract +from io_scene_gltf2.blender.com import gltf2_blender_math + + +@cached +def gather_joint(blender_bone, export_settings): + """ + Generate a glTF2 node from a blender bone, as joints in glTF2 are simply nodes. + + :param blender_bone: a blender PoseBone + :param export_settings: the settings for this export + :return: a glTF2 node (acting as a joint) + """ + axis_basis_change = mathutils.Matrix.Identity(4) + if export_settings[gltf2_blender_export_keys.YUP]: + axis_basis_change = mathutils.Matrix( + ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) + + # extract bone transform + if blender_bone.parent is None: + correction_matrix_local = gltf2_blender_math.multiply(axis_basis_change, blender_bone.bone.matrix_local) + else: + correction_matrix_local = gltf2_blender_math.multiply( + blender_bone.parent.bone.matrix_local.inverted(), blender_bone.bone.matrix_local) + matrix_basis = blender_bone.matrix_basis + if export_settings[gltf2_blender_export_keys.BAKE_SKINS]: + gltf2_io_debug.print_console("WARNING", "glTF bake skins not supported") + # matrix_basis = blender_object.convert_space(blender_bone, blender_bone.matrix, from_space='POSE', + # to_space='LOCAL') + trans, rot, sca = gltf2_blender_extract.decompose_transition( + gltf2_blender_math.multiply(correction_matrix_local, matrix_basis), 'JOINT', export_settings) + translation, rotation, scale = (None, None, None) + if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0: + translation = [trans[0], trans[1], trans[2]] + if rot[0] != 0.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 1.0: + rotation = [rot[0], rot[1], rot[2], rot[3]] + if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0: + scale = [sca[0], sca[1], sca[2]] + + # traverse into children + children = [] + for bone in blender_bone.children: + children.append(gather_joint(bone, export_settings)) + + # finally add to the joints array containing all the joints in the hierarchy + return gltf2_io.Node( + camera=None, + children=children, + extensions=None, + extras=None, + matrix=None, + mesh=None, + name=blender_bone.name, + rotation=rotation, + scale=scale, + skin=None, + translation=translation, + weights=None + ) + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py new file mode 100755 index 00000000..0d314681 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py @@ -0,0 +1,113 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import typing +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture +from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree + + +@cached +def gather_material_normal_texture_info_class(blender_shader_sockets_or_texture_slots: typing.Union[ + typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]], + export_settings): + if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): + return None + + texture_info = gltf2_io.MaterialNormalTextureInfoClass( + extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings), + extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings), + scale=__gather_scale(blender_shader_sockets_or_texture_slots, export_settings), + index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings), + tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings) + ) + + return texture_info + + +def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): + if not blender_shader_sockets_or_texture_slots: + return False + if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]): + return False + if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket): + if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]): + # sockets do not lead to a texture --> discard + return False + return True + + +def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings): + return None + + +def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings): + return None + + +def __gather_scale(blender_shader_sockets_or_texture_slots, export_settings): + return None + + +def __gather_index(blender_shader_sockets_or_texture_slots, export_settings): + # We just put the actual shader into the 'index' member + return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings) + + +def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings): + if __is_socket(blender_shader_sockets_or_texture_slots): + blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node + if len(blender_shader_node.inputs['Vector'].links) == 0: + return 0 + + input_node = blender_shader_node.inputs['Vector'].links[0].from_node + + if isinstance(input_node, bpy.types.ShaderNodeMapping): + + if len(input_node.inputs['Vector'].links) == 0: + return 0 + + input_node = input_node.inputs['Vector'].links[0].from_node + + if not isinstance(input_node, bpy.types.ShaderNodeUVMap): + return 0 + + if input_node.uv_map == '': + return 0 + + # Try to gather map index. + for blender_mesh in bpy.data.meshes: + texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map) + if texCoordIndex >= 0: + return texCoordIndex + + return 0 + else: + raise NotImplementedError() + + +def __is_socket(sockets_or_slots): + return isinstance(sockets_or_slots[0], bpy.types.NodeSocket) + + +def __get_tex_from_socket(socket): + result = gltf2_blender_search_node_tree.from_socket( + socket, + gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) + if not result: + return None + return result[0] + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py new file mode 100755 index 00000000..af219318 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py @@ -0,0 +1,113 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import typing +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture +from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree + + +@cached +def gather_material_occlusion_texture_info_class(blender_shader_sockets_or_texture_slots: typing.Union[ + typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]], + export_settings): + if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): + return None + + texture_info = gltf2_io.MaterialOcclusionTextureInfoClass( + extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings), + extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings), + strength=__gather_scale(blender_shader_sockets_or_texture_slots, export_settings), + index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings), + tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings) + ) + + return texture_info + + +def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): + if not blender_shader_sockets_or_texture_slots: + return False + if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]): + return False + if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket): + if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]): + # sockets do not lead to a texture --> discard + return False + return True + + +def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings): + return None + + +def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings): + return None + + +def __gather_scale(blender_shader_sockets_or_texture_slots, export_settings): + return None + + +def __gather_index(blender_shader_sockets_or_texture_slots, export_settings): + # We just put the actual shader into the 'index' member + return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings) + + +def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings): + if __is_socket(blender_shader_sockets_or_texture_slots): + blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node + if len(blender_shader_node.inputs['Vector'].links) == 0: + return 0 + + input_node = blender_shader_node.inputs['Vector'].links[0].from_node + + if isinstance(input_node, bpy.types.ShaderNodeMapping): + + if len(input_node.inputs['Vector'].links) == 0: + return 0 + + input_node = input_node.inputs['Vector'].links[0].from_node + + if not isinstance(input_node, bpy.types.ShaderNodeUVMap): + return 0 + + if input_node.uv_map == '': + return 0 + + # Try to gather map index. + for blender_mesh in bpy.data.meshes: + texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map) + if texCoordIndex >= 0: + return texCoordIndex + + return 0 + else: + raise NotImplementedError() + + +def __is_socket(sockets_or_slots): + return isinstance(sockets_or_slots[0], bpy.types.NodeSocket) + + +def __get_tex_from_socket(socket): + result = gltf2_blender_search_node_tree.from_socket( + socket, + gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) + if not result: + return None + return result[0] + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py new file mode 100755 index 00000000..d801eca7 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py @@ -0,0 +1,138 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy + +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info +from io_scene_gltf2.blender.exp import gltf2_blender_gather_material_normal_texture_info_class +from io_scene_gltf2.blender.exp import gltf2_blender_gather_material_occlusion_texture_info_class + +from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_pbr_metallic_roughness +from io_scene_gltf2.blender.exp import gltf2_blender_get + + +@cached +def gather_material(blender_material, export_settings): + """ + Gather the material used by the blender primitive. + + :param blender_material: the blender material used in the glTF primitive + :param export_settings: + :return: a glTF material + """ + if not __filter_material(blender_material, export_settings): + return None + + material = gltf2_io.Material( + alpha_cutoff=__gather_alpha_cutoff(blender_material, export_settings), + alpha_mode=__gather_alpha_mode(blender_material, export_settings), + double_sided=__gather_double_sided(blender_material, export_settings), + emissive_factor=__gather_emmissive_factor(blender_material, export_settings), + emissive_texture=__gather_emissive_texture(blender_material, export_settings), + extensions=__gather_extensions(blender_material, export_settings), + extras=__gather_extras(blender_material, export_settings), + name=__gather_name(blender_material, export_settings), + normal_texture=__gather_normal_texture(blender_material, export_settings), + occlusion_texture=__gather_occlusion_texture(blender_material, export_settings), + pbr_metallic_roughness=__gather_pbr_metallic_roughness(blender_material, export_settings) + ) + + return material + # material = blender_primitive['material'] + # + # if get_material_requires_texcoords(glTF, material) and not export_settings['gltf_texcoords']: + # material = -1 + # + # if get_material_requires_normals(glTF, material) and not export_settings['gltf_normals']: + # material = -1 + # + # # Meshes/primitives without material are allowed. + # if material >= 0: + # primitive.material = material + # else: + # print_console('WARNING', 'Material ' + internal_primitive[ + # 'material'] + ' not found. Please assign glTF 2.0 material or enable Blinn-Phong material in export.') + + +def __filter_material(blender_material, export_settings): + # if not blender_material.use_nodes: + # return False + # if not blender_material.node_tree: + # return False + return True + + +def __gather_alpha_cutoff(blender_material, export_settings): + return None + + +def __gather_alpha_mode(blender_material, export_settings): + return None + + +def __gather_double_sided(blender_material, export_settings): + return None + + +def __gather_emmissive_factor(blender_material, export_settings): + emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Emissive") + if isinstance(emissive, bpy.types.NodeSocket): + return emissive.default_value + return None + + +def __gather_emissive_texture(blender_material, export_settings): + emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Emissive") + return gltf2_blender_gather_texture_info.gather_texture_info((emissive,), export_settings) + + +def __gather_extensions(blender_material, export_settings): + extensions = {} + + + # TODO specular glossiness extension + + return extensions if extensions else None + + +def __gather_extras(blender_material, export_setttings): + return None + + +def __gather_name(blender_material, export_settings): + + return None + + +def __gather_normal_texture(blender_material, export_settings): + normal = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Normal") + return gltf2_blender_gather_material_normal_texture_info_class.gather_material_normal_texture_info_class( + (normal,), + export_settings) + + +def __gather_occlusion_texture(blender_material, export_settings): + emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Occlusion") + return gltf2_blender_gather_material_occlusion_texture_info_class.gather_material_occlusion_texture_info_class( + (emissive,), + export_settings) + + +def __gather_pbr_metallic_roughness(blender_material, export_settings): + return gltf2_blender_gather_materials_pbr_metallic_roughness.gather_material_pbr_metallic_roughness( + blender_material, + export_settings) + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py new file mode 100755 index 00000000..7a567bc3 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py @@ -0,0 +1,93 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy + +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info +from io_scene_gltf2.blender.exp import gltf2_blender_get +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached + + +@cached +def gather_material_pbr_metallic_roughness(blender_material, export_settings): + if not __filter_pbr_material(blender_material, export_settings): + return None + + material = gltf2_io.MaterialPBRMetallicRoughness( + base_color_factor=__gather_base_color_factor(blender_material, export_settings), + base_color_texture=__gather_base_color_texture(blender_material, export_settings), + extensions=__gather_extensions(blender_material, export_settings), + extras=__gather_extras(blender_material, export_settings), + metallic_factor=__gather_metallic_factor(blender_material, export_settings), + metallic_roughness_texture=__gather_metallic_roughness_texture(blender_material, export_settings), + roughness_factor=__gather_roughness_factor(blender_material, export_settings) + ) + + return material + + +def __filter_pbr_material(blender_material, export_settings): + return True + + +def __gather_base_color_factor(blender_material, export_settings): + base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color") + if base_color_socket is None: + base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor") + if isinstance(base_color_socket, bpy.types.NodeSocket): + return list(base_color_socket.default_value) + return None + +def __gather_base_color_texture(blender_material, export_settings): + base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color") + if base_color_socket is None: + base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor") + return gltf2_blender_gather_texture_info.gather_texture_info((base_color_socket,), export_settings) + + +def __gather_extensions(blender_material, export_settings): + return None + + +def __gather_extras(blender_material, export_settings): + return None + + +def __gather_metallic_factor(blender_material, export_settings): + metallic_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Metallic") + if isinstance(metallic_socket, bpy.types.NodeSocket): + return metallic_socket.default_value + return None + + +def __gather_metallic_roughness_texture(blender_material, export_settings): + metallic_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Metallic") + roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Roughness") + + if metallic_socket is None and roughness_socket is None: + metallic_roughness = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "MetallicRoughness") + texture_input = (metallic_roughness,) + else: + texture_input = (metallic_socket, roughness_socket) + + return gltf2_blender_gather_texture_info.gather_texture_info(texture_input, export_settings) + + +def __gather_roughness_factor(blender_material, export_settings): + roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Roughness") + if isinstance(roughness_socket, bpy.types.NodeSocket): + return roughness_socket.default_value + return None + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py new file mode 100755 index 00000000..57903287 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py @@ -0,0 +1,90 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from typing import Optional, Dict, List, Any +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitives + + +@cached +def gather_mesh(blender_mesh: bpy.types.Mesh, + vertex_groups: Optional[bpy.types.VertexGroups], + modifiers: Optional[bpy.types.ObjectModifiers], + export_settings + ) -> Optional[gltf2_io.Mesh]: + if not __filter_mesh(blender_mesh, vertex_groups, modifiers, export_settings): + return None + + mesh = gltf2_io.Mesh( + extensions=__gather_extensions(blender_mesh, vertex_groups, modifiers, export_settings), + extras=__gather_extras(blender_mesh, vertex_groups, modifiers, export_settings), + name=__gather_name(blender_mesh, vertex_groups, modifiers, export_settings), + primitives=__gather_primitives(blender_mesh, vertex_groups, modifiers, export_settings), + weights=__gather_weights(blender_mesh, vertex_groups, modifiers, export_settings) + ) + + return mesh + + +def __filter_mesh(blender_mesh: bpy.types.Mesh, + vertex_groups: Optional[bpy.types.VertexGroups], + modifiers: Optional[bpy.types.ObjectModifiers], + export_settings + ) -> bool: + if blender_mesh.users == 0: + return False + return True + + +def __gather_extensions(blender_mesh: bpy.types.Mesh, + vertex_groups: Optional[bpy.types.VertexGroups], + modifiers: Optional[bpy.types.ObjectModifiers], + export_settings + ) -> Any: + return None + + +def __gather_extras(blender_mesh: bpy.types.Mesh, + vertex_groups: Optional[bpy.types.VertexGroups], + modifiers: Optional[bpy.types.ObjectModifiers], + export_settings + ) -> Optional[Dict[Any, Any]]: + return None + + +def __gather_name(blender_mesh: bpy.types.Mesh, + vertex_groups: Optional[bpy.types.VertexGroups], + modifiers: Optional[bpy.types.ObjectModifiers], + export_settings + ) -> str: + return blender_mesh.name + + +def __gather_primitives(blender_mesh: bpy.types.Mesh, + vertex_groups: Optional[bpy.types.VertexGroups], + modifiers: Optional[bpy.types.ObjectModifiers], + export_settings + ) -> List[gltf2_io.MeshPrimitive]: + return gltf2_blender_gather_primitives.gather_primitives(blender_mesh, vertex_groups, modifiers, export_settings) + + +def __gather_weights(blender_mesh: bpy.types.Mesh, + vertex_groups: Optional[bpy.types.VertexGroups], + modifiers: Optional[bpy.types.ObjectModifiers], + export_settings + ) -> Optional[List[float]]: + return None + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py new file mode 100755 index 00000000..dc7192d8 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py @@ -0,0 +1,148 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import gltf2_blender_export_keys +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins +from io_scene_gltf2.blender.exp import gltf2_blender_gather_cameras +from io_scene_gltf2.blender.exp import gltf2_blender_gather_mesh +from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints +from io_scene_gltf2.blender.exp import gltf2_blender_extract +from io_scene_gltf2.io.com import gltf2_io + + +@cached +def gather_node(blender_object, export_settings): + if not __filter_node(blender_object, export_settings): + return None + + node = gltf2_io.Node( + camera=__gather_camera(blender_object, export_settings), + children=__gather_children(blender_object, export_settings), + extensions=__gather_extensions(blender_object, export_settings), + extras=__gather_extras(blender_object, export_settings), + matrix=__gather_matrix(blender_object, export_settings), + mesh=__gather_mesh(blender_object, export_settings), + name=__gather_name(blender_object, export_settings), + rotation=None, + scale=None, + skin=__gather_skin(blender_object, export_settings), + translation=None, + weights=__gather_weights(blender_object, export_settings) + ) + node.translation, node.rotation, node.scale = __gather_trans_rot_scale(blender_object, export_settings) + + return node + + +def __filter_node(blender_object, export_settings): + if blender_object.users == 0: + return False + if export_settings[gltf2_blender_export_keys.SELECTED] and not blender_object.select: + return False + if not export_settings[gltf2_blender_export_keys.LAYERS] and not blender_object.layers[0]: + return False + if blender_object.dupli_group is not None and not blender_object.dupli_group.layers[0]: + return False + + return True + + +def __gather_camera(blender_object, export_settings): + return gltf2_blender_gather_cameras.gather_camera(blender_object, export_settings) + + +def __gather_children(blender_object, export_settings): + children = [] + # standard children + for child_object in blender_object.children: + node = gather_node(child_object, export_settings) + if node is not None: + children.append(node) + # blender dupli objects + if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group: + for dupli_object in blender_object.dupli_group.objects: + node = gather_node(dupli_object, export_settings) + if node is not None: + children.append(node) + + # blender bones + if blender_object.type == "ARMATURE": + for blender_bone in blender_object.pose.bones: + if not blender_bone.parent: + children.append(gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings)) + + return children + + +def __gather_extensions(blender_object, export_settings): + return None + + +def __gather_extras(blender_object, export_settings): + return None + + +def __gather_matrix(blender_object, export_settings): + # return blender_object.matrix_local + return [] + + +def __gather_mesh(blender_object, export_settings): + if blender_object.type == "MESH": + # If not using vertex group, they are irrelevant for caching --> ensure that they do not trigger a cache miss + vertex_groups = blender_object.vertex_groups + modifiers = blender_object.modifiers + if len(vertex_groups) == 0: + vertex_groups = None + if len(modifiers) == 0: + modifiers = None + + return gltf2_blender_gather_mesh.gather_mesh(blender_object.data, vertex_groups, modifiers, export_settings) + else: + return None + + +def __gather_name(blender_object, export_settings): + if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group: + return "Duplication_Offset_" + blender_object.name + return blender_object.name + + +def __gather_trans_rot_scale(blender_object, export_settings): + trans, rot, sca = gltf2_blender_extract.decompose_transition(blender_object.matrix_local, 'NODE', export_settings) + if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group: + trans = -gltf2_blender_extract.convert_swizzle_location( + blender_object.dupli_group.dupli_offset, export_settings) + translation, rotation, scale = (None, None, None) + if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0: + translation = [trans[0], trans[1], trans[2]] + if rot[0] != 0.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 1.0: + rotation = [rot[0], rot[1], rot[2], rot[3]] + if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0: + scale = [sca[0], sca[1], sca[2]] + return translation, rotation, scale + + +def __gather_skin(blender_object, export_settings): + modifiers = {m.type: m for m in blender_object.modifiers} + + if "ARMATURE" in modifiers: + # Skins and meshes must be in the same glTF node, which is different from how blender handles armatures + return gltf2_blender_gather_skins.gather_skin(modifiers["ARMATURE"].object, export_settings) + + +def __gather_weights(blender_object, export_settings): + return None + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py new file mode 100755 index 00000000..bd1294cd --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py @@ -0,0 +1,217 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import gltf2_blender_export_keys +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.io.com import gltf2_io_constants +from io_scene_gltf2.io.com import gltf2_io_debug +from io_scene_gltf2.io.exp import gltf2_io_binary_data +from io_scene_gltf2.blender.exp import gltf2_blender_utils + + +def gather_primitive_attributes(blender_primitive, export_settings): + """ + Gathers the attributes, such as POSITION, NORMAL, TANGENT from a blender primitive. + + :return: a dictionary of attributes + """ + attributes = {} + attributes.update(__gather_position(blender_primitive, export_settings)) + attributes.update(__gather_normal(blender_primitive, export_settings)) + attributes.update(__gather_tangent(blender_primitive, export_settings)) + attributes.update(__gather_texcoord(blender_primitive, export_settings)) + attributes.update(__gather_colors(blender_primitive, export_settings)) + attributes.update(__gather_skins(blender_primitive, export_settings)) + return attributes + + +def __gather_position(blender_primitive, export_settings): + position = blender_primitive["attributes"]["POSITION"] + componentType = gltf2_io_constants.ComponentType.Float + return { + "POSITION": gltf2_io.Accessor( + buffer_view=gltf2_io_binary_data.BinaryData.from_list(position, componentType), + byte_offset=None, + component_type=componentType, + count=len(position) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec3), + extensions=None, + extras=None, + max=gltf2_blender_utils.max_components(position, gltf2_io_constants.DataType.Vec3), + min=gltf2_blender_utils.min_components(position, gltf2_io_constants.DataType.Vec3), + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Vec3 + ) + } + + +def __gather_normal(blender_primitive, export_settings): + if export_settings[gltf2_blender_export_keys.NORMALS]: + normal = blender_primitive["attributes"]['NORMAL'] + return { + "NORMAL": gltf2_io.Accessor( + buffer_view=gltf2_io_binary_data.BinaryData.from_list(normal, gltf2_io_constants.ComponentType.Float), + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(normal) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec3), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Vec3 + ) + } + return {} + + +def __gather_tangent(blender_primitive, export_settings): + if export_settings[gltf2_blender_export_keys.TANGENTS]: + if blender_primitive["attributes"].get('TANGENT') is not None: + tangent = blender_primitive["attributes"]['TANGENT'] + return { + "TANGENT": gltf2_io.Accessor( + buffer_view=gltf2_io_binary_data.BinaryData.from_list( + tangent, gltf2_io_constants.ComponentType.Float), + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(tangent) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Vec4 + ) + } + + return {} + + +def __gather_texcoord(blender_primitive, export_settings): + attributes = {} + if export_settings[gltf2_blender_export_keys.TEX_COORDS]: + tex_coord_index = 0 + tex_coord_id = 'TEXCOORD_' + str(tex_coord_index) + while blender_primitive["attributes"].get(tex_coord_id) is not None: + tex_coord = blender_primitive["attributes"][tex_coord_id] + attributes[tex_coord_id] = gltf2_io.Accessor( + buffer_view=gltf2_io_binary_data.BinaryData.from_list( + tex_coord, gltf2_io_constants.ComponentType.Float), + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(tex_coord) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec2), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Vec2 + ) + tex_coord_index += 1 + tex_coord_id = 'TEXCOORD_' + str(tex_coord_index) + return attributes + + +def __gather_colors(blender_primitive, export_settings): + attributes = {} + if export_settings[gltf2_blender_export_keys.COLORS]: + color_index = 0 + color_id = 'COLOR_' + str(color_index) + while blender_primitive["attributes"].get(color_id) is not None: + internal_color = blender_primitive["attributes"][color_id] + attributes[color_id] = gltf2_io.Accessor( + buffer_view=gltf2_io_binary_data.BinaryData.from_list( + internal_color, gltf2_io_constants.ComponentType.Float), + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(internal_color) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Vec4 + ) + color_index += 1 + color_id = 'COLOR_' + str(color_index) + return attributes + + +def __gather_skins(blender_primitive, export_settings): + attributes = {} + if export_settings[gltf2_blender_export_keys.SKINS]: + bone_index = 0 + joint_id = 'JOINTS_' + str(bone_index) + weight_id = 'WEIGHTS_' + str(bone_index) + while blender_primitive["attributes"].get(joint_id) and blender_primitive["attributes"].get(weight_id): + if bone_index >= 4: + gltf2_io_debug.print_console("WARNING", "There are more than 4 joint vertex influences." + "Consider to apply blenders Limit Total function.") + # TODO: add option to stop after 4 + # break + + # joints + internal_joint = blender_primitive["attributes"][joint_id] + joint = gltf2_io.Accessor( + buffer_view=gltf2_io_binary_data.BinaryData.from_list( + internal_joint, gltf2_io_constants.ComponentType.UnsignedShort), + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.UnsignedShort, + count=len(internal_joint) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Vec4 + ) + attributes[joint_id] = joint + + # weights + internal_weight = blender_primitive["attributes"][weight_id] + weight = gltf2_io.Accessor( + buffer_view=gltf2_io_binary_data.BinaryData.from_list( + internal_weight, gltf2_io_constants.ComponentType.Float), + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(internal_weight) // gltf2_io_constants.DataType.num_elements( + gltf2_io_constants.DataType.Vec4), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Vec4 + ) + attributes[weight_id] = weight + + bone_index += 1 + joint_id = 'JOINTS_' + str(bone_index) + weight_id = 'WEIGHTS_' + str(bone_index) + return attributes + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py new file mode 100755 index 00000000..5b3e607b --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py @@ -0,0 +1,200 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from typing import List, Optional + +from .gltf2_blender_export_keys import INDICES, FORCE_INDICES, NORMALS, MORPH_NORMAL, TANGENTS, MORPH_TANGENT, MORPH + +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.blender.exp import gltf2_blender_extract +from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes +from io_scene_gltf2.blender.exp import gltf2_blender_utils +from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials + +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.io.exp import gltf2_io_binary_data +from io_scene_gltf2.io.com import gltf2_io_constants +from io_scene_gltf2.io.com.gltf2_io_debug import print_console + + +@cached +def gather_primitives( + blender_mesh: bpy.types.Mesh, + vertex_groups: Optional[bpy.types.VertexGroups], + modifiers: Optional[bpy.types.ObjectModifiers], + export_settings + ) -> List[gltf2_io.MeshPrimitive]: + """ + Extract the mesh primitives from a blender object + + :return: a list of glTF2 primitives + """ + primitives = [] + blender_primitives = gltf2_blender_extract.extract_primitives( + None, blender_mesh, vertex_groups, modifiers, export_settings) + + for internal_primitive in blender_primitives: + + primitive = gltf2_io.MeshPrimitive( + attributes=__gather_attributes(internal_primitive, blender_mesh, modifiers, export_settings), + extensions=None, + extras=None, + indices=__gather_indices(internal_primitive, blender_mesh, modifiers, export_settings), + material=__gather_materials(internal_primitive, blender_mesh, modifiers, export_settings), + mode=None, + targets=__gather_targets(internal_primitive, blender_mesh, modifiers, export_settings) + ) + primitives.append(primitive) + + return primitives + + +def __gather_materials(blender_primitive, blender_mesh, modifiers, export_settings): + if not blender_primitive['material']: + # TODO: fix 'extract_promitives' so that the value of 'material' is None and not empty string + return None + material = bpy.data.materials[blender_primitive['material']] + return gltf2_blender_gather_materials.gather_material(material, export_settings) + + +def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings): + indices = blender_primitive['indices'] + + max_index = max(indices) + if max_index < (1 << 8): + component_type = gltf2_io_constants.ComponentType.UnsignedByte + elif max_index < (1 << 16): + component_type = gltf2_io_constants.ComponentType.UnsignedShort + elif max_index < (1 << 32): + component_type = gltf2_io_constants.ComponentType.UnsignedInt + else: + print_console('ERROR', 'Invalid max_index: ' + str(max_index)) + return None + + if export_settings[FORCE_INDICES]: + component_type = gltf2_io_constants.ComponentType.from_legacy_define(export_settings[INDICES]) + + element_type = gltf2_io_constants.DataType.Scalar + binary_data = gltf2_io_binary_data.BinaryData.from_list(indices, component_type) + return gltf2_io.Accessor( + buffer_view=binary_data, + byte_offset=None, + component_type=component_type, + count=len(indices) // gltf2_io_constants.DataType.num_elements(element_type), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + type=element_type + ) + + +def __gather_attributes(blender_primitive, blender_mesh, modifiers, export_settings): + return gltf2_blender_gather_primitive_attributes.gather_primitive_attributes(blender_primitive, export_settings) + + +def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings): + if export_settings[MORPH]: + targets = [] + if blender_mesh.shape_keys is not None: + morph_index = 0 + for blender_shape_key in blender_mesh.shape_keys.key_blocks: + if blender_shape_key != blender_shape_key.relative_key: + + target_position_id = 'MORPH_POSITION_' + str(morph_index) + target_normal_id = 'MORPH_NORMAL_' + str(morph_index) + target_tangent_id = 'MORPH_TANGENT_' + str(morph_index) + + if blender_primitive["attributes"].get(target_position_id): + target = {} + internal_target_position = blender_primitive["attributes"][target_position_id] + binary_data = gltf2_io_binary_data.BinaryData.from_list( + internal_target_position, + gltf2_io_constants.ComponentType.Float + ) + target["POSITION"] = gltf2_io.Accessor( + buffer_view=binary_data, + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(internal_target_position) // gltf2_io_constants.DataType.num_elements( + gltf2_io_constants.DataType.Vec3), + extensions=None, + extras=None, + max=gltf2_blender_utils.max_components( + internal_target_position, gltf2_io_constants.DataType.Vec3), + min=gltf2_blender_utils.min_components( + internal_target_position, gltf2_io_constants.DataType.Vec3), + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Vec3 + ) + + if export_settings[NORMALS] \ + and export_settings[MORPH_NORMAL] \ + and blender_primitive["attributes"].get(target_normal_id): + + internal_target_normal = blender_primitive["attributes"][target_normal_id] + binary_data = gltf2_io_binary_data.BinaryData.from_list( + internal_target_normal, + gltf2_io_constants.ComponentType.Float, + ) + target['NORMAL'] = gltf2_io.Accessor( + buffer_view=binary_data, + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(internal_target_normal) // gltf2_io_constants.DataType.num_elements( + gltf2_io_constants.DataType.Vec3), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Vec3 + ) + + if export_settings[TANGENTS] \ + and export_settings[MORPH_TANGENT] \ + and blender_primitive["attributes"].get(target_tangent_id): + internal_target_tangent = blender_primitive["attributes"][target_tangent_id] + binary_data = gltf2_io_binary_data.BinaryData.from_list( + internal_target_tangent, + gltf2_io_constants.ComponentType.Float, + ) + target['TANGENT'] = gltf2_io.Accessor( + buffer_view=binary_data, + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(internal_target_tangent) // gltf2_io_constants.DataType.num_elements( + gltf2_io_constants.DataType.Vec3), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Vec3 + ) + targets.append(target) + morph_index += 1 + return targets + return None + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py new file mode 100755 index 00000000..840c98f4 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py @@ -0,0 +1,98 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached + + +@cached +def gather_sampler(blender_shader_node: bpy.types.Node, export_settings): + if not __filter_sampler(blender_shader_node, export_settings): + return None + + return gltf2_io.Sampler( + extensions=__gather_extensions(blender_shader_node, export_settings), + extras=__gather_extras(blender_shader_node, export_settings), + mag_filter=__gather_mag_filter(blender_shader_node, export_settings), + min_filter=__gather_min_filter(blender_shader_node, export_settings), + name=__gather_name(blender_shader_node, export_settings), + wrap_s=__gather_wrap_s(blender_shader_node, export_settings), + wrap_t=__gather_wrap_t(blender_shader_node, export_settings) + ) + + +def __filter_sampler(blender_shader_node, export_settings): + if not blender_shader_node.interpolation == 'Closest' and not blender_shader_node.extension == 'CLIP': + return False + return True + + +def __gather_extensions(blender_shader_node, export_settings): + return None + + +def __gather_extras(blender_shader_node, export_settings): + return None + + +def __gather_mag_filter(blender_shader_node, export_settings): + if blender_shader_node.interpolation == 'Closest': + return 9728 # NEAREST + return 9729 # LINEAR + + +def __gather_min_filter(blender_shader_node, export_settings): + if blender_shader_node.interpolation == 'Closest': + return 9984 # NEAREST_MIPMAP_NEAREST + return 9986 # NEAREST_MIPMAP_LINEAR + + +def __gather_name(blender_shader_node, export_settings): + return None + + +def __gather_wrap_s(blender_shader_node, export_settings): + if blender_shader_node.extension == 'CLIP': + return 33071 + return None + + +def __gather_wrap_t(blender_shader_node, export_settings): + if blender_shader_node.extension == 'CLIP': + return 33071 + return None + + +@cached +def gather_sampler_from_texture_slot(blender_texture: bpy.types.TextureSlot, export_settings): + magFilter = 9729 + wrap = 10497 + if blender_texture.texture.extension == 'CLIP': + wrap = 33071 + + minFilter = 9986 + if magFilter == 9728: + minFilter = 9984 + + return gltf2_io.Sampler( + extensions=None, + extras=None, + mag_filter=magFilter, + min_filter=minFilter, + name=None, + wrap_s=wrap, + wrap_t=wrap + ) + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py new file mode 100755 index 00000000..84703414 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py @@ -0,0 +1,150 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mathutils +from . import gltf2_blender_export_keys +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.io.exp import gltf2_io_binary_data +from io_scene_gltf2.io.com import gltf2_io_constants +from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints +from io_scene_gltf2.blender.com import gltf2_blender_math + + +@cached +def gather_skin(blender_object, export_settings): + """ + Gather armatures, bones etc into a glTF2 skin object. + + :param blender_object: the object which may contain a skin + :param export_settings: + :return: a glTF2 skin object + """ + if not __filter_skin(blender_object, export_settings): + return None + + return gltf2_io.Skin( + extensions=__gather_extensions(blender_object, export_settings), + extras=__gather_extras(blender_object, export_settings), + inverse_bind_matrices=__gather_inverse_bind_matrices(blender_object, export_settings), + joints=__gather_joints(blender_object, export_settings), + name=__gather_name(blender_object, export_settings), + skeleton=__gather_skeleton(blender_object, export_settings) + ) + + +def __filter_skin(blender_object, export_settings): + if not export_settings[gltf2_blender_export_keys.SKINS]: + return False + if blender_object.type != 'ARMATURE' or len(blender_object.pose.bones) == 0: + return False + + return True + + +def __gather_extensions(blender_object, export_settings): + return None + + +def __gather_extras(blender_object, export_settings): + return None + + +def __gather_inverse_bind_matrices(blender_object, export_settings): + inverse_matrices = [] + + axis_basis_change = mathutils.Matrix.Identity(4) + if export_settings[gltf2_blender_export_keys.YUP]: + axis_basis_change = mathutils.Matrix( + ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) + + # # artificial torso, as needed by glTF + # inverse_bind_matrix = blender_object.matrix_world.inverted() * axis_basis_change.inverted() + # for column in range(0, 4): + # for row in range(0, 4): + # inverse_matrices.append(inverse_bind_matrix[row][column]) + + # + for blender_bone in blender_object.pose.bones: + inverse_bind_matrix = gltf2_blender_math.multiply(axis_basis_change, blender_bone.bone.matrix_local) + bind_shape_matrix = gltf2_blender_math.multiply(gltf2_blender_math.multiply( + axis_basis_change, blender_object.matrix_world.inverted()), axis_basis_change.inverted()) + + inverse_bind_matrix = gltf2_blender_math.multiply(inverse_bind_matrix.inverted(), bind_shape_matrix) + for column in range(0, 4): + for row in range(0, 4): + inverse_matrices.append(inverse_bind_matrix[row][column]) + + binary_data = gltf2_io_binary_data.BinaryData.from_list(inverse_matrices, gltf2_io_constants.ComponentType.Float) + return gltf2_io.Accessor( + buffer_view=binary_data, + byte_offset=None, + component_type=gltf2_io_constants.ComponentType.Float, + count=len(inverse_matrices) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Mat4), + extensions=None, + extras=None, + max=None, + min=None, + name=None, + normalized=None, + sparse=None, + type=gltf2_io_constants.DataType.Mat4 + ) + + +def __gather_joints(blender_object, export_settings): + # # the skeletal hierarchy groups below a 'root' joint + # # TODO: add transform? + # torso = gltf2_io.Node( + # camera=None, + # children=[], + # extensions={}, + # extras=None, + # matrix=[], + # mesh=None, + # name="Skeleton_" + blender_object.name, + # rotation=None, + # scale=None, + # skin=None, + # translation=None, + # weights=None + # ) + + root_joints = [] + # build the hierarchy of nodes out of the bones + for blender_bone in blender_object.pose.bones: + if not blender_bone.parent: + root_joints.append(gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings)) + + # joints is a flat list containing all nodes belonging to the skin + joints = [] + + def __collect_joints(node): + joints.append(node) + for child in node.children: + __collect_joints(child) + for joint in root_joints: + __collect_joints(joint) + + return joints + + +def __gather_name(blender_object, export_settings): + return blender_object.name + + +def __gather_skeleton(blender_object, export_settings): + # In the future support the result of https://github.com/KhronosGroup/glTF/pull/1195 + return None # gltf2_blender_gather_nodes.gather_node(blender_object, export_settings) + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py new file mode 100755 index 00000000..93db33f9 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py @@ -0,0 +1,100 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing +import bpy +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached + +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp import gltf2_blender_gather_sampler +from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree +from io_scene_gltf2.blender.exp import gltf2_blender_gather_image +from io_scene_gltf2.io.com import gltf2_io_debug + + +@cached +def gather_texture( + blender_shader_sockets_or_texture_slots: typing.Union[ + typing.Tuple[bpy.types.NodeSocket], typing.Tuple[typing.Any]], + export_settings): + """ + Gather texture sampling information and image channels from a blender shader textu re attached to a shader socket. + + :param blender_shader_sockets: The sockets of the material which should contribute to the texture + :param export_settings: configuration of the export + :return: a glTF 2.0 texture with sampler and source embedded (will be converted to references by the exporter) + """ + # TODO: extend to texture slots + if not __filter_texture(blender_shader_sockets_or_texture_slots, export_settings): + return None + + return gltf2_io.Texture( + extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings), + extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings), + name=__gather_name(blender_shader_sockets_or_texture_slots, export_settings), + sampler=__gather_sampler(blender_shader_sockets_or_texture_slots, export_settings), + source=__gather_source(blender_shader_sockets_or_texture_slots, export_settings) + ) + + +def __filter_texture(blender_shader_sockets_or_texture_slots, export_settings): + return True + + +def __gather_extensions(blender_shader_sockets, export_settings): + return None + + +def __gather_extras(blender_shader_sockets, export_settings): + return None + + +def __gather_name(blender_shader_sockets, export_settings): + return None + + +def __gather_sampler(blender_shader_sockets_or_texture_slots, export_settings): + if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket): + shader_nodes = [__get_tex_from_socket(socket).shader_node for socket in blender_shader_sockets_or_texture_slots] + if len(shader_nodes) > 1: + gltf2_io_debug.print_console("WARNING", + "More than one shader node tex image used for a texture. " + "The resulting glTF sampler will behave like the first shader node tex image.") + return gltf2_blender_gather_sampler.gather_sampler( + shader_nodes[0], + export_settings) + elif isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.MaterialTextureSlot): + return gltf2_blender_gather_sampler.gather_sampler_from_texture_slot( + blender_shader_sockets_or_texture_slots[0], + export_settings + ) + else: + # TODO: implement texture slot sampler + raise NotImplementedError() + + +def __gather_source(blender_shader_sockets_or_texture_slots, export_settings): + return gltf2_blender_gather_image.gather_image(blender_shader_sockets_or_texture_slots, export_settings) + +# Helpers + + +def __get_tex_from_socket(socket): + result = gltf2_blender_search_node_tree.from_socket( + socket, + gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) + if not result: + return None + return result[0] + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py new file mode 100755 index 00000000..149a2a84 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py @@ -0,0 +1,107 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import typing +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture +from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree + + +@cached +def gather_texture_info(blender_shader_sockets_or_texture_slots: typing.Union[ + typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]], + export_settings): + if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): + return None + + texture_info = gltf2_io.TextureInfo( + extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings), + extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings), + index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings), + tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings) + ) + + return texture_info + + +def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): + if not blender_shader_sockets_or_texture_slots: + return False + if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]): + return False + if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket): + if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]): + # sockets do not lead to a texture --> discard + return False + return True + + +def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings): + return None + + +def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings): + return None + + +def __gather_index(blender_shader_sockets_or_texture_slots, export_settings): + # We just put the actual shader into the 'index' member + return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings) + + +def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings): + if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket): + blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node + if len(blender_shader_node.inputs['Vector'].links) == 0: + return 0 + + input_node = blender_shader_node.inputs['Vector'].links[0].from_node + + if isinstance(input_node, bpy.types.ShaderNodeMapping): + + if len(input_node.inputs['Vector'].links) == 0: + return 0 + + input_node = input_node.inputs['Vector'].links[0].from_node + + if not isinstance(input_node, bpy.types.ShaderNodeUVMap): + return 0 + + if input_node.uv_map == '': + return 0 + + # Try to gather map index. + for blender_mesh in bpy.data.meshes: + texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map) + if texCoordIndex >= 0: + return texCoordIndex + + return 0 + elif isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.MaterialTextureSlot): + # TODO: implement for texture slots + return 0 + else: + raise NotImplementedError() + + +def __get_tex_from_socket(socket): + result = gltf2_blender_search_node_tree.from_socket( + socket, + gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) + if not result: + return None + return result[0] + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py b/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py new file mode 100755 index 00000000..c26c494a --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py @@ -0,0 +1,64 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import bpy +from io_scene_gltf2.blender.com import gltf2_blender_json + + +def generate_extras(blender_element): + """Filter and create a custom property, which is stored in the glTF extra field.""" + if not blender_element: + return None + + extras = {} + + # Custom properties, which are in most cases present and should not be exported. + black_list = ['cycles', 'cycles_visibility', 'cycles_curves', '_RNA_UI'] + + count = 0 + for custom_property in blender_element.keys(): + if custom_property in black_list: + continue + + value = blender_element[custom_property] + + add_value = False + + if isinstance(value, bpy.types.ID): + add_value = True + + if isinstance(value, str): + add_value = True + + if isinstance(value, (int, float)): + add_value = True + + if hasattr(value, "to_list"): + value = value.to_list() + add_value = True + + if hasattr(value, "to_dict"): + value = value.to_dict() + add_value = gltf2_blender_json.is_json_convertible(value) + + if add_value: + extras[custom_property] = value + count += 1 + + if count == 0: + return None + + return extras + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_get.py b/io_scene_gltf2/blender/exp/gltf2_blender_get.py new file mode 100755 index 00000000..27135224 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_get.py @@ -0,0 +1,376 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Imports +# + +import bpy + +from . import gltf2_blender_export_keys +from ...io.exp import gltf2_io_get +from io_scene_gltf2.io.com import gltf2_io_debug +# +# Globals +# + +# +# Functions +# + + +def get_animation_target(action_group: bpy.types.ActionGroup): + return action_group.channels[0].data_path.split('.')[-1] + + +def get_socket_or_texture_slot(blender_material: bpy.types.Material, name: str): + """ + For a given material input name, retrieve the corresponding node tree socket or blender render texture slot. + + :param blender_material: a blender material for which to get the socket/slot + :param name: the name of the socket/slot + :return: either a blender NodeSocket, if the material is a node tree or a blender Texture otherwise + """ + if blender_material.node_tree and blender_material.use_nodes: + if name == "Emissive": + # Emissive is a special case as the input node in the 'Emission' shader node is named 'Color' and only the + # output is named 'Emission' + links = [link for link in blender_material.node_tree.links if link.from_socket.name == 'Emission'] + if not links: + return None + return links[0].to_socket + i = [input for input in blender_material.node_tree.inputs] + o = [output for output in blender_material.node_tree.outputs] + nodes = [node for node in blender_material.node_tree.nodes] + nodes = filter(lambda n: isinstance(n, bpy.types.ShaderNodeBsdfPrincipled), nodes) + inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], []) + if not inputs: + return None + return inputs[0] + + + + return None + + +def find_shader_image_from_shader_socket(shader_socket, max_hops=10): + """Find any ShaderNodeTexImage in the path from the socket.""" + if shader_socket is None: + return None + + if max_hops <= 0: + return None + + for link in shader_socket.links: + if isinstance(link.from_node, bpy.types.ShaderNodeTexImage): + return link.from_node + + for socket in link.from_node.inputs.values(): + image = find_shader_image_from_shader_socket(shader_socket=socket, max_hops=max_hops - 1) + if image is not None: + return image + + return None + + +def get_shader_add_to_shader_node(shader_node): + + if shader_node is None: + return None + + if len(shader_node.outputs['BSDF'].links) == 0: + return None + + to_node = shader_node.outputs['BSDF'].links[0].to_node + + if not isinstance(to_node, bpy.types.ShaderNodeAddShader): + return None + + return to_node + +# + + +def get_shader_emission_from_shader_add(shader_add): + + if shader_add is None: + return None + + if not isinstance(shader_add, bpy.types.ShaderNodeAddShader): + return None + + from_node = None + + for input in shader_add.inputs: + + if len(input.links) == 0: + continue + + from_node = input.links[0].from_node + + if isinstance(from_node, bpy.types.ShaderNodeEmission): + break + + return from_node + + +def get_shader_mapping_from_shader_image(shader_image): + + if shader_image is None: + return None + + if not isinstance(shader_image, bpy.types.ShaderNodeTexImage): + return None + + if shader_image.inputs.get('Vector') is None: + return None + + if len(shader_image.inputs['Vector'].links) == 0: + return None + + from_node = shader_image.inputs['Vector'].links[0].from_node + + # + + if not isinstance(from_node, bpy.types.ShaderNodeMapping): + return None + + return from_node + + +def get_image_material_usage_to_socket(shader_image, socket_name): + if shader_image is None: + return -1 + + if not isinstance(shader_image, bpy.types.ShaderNodeTexImage): + return -2 + + if shader_image.outputs.get('Color') is None: + return -3 + + if len(shader_image.outputs.get('Color').links) == 0: + return -4 + + for img_link in shader_image.outputs.get('Color').links: + separate_rgb = img_link.to_node + + if not isinstance(separate_rgb, bpy.types.ShaderNodeSeparateRGB): + continue + + for i, channel in enumerate("RGB"): + if separate_rgb.outputs.get(channel) is None: + continue + for link in separate_rgb.outputs.get(channel).links: + if socket_name == link.to_socket.name: + return i + + return -6 + + +def get_emission_node_from_lamp_output_node(lamp_node): + if lamp_node is None: + return None + + if not isinstance(lamp_node, bpy.types.ShaderNodeOutputLamp): + return None + + if lamp_node.inputs.get('Surface') is None: + return None + + if len(lamp_node.inputs.get('Surface').links) == 0: + return None + + from_node = lamp_node.inputs.get('Surface').links[0].from_node + if isinstance(from_node, bpy.types.ShaderNodeEmission): + return from_node + + return None + + +def get_ligth_falloff_node_from_emission_node(emission_node, type): + if emission_node is None: + return None + + if not isinstance(emission_node, bpy.types.ShaderNodeEmission): + return None + + if emission_node.inputs.get('Strength') is None: + return None + + if len(emission_node.inputs.get('Strength').links) == 0: + return None + + from_node = emission_node.inputs.get('Strength').links[0].from_node + if not isinstance(from_node, bpy.types.ShaderNodeLightFalloff): + return None + + if from_node.outputs.get(type) is None: + return None + + if len(from_node.outputs.get(type).links) == 0: + return None + + if emission_node != from_node.outputs.get(type).links[0].to_node: + return None + + return from_node + + +def get_shader_image_from_shader_node(name, shader_node): + + if shader_node is None: + return None + + if not isinstance(shader_node, bpy.types.ShaderNodeGroup) and \ + not isinstance(shader_node, bpy.types.ShaderNodeBsdfPrincipled) and \ + not isinstance(shader_node, bpy.types.ShaderNodeEmission): + return None + + if shader_node.inputs.get(name) is None: + return None + + if len(shader_node.inputs[name].links) == 0: + return None + + from_node = shader_node.inputs[name].links[0].from_node + + # + + if isinstance(from_node, bpy.types.ShaderNodeNormalMap): + + name = 'Color' + + if len(from_node.inputs[name].links) == 0: + return None + + from_node = from_node.inputs[name].links[0].from_node + + # + + if not isinstance(from_node, bpy.types.ShaderNodeTexImage): + return None + + return from_node + + +def get_texture_index_from_shader_node(export_settings, glTF, name, shader_node): + """Return the texture index in the glTF array.""" + from_node = get_shader_image_from_shader_node(name, shader_node) + + if from_node is None: + return -1 + + # + + if from_node.image is None or from_node.image.size[0] == 0 or from_node.image.size[1] == 0: + return -1 + + return gltf2_io_get.get_texture_index(glTF, from_node.image.name) + + +def get_texture_index_from_export_settings(export_settings, name): + """Return the texture index in the glTF array.""" + + +def get_texcoord_index_from_shader_node(glTF, name, shader_node): + """Return the texture coordinate index, if assigned and used.""" + from_node = get_shader_image_from_shader_node(name, shader_node) + + if from_node is None: + return 0 + + # + + if len(from_node.inputs['Vector'].links) == 0: + return 0 + + input_node = from_node.inputs['Vector'].links[0].from_node + + # + + if isinstance(input_node, bpy.types.ShaderNodeMapping): + + if len(input_node.inputs['Vector'].links) == 0: + return 0 + + input_node = input_node.inputs['Vector'].links[0].from_node + + # + + if not isinstance(input_node, bpy.types.ShaderNodeUVMap): + return 0 + + if input_node.uv_map == '': + return 0 + + # + + # Try to gather map index. + for blender_mesh in bpy.data.meshes: + texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map) + if texCoordIndex >= 0: + return texCoordIndex + + return 0 + + +def get_image_uri(export_settings, blender_image): + """Return the final URI depending on a file path.""" + file_format = get_image_format(export_settings, blender_image) + extension = '.jpg' if file_format == 'JPEG' else '.png' + + return gltf2_io_get.get_image_name(blender_image.name) + extension + + +def get_image_format(export_settings, blender_image): + """ + Return the final output format of the given image. + + Only PNG and JPEG are supported as outputs - all other formats must be converted. + """ + if blender_image.file_format in ['PNG', 'JPEG']: + return blender_image.file_format + + use_alpha = export_settings[gltf2_blender_export_keys.FILTERED_IMAGES_USE_ALPHA].get(blender_image.name) + + return 'PNG' if use_alpha else 'JPEG' + + +def get_node(data_path): + """Return Blender node on a given Blender data path.""" + if data_path is None: + return None + + index = data_path.find("[\"") + if (index == -1): + return None + + node_name = data_path[(index + 2):] + + index = node_name.find("\"") + if (index == -1): + return None + + return node_name[:(index)] + + +def get_data_path(data_path): + """Return Blender data path.""" + index = data_path.rfind('.') + + if index == -1: + return data_path + + return data_path[(index + 1):] + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py new file mode 100755 index 00000000..14b62c23 --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py @@ -0,0 +1,263 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.io.exp import gltf2_io_binary_data +from io_scene_gltf2.io.exp import gltf2_io_image_data +from io_scene_gltf2.io.exp import gltf2_io_buffer + + +class GlTF2Exporter: + """ + The glTF exporter flattens a scene graph to a glTF serializable format. + + Any child properties are replaced with references where necessary + """ + + def __init__(self, copyright=None): + self.__finalized = False + + asset = gltf2_io.Asset( + copyright=copyright, + extensions=None, + extras=None, + generator='Khronos Blender glTF 2.0 I/O', + min_version=None, + version='2.0') + + self.__gltf = gltf2_io.Gltf( + accessors=[], + animations=[], + asset=asset, + buffers=[], + buffer_views=[], + cameras=[], + extensions={}, + extensions_required=[], + extensions_used=[], + extras=None, + images=[], + materials=[], + meshes=[], + nodes=[], + samplers=[], + scene=-1, + scenes=[], + skins=[], + textures=[] + ) + + self.__buffer = gltf2_io_buffer.Buffer() + self.__images = [] + + # mapping of all glTFChildOfRootProperty types to their corresponding root level arrays + self.__childOfRootPropertyTypeLookup = { + gltf2_io.Accessor: self.__gltf.accessors, + gltf2_io.Animation: self.__gltf.animations, + gltf2_io.Buffer: self.__gltf.buffers, + gltf2_io.BufferView: self.__gltf.buffer_views, + gltf2_io.Camera: self.__gltf.cameras, + gltf2_io.Image: self.__gltf.images, + gltf2_io.Material: self.__gltf.materials, + gltf2_io.Mesh: self.__gltf.meshes, + gltf2_io.Node: self.__gltf.nodes, + gltf2_io.Sampler: self.__gltf.samplers, + gltf2_io.Scene: self.__gltf.scenes, + gltf2_io.Skin: self.__gltf.skins, + gltf2_io.Texture: self.__gltf.textures + } + + self.__propertyTypeLookup = [ + gltf2_io.AccessorSparseIndices, + gltf2_io.AccessorSparse, + gltf2_io.AccessorSparseValues, + gltf2_io.AnimationChannel, + gltf2_io.AnimationChannelTarget, + gltf2_io.AnimationSampler, + gltf2_io.Asset, + gltf2_io.CameraOrthographic, + gltf2_io.CameraPerspective, + gltf2_io.MeshPrimitive, + gltf2_io.TextureInfo, + gltf2_io.MaterialPBRMetallicRoughness, + gltf2_io.MaterialNormalTextureInfoClass, + gltf2_io.MaterialOcclusionTextureInfoClass + ] + + @property + def glTF(self): + if not self.__finalized: + raise RuntimeError("glTF requested, but buffers are not finalized yet") + return self.__gltf + + def finalize_buffer(self, output_path=None, buffer_name=None, is_glb=False): + """ + Finalize the glTF and write buffers. + + :param buffer_path: + :return: + """ + if self.__finalized: + raise RuntimeError("Tried to finalize buffers for finalized glTF file") + + if is_glb: + uri = None + elif output_path and buffer_name: + with open(output_path + buffer_name, 'wb') as f: + f.write(self.__buffer.to_bytes()) + uri = buffer_name + else: + uri = self.__buffer.to_embed_string() + + buffer = gltf2_io.Buffer( + byte_length=self.__buffer.byte_length, + extensions=None, + extras=None, + name=None, + uri=uri + ) + self.__gltf.buffers.append(buffer) + + self.__finalized = True + + if is_glb: + return self.__buffer.to_bytes() + + def finalize_images(self, output_path): + """ + Write all images. + + Due to a current limitation the output_path must be the same as that of the glTF file + :param output_path: + :return: + """ + for image in self.__images: + uri = output_path + image.name + ".png" + with open(uri, 'wb') as f: + f.write(image.to_png_data()) + + def add_scene(self, scene: gltf2_io.Scene, active: bool = True): + """ + Add a scene to the glTF. + + The scene should be built up with the generated glTF classes + :param scene: gltf2_io.Scene type. Root node of the scene graph + :param active: If true, sets the glTD.scene index to the added scene + :return: nothing + """ + if self.__finalized: + raise RuntimeError("Tried to add scene to finalized glTF file") + + # for node in scene.nodes: + # self.__traverse(node) + scene_num = self.__traverse(scene) + if active: + self.__gltf.scene = scene_num + + def add_animation(self, animation: gltf2_io.Animation): + """ + Add an animation to the glTF. + + :param animation: glTF animation, with python style references (names) + :return: nothing + """ + if self.__finalized: + raise RuntimeError("Tried to add animation to finalized glTF file") + + self.__traverse(animation) + + def __to_reference(self, property): + """ + Append a child of root property to its respective list and return a reference into said list. + + If the property is not child of root, the property itself is returned. + :param property: A property type object that should be converted to a reference + :return: a reference or the object itself if it is not child or root + """ + gltf_list = self.__childOfRootPropertyTypeLookup.get(type(property), None) + if gltf_list is None: + # The object is not of a child of root --> don't convert to reference + return property + + return self.__append_unique_and_get_index(gltf_list, property) + + @staticmethod + def __append_unique_and_get_index(target: list, obj): + if obj in target: + return target.index(obj) + else: + index = len(target) + target.append(obj) + return index + + def __add_image(self, image: gltf2_io_image_data.ImageData): + self.__images.append(image) + # TODO: we need to know the image url at this point already --> maybe add all options to the constructor of the + # exporter + # TODO: allow embedding of images (base64) + return image.name + ".png" + + def __traverse(self, node): + """ + Recursively traverse a scene graph consisting of gltf compatible elements. + + The tree is traversed downwards until a primitive is reached. Then any ChildOfRoot property + is stored in the according list in the glTF and replaced with a index reference in the upper level. + """ + def traverse_all_members(node): + for member_name in [a for a in dir(node) if not a.startswith('__') and not callable(getattr(node, a))]: + new_value = self.__traverse(getattr(node, member_name)) + setattr(node, member_name, new_value) # usually this is the same as before + + # TODO: maybe with extensions hooks we can find a more elegant solution + if member_name == "extensions" and new_value is not None: + for extension_name in new_value.keys(): + self.__append_unique_and_get_index(self.__gltf.extensions_used, extension_name) + self.__append_unique_and_get_index(self.__gltf.extensions_required, extension_name) + return node + + # traverse nodes of a child of root property type and add them to the glTF root + if type(node) in self.__childOfRootPropertyTypeLookup: + node = traverse_all_members(node) + idx = self.__to_reference(node) + # child of root properties are only present at root level --> replace with index in upper level + return idx + + # traverse lists, such as children and replace them with indices + if isinstance(node, list): + for i in range(len(node)): + node[i] = self.__traverse(node[i]) + return node + + if isinstance(node, dict): + for key in node.keys(): + node[key] = self.__traverse(node[key]) + return node + + # traverse into any other property + if type(node) in self.__propertyTypeLookup: + return traverse_all_members(node) + + # binary data needs to be moved to a buffer and referenced with a buffer view + if isinstance(node, gltf2_io_binary_data.BinaryData): + buffer_view = self.__buffer.add_and_get_view(node) + return self.__to_reference(buffer_view) + + # image data needs to be saved to file + if isinstance(node, gltf2_io_image_data.ImageData): + return self.__add_image(node) + + # do nothing for any type that does not match a glTF schema (primitives) + return node + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py new file mode 100755 index 00000000..a9dc86cf --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py @@ -0,0 +1,97 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Imports +# + +import bpy +import typing + + +class Filter: + """Base class for all node tree filter operations.""" + + def __init__(self): + pass + + def __call__(self, shader_node): + return True + + +class FilterByName(Filter): + """ + Filter the material node tree by name. + + example usage: + find_from_socket(start_socket, ShaderNodeFilterByName("Normal")) + """ + + def __init__(self, name): + self.name = name + super(FilterByName, self).__init__() + + def __call__(self, shader_node): + return shader_node.name == self.name + + +class FilterByType(Filter): + """Filter the material node tree by type.""" + + def __init__(self, type): + self.type = type + super(FilterByType, self).__init__() + + def __call__(self, shader_node): + return isinstance(shader_node, self.type) + + +class NodeTreeSearchResult: + def __init__(self, shader_node: bpy.types.Node, path: typing.List[bpy.types.NodeLink]): + self.shader_node = shader_node + self.path = path + + +# TODO: cache these searches +def from_socket(start_socket: bpy.types.NodeSocket, + shader_node_filter: typing.Union[Filter, typing.Callable]) -> typing.List[NodeTreeSearchResult]: + """ + Find shader nodes where the filter expression is true. + + :param start_socket: the beginning of the traversal + :param shader_node_filter: should be a function(x: shader_node) -> bool + :return: a list of shader nodes for which filter is true + """ + # hide implementation (especially the search path + def __search_from_socket(start_socket: bpy.types.NodeSocket, + shader_node_filter: typing.Union[Filter, typing.Callable], + search_path: typing.List[bpy.types.NodeLink]) -> typing.List[NodeTreeSearchResult]: + results = [] + + for link in start_socket.links: + # follow the link to a shader node + linked_node = link.from_node + # add the link to the current path + search_path.append(link) + # check if the node matches the filter + if shader_node_filter(linked_node): + results.append(NodeTreeSearchResult(linked_node, search_path)) + # traverse into inputs of the node + for input_socket in linked_node.inputs: + results += __search_from_socket(input_socket, shader_node_filter, search_path) + + return results + + return __search_from_socket(start_socket, shader_node_filter, []) + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py b/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py new file mode 100755 index 00000000..0fa7db6e --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py @@ -0,0 +1,89 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import typing + + +class Filter: + """Base class for all node tree filter operations.""" + + def __call__(self, obj: bpy.types.Object): + return True + + +class ByName(Filter): + """ + Filter the objects by name. + + example usage: + find_objects(FilterByName("Cube")) + """ + + def __init__(self, name): + self.name = name + + def __call__(self, obj: bpy.types.Object): + return obj.name == self.name + + +class ByDataType(Filter): + """Filter the scene objects by their data type.""" + + def __init__(self, data_type: str): + self.type = data_type + + def __call__(self, obj: bpy.types.Object): + return obj.type == self.type + + +class ByDataInstance(Filter): + """Filter the scene objects by a specific ID instance.""" + + def __init__(self, data_instance: bpy.types.ID): + self.data = data_instance + + def __call__(self, obj: bpy.types.Object): + return self.data == obj.data + + +def find_objects(object_filter: typing.Union[Filter, typing.Callable]): + """ + Find objects in the scene where the filter expression is true. + + :param object_filter: should be a function(x: object) -> bool + :return: a list of shader nodes for which filter is true + """ + results = [] + for obj in bpy.context.scene.objects: + if object_filter(obj): + results.append(obj) + return results + + +def find_objects_from(obj: bpy.types.Object, object_filter: typing.Union[Filter, typing.Callable]): + """ + Search for objects matching a filter function below a specified object. + + :param obj: the starting point of the search + :param object_filter: a function(x: object) -> bool + :return: a list of objects which passed the filter + """ + results = [] + if object_filter(obj): + results.append(obj) + for child in obj.children: + results += find_objects_from(child, object_filter) + return results + diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_utils.py b/io_scene_gltf2/blender/exp/gltf2_blender_utils.py new file mode 100755 index 00000000..c3e0d6ee --- /dev/null +++ b/io_scene_gltf2/blender/exp/gltf2_blender_utils.py @@ -0,0 +1,68 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from io_scene_gltf2.io.com import gltf2_io_constants + + +# TODO: we could apply functional programming to these problems (currently we only have a single use case) + +def split_list_by_data_type(l: list, data_type: gltf2_io_constants.DataType): + """ + Split a flat list of components by their data type. + + E.g.: A list [0,1,2,3,4,5] of data type Vec3 would be split to [[0,1,2], [3,4,5]] + :param l: the flat list + :param data_type: the data type of the list + :return: a list of lists, where each element list contains the components of the data type + """ + if not (len(l) % gltf2_io_constants.DataType.num_elements(data_type) == 0): + raise ValueError("List length does not match specified data type") + num_elements = gltf2_io_constants.DataType.num_elements(data_type) + return [l[i:i + num_elements] for i in range(0, len(l), num_elements)] + + +def max_components(l: list, data_type: gltf2_io_constants.DataType) -> list: + """ + Find the maximum components in a flat list. + + This is required, for example, for the glTF2.0 accessor min and max properties + :param l: the flat list of components + :param data_type: the data type of the list (determines the length of the result) + :return: a list with length num_elements(data_type) containing the maximum per component along the list + """ + components_lists = split_list_by_data_type(l, data_type) + result = [-math.inf] * gltf2_io_constants.DataType.num_elements(data_type) + for components in components_lists: + for i, c in enumerate(components): + result[i] = max(result[i], c) + return result + + +def min_components(l: list, data_type: gltf2_io_constants.DataType) -> list: + """ + Find the minimum components in a flat list. + + This is required, for example, for the glTF2.0 accessor min and max properties + :param l: the flat list of components + :param data_type: the data type of the list (determines the length of the result) + :return: a list with length num_elements(data_type) containing the minimum per component along the list + """ + components_lists = split_list_by_data_type(l, data_type) + result = [math.inf] * gltf2_io_constants.DataType.num_elements(data_type) + for components in components_lists: + for i, c in enumerate(components): + result[i] = min(result[i], c) + return result + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py new file mode 100755 index 00000000..610da22e --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py @@ -0,0 +1,327 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from .gltf2_blender_texture import BlenderTextureInfo + + +class BlenderKHR_materials_pbrSpecularGlossiness(): + """Blender KHR_materials_pbrSpecularGlossiness extension.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, pbrSG, mat_name, vertex_color): + """KHR_materials_pbrSpecularGlossiness creation.""" + engine = bpy.context.scene.render.engine + if engine in ['CYCLES', 'BLENDER_EEVEE']: + BlenderKHR_materials_pbrSpecularGlossiness.create_nodetree(gltf, pbrSG, mat_name, vertex_color) + + @staticmethod + def create_nodetree(gltf, pbrSG, mat_name, vertex_color): + """Node tree creation.""" + material = bpy.data.materials[mat_name] + material.use_nodes = True + node_tree = material.node_tree + + # delete all nodes except output + for node in list(node_tree.nodes): + if not node.type == 'OUTPUT_MATERIAL': + node_tree.nodes.remove(node) + + output_node = node_tree.nodes[0] + output_node.location = 1000, 0 + + # create PBR node + diffuse = node_tree.nodes.new('ShaderNodeBsdfDiffuse') + diffuse.location = 0, 0 + glossy = node_tree.nodes.new('ShaderNodeBsdfGlossy') + glossy.location = 0, 100 + mix = node_tree.nodes.new('ShaderNodeMixShader') + mix.location = 500, 0 + + glossy.inputs[1].default_value = 1 - pbrSG['glossinessFactor'] + + if pbrSG['diffuse_type'] == gltf.SIMPLE: + if not vertex_color: + # change input values + diffuse.inputs[0].default_value = pbrSG['diffuseFactor'] + + else: + # Create attribute node to get COLOR_0 data + attribute_node = node_tree.nodes.new('ShaderNodeAttribute') + attribute_node.attribute_name = 'COLOR_0' + attribute_node.location = -500, 0 + + # links + node_tree.links.new(diffuse.inputs[0], attribute_node.outputs[1]) + + elif pbrSG['diffuse_type'] == gltf.TEXTURE_FACTOR: + + # TODO alpha ? + if vertex_color: + # TODO tree locations + # Create attribute / separate / math nodes + attribute_node = node_tree.nodes.new('ShaderNodeAttribute') + attribute_node.attribute_name = 'COLOR_0' + + separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB') + math_vc_R = node_tree.nodes.new('ShaderNodeMath') + math_vc_R.operation = 'MULTIPLY' + + math_vc_G = node_tree.nodes.new('ShaderNodeMath') + math_vc_G.operation = 'MULTIPLY' + + math_vc_B = node_tree.nodes.new('ShaderNodeMath') + math_vc_B.operation = 'MULTIPLY' + + BlenderTextureInfo.create(gltf, pbrSG['diffuseTexture']['index']) + + # create UV Map / Mapping / Texture nodes / separate & math and combine + text_node = node_tree.nodes.new('ShaderNodeTexImage') + text_node.image = \ + bpy.data.images[ + gltf.data.images[gltf.data.textures[pbrSG['diffuseTexture']['index']].source].blender_image_name + ] + text_node.location = -1000, 500 + + combine = node_tree.nodes.new('ShaderNodeCombineRGB') + combine.location = -250, 500 + + math_R = node_tree.nodes.new('ShaderNodeMath') + math_R.location = -500, 750 + math_R.operation = 'MULTIPLY' + math_R.inputs[1].default_value = pbrSG['diffuseFactor'][0] + + math_G = node_tree.nodes.new('ShaderNodeMath') + math_G.location = -500, 500 + math_G.operation = 'MULTIPLY' + math_G.inputs[1].default_value = pbrSG['diffuseFactor'][1] + + math_B = node_tree.nodes.new('ShaderNodeMath') + math_B.location = -500, 250 + math_B.operation = 'MULTIPLY' + math_B.inputs[1].default_value = pbrSG['diffuseFactor'][2] + + separate = node_tree.nodes.new('ShaderNodeSeparateRGB') + separate.location = -750, 500 + + mapping = node_tree.nodes.new('ShaderNodeMapping') + mapping.location = -1500, 500 + + uvmap = node_tree.nodes.new('ShaderNodeUVMap') + uvmap.location = -2000, 500 + if 'texCoord' in pbrSG['diffuseTexture'].keys(): + uvmap["gltf2_texcoord"] = pbrSG['diffuseTexture']['texCoord'] # Set custom flag to retrieve TexCoord + else: + uvmap["gltf2_texcoord"] = 0 # TODO: set in precompute instead of here? + # UV Map will be set after object/UVMap creation + + # Create links + if vertex_color: + node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0]) + node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0]) + node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1]) + node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2]) + node_tree.links.new(math_vc_R.inputs[0], math_R.outputs[0]) + node_tree.links.new(math_vc_G.inputs[0], math_G.outputs[0]) + node_tree.links.new(math_vc_B.inputs[0], math_B.outputs[0]) + node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0]) + node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0]) + node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0]) + + else: + node_tree.links.new(combine.inputs[0], math_R.outputs[0]) + node_tree.links.new(combine.inputs[1], math_G.outputs[0]) + node_tree.links.new(combine.inputs[2], math_B.outputs[0]) + + # Common for both mode (non vertex color / vertex color) + node_tree.links.new(math_R.inputs[0], separate.outputs[0]) + node_tree.links.new(math_G.inputs[0], separate.outputs[1]) + node_tree.links.new(math_B.inputs[0], separate.outputs[2]) + + node_tree.links.new(mapping.inputs[0], uvmap.outputs[0]) + node_tree.links.new(text_node.inputs[0], mapping.outputs[0]) + node_tree.links.new(separate.inputs[0], text_node.outputs[0]) + + node_tree.links.new(diffuse.inputs[0], combine.outputs[0]) + + elif pbrSG['diffuse_type'] == gltf.TEXTURE: + + BlenderTextureInfo.create(gltf, pbrSG['diffuseTexture']['index']) + + # TODO alpha ? + if vertex_color: + # Create attribute / separate / math nodes + attribute_node = node_tree.nodes.new('ShaderNodeAttribute') + attribute_node.attribute_name = 'COLOR_0' + attribute_node.location = -2000, 250 + + separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB') + separate_vertex_color.location = -1500, 250 + + math_vc_R = node_tree.nodes.new('ShaderNodeMath') + math_vc_R.operation = 'MULTIPLY' + math_vc_R.location = -1000, 750 + + math_vc_G = node_tree.nodes.new('ShaderNodeMath') + math_vc_G.operation = 'MULTIPLY' + math_vc_G.location = -1000, 500 + + math_vc_B = node_tree.nodes.new('ShaderNodeMath') + math_vc_B.operation = 'MULTIPLY' + math_vc_B.location = -1000, 250 + + combine = node_tree.nodes.new('ShaderNodeCombineRGB') + combine.location = -500, 500 + + separate = node_tree.nodes.new('ShaderNodeSeparateRGB') + separate.location = -1500, 500 + + # create UV Map / Mapping / Texture nodes / separate & math and combine + text_node = node_tree.nodes.new('ShaderNodeTexImage') + text_node.image = bpy.data.images[ + gltf.data.images[gltf.data.textures[pbrSG['diffuseTexture']['index']].source].blender_image_name + ] + if vertex_color: + text_node.location = -2000, 500 + else: + text_node.location = -500, 500 + + mapping = node_tree.nodes.new('ShaderNodeMapping') + if vertex_color: + mapping.location = -2500, 500 + else: + mapping.location = -1500, 500 + + uvmap = node_tree.nodes.new('ShaderNodeUVMap') + if vertex_color: + uvmap.location = -3000, 500 + else: + uvmap.location = -2000, 500 + if 'texCoord' in pbrSG['diffuseTexture'].keys(): + uvmap["gltf2_texcoord"] = pbrSG['diffuseTexture']['texCoord'] # Set custom flag to retrieve TexCoord + else: + uvmap["gltf2_texcoord"] = 0 # TODO: set in precompute instead of here? + # UV Map will be set after object/UVMap creation + + # Create links + if vertex_color: + node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0]) + + node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0]) + node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1]) + node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2]) + + node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0]) + node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0]) + node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0]) + + node_tree.links.new(separate.inputs[0], text_node.outputs[0]) + + node_tree.links.new(principled.inputs[0], combine.outputs[0]) + + node_tree.links.new(math_vc_R.inputs[0], separate.outputs[0]) + node_tree.links.new(math_vc_G.inputs[0], separate.outputs[1]) + node_tree.links.new(math_vc_B.inputs[0], separate.outputs[2]) + + else: + node_tree.links.new(diffuse.inputs[0], text_node.outputs[0]) + + # Common for both mode (non vertex color / vertex color) + + node_tree.links.new(mapping.inputs[0], uvmap.outputs[0]) + node_tree.links.new(text_node.inputs[0], mapping.outputs[0]) + + if pbrSG['specgloss_type'] == gltf.SIMPLE: + + combine = node_tree.nodes.new('ShaderNodeCombineRGB') + combine.inputs[0].default_value = pbrSG['specularFactor'][0] + combine.inputs[1].default_value = pbrSG['specularFactor'][1] + combine.inputs[2].default_value = pbrSG['specularFactor'][2] + + # links + node_tree.links.new(glossy.inputs[0], combine.outputs[0]) + + elif pbrSG['specgloss_type'] == gltf.TEXTURE: + BlenderTextureInfo.create(gltf, pbrSG['specularGlossinessTexture']['index']) + spec_text = node_tree.nodes.new('ShaderNodeTexImage') + spec_text.image = bpy.data.images[ + gltf.data.images[ + gltf.data.textures[pbrSG['specularGlossinessTexture']['index']].source + ].blender_image_name + ] + spec_text.color_space = 'NONE' + spec_text.location = -500, 0 + + spec_mapping = node_tree.nodes.new('ShaderNodeMapping') + spec_mapping.location = -1000, 0 + + spec_uvmap = node_tree.nodes.new('ShaderNodeUVMap') + spec_uvmap.location = -1500, 0 + if 'texCoord' in pbrSG['specularGlossinessTexture'].keys(): + # Set custom flag to retrieve TexCoord + spec_uvmap["gltf2_texcoord"] = pbrSG['specularGlossinessTexture']['texCoord'] + else: + spec_uvmap["gltf2_texcoord"] = 0 # TODO: set in precompute instead of here? + + # links + node_tree.links.new(glossy.inputs[0], spec_text.outputs[0]) + node_tree.links.new(mix.inputs[0], spec_text.outputs[1]) + + node_tree.links.new(spec_mapping.inputs[0], spec_uvmap.outputs[0]) + node_tree.links.new(spec_text.inputs[0], spec_mapping.outputs[0]) + + elif pbrSG['specgloss_type'] == gltf.TEXTURE_FACTOR: + + BlenderTextureInfo.create(gltf, pbrSG['specularGlossinessTexture']['index']) + + spec_text = node_tree.nodes.new('ShaderNodeTexImage') + spec_text.image = bpy.data.images[gltf.data.images[ + gltf.data.textures[pbrSG['specularGlossinessTexture']['index']].source + ].blender_image_name] + spec_text.color_space = 'NONE' + spec_text.location = -1000, 0 + + spec_math = node_tree.nodes.new('ShaderNodeMath') + spec_math.operation = 'MULTIPLY' + spec_math.inputs[0].default_value = pbrSG['glossinessFactor'] + spec_math.location = -250, 100 + + spec_mapping = node_tree.nodes.new('ShaderNodeMapping') + spec_mapping.location = -1000, 0 + + spec_uvmap = node_tree.nodes.new('ShaderNodeUVMap') + spec_uvmap.location = -1500, 0 + if 'texCoord' in pbrSG['specularGlossinessTexture'].keys(): + # Set custom flag to retrieve TexCoord + spec_uvmap["gltf2_texcoord"] = pbrSG['specularGlossinessTexture']['texCoord'] + else: + spec_uvmap["gltf2_texcoord"] = 0 # TODO: set in precompute instead of here? + + # links + + node_tree.links.new(spec_math.inputs[1], spec_text.outputs[0]) + node_tree.links.new(mix.inputs[0], spec_text.outputs[1]) + node_tree.links.new(glossy.inputs[1], spec_math.outputs[0]) + node_tree.links.new(glossy.inputs[0], spec_text.outputs[0]) + + node_tree.links.new(spec_mapping.inputs[0], spec_uvmap.outputs[0]) + node_tree.links.new(spec_text.inputs[0], spec_mapping.outputs[0]) + + # link node to output + node_tree.links.new(mix.inputs[2], diffuse.outputs[0]) + node_tree.links.new(mix.inputs[1], glossy.outputs[0]) + node_tree.links.new(output_node.inputs[0], mix.outputs[0]) + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py new file mode 100755 index 00000000..4180672a --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py @@ -0,0 +1,35 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .gltf2_blender_animation_bone import BlenderBoneAnim +from .gltf2_blender_animation_node import BlenderNodeAnim + + +class BlenderAnimation(): + """Dispatch Animation to bone or object animation.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def anim(gltf, anim_idx, node_idx): + """Dispatch Animation to bone or object.""" + if gltf.data.nodes[node_idx].is_joint: + BlenderBoneAnim.anim(gltf, anim_idx, node_idx) + else: + BlenderNodeAnim.anim(gltf, anim_idx, node_idx) + + if gltf.data.nodes[node_idx].children: + for child in gltf.data.nodes[node_idx].children: + BlenderAnimation.anim(gltf, anim_idx, child) + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py new file mode 100755 index 00000000..6b670aea --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py @@ -0,0 +1,188 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from mathutils import Matrix + +from ..com.gltf2_blender_conversion import loc_gltf_to_blender, quaternion_gltf_to_blender, scale_to_matrix +from ...io.imp.gltf2_io_binary import BinaryData + + +class BlenderBoneAnim(): + """Blender Bone Animation.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def set_interpolation(interpolation, kf): + """Set interpolation.""" + if interpolation == "LINEAR": + kf.interpolation = 'LINEAR' + elif interpolation == "STEP": + kf.interpolation = 'CONSTANT' + elif interpolation == "CUBICSPLINE": + kf.interpolation = 'BEZIER' + else: + kf.interpolation = 'BEZIER' + + @staticmethod + def parse_translation_channel(gltf, node, obj, bone, channel, animation): + """Manage Location animation.""" + fps = bpy.context.scene.render.fps + blender_path = "location" + + keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input) + values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output) + inv_bind_matrix = node.blender_bone_matrix.to_quaternion().to_matrix().to_4x4().inverted() \ + @ Matrix.Translation(node.blender_bone_matrix.to_translation()).inverted() + + for idx, key in enumerate(keys): + if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": + # TODO manage tangent? + translation_keyframe = loc_gltf_to_blender(values[idx * 3 + 1]) + else: + translation_keyframe = loc_gltf_to_blender(values[idx]) + if not node.parent: + parent_mat = Matrix() + else: + if not gltf.data.nodes[node.parent].is_joint: + parent_mat = Matrix() + else: + parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix + + # Pose is in object (armature) space and it's value if the offset from the bind pose + # (which is also in object space) + # Scale is not taken into account + final_trans = (parent_mat @ Matrix.Translation(translation_keyframe)).to_translation() + bone.location = inv_bind_matrix @ final_trans + bone.keyframe_insert(blender_path, frame=key[0] * fps, group="location") + + for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "location"]: + for kf in fcurve.keyframe_points: + BlenderBoneAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf) + + @staticmethod + def parse_rotation_channel(gltf, node, obj, bone, channel, animation): + """Manage rotation animation.""" + # Note: some operations lead to issue with quaternions. Converting to matrix and then back to quaternions breaks + # quaternion continuity + # (see antipodal quaternions). Blender interpolates between two antipodal quaternions, which causes glitches in + # animation. + # Converting to euler and then back to quaternion is a dirty fix preventing this issue in animation, until a + # better solution is found + # This fix is skipped when parent matrix is identity + fps = bpy.context.scene.render.fps + blender_path = "rotation_quaternion" + + keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input) + values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output) + bind_rotation = node.blender_bone_matrix.to_quaternion() + + for idx, key in enumerate(keys): + if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": + # TODO manage tangent? + quat_keyframe = quaternion_gltf_to_blender(values[idx * 3 + 1]) + else: + quat_keyframe = quaternion_gltf_to_blender(values[idx]) + if not node.parent: + bone.rotation_quaternion = bind_rotation.inverted() @ quat_keyframe + else: + if not gltf.data.nodes[node.parent].is_joint: + parent_mat = Matrix() + else: + parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix + + if parent_mat != parent_mat.inverted(): + final_rot = (parent_mat @ quat_keyframe.to_matrix().to_4x4()).to_quaternion() + bone.rotation_quaternion = bind_rotation.rotation_difference(final_rot).to_euler().to_quaternion() + else: + bone.rotation_quaternion = \ + bind_rotation.rotation_difference(quat_keyframe).to_euler().to_quaternion() + + bone.keyframe_insert(blender_path, frame=key[0] * fps, group='rotation') + + for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "rotation"]: + for kf in fcurve.keyframe_points: + BlenderBoneAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf) + + @staticmethod + def parse_scale_channel(gltf, node, obj, bone, channel, animation): + """Manage scaling animation.""" + fps = bpy.context.scene.render.fps + blender_path = "scale" + + keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input) + values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output) + bind_scale = scale_to_matrix(node.blender_bone_matrix.to_scale()) + + for idx, key in enumerate(keys): + if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": + # TODO manage tangent? + scale_mat = scale_to_matrix(loc_gltf_to_blender(values[idx * 3 + 1])) + else: + scale_mat = scale_to_matrix(loc_gltf_to_blender(values[idx])) + if not node.parent: + bone.scale = (bind_scale.inverted() @ scale_mat).to_scale() + else: + if not gltf.data.nodes[node.parent].is_joint: + parent_mat = Matrix() + else: + parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix + + bone.scale = ( + bind_scale.inverted() @ scale_to_matrix(parent_mat.to_scale()) @ scale_mat + ).to_scale() + + bone.keyframe_insert(blender_path, frame=key[0] * fps, group='scale') + + for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "scale"]: + for kf in fcurve.keyframe_points: + BlenderBoneAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf) + + @staticmethod + def anim(gltf, anim_idx, node_idx): + """Manage animation.""" + node = gltf.data.nodes[node_idx] + obj = bpy.data.objects[gltf.data.skins[node.skin_id].blender_armature_name] + bone = obj.pose.bones[node.blender_bone_name] + + if anim_idx not in node.animations.keys(): + return + + animation = gltf.data.animations[anim_idx] + + if animation.name: + name = animation.name + "_" + obj.name + else: + name = "Animation_" + str(anim_idx) + "_" + obj.name + if name not in bpy.data.actions: + action = bpy.data.actions.new(name) + else: + action = bpy.data.actions[name] + if not obj.animation_data: + obj.animation_data_create() + obj.animation_data.action = bpy.data.actions[action.name] + + for channel_idx in node.animations[anim_idx]: + channel = animation.channels[channel_idx] + + if channel.target.path == "translation": + BlenderBoneAnim.parse_translation_channel(gltf, node, obj, bone, channel, animation) + + elif channel.target.path == "rotation": + BlenderBoneAnim.parse_rotation_channel(gltf, node, obj, bone, channel, animation) + + elif channel.target.path == "scale": + BlenderBoneAnim.parse_scale_channel(gltf, node, obj, bone, channel, animation) + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py new file mode 100755 index 00000000..a5d4b40f --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py @@ -0,0 +1,132 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from mathutils import Vector + +from ..com.gltf2_blender_conversion import loc_gltf_to_blender, quaternion_gltf_to_blender, scale_gltf_to_blender +from ...io.imp.gltf2_io_binary import BinaryData + + +class BlenderNodeAnim(): + """Blender Object Animation.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def set_interpolation(interpolation, kf): + """Manage interpolation.""" + if interpolation == "LINEAR": + kf.interpolation = 'LINEAR' + elif interpolation == "STEP": + kf.interpolation = 'CONSTANT' + elif interpolation == "CUBICSPLINE": + kf.interpolation = 'BEZIER' + else: + kf.interpolation = 'BEZIER' + + @staticmethod + def anim(gltf, anim_idx, node_idx): + """Manage animation.""" + node = gltf.data.nodes[node_idx] + obj = bpy.data.objects[node.blender_object] + fps = bpy.context.scene.render.fps + + if anim_idx not in node.animations.keys(): + return + + animation = gltf.data.animations[anim_idx] + + if animation.name: + name = animation.name + "_" + obj.name + else: + name = "Animation_" + str(anim_idx) + "_" + obj.name + action = bpy.data.actions.new(name) + if not obj.animation_data: + obj.animation_data_create() + obj.animation_data.action = bpy.data.actions[action.name] + + for channel_idx in node.animations[anim_idx]: + channel = animation.channels[channel_idx] + + keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input) + values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output) + + if channel.target.path in ['translation', 'rotation', 'scale']: + + if channel.target.path == "translation": + blender_path = "location" + for idx, key in enumerate(keys): + if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": + # TODO manage tangent? + obj.location = Vector(loc_gltf_to_blender(list(values[idx * 3 + 1]))) + else: + obj.location = Vector(loc_gltf_to_blender(list(values[idx]))) + obj.keyframe_insert(blender_path, frame=key[0] * fps, group='location') + + # Setting interpolation + for fcurve in [curve for curve in obj.animation_data.action.fcurves + if curve.group.name == "location"]: + for kf in fcurve.keyframe_points: + BlenderNodeAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf) + + elif channel.target.path == "rotation": + blender_path = "rotation_quaternion" + for idx, key in enumerate(keys): + if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": + # TODO manage tangent? + obj.rotation_quaternion = quaternion_gltf_to_blender(values[idx * 3 + 1]) + else: + obj.rotation_quaternion = quaternion_gltf_to_blender(values[idx]) + obj.keyframe_insert(blender_path, frame=key[0] * fps, group='rotation') + + # Setting interpolation + for fcurve in [curve for curve in obj.animation_data.action.fcurves + if curve.group.name == "rotation"]: + for kf in fcurve.keyframe_points: + BlenderNodeAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf) + + elif channel.target.path == "scale": + blender_path = "scale" + for idx, key in enumerate(keys): + # TODO manage tangent? + if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": + obj.scale = Vector(scale_gltf_to_blender(list(values[idx * 3 + 1]))) + else: + obj.scale = Vector(scale_gltf_to_blender(list(values[idx]))) + obj.keyframe_insert(blender_path, frame=key[0] * fps, group='scale') + + # Setting interpolation + for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "scale"]: + for kf in fcurve.keyframe_points: + BlenderNodeAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf) + + elif channel.target.path == 'weights': + + # retrieve number of targets + nb_targets = 0 + for prim in gltf.data.meshes[gltf.data.nodes[node_idx].mesh].primitives: + if prim.targets: + if len(prim.targets) > nb_targets: + nb_targets = len(prim.targets) + + for idx, key in enumerate(keys): + for sk in range(nb_targets): + obj.data.shape_keys.key_blocks[sk + 1].value = values[idx * nb_targets + sk][0] + obj.data.shape_keys.key_blocks[sk + 1].keyframe_insert( + "value", + frame=key[0] * fps, + group='ShapeKeys' + ) + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_camera.py b/io_scene_gltf2/blender/imp/gltf2_blender_camera.py new file mode 100755 index 00000000..b5a10ac5 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_camera.py @@ -0,0 +1,47 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy + + +class BlenderCamera(): + """Blender Camera.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, camera_id): + """Camera creation.""" + pycamera = gltf.data.cameras[camera_id] + + if not pycamera.name: + pycamera.name = "Camera" + + cam = bpy.data.cameras.new(pycamera.name) + + # Blender create a perspective camera by default + if pycamera.type == "orthographic": + cam.type = "ORTHO" + + # TODO: lot's of work for camera here... + if hasattr(pycamera, "znear"): + cam.clip_start = pycamera.znear + + if hasattr(pycamera, "zfar"): + cam.clip_end = pycamera.zfar + + obj = bpy.data.objects.new(pycamera.name, cam) + bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj) + return obj + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py new file mode 100755 index 00000000..9f8a3584 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py @@ -0,0 +1,217 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from .gltf2_blender_scene import BlenderScene +from ...io.com.gltf2_io_trs import TRS + + +class BlenderGlTF(): + """Main glTF import class.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf): + """Create glTF main method.""" + bpy.context.scene.render.engine = 'BLENDER_EEVEE' + BlenderGlTF.pre_compute(gltf) + + for scene_idx, scene in enumerate(gltf.data.scenes): + BlenderScene.create(gltf, scene_idx) + + # Armature correction + # Try to detect bone chains, and set bone lengths + # To detect if a bone is in a chain, we try to detect if a bone head is aligned + # with parent_bone : + # Parent bone defined a line (between head & tail) + # Bone head defined a point + # Calcul of distance between point and line + # If < threshold --> In a chain + # Based on an idea of @Menithal, but added alignement detection to avoid some bad cases + + threshold = 0.001 + for armobj in [obj for obj in bpy.data.objects if obj.type == "ARMATURE"]: + bpy.context.view_layer.objects.active = armobj + armature = armobj.data + bpy.ops.object.mode_set(mode="EDIT") + for bone in armature.edit_bones: + if bone.parent is None: + continue + + parent = bone.parent + + # case where 2 bones are aligned (not in chain, same head) + if (bone.head - parent.head).length < threshold: + continue + + u = (parent.tail - parent.head).normalized() + point = bone.head + distance = ((point - parent.head).cross(u)).length / u.length + if distance < threshold: + save_parent_direction = (parent.tail - parent.head).normalized().copy() + save_parent_tail = parent.tail.copy() + parent.tail = bone.head + + # case where 2 bones are aligned (not in chain, same head) + # bone is no more is same direction + if (parent.tail - parent.head).normalized().dot(save_parent_direction) < 0.9: + parent.tail = save_parent_tail + + bpy.ops.object.mode_set(mode="OBJECT") + + @staticmethod + def pre_compute(gltf): + """Pre compute, just before creation.""" + # default scene used + gltf.blender_scene = None + + # Blender material + if gltf.data.materials: + for material in gltf.data.materials: + material.blender_material = None + + if material.pbr_metallic_roughness: + # Init + material.pbr_metallic_roughness.color_type = gltf.SIMPLE + material.pbr_metallic_roughness.vertex_color = False + material.pbr_metallic_roughness.metallic_type = gltf.SIMPLE + + if material.pbr_metallic_roughness.base_color_texture: + material.pbr_metallic_roughness.color_type = gltf.TEXTURE + + if material.pbr_metallic_roughness.metallic_roughness_texture: + material.pbr_metallic_roughness.metallic_type = gltf.TEXTURE + + if material.pbr_metallic_roughness.base_color_factor: + if material.pbr_metallic_roughness.color_type == gltf.TEXTURE and \ + material.pbr_metallic_roughness.base_color_factor != [1.0, 1.0, 1.0, 1.0]: + material.pbr_metallic_roughness.color_type = gltf.TEXTURE_FACTOR + else: + material.pbr_metallic_roughness.base_color_factor = [1.0, 1.0, 1.0, 1.0] + + if material.pbr_metallic_roughness.metallic_factor is not None: + if material.pbr_metallic_roughness.metallic_type == gltf.TEXTURE \ + and material.pbr_metallic_roughness.metallic_factor != 1.0: + material.pbr_metallic_roughness.metallic_type = gltf.TEXTURE_FACTOR + else: + material.pbr_metallic_roughness.metallic_factor = 1.0 + + if material.pbr_metallic_roughness.roughness_factor is not None: + if material.pbr_metallic_roughness.metallic_type == gltf.TEXTURE \ + and material.pbr_metallic_roughness.roughness_factor != 1.0: + material.pbr_metallic_roughness.metallic_type = gltf.TEXTURE_FACTOR + else: + material.pbr_metallic_roughness.roughness_factor = 1.0 + + # pre compute material for KHR_materials_pbrSpecularGlossiness + if material.extensions is not None \ + and 'KHR_materials_pbrSpecularGlossiness' in material.extensions.keys(): + # Init + material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] = gltf.SIMPLE + material.extensions['KHR_materials_pbrSpecularGlossiness']['vertex_color'] = False + material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] = gltf.SIMPLE + + if 'diffuseTexture' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys(): + material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] = gltf.TEXTURE + + if 'diffuseFactor' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys(): + if material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] == gltf.TEXTURE \ + and material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuseFactor'] != \ + [1.0, 1.0, 1.0, 1.0]: + material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] = \ + gltf.TEXTURE_FACTOR + else: + material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuseFactor'] = \ + [1.0, 1.0, 1.0, 1.0] + + if 'specularGlossinessTexture' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys(): + material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] = gltf.TEXTURE + + if 'specularFactor' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys(): + if material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] == \ + gltf.TEXTURE \ + and material.extensions['KHR_materials_pbrSpecularGlossiness']['specularFactor'] != \ + [1.0, 1.0, 1.0]: + material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] = \ + gltf.TEXTURE_FACTOR + else: + material.extensions['KHR_materials_pbrSpecularGlossiness']['specularFactor'] = [1.0, 1.0, 1.0] + + if 'glossinessFactor' not in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys(): + material.extensions['KHR_materials_pbrSpecularGlossiness']['glossinessFactor'] = 1.0 + + for node_idx, node in enumerate(gltf.data.nodes): + + # skin management + if node.skin is not None and node.mesh is not None: + if not hasattr(gltf.data.skins[node.skin], "node_ids"): + gltf.data.skins[node.skin].node_ids = [] + + gltf.data.skins[node.skin].node_ids.append(node_idx) + + # transform management + if node.matrix: + node.transform = node.matrix + continue + + # No matrix, but TRS + mat = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] # init + + if node.scale: + mat = TRS.scale_to_matrix(node.scale) + + if node.rotation: + q_mat = TRS.quaternion_to_matrix(node.rotation) + mat = TRS.matrix_multiply(q_mat, mat) + + if node.translation: + loc_mat = TRS.translation_to_matrix(node.translation) + mat = TRS.matrix_multiply(loc_mat, mat) + + node.transform = mat + + # joint management + for node_idx, node in enumerate(gltf.data.nodes): + is_joint, skin_idx = gltf.is_node_joint(node_idx) + if is_joint: + node.is_joint = True + node.skin_id = skin_idx + else: + node.is_joint = False + + if gltf.data.skins: + for skin_id, skin in enumerate(gltf.data.skins): + # init blender values + skin.blender_armature_name = None + # if skin.skeleton and skin.skeleton not in skin.joints: + # gltf.data.nodes[skin.skeleton].is_joint = True + # gltf.data.nodes[skin.skeleton].skin_id = skin_id + + # Dispatch animation + if gltf.data.animations: + for node_idx, node in enumerate(gltf.data.nodes): + node.animations = {} + + for anim_idx, anim in enumerate(gltf.data.animations): + for channel_idx, channel in enumerate(anim.channels): + if anim_idx not in gltf.data.nodes[channel.target.node].animations.keys(): + gltf.data.nodes[channel.target.node].animations[anim_idx] = [] + gltf.data.nodes[channel.target.node].animations[anim_idx].append(channel_idx) + + # Meshes + if gltf.data.meshes: + for mesh in gltf.data.meshes: + mesh.blender_name = None + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_image.py b/io_scene_gltf2/blender/imp/gltf2_blender_image.py new file mode 100755 index 00000000..ca1eb626 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_image.py @@ -0,0 +1,101 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import os +import tempfile +from os.path import dirname, join, isfile, basename + +from ...io.imp.gltf2_io_binary import BinaryData + + +# Note that Image is not a glTF2.0 object +class BlenderImage(): + """Manage Image.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def get_image_path(gltf, img_idx): + """Return image path.""" + pyimage = gltf.data.images[img_idx] + + image_name = "Image_" + str(img_idx) + + if pyimage.uri: + sep = ';base64,' + if pyimage.uri[:5] == 'data:': + idx = pyimage.uri.find(sep) + if idx != -1: + return False, None, None + + if isfile(join(dirname(gltf.filename), pyimage.uri)): + return True, join(dirname(gltf.filename), pyimage.uri), basename(join(dirname(gltf.filename), pyimage.uri)) + else: + pyimage.gltf.log.error("Missing file (index " + str(img_idx) + "): " + pyimage.uri) + return False, None, None + + if pyimage.buffer_view is None: + return False, None, None + + return False, None, None + + @staticmethod + def create(gltf, img_idx): + """Image creation.""" + img = gltf.data.images[img_idx] + + img.blender_image_name = None + + if gltf.import_settings['import_pack_images'] is False: + + # Images are not packed (if image is a real file) + real, path, img_name = BlenderImage.get_image_path(gltf, img_idx) + + if real is True: + + # Check if image is already loaded + for img_ in bpy.data.images: + if img_.filepath == path: + # Already loaded, not needed to reload it + img.blender_image_name = img_.name + return + + blender_image = bpy.data.images.load(path) + blender_image.name = img_name + img.blender_image_name = blender_image.name + return + + # Check if the file is already loaded (packed file) + file_creation_needed = True + for img_ in bpy.data.images: + if hasattr(img_, "gltf_index") and img_['gltf_index'] == img_idx: + file_creation_needed = False + img.blender_image_name = img_.name + break + + if file_creation_needed is True: + # Create a temp image, pack, and delete image + tmp_image = tempfile.NamedTemporaryFile(delete=False) + img_data, img_name = BinaryData.get_image_data(gltf, img_idx) + tmp_image.write(img_data) + tmp_image.close() + + blender_image = bpy.data.images.load(tmp_image.name) + blender_image.pack() + blender_image.name = img_name + img.blender_image_name = blender_image.name + blender_image['gltf_index'] = img_idx + os.remove(tmp_image.name) + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_map_emissive.py b/io_scene_gltf2/blender/imp/gltf2_blender_map_emissive.py new file mode 100755 index 00000000..81cfd76e --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_map_emissive.py @@ -0,0 +1,110 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from .gltf2_blender_texture import BlenderTextureInfo +from ..com.gltf2_blender_material_helpers import get_preoutput_node_output + + +class BlenderEmissiveMap(): + """Blender Emissive Map.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, material_idx): + """Create emissive map.""" + engine = bpy.context.scene.render.engine + if engine in ['CYCLES', 'BLENDER_EEVEE']: + BlenderEmissiveMap.create_nodetree(gltf, material_idx) + + def create_nodetree(gltf, material_idx): + """Create node tree.""" + pymaterial = gltf.data.materials[material_idx] + + material = bpy.data.materials[pymaterial.blender_material] + node_tree = material.node_tree + + BlenderTextureInfo.create(gltf, pymaterial.emissive_texture.index) + + # check if there is some emssive_factor on material + if pymaterial.emissive_factor is None: + pymaterial.emissive_factor = [1.0, 1.0, 1.0] + + # retrieve principled node and output node + principled = get_preoutput_node_output(node_tree) + output = [node for node in node_tree.nodes if node.type == 'OUTPUT_MATERIAL'][0] + + # add nodes + emit = node_tree.nodes.new('ShaderNodeEmission') + emit.location = 0, 1000 + if pymaterial.emissive_factor != [1.0, 1.0, 1.0]: + separate = node_tree.nodes.new('ShaderNodeSeparateRGB') + separate.location = -750, 1000 + combine = node_tree.nodes.new('ShaderNodeCombineRGB') + combine.location = -250, 1000 + mapping = node_tree.nodes.new('ShaderNodeMapping') + mapping.location = -1500, 1000 + uvmap = node_tree.nodes.new('ShaderNodeUVMap') + uvmap.location = -2000, 1000 + if pymaterial.emissive_texture.tex_coord is not None: + uvmap["gltf2_texcoord"] = pymaterial.emissive_texture.tex_coord # Set custom flag to retrieve TexCoord + else: + uvmap["gltf2_texcoord"] = 0 # TODO: set in precompute instead of here? + + text = node_tree.nodes.new('ShaderNodeTexImage') + text.image = bpy.data.images[gltf.data.images[ + gltf.data.textures[pymaterial.emissive_texture.index].source + ].blender_image_name] + text.label = 'EMISSIVE' + text.location = -1000, 1000 + add = node_tree.nodes.new('ShaderNodeAddShader') + add.location = 500, 500 + + if pymaterial.emissive_factor != [1.0, 1.0, 1.0]: + math_R = node_tree.nodes.new('ShaderNodeMath') + math_R.location = -500, 1500 + math_R.operation = 'MULTIPLY' + math_R.inputs[1].default_value = pymaterial.emissive_factor[0] + + math_G = node_tree.nodes.new('ShaderNodeMath') + math_G.location = -500, 1250 + math_G.operation = 'MULTIPLY' + math_G.inputs[1].default_value = pymaterial.emissive_factor[1] + + math_B = node_tree.nodes.new('ShaderNodeMath') + math_B.location = -500, 1000 + math_B.operation = 'MULTIPLY' + math_B.inputs[1].default_value = pymaterial.emissive_factor[2] + + # create links + node_tree.links.new(mapping.inputs[0], uvmap.outputs[0]) + node_tree.links.new(text.inputs[0], mapping.outputs[0]) + if pymaterial.emissive_factor != [1.0, 1.0, 1.0]: + node_tree.links.new(separate.inputs[0], text.outputs[0]) + node_tree.links.new(math_R.inputs[0], separate.outputs[0]) + node_tree.links.new(math_G.inputs[0], separate.outputs[1]) + node_tree.links.new(math_B.inputs[0], separate.outputs[2]) + node_tree.links.new(combine.inputs[0], math_R.outputs[0]) + node_tree.links.new(combine.inputs[1], math_G.outputs[0]) + node_tree.links.new(combine.inputs[2], math_B.outputs[0]) + node_tree.links.new(emit.inputs[0], combine.outputs[0]) + else: + node_tree.links.new(emit.inputs[0], text.outputs[0]) + + # following links will modify PBR node tree + node_tree.links.new(add.inputs[0], emit.outputs[0]) + node_tree.links.new(add.inputs[1], principled) + node_tree.links.new(output.inputs[0], add.outputs[0]) + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_map_normal.py b/io_scene_gltf2/blender/imp/gltf2_blender_map_normal.py new file mode 100755 index 00000000..ab09f24c --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_map_normal.py @@ -0,0 +1,89 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from .gltf2_blender_texture import BlenderTextureInfo + + +class BlenderNormalMap(): + """Blender Normal map.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, material_idx): + """Creation of Normal map.""" + engine = bpy.context.scene.render.engine + if engine in ['CYCLES', 'BLENDER_EEVEE']: + BlenderNormalMap.create_nodetree(gltf, material_idx) + + def create_nodetree(gltf, material_idx): + """Creation of Nodetree.""" + pymaterial = gltf.data.materials[material_idx] + + material = bpy.data.materials[pymaterial.blender_material] + node_tree = material.node_tree + + BlenderTextureInfo.create(gltf, pymaterial.normal_texture.index) + + # retrieve principled node and output node + principled = None + diffuse = None + glossy = None + if len([node for node in node_tree.nodes if node.type == "BSDF_PRINCIPLED"]) != 0: + principled = [node for node in node_tree.nodes if node.type == "BSDF_PRINCIPLED"][0] + else: + # No principled, we are probably coming from extension + diffuse = [node for node in node_tree.nodes if node.type == "BSDF_DIFFUSE"][0] + glossy = [node for node in node_tree.nodes if node.type == "BSDF_GLOSSY"][0] + + # add nodes + mapping = node_tree.nodes.new('ShaderNodeMapping') + mapping.location = -1000, -500 + uvmap = node_tree.nodes.new('ShaderNodeUVMap') + uvmap.location = -1500, -500 + if pymaterial.normal_texture.tex_coord is not None: + uvmap["gltf2_texcoord"] = pymaterial.normal_texture.tex_coord # Set custom flag to retrieve TexCoord + else: + uvmap["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here + + text = node_tree.nodes.new('ShaderNodeTexImage') + text.image = bpy.data.images[gltf.data.images[ + gltf.data.textures[pymaterial.normal_texture.index].source + ].blender_image_name] + text.label = 'NORMALMAP' + text.color_space = 'NONE' + text.location = -500, -500 + + normalmap_node = node_tree.nodes.new('ShaderNodeNormalMap') + normalmap_node.location = -250, -500 + if pymaterial.normal_texture.tex_coord is not None: + # Set custom flag to retrieve TexCoord + normalmap_node["gltf2_texcoord"] = pymaterial.normal_texture.tex_coord + else: + normalmap_node["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here + + # create links + node_tree.links.new(mapping.inputs[0], uvmap.outputs[0]) + node_tree.links.new(text.inputs[0], mapping.outputs[0]) + node_tree.links.new(normalmap_node.inputs[1], text.outputs[0]) + + # following links will modify PBR node tree + if principled: + node_tree.links.new(principled.inputs[17], normalmap_node.outputs[0]) + if diffuse: + node_tree.links.new(diffuse.inputs[2], normalmap_node.outputs[0]) + if glossy: + node_tree.links.new(glossy.inputs[2], normalmap_node.outputs[0]) + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_map_occlusion.py b/io_scene_gltf2/blender/imp/gltf2_blender_map_occlusion.py new file mode 100755 index 00000000..5c41ce9e --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_map_occlusion.py @@ -0,0 +1,41 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from .gltf2_blender_texture import BlenderTextureInfo + + +class BlenderOcclusionMap(): + """Blender Occlusion map.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, material_idx): + """Occlusion map creation.""" + engine = bpy.context.scene.render.engine + if engine in ['CYCLES', 'BLENDER_EEVEE']: + BlenderOcclusionMap.create_nodetree(gltf, material_idx) + + def create_nodetree(gltf, material_idx): + """Nodetree creation.""" + pymaterial = gltf.data.materials[material_idx] + + BlenderTextureInfo.create(gltf, pymaterial.occlusion_texture.index) + + # Pack texture, but doesn't use it for now. Occlusion is calculated from Cycles. + bpy.data.images[gltf.data.images[gltf.data.textures[ + pymaterial.occlusion_texture.index + ].source].blender_image_name].use_fake_user = True + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_material.py b/io_scene_gltf2/blender/imp/gltf2_blender_material.py new file mode 100755 index 00000000..c9fa6927 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_material.py @@ -0,0 +1,150 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from .gltf2_blender_pbrMetallicRoughness import BlenderPbr +from .gltf2_blender_KHR_materials_pbrSpecularGlossiness import BlenderKHR_materials_pbrSpecularGlossiness +from .gltf2_blender_map_emissive import BlenderEmissiveMap +from .gltf2_blender_map_normal import BlenderNormalMap +from .gltf2_blender_map_occlusion import BlenderOcclusionMap +from ..com.gltf2_blender_material_helpers import get_output_surface_input +from ..com.gltf2_blender_material_helpers import get_preoutput_node_output +from ..com.gltf2_blender_material_helpers import get_base_color_node +from ...io.com.gltf2_io import MaterialPBRMetallicRoughness + + +class BlenderMaterial(): + """Blender Material.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, material_idx, vertex_color): + """Material creation.""" + pymaterial = gltf.data.materials[material_idx] + + if pymaterial.name is not None: + name = pymaterial.name + else: + name = "Material_" + str(material_idx) + + mat = bpy.data.materials.new(name) + pymaterial.blender_material = mat.name + + if pymaterial.extensions is not None and 'KHR_materials_pbrSpecularGlossiness' in pymaterial.extensions.keys(): + BlenderKHR_materials_pbrSpecularGlossiness.create( + gltf, pymaterial.extensions['KHR_materials_pbrSpecularGlossiness'], mat.name, vertex_color + ) + else: + # create pbr material + if pymaterial.pbr_metallic_roughness is None: + # If no pbr material is set, we need to apply all default of pbr + pbr = {} + pbr["baseColorFactor"] = [1.0, 1.0, 1.0, 1.0] + pbr["metallicFactor"] = 1.0 + pbr["roughnessFactor"] = 1.0 + pymaterial.pbr_metallic_roughness = MaterialPBRMetallicRoughness.from_dict(pbr) + pymaterial.pbr_metallic_roughness.color_type = gltf.SIMPLE + pymaterial.pbr_metallic_roughness.metallic_type = gltf.SIMPLE + + BlenderPbr.create(gltf, pymaterial.pbr_metallic_roughness, mat.name, vertex_color) + + # add emission map if needed + if pymaterial.emissive_texture is not None: + BlenderEmissiveMap.create(gltf, material_idx) + + # add normal map if needed + if pymaterial.normal_texture is not None: + BlenderNormalMap.create(gltf, material_idx) + + # add occlusion map if needed + # will be pack, but not used + if pymaterial.occlusion_texture is not None: + BlenderOcclusionMap.create(gltf, material_idx) + + if pymaterial.alpha_mode is not None and pymaterial.alpha_mode != 'OPAQUE': + BlenderMaterial.blender_alpha(gltf, material_idx) + + @staticmethod + def set_uvmap(gltf, material_idx, prim, obj): + """Set UV Map.""" + pymaterial = gltf.data.materials[material_idx] + + node_tree = bpy.data.materials[pymaterial.blender_material].node_tree + uvmap_nodes = [node for node in node_tree.nodes if node.type in ['UVMAP', 'NORMAL_MAP']] + for uvmap_node in uvmap_nodes: + if uvmap_node["gltf2_texcoord"] in prim.blender_texcoord.keys(): + uvmap_node.uv_map = prim.blender_texcoord[uvmap_node["gltf2_texcoord"]] + + @staticmethod + def blender_alpha(gltf, material_idx): + """Set alpha.""" + pymaterial = gltf.data.materials[material_idx] + material = bpy.data.materials[pymaterial.blender_material] + + node_tree = material.node_tree + # Add nodes for basic transparency + # Add mix shader between output and Principled BSDF + trans = node_tree.nodes.new('ShaderNodeBsdfTransparent') + trans.location = 750, -500 + mix = node_tree.nodes.new('ShaderNodeMixShader') + mix.location = 1000, 0 + + output_surface_input = get_output_surface_input(node_tree) + preoutput_node_output = get_preoutput_node_output(node_tree) + + link = output_surface_input.links[0] + node_tree.links.remove(link) + + # PBR => Mix input 1 + node_tree.links.new(preoutput_node_output, mix.inputs[1]) + + # Trans => Mix input 2 + node_tree.links.new(trans.outputs['BSDF'], mix.inputs[2]) + + # Mix => Output + node_tree.links.new(mix.outputs['Shader'], output_surface_input) + + # alpha blend factor + add = node_tree.nodes.new('ShaderNodeMath') + add.operation = 'ADD' + add.location = 750, -250 + + diffuse_factor = 1.0 + if pymaterial.extensions is not None and 'KHR_materials_pbrSpecularGlossiness' in pymaterial.extensions: + diffuse_factor = pymaterial.extensions['KHR_materials_pbrSpecularGlossiness']['diffuseFactor'][3] + elif pymaterial.pbr_metallic_roughness: + diffuse_factor = pymaterial.pbr_metallic_roughness.base_color_factor[3] + + add.inputs[0].default_value = abs(1.0 - diffuse_factor) + add.inputs[1].default_value = 0.0 + node_tree.links.new(add.outputs['Value'], mix.inputs[0]) + + # Take diffuse texture alpha into account if any + diffuse_texture = get_base_color_node(node_tree) + if diffuse_texture: + inverter = node_tree.nodes.new('ShaderNodeInvert') + inverter.location = 250, -250 + inverter.inputs[1].default_value = (1.0, 1.0, 1.0, 1.0) + node_tree.links.new(diffuse_texture.outputs['Alpha'], inverter.inputs[0]) + + mult = node_tree.nodes.new('ShaderNodeMath') + mult.operation = 'MULTIPLY' if pymaterial.alpha_mode == 'BLEND' else 'GREATER_THAN' + mult.location = 500, -250 + alpha_cutoff = 1.0 if pymaterial.alpha_mode == 'BLEND' else \ + 1.0 - pymaterial.alpha_cutoff if pymaterial.alpha_cutoff is not None else 0.5 + mult.inputs[1].default_value = alpha_cutoff + node_tree.links.new(inverter.outputs['Color'], mult.inputs[0]) + node_tree.links.new(mult.outputs['Value'], add.inputs[0]) + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py new file mode 100755 index 00000000..88396455 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py @@ -0,0 +1,158 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +import bmesh + +from .gltf2_blender_primitive import BlenderPrimitive +from ...io.imp.gltf2_io_binary import BinaryData +from ..com.gltf2_blender_conversion import loc_gltf_to_blender + + +class BlenderMesh(): + """Blender Mesh.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, mesh_idx, node_idx, parent): + """Mesh creation.""" + pymesh = gltf.data.meshes[mesh_idx] + + # Geometry + if pymesh.name: + mesh_name = pymesh.name + else: + mesh_name = "Mesh_" + str(mesh_idx) + + mesh = bpy.data.meshes.new(mesh_name) + verts = [] + edges = [] + faces = [] + for prim in pymesh.primitives: + verts, edges, faces = BlenderPrimitive.create(gltf, prim, verts, edges, faces) + + mesh.from_pydata(verts, edges, faces) + mesh.validate() + + pymesh.blender_name = mesh.name + + return mesh + + @staticmethod + def set_mesh(gltf, pymesh, mesh, obj): + """Set all data after mesh creation.""" + # Normals + offset = 0 + for prim in pymesh.primitives: + offset = BlenderPrimitive.set_normals(gltf, prim, mesh, offset) + + mesh.update() + + # manage UV + offset = 0 + for prim in pymesh.primitives: + offset = BlenderPrimitive.set_UV(gltf, prim, obj, mesh, offset) + + mesh.update() + + # Object and UV are now created, we can set UVMap into material + for prim in pymesh.primitives: + BlenderPrimitive.set_UV_in_mat(gltf, prim, obj) + + # Assign materials to mesh + offset = 0 + cpt_index_mat = 0 + bm = bmesh.new() + bm.from_mesh(obj.data) + bm.faces.ensure_lookup_table() + for prim in pymesh.primitives: + offset, cpt_index_mat = BlenderPrimitive.assign_material(gltf, prim, obj, bm, offset, cpt_index_mat) + + bm.to_mesh(obj.data) + bm.free() + + # Create shapekeys if needed + max_shape_to_create = 0 + for prim in pymesh.primitives: + if prim.targets: + if len(prim.targets) > max_shape_to_create: + max_shape_to_create = len(prim.targets) + + # Create basis shape key + if max_shape_to_create > 0: + obj.shape_key_add(name="Basis") + + for i in range(max_shape_to_create): + + obj.shape_key_add(name="target_" + str(i)) + + offset_idx = 0 + for prim in pymesh.primitives: + if prim.targets is None: + continue + if i >= len(prim.targets): + continue + + bm = bmesh.new() + bm.from_mesh(mesh) + + shape_layer = bm.verts.layers.shape[i + 1] + + pos = BinaryData.get_data_from_accessor(gltf, prim.targets[i]['POSITION']) + + for vert in bm.verts: + if vert.index not in range(offset_idx, offset_idx + prim.vertices_length): + continue + + shape = vert[shape_layer] + + co = loc_gltf_to_blender(list(pos[vert.index - offset_idx])) + shape.x = obj.data.vertices[vert.index].co.x + co[0] + shape.y = obj.data.vertices[vert.index].co.y + co[1] + shape.z = obj.data.vertices[vert.index].co.z + co[2] + + bm.to_mesh(obj.data) + bm.free() + offset_idx += prim.vertices_length + + # set default weights for shape keys, and names + if pymesh.weights is not None: + for i in range(max_shape_to_create): + if i < len(pymesh.weights): + obj.data.shape_keys.key_blocks[i + 1].value = pymesh.weights[i] + if gltf.data.accessors[pymesh.primitives[0].targets[i]['POSITION']].name is not None: + obj.data.shape_keys.key_blocks[i + 1].name = \ + gltf.data.accessors[pymesh.primitives[0].targets[i]['POSITION']].name + + # Apply vertex color. + vertex_color = None + offset = 0 + for prim in pymesh.primitives: + if 'COLOR_0' in prim.attributes.keys(): + # Create vertex color, once only per object + if vertex_color is None: + vertex_color = obj.data.vertex_colors.new("COLOR_0") + + color_data = BinaryData.get_data_from_accessor(gltf, prim.attributes['COLOR_0']) + + for poly in mesh.polygons: + for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total): + vert_idx = mesh.loops[loop_idx].vertex_index + if vert_idx in range(offset, offset + prim.vertices_length): + cpt_idx = vert_idx - offset + vertex_color.data[loop_idx].color = color_data[cpt_idx][0:3] + # TODO : no alpha in vertex color + offset = offset + prim.vertices_length + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_node.py b/io_scene_gltf2/blender/imp/gltf2_blender_node.py new file mode 100755 index 00000000..a82f1db7 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_node.py @@ -0,0 +1,184 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from .gltf2_blender_mesh import BlenderMesh +from .gltf2_blender_camera import BlenderCamera +from .gltf2_blender_skin import BlenderSkin +from ..com.gltf2_blender_conversion import scale_to_matrix, matrix_gltf_to_blender + + +class BlenderNode(): + """Blender Node.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, node_idx, parent): + """Node creation.""" + pynode = gltf.data.nodes[node_idx] + + # Blender attributes initialization + pynode.blender_object = "" + pynode.parent = parent + + if pynode.mesh is not None: + + if gltf.data.meshes[pynode.mesh].blender_name is not None: + # Mesh is already created, only create instance + mesh = bpy.data.meshes[gltf.data.meshes[pynode.mesh].blender_name] + else: + if pynode.name: + gltf.log.info("Blender create Mesh node " + pynode.name) + else: + gltf.log.info("Blender create Mesh node") + + mesh = BlenderMesh.create(gltf, pynode.mesh, node_idx, parent) + + if pynode.name: + name = pynode.name + else: + # Take mesh name if exist + if gltf.data.meshes[pynode.mesh].name: + name = gltf.data.meshes[pynode.mesh].name + else: + name = "Object_" + str(node_idx) + + obj = bpy.data.objects.new(name, mesh) + obj.rotation_mode = 'QUATERNION' + bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj) + + # Transforms apply only if this mesh is not skinned + # See implementation node of gltf2 specification + if not (pynode.mesh and pynode.skin is not None): + BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent) + pynode.blender_object = obj.name + BlenderNode.set_parent(gltf, pynode, obj, parent) + + BlenderMesh.set_mesh(gltf, gltf.data.meshes[pynode.mesh], mesh, obj) + + if pynode.children: + for child_idx in pynode.children: + BlenderNode.create(gltf, child_idx, node_idx) + + return + + if pynode.camera is not None: + if pynode.name: + gltf.log.info("Blender create Camera node " + pynode.name) + else: + gltf.log.info("Blender create Camera node") + obj = BlenderCamera.create(gltf, pynode.camera) + BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent) # TODO default rotation of cameras ? + pynode.blender_object = obj.name + BlenderNode.set_parent(gltf, pynode, obj, parent) + + return + + if pynode.is_joint: + if pynode.name: + gltf.log.info("Blender create Bone node " + pynode.name) + else: + gltf.log.info("Blender create Bone node") + # Check if corresponding armature is already created, create it if needed + if gltf.data.skins[pynode.skin_id].blender_armature_name is None: + BlenderSkin.create_armature(gltf, pynode.skin_id, parent) + + BlenderSkin.create_bone(gltf, pynode.skin_id, node_idx, parent) + + if pynode.children: + for child_idx in pynode.children: + BlenderNode.create(gltf, child_idx, node_idx) + + return + + # No mesh, no camera. For now, create empty #TODO + + if pynode.name: + gltf.log.info("Blender create Empty node " + pynode.name) + obj = bpy.data.objects.new(pynode.name, None) + else: + gltf.log.info("Blender create Empty node") + obj = bpy.data.objects.new("Node", None) + obj.rotation_mode = 'QUATERNION' + bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj) + BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent) + pynode.blender_object = obj.name + BlenderNode.set_parent(gltf, pynode, obj, parent) + + if pynode.children: + for child_idx in pynode.children: + BlenderNode.create(gltf, child_idx, node_idx) + + @staticmethod + def set_parent(gltf, pynode, obj, parent): + """Set parent.""" + if parent is None: + return + + for node_idx, node in enumerate(gltf.data.nodes): + if node_idx == parent: + if node.is_joint is True: + bpy.ops.object.select_all(action='DESELECT') + bpy.data.objects[node.blender_armature_name].select_set(True) + bpy.context.view_layer.objects.active = bpy.data.objects[node.blender_armature_name] + + bpy.ops.object.mode_set(mode='EDIT') + bpy.data.objects[node.blender_armature_name].data.edit_bones.active = \ + bpy.data.objects[node.blender_armature_name].data.edit_bones[node.blender_bone_name] + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.data.objects[node.blender_armature_name].select_set(True) + bpy.context.view_layer.objects.active = bpy.data.objects[node.blender_armature_name] + bpy.context.scene.update() + bpy.ops.object.parent_set(type='BONE_RELATIVE', keep_transform=True) + # From world transform to local (-armature transform -bone transform) + bone_trans = bpy.data.objects[node.blender_armature_name] \ + .pose.bones[node.blender_bone_name].matrix.to_translation().copy() + bone_rot = bpy.data.objects[node.blender_armature_name] \ + .pose.bones[node.blender_bone_name].matrix.to_quaternion().copy() + bone_scale_mat = scale_to_matrix(node.blender_bone_matrix.to_scale()) + obj.location = bone_scale_mat @ obj.location + obj.location = bone_rot @ obj.location + obj.location += bone_trans + obj.location = bpy.data.objects[node.blender_armature_name].matrix_world.to_quaternion() \ + @ obj.location + obj.rotation_quaternion = obj.rotation_quaternion \ + @ bpy.data.objects[node.blender_armature_name].matrix_world.to_quaternion() + obj.scale = bone_scale_mat @ obj.scale + + return + if node.blender_object: + obj.parent = bpy.data.objects[node.blender_object] + return + + gltf.log.error("ERROR, parent not found") + + @staticmethod + def set_transforms(gltf, node_idx, pynode, obj, parent): + """Set transforms.""" + if parent is None: + obj.matrix_world = matrix_gltf_to_blender(pynode.transform) + return + + for idx, node in enumerate(gltf.data.nodes): + if idx == parent: + if node.is_joint is True: + obj.matrix_world = matrix_gltf_to_blender(pynode.transform) + return + else: + obj.matrix_world = matrix_gltf_to_blender(pynode.transform) + return + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py b/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py new file mode 100755 index 00000000..cf1f48c1 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py @@ -0,0 +1,347 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from .gltf2_blender_texture import BlenderTextureInfo + + +class BlenderPbr(): + """Blender Pbr.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + def create(gltf, pypbr, mat_name, vertex_color): + """Pbr creation.""" + engine = bpy.context.scene.render.engine + if engine in ['CYCLES', 'BLENDER_EEVEE']: + BlenderPbr.create_nodetree(gltf, pypbr, mat_name, vertex_color) + + def create_nodetree(gltf, pypbr, mat_name, vertex_color): + """Nodetree creation.""" + material = bpy.data.materials[mat_name] + material.use_nodes = True + node_tree = material.node_tree + + # If there is no diffuse texture, but only a color, wihtout + # vertex_color, we set this color in viewport color + if pypbr.color_type == gltf.SIMPLE and not vertex_color: + material.diffuse_color = pypbr.base_color_factor[:3] + + # delete all nodes except output + for node in list(node_tree.nodes): + if not node.type == 'OUTPUT_MATERIAL': + node_tree.nodes.remove(node) + + output_node = node_tree.nodes[0] + output_node.location = 1250, 0 + + # create PBR node + principled = node_tree.nodes.new('ShaderNodeBsdfPrincipled') + principled.location = 0, 0 + + if pypbr.color_type == gltf.SIMPLE: + + if not vertex_color: + + # change input values + principled.inputs[0].default_value = pypbr.base_color_factor + # TODO : currently set metallic & specular in same way + principled.inputs[5].default_value = pypbr.metallic_factor + principled.inputs[7].default_value = pypbr.roughness_factor + + else: + # Create attribute node to get COLOR_0 data + attribute_node = node_tree.nodes.new('ShaderNodeAttribute') + attribute_node.attribute_name = 'COLOR_0' + attribute_node.location = -500, 0 + + # TODO : currently set metallic & specular in same way + principled.inputs[5].default_value = pypbr.metallic_factor + principled.inputs[7].default_value = pypbr.roughness_factor + + # links + rgb_node = node_tree.nodes.new('ShaderNodeMixRGB') + rgb_node.blend_type = 'MULTIPLY' + rgb_node.inputs['Fac'].default_value = 1.0 + rgb_node.inputs['Color1'].default_value = pypbr.base_color_factor + node_tree.links.new(rgb_node.inputs['Color2'], attribute_node.outputs[0]) + node_tree.links.new(principled.inputs[0], rgb_node.outputs[0]) + + elif pypbr.color_type == gltf.TEXTURE_FACTOR: + + # TODO alpha ? + if vertex_color: + # TODO tree locations + # Create attribute / separate / math nodes + attribute_node = node_tree.nodes.new('ShaderNodeAttribute') + attribute_node.attribute_name = 'COLOR_0' + + separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB') + math_vc_R = node_tree.nodes.new('ShaderNodeMath') + math_vc_R.operation = 'MULTIPLY' + + math_vc_G = node_tree.nodes.new('ShaderNodeMath') + math_vc_G.operation = 'MULTIPLY' + + math_vc_B = node_tree.nodes.new('ShaderNodeMath') + math_vc_B.operation = 'MULTIPLY' + + BlenderTextureInfo.create(gltf, pypbr.base_color_texture.index) + + # create UV Map / Mapping / Texture nodes / separate & math and combine + text_node = node_tree.nodes.new('ShaderNodeTexImage') + text_node.image = bpy.data.images[gltf.data.images[ + gltf.data.textures[pypbr.base_color_texture.index].source + ].blender_image_name] + text_node.label = 'BASE COLOR' + text_node.location = -1000, 500 + + combine = node_tree.nodes.new('ShaderNodeCombineRGB') + combine.location = -250, 500 + + math_R = node_tree.nodes.new('ShaderNodeMath') + math_R.location = -500, 750 + math_R.operation = 'MULTIPLY' + math_R.inputs[1].default_value = pypbr.base_color_factor[0] + + math_G = node_tree.nodes.new('ShaderNodeMath') + math_G.location = -500, 500 + math_G.operation = 'MULTIPLY' + math_G.inputs[1].default_value = pypbr.base_color_factor[1] + + math_B = node_tree.nodes.new('ShaderNodeMath') + math_B.location = -500, 250 + math_B.operation = 'MULTIPLY' + math_B.inputs[1].default_value = pypbr.base_color_factor[2] + + separate = node_tree.nodes.new('ShaderNodeSeparateRGB') + separate.location = -750, 500 + + mapping = node_tree.nodes.new('ShaderNodeMapping') + mapping.location = -1500, 500 + + uvmap = node_tree.nodes.new('ShaderNodeUVMap') + uvmap.location = -2000, 500 + if pypbr.base_color_texture.tex_coord is not None: + uvmap["gltf2_texcoord"] = pypbr.base_color_texture.tex_coord # Set custom flag to retrieve TexCoord + else: + uvmap["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here + # UV Map will be set after object/UVMap creation + + # Create links + if vertex_color: + node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0]) + node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0]) + node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1]) + node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2]) + node_tree.links.new(math_vc_R.inputs[0], math_R.outputs[0]) + node_tree.links.new(math_vc_G.inputs[0], math_G.outputs[0]) + node_tree.links.new(math_vc_B.inputs[0], math_B.outputs[0]) + node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0]) + node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0]) + node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0]) + + else: + node_tree.links.new(combine.inputs[0], math_R.outputs[0]) + node_tree.links.new(combine.inputs[1], math_G.outputs[0]) + node_tree.links.new(combine.inputs[2], math_B.outputs[0]) + + # Common for both mode (non vertex color / vertex color) + node_tree.links.new(math_R.inputs[0], separate.outputs[0]) + node_tree.links.new(math_G.inputs[0], separate.outputs[1]) + node_tree.links.new(math_B.inputs[0], separate.outputs[2]) + + node_tree.links.new(mapping.inputs[0], uvmap.outputs[0]) + node_tree.links.new(text_node.inputs[0], mapping.outputs[0]) + node_tree.links.new(separate.inputs[0], text_node.outputs[0]) + + node_tree.links.new(principled.inputs[0], combine.outputs[0]) + + elif pypbr.color_type == gltf.TEXTURE: + + BlenderTextureInfo.create(gltf, pypbr.base_color_texture.index) + + # TODO alpha ? + if vertex_color: + # Create attribute / separate / math nodes + attribute_node = node_tree.nodes.new('ShaderNodeAttribute') + attribute_node.attribute_name = 'COLOR_0' + attribute_node.location = -2000, 250 + + separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB') + separate_vertex_color.location = -1500, 250 + + math_vc_R = node_tree.nodes.new('ShaderNodeMath') + math_vc_R.operation = 'MULTIPLY' + math_vc_R.location = -1000, 750 + + math_vc_G = node_tree.nodes.new('ShaderNodeMath') + math_vc_G.operation = 'MULTIPLY' + math_vc_G.location = -1000, 500 + + math_vc_B = node_tree.nodes.new('ShaderNodeMath') + math_vc_B.operation = 'MULTIPLY' + math_vc_B.location = -1000, 250 + + combine = node_tree.nodes.new('ShaderNodeCombineRGB') + combine.location = -500, 500 + + separate = node_tree.nodes.new('ShaderNodeSeparateRGB') + separate.location = -1500, 500 + + # create UV Map / Mapping / Texture nodes / separate & math and combine + text_node = node_tree.nodes.new('ShaderNodeTexImage') + text_node.image = bpy.data.images[gltf.data.images[ + gltf.data.textures[pypbr.base_color_texture.index].source + ].blender_image_name] + text_node.label = 'BASE COLOR' + if vertex_color: + text_node.location = -2000, 500 + else: + text_node.location = -500, 500 + + mapping = node_tree.nodes.new('ShaderNodeMapping') + if vertex_color: + mapping.location = -2500, 500 + else: + mapping.location = -1500, 500 + + uvmap = node_tree.nodes.new('ShaderNodeUVMap') + if vertex_color: + uvmap.location = -3000, 500 + else: + uvmap.location = -2000, 500 + if pypbr.base_color_texture.tex_coord is not None: + uvmap["gltf2_texcoord"] = pypbr.base_color_texture.tex_coord # Set custom flag to retrieve TexCoord + else: + uvmap["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here + # UV Map will be set after object/UVMap creation + + # Create links + if vertex_color: + node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0]) + + node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0]) + node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1]) + node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2]) + + node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0]) + node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0]) + node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0]) + + node_tree.links.new(separate.inputs[0], text_node.outputs[0]) + + node_tree.links.new(principled.inputs[0], combine.outputs[0]) + + node_tree.links.new(math_vc_R.inputs[0], separate.outputs[0]) + node_tree.links.new(math_vc_G.inputs[0], separate.outputs[1]) + node_tree.links.new(math_vc_B.inputs[0], separate.outputs[2]) + + else: + node_tree.links.new(principled.inputs[0], text_node.outputs[0]) + + # Common for both mode (non vertex color / vertex color) + + node_tree.links.new(mapping.inputs[0], uvmap.outputs[0]) + node_tree.links.new(text_node.inputs[0], mapping.outputs[0]) + + # Says metallic, but it means metallic & Roughness values + if pypbr.metallic_type == gltf.SIMPLE: + principled.inputs[4].default_value = pypbr.metallic_factor + principled.inputs[7].default_value = pypbr.roughness_factor + + elif pypbr.metallic_type == gltf.TEXTURE: + BlenderTextureInfo.create(gltf, pypbr.metallic_roughness_texture.index) + metallic_text = node_tree.nodes.new('ShaderNodeTexImage') + metallic_text.image = bpy.data.images[gltf.data.images[ + gltf.data.textures[pypbr.metallic_roughness_texture.index].source + ].blender_image_name] + metallic_text.color_space = 'NONE' + metallic_text.label = 'METALLIC ROUGHNESS' + metallic_text.location = -500, 0 + + metallic_separate = node_tree.nodes.new('ShaderNodeSeparateRGB') + metallic_separate.location = -250, 0 + + metallic_mapping = node_tree.nodes.new('ShaderNodeMapping') + metallic_mapping.location = -1000, 0 + + metallic_uvmap = node_tree.nodes.new('ShaderNodeUVMap') + metallic_uvmap.location = -1500, 0 + if pypbr.metallic_roughness_texture.tex_coord is not None: + # Set custom flag to retrieve TexCoord + metallic_uvmap["gltf2_texcoord"] = pypbr.metallic_roughness_texture.tex_coord + else: + metallic_uvmap["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here + + # links + node_tree.links.new(metallic_separate.inputs[0], metallic_text.outputs[0]) + node_tree.links.new(principled.inputs[4], metallic_separate.outputs[2]) # metallic + node_tree.links.new(principled.inputs[7], metallic_separate.outputs[1]) # Roughness + + node_tree.links.new(metallic_mapping.inputs[0], metallic_uvmap.outputs[0]) + node_tree.links.new(metallic_text.inputs[0], metallic_mapping.outputs[0]) + + elif pypbr.metallic_type == gltf.TEXTURE_FACTOR: + + BlenderTextureInfo.create(gltf, pypbr.metallic_roughness_texture.index) + metallic_text = node_tree.nodes.new('ShaderNodeTexImage') + metallic_text.image = bpy.data.images[gltf.data.images[ + gltf.data.textures[pypbr.metallic_roughness_texture.index].source + ].blender_image_name] + metallic_text.color_space = 'NONE' + metallic_text.label = 'METALLIC ROUGHNESS' + metallic_text.location = -1000, 0 + + metallic_separate = node_tree.nodes.new('ShaderNodeSeparateRGB') + metallic_separate.location = -500, 0 + + metallic_math = node_tree.nodes.new('ShaderNodeMath') + metallic_math.operation = 'MULTIPLY' + metallic_math.inputs[1].default_value = pypbr.metallic_factor + metallic_math.location = -250, 100 + + roughness_math = node_tree.nodes.new('ShaderNodeMath') + roughness_math.operation = 'MULTIPLY' + roughness_math.inputs[1].default_value = pypbr.roughness_factor + roughness_math.location = -250, -100 + + metallic_mapping = node_tree.nodes.new('ShaderNodeMapping') + metallic_mapping.location = -1000, 0 + + metallic_uvmap = node_tree.nodes.new('ShaderNodeUVMap') + metallic_uvmap.location = -1500, 0 + if pypbr.metallic_roughness_texture.tex_coord is not None: + # Set custom flag to retrieve TexCoord + metallic_uvmap["gltf2_texcoord"] = pypbr.metallic_roughness_texture.tex_coord + else: + metallic_uvmap["gltf2_texcoord"] = 0 # TODO set in pre_compute instead of here + + # links + node_tree.links.new(metallic_separate.inputs[0], metallic_text.outputs[0]) + + # metallic + node_tree.links.new(metallic_math.inputs[0], metallic_separate.outputs[2]) + node_tree.links.new(principled.inputs[4], metallic_math.outputs[0]) + + # roughness + node_tree.links.new(roughness_math.inputs[0], metallic_separate.outputs[1]) + node_tree.links.new(principled.inputs[7], roughness_math.outputs[0]) + + node_tree.links.new(metallic_mapping.inputs[0], metallic_uvmap.outputs[0]) + node_tree.links.new(metallic_text.inputs[0], metallic_mapping.outputs[0]) + + # link node to output + node_tree.links.new(output_node.inputs[0], principled.outputs[0]) + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py b/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py new file mode 100755 index 00000000..bc12f437 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py @@ -0,0 +1,170 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from mathutils import Vector + +from .gltf2_blender_material import BlenderMaterial +from ..com.gltf2_blender_conversion import loc_gltf_to_blender +from ...io.imp.gltf2_io_binary import BinaryData + + +class BlenderPrimitive(): + """Blender Primitive.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, pyprimitive, verts, edges, faces): + """Primitive creation.""" + pyprimitive.blender_texcoord = {} + + # TODO mode of primitive 4 for now. + current_length = len(verts) + pos = BinaryData.get_data_from_accessor(gltf, pyprimitive.attributes['POSITION']) + if pyprimitive.indices is not None: + indices = BinaryData.get_data_from_accessor(gltf, pyprimitive.indices) + else: + indices = [] + indices_ = range(0, len(pos)) + for i in indices_: + indices.append((i,)) + + prim_verts = [loc_gltf_to_blender(vert) for vert in pos] + pyprimitive.vertices_length = len(prim_verts) + verts.extend(prim_verts) + prim_faces = [] + for i in range(0, len(indices), 3): + vals = indices[i:i + 3] + new_vals = [] + for y in vals: + new_vals.append(y[0] + current_length) + prim_faces.append(tuple(new_vals)) + faces.extend(prim_faces) + pyprimitive.faces_length = len(prim_faces) + + # manage material of primitive + if pyprimitive.material is not None: + + # Create Blender material + # TODO, a same material can have difference COLOR_0 multiplicator + if gltf.data.materials[pyprimitive.material].blender_material is None: + vertex_color = None + if 'COLOR_0' in pyprimitive.attributes.keys(): + vertex_color = pyprimitive.attributes['COLOR_0'] + BlenderMaterial.create(gltf, pyprimitive.material, vertex_color) + + return verts, edges, faces + + def set_normals(gltf, pyprimitive, mesh, offset): + """Set Normal.""" + if 'NORMAL' in pyprimitive.attributes.keys(): + normal_data = BinaryData.get_data_from_accessor(gltf, pyprimitive.attributes['NORMAL']) + for poly in mesh.polygons: + if gltf.import_settings['import_shading'] == "NORMALS": + calc_norm_vertices = [] + for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total): + vert_idx = mesh.loops[loop_idx].vertex_index + if vert_idx in range(offset, offset + pyprimitive.vertices_length): + cpt_vert = vert_idx - offset + mesh.vertices[vert_idx].normal = normal_data[cpt_vert] + calc_norm_vertices.append(vert_idx) + + if len(calc_norm_vertices) == 3: + # Calcul normal + vert0 = mesh.vertices[calc_norm_vertices[0]].co + vert1 = mesh.vertices[calc_norm_vertices[1]].co + vert2 = mesh.vertices[calc_norm_vertices[2]].co + calc_normal = (vert1 - vert0).cross(vert2 - vert0).normalized() + + # Compare normal to vertex normal + for i in calc_norm_vertices: + cpt_vert = vert_idx - offset + vec = Vector( + (normal_data[cpt_vert][0], normal_data[cpt_vert][1], normal_data[cpt_vert][2]) + ) + if not calc_normal.dot(vec) > 0.9999999: + poly.use_smooth = True + break + elif gltf.import_settings['import_shading'] == "FLAT": + poly.use_smooth = False + elif gltf.import_settings['import_shading'] == "SMOOTH": + poly.use_smooth = True + else: + pass # Should not happend + + offset = offset + pyprimitive.vertices_length + return offset + + def set_UV(gltf, pyprimitive, obj, mesh, offset): + """Set UV Map.""" + for texcoord in [attr for attr in pyprimitive.attributes.keys() if attr[:9] == "TEXCOORD_"]: + if texcoord not in mesh.uv_layers: + mesh.uv_layers.new(name=texcoord) + pyprimitive.blender_texcoord[int(texcoord[9:])] = texcoord + + texcoord_data = BinaryData.get_data_from_accessor(gltf, pyprimitive.attributes[texcoord]) + for poly in mesh.polygons: + for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total): + vert_idx = mesh.loops[loop_idx].vertex_index + if vert_idx in range(offset, offset + pyprimitive.vertices_length): + obj.data.uv_layers[texcoord].data[loop_idx].uv = \ + Vector((texcoord_data[vert_idx - offset][0], 1 - texcoord_data[vert_idx - offset][1])) + + offset = offset + pyprimitive.vertices_length + return offset + + def set_UV_in_mat(gltf, pyprimitive, obj): + """After nodetree creation, set UVMap in nodes.""" + if pyprimitive.material is None: + return + if gltf.data.materials[pyprimitive.material].extensions \ + and "KHR_materials_pbrSpecularGlossiness" in \ + gltf.data.materials[pyprimitive.material].extensions.keys(): + if pyprimitive.material is not None \ + and gltf.data.materials[pyprimitive.material].extensions[ + 'KHR_materials_pbrSpecularGlossiness' + ]['diffuse_type'] in [gltf.TEXTURE, gltf.TEXTURE_FACTOR]: + BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj) + else: + if pyprimitive.material is not None \ + and gltf.data.materials[pyprimitive.material].extensions[ + 'KHR_materials_pbrSpecularGlossiness' + ]['specgloss_type'] in [gltf.TEXTURE, gltf.TEXTURE_FACTOR]: + BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj) + + else: + if pyprimitive.material is not None \ + and gltf.data.materials[pyprimitive.material].pbr_metallic_roughness.color_type in \ + [gltf.TEXTURE, gltf.TEXTURE_FACTOR]: + BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj) + else: + if pyprimitive.material is not None \ + and gltf.data.materials[pyprimitive.material].pbr_metallic_roughness.metallic_type in \ + [gltf.TEXTURE, gltf.TEXTURE_FACTOR]: + BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj) + + def assign_material(gltf, pyprimitive, obj, bm, offset, cpt_index_mat): + """Assign material to faces of primitives.""" + if pyprimitive.material is not None: + obj.data.materials.append(bpy.data.materials[gltf.data.materials[pyprimitive.material].blender_material]) + for vert in bm.verts: + if vert.index in range(offset, offset + pyprimitive.vertices_length): + for loop in vert.link_loops: + face = loop.face.index + bm.faces[face].material_index = cpt_index_mat + cpt_index_mat += 1 + offset = offset + pyprimitive.vertices_length + return offset, cpt_index_mat + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_scene.py b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py new file mode 100755 index 00000000..c5e89659 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py @@ -0,0 +1,94 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from math import sqrt +from mathutils import Quaternion +from .gltf2_blender_node import BlenderNode +from .gltf2_blender_skin import BlenderSkin +from .gltf2_blender_animation import BlenderAnimation + + +class BlenderScene(): + """Blender Scene.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, scene_idx): + """Scene creation.""" + pyscene = gltf.data.scenes[scene_idx] + + # Create a new scene only if not already exists in .blend file + # TODO : put in current scene instead ? + if pyscene.name not in [scene.name for scene in bpy.data.scenes]: + # TODO: There is a bug in 2.8 alpha that break CLEAR_KEEP_TRANSFORM + # if we are creating a new scene + scene = bpy.context.scene + if bpy.app.version < (2, 80, 0): + scene.render.engine = "CYCLES" + else: + scene.render.engine = "BLENDER_EEVEE" + + gltf.blender_scene = scene.name + else: + gltf.blender_scene = pyscene.name + + # Create Yup2Zup empty + obj_rotation = bpy.data.objects.new("Yup2Zup", None) + obj_rotation.rotation_mode = 'QUATERNION' + obj_rotation.rotation_quaternion = Quaternion((sqrt(2) / 2, sqrt(2) / 2, 0.0, 0.0)) + + bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj_rotation) + + if pyscene.nodes is not None: + for node_idx in pyscene.nodes: + BlenderNode.create(gltf, node_idx, None) # None => No parent + + # Now that all mesh / bones are created, create vertex groups on mesh + if gltf.data.skins: + for skin_id, skin in enumerate(gltf.data.skins): + if hasattr(skin, "node_ids"): + BlenderSkin.create_vertex_groups(gltf, skin_id) + + for skin_id, skin in enumerate(gltf.data.skins): + if hasattr(skin, "node_ids"): + BlenderSkin.assign_vertex_groups(gltf, skin_id) + + for skin_id, skin in enumerate(gltf.data.skins): + if hasattr(skin, "node_ids"): + BlenderSkin.create_armature_modifiers(gltf, skin_id) + + if gltf.data.animations: + for anim_idx, anim in enumerate(gltf.data.animations): + for node_idx in pyscene.nodes: + BlenderAnimation.anim(gltf, anim_idx, node_idx) + + # Parent root node to rotation object + if pyscene.nodes is not None: + for node_idx in pyscene.nodes: + bpy.data.objects[gltf.data.nodes[node_idx].blender_object].parent = obj_rotation + + + for node_idx in pyscene.nodes: + for obj_ in bpy.context.scene.objects: + obj_.select_set(False) + bpy.data.objects[gltf.data.nodes[node_idx].blender_object].select_set(True) + bpy.context.view_layer.objects.active = bpy.data.objects[gltf.data.nodes[node_idx].blender_object] + + bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') + + bpy.context.scene.collection.objects.unlink(obj_rotation) + bpy.data.objects.remove(obj_rotation) + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_skin.py b/io_scene_gltf2/blender/imp/gltf2_blender_skin.py new file mode 100755 index 00000000..db0e50f9 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_skin.py @@ -0,0 +1,209 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import bpy +from mathutils import Vector, Matrix +from ..com.gltf2_blender_conversion import matrix_gltf_to_blender, scale_to_matrix +from ...io.imp.gltf2_io_binary import BinaryData + + +class BlenderSkin(): + """Blender Skinning / Armature.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create_armature(gltf, skin_id, parent): + """Armature creation.""" + pyskin = gltf.data.skins[skin_id] + + if pyskin.name is not None: + name = pyskin.name + else: + name = "Armature_" + str(skin_id) + + armature = bpy.data.armatures.new(name) + obj = bpy.data.objects.new(name, armature) + bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj) + pyskin.blender_armature_name = obj.name + if parent is not None: + obj.parent = bpy.data.objects[gltf.data.nodes[parent].blender_object] + + @staticmethod + def set_bone_transforms(gltf, skin_id, bone, node_id, parent): + """Set bone transformations.""" + pyskin = gltf.data.skins[skin_id] + pynode = gltf.data.nodes[node_id] + + obj = bpy.data.objects[pyskin.blender_armature_name] + + # Set bone bind_pose by inverting bindpose matrix + if node_id in pyskin.joints: + index_in_skel = pyskin.joints.index(node_id) + inverse_bind_matrices = BinaryData.get_data_from_accessor(gltf, pyskin.inverse_bind_matrices) + # Needed to keep scale in matrix, as bone.matrix seems to drop it + if index_in_skel < len(inverse_bind_matrices): + pynode.blender_bone_matrix = matrix_gltf_to_blender( + inverse_bind_matrices[index_in_skel] + ).inverted() + bone.matrix = pynode.blender_bone_matrix + else: + gltf.log.error("Error with inverseBindMatrix for skin " + pyskin) + else: + print('No invBindMatrix for bone ' + str(node_id)) + pynode.blender_bone_matrix = Matrix() + + # Parent the bone + if parent is not None and hasattr(gltf.data.nodes[parent], "blender_bone_name"): + bone.parent = obj.data.edit_bones[gltf.data.nodes[parent].blender_bone_name] # TODO if in another scene + + # Switch to Pose mode + bpy.ops.object.mode_set(mode="POSE") + obj.data.pose_position = 'POSE' + + # Set posebone location/rotation/scale (in armature space) + # location is actual bone location minus it's original (bind) location + bind_location = Matrix.Translation(pynode.blender_bone_matrix.to_translation()) + bind_rotation = pynode.blender_bone_matrix.to_quaternion() + bind_scale = scale_to_matrix(pynode.blender_bone_matrix.to_scale()) + + location, rotation, scale = matrix_gltf_to_blender(pynode.transform).decompose() + if parent is not None and hasattr(gltf.data.nodes[parent], "blender_bone_matrix"): + parent_mat = gltf.data.nodes[parent].blender_bone_matrix + + # Get armature space location (bindpose + pose) + # Then, remove original bind location from armspace location, and bind rotation + final_location = (bind_location.inverted() @ parent_mat @ Matrix.Translation(location)).to_translation() + obj.pose.bones[pynode.blender_bone_name].location = \ + bind_rotation.inverted().to_matrix().to_4x4() @ final_location + + # Do the same for rotation + obj.pose.bones[pynode.blender_bone_name].rotation_quaternion = \ + (bind_rotation.to_matrix().to_4x4().inverted() @ parent_mat @ + rotation.to_matrix().to_4x4()).to_quaternion() + obj.pose.bones[pynode.blender_bone_name].scale = \ + (bind_scale.inverted() @ parent_mat @ scale_to_matrix(scale)).to_scale() + + else: + obj.pose.bones[pynode.blender_bone_name].location = bind_location.inverted() @ location + obj.pose.bones[pynode.blender_bone_name].rotation_quaternion = bind_rotation.inverted() @ rotation + obj.pose.bones[pynode.blender_bone_name].scale = bind_scale.inverted() @ scale + + @staticmethod + def create_bone(gltf, skin_id, node_id, parent): + """Bone creation.""" + pyskin = gltf.data.skins[skin_id] + pynode = gltf.data.nodes[node_id] + + scene = bpy.data.scenes[gltf.blender_scene] + obj = bpy.data.objects[pyskin.blender_armature_name] + + bpy.context.window.scene = scene + bpy.context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode="EDIT") + + if pynode.name: + name = pynode.name + else: + name = "Bone_" + str(node_id) + + bone = obj.data.edit_bones.new(name) + pynode.blender_bone_name = bone.name + pynode.blender_armature_name = pyskin.blender_armature_name + bone.tail = Vector((0.0, 1.0, 0.0)) # Needed to keep bone alive + + # set bind and pose transforms + BlenderSkin.set_bone_transforms(gltf, skin_id, bone, node_id, parent) + bpy.ops.object.mode_set(mode="OBJECT") + + @staticmethod + def create_vertex_groups(gltf, skin_id): + """Vertex Group creation.""" + pyskin = gltf.data.skins[skin_id] + for node_id in pyskin.node_ids: + obj = bpy.data.objects[gltf.data.nodes[node_id].blender_object] + for bone in pyskin.joints: + obj.vertex_groups.new(name=gltf.data.nodes[bone].blender_bone_name) + + @staticmethod + def assign_vertex_groups(gltf, skin_id): + """Assign vertex groups to vertices.""" + pyskin = gltf.data.skins[skin_id] + for node_id in pyskin.node_ids: + node = gltf.data.nodes[node_id] + obj = bpy.data.objects[node.blender_object] + + offset = 0 + for prim in gltf.data.meshes[node.mesh].primitives: + idx_already_done = {} + + if 'JOINTS_0' in prim.attributes.keys() and 'WEIGHTS_0' in prim.attributes.keys(): + joint_ = BinaryData.get_data_from_accessor(gltf, prim.attributes['JOINTS_0']) + weight_ = BinaryData.get_data_from_accessor(gltf, prim.attributes['WEIGHTS_0']) + + for poly in obj.data.polygons: + for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total): + vert_idx = obj.data.loops[loop_idx].vertex_index + + if vert_idx in idx_already_done.keys(): + continue + idx_already_done[vert_idx] = True + + if vert_idx in range(offset, offset + prim.vertices_length): + + tab_index = vert_idx - offset + cpt = 0 + for joint_idx in joint_[tab_index]: + weight_val = weight_[tab_index][cpt] + if weight_val != 0.0: # It can be a problem to assign weights of 0 + # for bone index 0, if there is always 4 indices in joint_ + # tuple + group = obj.vertex_groups[gltf.data.nodes[ + pyskin.joints[joint_idx] + ].blender_bone_name] + group.add([vert_idx], weight_val, 'REPLACE') + cpt += 1 + else: + gltf.log.error("No Skinning ?????") # TODO + + offset = offset + prim.vertices_length + + @staticmethod + def create_armature_modifiers(gltf, skin_id): + """Create Armature modifier.""" + pyskin = gltf.data.skins[skin_id] + + if pyskin.blender_armature_name is None: + # TODO seems something is wrong + # For example, some joints are in skin 0, and are in another skin too + # Not sure this is glTF compliant, will check it + return + + for node_id in pyskin.node_ids: + node = gltf.data.nodes[node_id] + obj = bpy.data.objects[node.blender_object] + + for obj_sel in bpy.context.scene.objects: + obj_sel.select_set(False) + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + # bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') + # Reparent skinned mesh to it's armature to avoid breaking + # skinning with interleaved transforms + obj.parent = bpy.data.objects[pyskin.blender_armature_name] + arma = obj.modifiers.new(name="Armature", type="ARMATURE") + arma.object = bpy.data.objects[pyskin.blender_armature_name] + diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_texture.py b/io_scene_gltf2/blender/imp/gltf2_blender_texture.py new file mode 100755 index 00000000..c8983d9c --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_texture.py @@ -0,0 +1,39 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .gltf2_blender_image import BlenderImage + + +class BlenderTextureInfo(): + """Blender Texture info.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, pytextureinfo_idx): + """Create Texture info.""" + BlenderTexture.create(gltf, pytextureinfo_idx) + + +class BlenderTexture(): + """Blender Texture.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def create(gltf, pytexture_idx): + """Create texture.""" + pytexture = gltf.data.textures[pytexture_idx] + BlenderImage.create(gltf, pytexture.source) + diff --git a/io_scene_gltf2/io/__init__.py b/io_scene_gltf2/io/__init__.py new file mode 100755 index 00000000..10973240 --- /dev/null +++ b/io_scene_gltf2/io/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .imp import * + diff --git a/io_scene_gltf2/io/com/gltf2_io.py b/io_scene_gltf2/io/com/gltf2_io.py new file mode 100755 index 00000000..1332adf6 --- /dev/null +++ b/io_scene_gltf2/io/com/gltf2_io.py @@ -0,0 +1,1200 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# NOTE: Generated from latest glTF 2.0 JSON Scheme specs using quicktype (https://github.com/quicktype/quicktype) +# command used: +# quicktype --src glTF.schema.json --src-lang schema -t gltf --lang python --python-version 3.5 + +# TODO: add __slots__ to all classes by extending the generator + +# TODO: REMOVE traceback import +import sys +import traceback + +from io_scene_gltf2.io.com import gltf2_io_debug + + +def from_int(x): + assert isinstance(x, int) and not isinstance(x, bool) + return x + + +def from_none(x): + assert x is None + return x + + +def from_union(fs, x): + tracebacks = [] + for f in fs: + try: + return f(x) + except AssertionError: + _, _, tb = sys.exc_info() + tracebacks.append(tb) + for tb in tracebacks: + traceback.print_tb(tb) # Fixed format + tb_info = traceback.extract_tb(tb) + for tbi in tb_info: + filename, line, func, text = tbi + gltf2_io_debug.print_console('ERROR', 'An error occurred on line {} in statement {}'.format(line, text)) + assert False + + +def from_dict(f, x): + assert isinstance(x, dict) + return {k: f(v) for (k, v) in x.items()} + + +def to_class(c, x): + assert isinstance(x, c) + return x.to_dict() + + +def from_list(f, x): + assert isinstance(x, list) + return [f(y) for y in x] + + +def from_float(x): + assert isinstance(x, (float, int)) and not isinstance(x, bool) + return float(x) + + +def from_str(x): + assert isinstance(x, str) + return x + + +def from_bool(x): + assert isinstance(x, bool) + return x + + +def to_float(x): + assert isinstance(x, float) + return x + + +class AccessorSparseIndices: + """Index array of size `count` that points to those accessor attributes that deviate from + their initialization value. Indices must strictly increase. + + Indices of those attributes that deviate from their initialization value. + """ + + def __init__(self, buffer_view, byte_offset, component_type, extensions, extras): + self.buffer_view = buffer_view + self.byte_offset = byte_offset + self.component_type = component_type + self.extensions = extensions + self.extras = extras + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + buffer_view = from_int(obj.get("bufferView")) + byte_offset = from_union([from_int, from_none], obj.get("byteOffset")) + component_type = from_int(obj.get("componentType")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + return AccessorSparseIndices(buffer_view, byte_offset, component_type, extensions, extras) + + def to_dict(self): + result = {} + result["bufferView"] = from_int(self.buffer_view) + result["byteOffset"] = from_union([from_int, from_none], self.byte_offset) + result["componentType"] = from_int(self.component_type) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + return result + + +class AccessorSparseValues: + """Array of size `count` times number of components, storing the displaced accessor + attributes pointed by `indices`. Substituted values must have the same `componentType` + and number of components as the base accessor. + + Array of size `accessor.sparse.count` times number of components storing the displaced + accessor attributes pointed by `accessor.sparse.indices`. + """ + + def __init__(self, buffer_view, byte_offset, extensions, extras): + self.buffer_view = buffer_view + self.byte_offset = byte_offset + self.extensions = extensions + self.extras = extras + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + buffer_view = from_int(obj.get("bufferView")) + byte_offset = from_union([from_int, from_none], obj.get("byteOffset")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + return AccessorSparseValues(buffer_view, byte_offset, extensions, extras) + + def to_dict(self): + result = {} + result["bufferView"] = from_int(self.buffer_view) + result["byteOffset"] = from_union([from_int, from_none], self.byte_offset) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + return result + + +class AccessorSparse: + """Sparse storage of attributes that deviate from their initialization value.""" + + def __init__(self, count, extensions, extras, indices, values): + self.count = count + self.extensions = extensions + self.extras = extras + self.indices = indices + self.values = values + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + count = from_int(obj.get("count")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + indices = AccessorSparseIndices.from_dict(obj.get("indices")) + values = AccessorSparseValues.from_dict(obj.get("values")) + return AccessorSparse(count, extensions, extras, indices, values) + + def to_dict(self): + result = {} + result["count"] = from_int(self.count) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["indices"] = to_class(AccessorSparseIndices, self.indices) + result["values"] = to_class(AccessorSparseValues, self.values) + return result + + +class Accessor: + """A typed view into a bufferView. A bufferView contains raw binary data. An accessor + provides a typed view into a bufferView or a subset of a bufferView similar to how + WebGL's `vertexAttribPointer()` defines an attribute in a buffer. + """ + + def __init__(self, buffer_view, byte_offset, component_type, count, extensions, extras, max, min, name, normalized, + sparse, type): + self.buffer_view = buffer_view + self.byte_offset = byte_offset + self.component_type = component_type + self.count = count + self.extensions = extensions + self.extras = extras + self.max = max + self.min = min + self.name = name + self.normalized = normalized + self.sparse = sparse + self.type = type + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + buffer_view = from_union([from_int, from_none], obj.get("bufferView")) + byte_offset = from_union([from_int, from_none], obj.get("byteOffset")) + component_type = from_int(obj.get("componentType")) + count = from_int(obj.get("count")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + max = from_union([lambda x: from_list(from_float, x), from_none], obj.get("max")) + min = from_union([lambda x: from_list(from_float, x), from_none], obj.get("min")) + name = from_union([from_str, from_none], obj.get("name")) + normalized = from_union([from_bool, from_none], obj.get("normalized")) + sparse = from_union([AccessorSparse.from_dict, from_none], obj.get("sparse")) + type = from_str(obj.get("type")) + return Accessor(buffer_view, byte_offset, component_type, count, extensions, extras, max, min, name, normalized, + sparse, type) + + def to_dict(self): + result = {} + result["bufferView"] = from_union([from_int, from_none], self.buffer_view) + result["byteOffset"] = from_union([from_int, from_none], self.byte_offset) + result["componentType"] = from_int(self.component_type) + result["count"] = from_int(self.count) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["max"] = from_union([lambda x: from_list(to_float, x), from_none], self.max) + result["min"] = from_union([lambda x: from_list(to_float, x), from_none], self.min) + result["name"] = from_union([from_str, from_none], self.name) + result["normalized"] = from_union([from_bool, from_none], self.normalized) + result["sparse"] = from_union([lambda x: to_class(AccessorSparse, x), from_none], self.sparse) + result["type"] = from_str(self.type) + return result + + +class AnimationChannelTarget: + """The index of the node and TRS property to target. + + The index of the node and TRS property that an animation channel targets. + """ + + def __init__(self, extensions, extras, node, path): + self.extensions = extensions + self.extras = extras + self.node = node + self.path = path + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + node = from_union([from_int, from_none], obj.get("node")) + path = from_str(obj.get("path")) + return AnimationChannelTarget(extensions, extras, node, path) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["node"] = from_union([from_int, from_none], self.node) + result["path"] = from_str(self.path) + return result + + +class AnimationChannel: + """Targets an animation's sampler at a node's property.""" + + def __init__(self, extensions, extras, sampler, target): + self.extensions = extensions + self.extras = extras + self.sampler = sampler + self.target = target + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + sampler = from_int(obj.get("sampler")) + target = AnimationChannelTarget.from_dict(obj.get("target")) + return AnimationChannel(extensions, extras, sampler, target) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["sampler"] = from_int(self.sampler) + result["target"] = to_class(AnimationChannelTarget, self.target) + return result + + +class AnimationSampler: + """Combines input and output accessors with an interpolation algorithm to define a keyframe + graph (but not its target). + """ + + def __init__(self, extensions, extras, input, interpolation, output): + self.extensions = extensions + self.extras = extras + self.input = input + self.interpolation = interpolation + self.output = output + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + input = from_int(obj.get("input")) + interpolation = from_union([from_str, from_none], obj.get("interpolation")) + output = from_int(obj.get("output")) + return AnimationSampler(extensions, extras, input, interpolation, output) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["input"] = from_int(self.input) + result["interpolation"] = from_union([from_str, from_none], self.interpolation) + result["output"] = from_int(self.output) + return result + + +class Animation: + """A keyframe animation.""" + + def __init__(self, channels, extensions, extras, name, samplers): + self.channels = channels + self.extensions = extensions + self.extras = extras + self.name = name + self.samplers = samplers + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + channels = from_list(AnimationChannel.from_dict, obj.get("channels")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + name = from_union([from_str, from_none], obj.get("name")) + samplers = from_list(AnimationSampler.from_dict, obj.get("samplers")) + return Animation(channels, extensions, extras, name, samplers) + + def to_dict(self): + result = {} + result["channels"] = from_list(lambda x: to_class(AnimationChannel, x), self.channels) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["name"] = from_union([from_str, from_none], self.name) + result["samplers"] = from_list(lambda x: to_class(AnimationSampler, x), self.samplers) + return result + + +class Asset: + """Metadata about the glTF asset.""" + + def __init__(self, copyright, extensions, extras, generator, min_version, version): + self.copyright = copyright + self.extensions = extensions + self.extras = extras + self.generator = generator + self.min_version = min_version + self.version = version + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + copyright = from_union([from_str, from_none], obj.get("copyright")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + generator = from_union([from_str, from_none], obj.get("generator")) + min_version = from_union([from_str, from_none], obj.get("minVersion")) + version = from_str(obj.get("version")) + return Asset(copyright, extensions, extras, generator, min_version, version) + + def to_dict(self): + result = {} + result["copyright"] = from_union([from_str, from_none], self.copyright) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["generator"] = from_union([from_str, from_none], self.generator) + result["minVersion"] = from_union([from_str, from_none], self.min_version) + result["version"] = from_str(self.version) + return result + + +class BufferView: + """A view into a buffer generally representing a subset of the buffer.""" + + def __init__(self, buffer, byte_length, byte_offset, byte_stride, extensions, extras, name, target): + self.buffer = buffer + self.byte_length = byte_length + self.byte_offset = byte_offset + self.byte_stride = byte_stride + self.extensions = extensions + self.extras = extras + self.name = name + self.target = target + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + buffer = from_int(obj.get("buffer")) + byte_length = from_int(obj.get("byteLength")) + byte_offset = from_union([from_int, from_none], obj.get("byteOffset")) + byte_stride = from_union([from_int, from_none], obj.get("byteStride")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + name = from_union([from_str, from_none], obj.get("name")) + target = from_union([from_int, from_none], obj.get("target")) + return BufferView(buffer, byte_length, byte_offset, byte_stride, extensions, extras, name, target) + + def to_dict(self): + result = {} + result["buffer"] = from_int(self.buffer) + result["byteLength"] = from_int(self.byte_length) + result["byteOffset"] = from_union([from_int, from_none], self.byte_offset) + result["byteStride"] = from_union([from_int, from_none], self.byte_stride) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["name"] = from_union([from_str, from_none], self.name) + result["target"] = from_union([from_int, from_none], self.target) + return result + + +class Buffer: + """A buffer points to binary geometry, animation, or skins.""" + + def __init__(self, byte_length, extensions, extras, name, uri): + self.byte_length = byte_length + self.extensions = extensions + self.extras = extras + self.name = name + self.uri = uri + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + byte_length = from_int(obj.get("byteLength")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + name = from_union([from_str, from_none], obj.get("name")) + uri = from_union([from_str, from_none], obj.get("uri")) + return Buffer(byte_length, extensions, extras, name, uri) + + def to_dict(self): + result = {} + result["byteLength"] = from_int(self.byte_length) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["name"] = from_union([from_str, from_none], self.name) + result["uri"] = from_union([from_str, from_none], self.uri) + return result + + +class CameraOrthographic: + """An orthographic camera containing properties to create an orthographic projection matrix.""" + + def __init__(self, extensions, extras, xmag, ymag, zfar, znear): + self.extensions = extensions + self.extras = extras + self.xmag = xmag + self.ymag = ymag + self.zfar = zfar + self.znear = znear + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + xmag = from_float(obj.get("xmag")) + ymag = from_float(obj.get("ymag")) + zfar = from_float(obj.get("zfar")) + znear = from_float(obj.get("znear")) + return CameraOrthographic(extensions, extras, xmag, ymag, zfar, znear) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["xmag"] = to_float(self.xmag) + result["ymag"] = to_float(self.ymag) + result["zfar"] = to_float(self.zfar) + result["znear"] = to_float(self.znear) + return result + + +class CameraPerspective: + """A perspective camera containing properties to create a perspective projection matrix.""" + + def __init__(self, aspect_ratio, extensions, extras, yfov, zfar, znear): + self.aspect_ratio = aspect_ratio + self.extensions = extensions + self.extras = extras + self.yfov = yfov + self.zfar = zfar + self.znear = znear + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + aspect_ratio = from_union([from_float, from_none], obj.get("aspectRatio")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + yfov = from_float(obj.get("yfov")) + zfar = from_union([from_float, from_none], obj.get("zfar")) + znear = from_float(obj.get("znear")) + return CameraPerspective(aspect_ratio, extensions, extras, yfov, zfar, znear) + + def to_dict(self): + result = {} + result["aspectRatio"] = from_union([to_float, from_none], self.aspect_ratio) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["yfov"] = to_float(self.yfov) + result["zfar"] = from_union([to_float, from_none], self.zfar) + result["znear"] = to_float(self.znear) + return result + + +class Camera: + """A camera's projection. A node can reference a camera to apply a transform to place the + camera in the scene. + """ + + def __init__(self, extensions, extras, name, orthographic, perspective, type): + self.extensions = extensions + self.extras = extras + self.name = name + self.orthographic = orthographic + self.perspective = perspective + self.type = type + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + name = from_union([from_str, from_none], obj.get("name")) + orthographic = from_union([CameraOrthographic.from_dict, from_none], obj.get("orthographic")) + perspective = from_union([CameraPerspective.from_dict, from_none], obj.get("perspective")) + type = from_str(obj.get("type")) + return Camera(extensions, extras, name, orthographic, perspective, type) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["name"] = from_union([from_str, from_none], self.name) + result["orthographic"] = from_union([lambda x: to_class(CameraOrthographic, x), from_none], self.orthographic) + result["perspective"] = from_union([lambda x: to_class(CameraPerspective, x), from_none], self.perspective) + result["type"] = from_str(self.type) + return result + + +class Image: + """Image data used to create a texture. Image can be referenced by URI or `bufferView` + index. `mimeType` is required in the latter case. + """ + + def __init__(self, buffer_view, extensions, extras, mime_type, name, uri): + self.buffer_view = buffer_view + self.extensions = extensions + self.extras = extras + self.mime_type = mime_type + self.name = name + self.uri = uri + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + buffer_view = from_union([from_int, from_none], obj.get("bufferView")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + name = from_union([from_str, from_none], obj.get("name")) + uri = from_union([from_str, from_none], obj.get("uri")) + return Image(buffer_view, extensions, extras, mime_type, name, uri) + + def to_dict(self): + result = {} + result["bufferView"] = from_union([from_int, from_none], self.buffer_view) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + result["name"] = from_union([from_str, from_none], self.name) + result["uri"] = from_union([from_str, from_none], self.uri) + return result + + +class TextureInfo: + """The emissive map texture. + + The base color texture. + + The metallic-roughness texture. + + Reference to a texture. + """ + + def __init__(self, extensions, extras, index, tex_coord): + self.extensions = extensions + self.extras = extras + self.index = index + self.tex_coord = tex_coord + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + index = from_int(obj.get("index")) + tex_coord = from_union([from_int, from_none], obj.get("texCoord")) + return TextureInfo(extensions, extras, index, tex_coord) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["index"] = from_int(self.index) + result["texCoord"] = from_union([from_int, from_none], self.tex_coord) + return result + + +class MaterialNormalTextureInfoClass: + """The normal map texture. + + Reference to a texture. + """ + + def __init__(self, extensions, extras, index, scale, tex_coord): + self.extensions = extensions + self.extras = extras + self.index = index + self.scale = scale + self.tex_coord = tex_coord + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + index = from_int(obj.get("index")) + scale = from_union([from_float, from_none], obj.get("scale")) + tex_coord = from_union([from_int, from_none], obj.get("texCoord")) + return MaterialNormalTextureInfoClass(extensions, extras, index, scale, tex_coord) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["index"] = from_int(self.index) + result["scale"] = from_union([to_float, from_none], self.scale) + result["texCoord"] = from_union([from_int, from_none], self.tex_coord) + return result + + +class MaterialOcclusionTextureInfoClass: + """The occlusion map texture. + + Reference to a texture. + """ + + def __init__(self, extensions, extras, index, strength, tex_coord): + self.extensions = extensions + self.extras = extras + self.index = index + self.strength = strength + self.tex_coord = tex_coord + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + index = from_int(obj.get("index")) + strength = from_union([from_float, from_none], obj.get("strength")) + tex_coord = from_union([from_int, from_none], obj.get("texCoord")) + return MaterialOcclusionTextureInfoClass(extensions, extras, index, strength, tex_coord) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["index"] = from_int(self.index) + result["strength"] = from_union([to_float, from_none], self.strength) + result["texCoord"] = from_union([from_int, from_none], self.tex_coord) + return result + + +class MaterialPBRMetallicRoughness: + """A set of parameter values that are used to define the metallic-roughness material model + from Physically-Based Rendering (PBR) methodology. When not specified, all the default + values of `pbrMetallicRoughness` apply. + + A set of parameter values that are used to define the metallic-roughness material model + from Physically-Based Rendering (PBR) methodology. + """ + + def __init__(self, base_color_factor, base_color_texture, extensions, extras, metallic_factor, + metallic_roughness_texture, roughness_factor): + self.base_color_factor = base_color_factor + self.base_color_texture = base_color_texture + self.extensions = extensions + self.extras = extras + self.metallic_factor = metallic_factor + self.metallic_roughness_texture = metallic_roughness_texture + self.roughness_factor = roughness_factor + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + base_color_factor = from_union([lambda x: from_list(from_float, x), from_none], obj.get("baseColorFactor")) + base_color_texture = from_union([TextureInfo.from_dict, from_none], obj.get("baseColorTexture")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + metallic_factor = from_union([from_float, from_none], obj.get("metallicFactor")) + metallic_roughness_texture = from_union([TextureInfo.from_dict, from_none], obj.get("metallicRoughnessTexture")) + roughness_factor = from_union([from_float, from_none], obj.get("roughnessFactor")) + return MaterialPBRMetallicRoughness(base_color_factor, base_color_texture, extensions, extras, metallic_factor, + metallic_roughness_texture, roughness_factor) + + def to_dict(self): + result = {} + result["baseColorFactor"] = from_union([lambda x: from_list(to_float, x), from_none], self.base_color_factor) + result["baseColorTexture"] = from_union([lambda x: to_class(TextureInfo, x), from_none], + self.base_color_texture) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["metallicFactor"] = from_union([to_float, from_none], self.metallic_factor) + result["metallicRoughnessTexture"] = from_union([lambda x: to_class(TextureInfo, x), from_none], + self.metallic_roughness_texture) + result["roughnessFactor"] = from_union([to_float, from_none], self.roughness_factor) + return result + + +class Material: + """The material appearance of a primitive.""" + + def __init__(self, alpha_cutoff, alpha_mode, double_sided, emissive_factor, emissive_texture, extensions, extras, + name, normal_texture, occlusion_texture, pbr_metallic_roughness): + self.alpha_cutoff = alpha_cutoff + self.alpha_mode = alpha_mode + self.double_sided = double_sided + self.emissive_factor = emissive_factor + self.emissive_texture = emissive_texture + self.extensions = extensions + self.extras = extras + self.name = name + self.normal_texture = normal_texture + self.occlusion_texture = occlusion_texture + self.pbr_metallic_roughness = pbr_metallic_roughness + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + alpha_cutoff = from_union([from_float, from_none], obj.get("alphaCutoff")) + alpha_mode = from_union([from_str, from_none], obj.get("alphaMode")) + double_sided = from_union([from_bool, from_none], obj.get("doubleSided")) + emissive_factor = from_union([lambda x: from_list(from_float, x), from_none], obj.get("emissiveFactor")) + emissive_texture = from_union([TextureInfo.from_dict, from_none], obj.get("emissiveTexture")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + name = from_union([from_str, from_none], obj.get("name")) + normal_texture = from_union([MaterialNormalTextureInfoClass.from_dict, from_none], obj.get("normalTexture")) + occlusion_texture = from_union([MaterialOcclusionTextureInfoClass.from_dict, from_none], + obj.get("occlusionTexture")) + pbr_metallic_roughness = from_union([MaterialPBRMetallicRoughness.from_dict, from_none], + obj.get("pbrMetallicRoughness")) + return Material(alpha_cutoff, alpha_mode, double_sided, emissive_factor, emissive_texture, extensions, extras, + name, normal_texture, occlusion_texture, pbr_metallic_roughness) + + def to_dict(self): + result = {} + result["alphaCutoff"] = from_union([to_float, from_none], self.alpha_cutoff) + result["alphaMode"] = from_union([from_str, from_none], self.alpha_mode) + result["doubleSided"] = from_union([from_bool, from_none], self.double_sided) + result["emissiveFactor"] = from_union([lambda x: from_list(to_float, x), from_none], self.emissive_factor) + result["emissiveTexture"] = from_union([lambda x: to_class(TextureInfo, x), from_none], self.emissive_texture) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["name"] = from_union([from_str, from_none], self.name) + result["normalTexture"] = from_union([lambda x: to_class(MaterialNormalTextureInfoClass, x), from_none], + self.normal_texture) + result["occlusionTexture"] = from_union([lambda x: to_class(MaterialOcclusionTextureInfoClass, x), from_none], + self.occlusion_texture) + result["pbrMetallicRoughness"] = from_union([lambda x: to_class(MaterialPBRMetallicRoughness, x), from_none], + self.pbr_metallic_roughness) + return result + + +class MeshPrimitive: + """Geometry to be rendered with the given material.""" + + def __init__(self, attributes, extensions, extras, indices, material, mode, targets): + self.attributes = attributes + self.extensions = extensions + self.extras = extras + self.indices = indices + self.material = material + self.mode = mode + self.targets = targets + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + attributes = from_dict(from_int, obj.get("attributes")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + indices = from_union([from_int, from_none], obj.get("indices")) + material = from_union([from_int, from_none], obj.get("material")) + mode = from_union([from_int, from_none], obj.get("mode")) + targets = from_union([lambda x: from_list(lambda x: from_dict(from_int, x), x), from_none], obj.get("targets")) + return MeshPrimitive(attributes, extensions, extras, indices, material, mode, targets) + + def to_dict(self): + result = {} + result["attributes"] = from_dict(from_int, self.attributes) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["indices"] = from_union([from_int, from_none], self.indices) + result["material"] = from_union([from_int, from_none], self.material) + result["mode"] = from_union([from_int, from_none], self.mode) + result["targets"] = from_union([lambda x: from_list(lambda x: from_dict(from_int, x), x), from_none], + self.targets) + return result + + +class Mesh: + """A set of primitives to be rendered. A node can contain one mesh. A node's transform + places the mesh in the scene. + """ + + def __init__(self, extensions, extras, name, primitives, weights): + self.extensions = extensions + self.extras = extras + self.name = name + self.primitives = primitives + self.weights = weights + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + name = from_union([from_str, from_none], obj.get("name")) + primitives = from_list(MeshPrimitive.from_dict, obj.get("primitives")) + weights = from_union([lambda x: from_list(from_float, x), from_none], obj.get("weights")) + return Mesh(extensions, extras, name, primitives, weights) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["name"] = from_union([from_str, from_none], self.name) + result["primitives"] = from_list(lambda x: to_class(MeshPrimitive, x), self.primitives) + result["weights"] = from_union([lambda x: from_list(to_float, x), from_none], self.weights) + return result + + +class Node: + """A node in the node hierarchy. When the node contains `skin`, all `mesh.primitives` must + contain `JOINTS_0` and `WEIGHTS_0` attributes. A node can have either a `matrix` or any + combination of `translation`/`rotation`/`scale` (TRS) properties. TRS properties are + converted to matrices and postmultiplied in the `T * R * S` order to compose the + transformation matrix; first the scale is applied to the vertices, then the rotation, and + then the translation. If none are provided, the transform is the identity. When a node is + targeted for animation (referenced by an animation.channel.target), only TRS properties + may be present; `matrix` will not be present. + """ + + def __init__(self, camera, children, extensions, extras, matrix, mesh, name, rotation, scale, skin, translation, + weights): + self.camera = camera + self.children = children + self.extensions = extensions + self.extras = extras + self.matrix = matrix + self.mesh = mesh + self.name = name + self.rotation = rotation + self.scale = scale + self.skin = skin + self.translation = translation + self.weights = weights + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + camera = from_union([from_int, from_none], obj.get("camera")) + children = from_union([lambda x: from_list(from_int, x), from_none], obj.get("children")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + matrix = from_union([lambda x: from_list(from_float, x), from_none], obj.get("matrix")) + mesh = from_union([from_int, from_none], obj.get("mesh")) + name = from_union([from_str, from_none], obj.get("name")) + rotation = from_union([lambda x: from_list(from_float, x), from_none], obj.get("rotation")) + scale = from_union([lambda x: from_list(from_float, x), from_none], obj.get("scale")) + skin = from_union([from_int, from_none], obj.get("skin")) + translation = from_union([lambda x: from_list(from_float, x), from_none], obj.get("translation")) + weights = from_union([lambda x: from_list(from_float, x), from_none], obj.get("weights")) + return Node(camera, children, extensions, extras, matrix, mesh, name, rotation, scale, skin, translation, + weights) + + def to_dict(self): + result = {} + result["camera"] = from_union([from_int, from_none], self.camera) + result["children"] = from_union([lambda x: from_list(from_int, x), from_none], self.children) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["matrix"] = from_union([lambda x: from_list(to_float, x), from_none], self.matrix) + result["mesh"] = from_union([from_int, from_none], self.mesh) + result["name"] = from_union([from_str, from_none], self.name) + result["rotation"] = from_union([lambda x: from_list(to_float, x), from_none], self.rotation) + result["scale"] = from_union([lambda x: from_list(to_float, x), from_none], self.scale) + result["skin"] = from_union([from_int, from_none], self.skin) + result["translation"] = from_union([lambda x: from_list(to_float, x), from_none], self.translation) + result["weights"] = from_union([lambda x: from_list(to_float, x), from_none], self.weights) + return result + + +class Sampler: + """Texture sampler properties for filtering and wrapping modes.""" + + def __init__(self, extensions, extras, mag_filter, min_filter, name, wrap_s, wrap_t): + self.extensions = extensions + self.extras = extras + self.mag_filter = mag_filter + self.min_filter = min_filter + self.name = name + self.wrap_s = wrap_s + self.wrap_t = wrap_t + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + mag_filter = from_union([from_int, from_none], obj.get("magFilter")) + min_filter = from_union([from_int, from_none], obj.get("minFilter")) + name = from_union([from_str, from_none], obj.get("name")) + wrap_s = from_union([from_int, from_none], obj.get("wrapS")) + wrap_t = from_union([from_int, from_none], obj.get("wrapT")) + return Sampler(extensions, extras, mag_filter, min_filter, name, wrap_s, wrap_t) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["magFilter"] = from_union([from_int, from_none], self.mag_filter) + result["minFilter"] = from_union([from_int, from_none], self.min_filter) + result["name"] = from_union([from_str, from_none], self.name) + result["wrapS"] = from_union([from_int, from_none], self.wrap_s) + result["wrapT"] = from_union([from_int, from_none], self.wrap_t) + return result + + +class Scene: + """The root nodes of a scene.""" + + def __init__(self, extensions, extras, name, nodes): + self.extensions = extensions + self.extras = extras + self.name = name + self.nodes = nodes + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + name = from_union([from_str, from_none], obj.get("name")) + nodes = from_union([lambda x: from_list(from_int, x), from_none], obj.get("nodes")) + return Scene(extensions, extras, name, nodes) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["name"] = from_union([from_str, from_none], self.name) + result["nodes"] = from_union([lambda x: from_list(from_int, x), from_none], self.nodes) + return result + + +class Skin: + """Joints and matrices defining a skin.""" + + def __init__(self, extensions, extras, inverse_bind_matrices, joints, name, skeleton): + self.extensions = extensions + self.extras = extras + self.inverse_bind_matrices = inverse_bind_matrices + self.joints = joints + self.name = name + self.skeleton = skeleton + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + inverse_bind_matrices = from_union([from_int, from_none], obj.get("inverseBindMatrices")) + joints = from_list(from_int, obj.get("joints")) + name = from_union([from_str, from_none], obj.get("name")) + skeleton = from_union([from_int, from_none], obj.get("skeleton")) + return Skin(extensions, extras, inverse_bind_matrices, joints, name, skeleton) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["inverseBindMatrices"] = from_union([from_int, from_none], self.inverse_bind_matrices) + result["joints"] = from_list(from_int, self.joints) + result["name"] = from_union([from_str, from_none], self.name) + result["skeleton"] = from_union([from_int, from_none], self.skeleton) + return result + + +class Texture: + """A texture and its sampler.""" + + def __init__(self, extensions, extras, name, sampler, source): + self.extensions = extensions + self.extras = extras + self.name = name + self.sampler = sampler + self.source = source + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extras = obj.get("extras") + name = from_union([from_str, from_none], obj.get("name")) + sampler = from_union([from_int, from_none], obj.get("sampler")) + source = from_int(obj.get("source")) + return Texture(extensions, extras, name, sampler, source) + + def to_dict(self): + result = {} + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extras"] = self.extras + result["name"] = from_union([from_str, from_none], self.name) + result["sampler"] = from_union([from_int, from_none], self.sampler) + result["source"] = from_int(self.source) + return result + + +class Gltf: + """The root object for a glTF asset.""" + + def __init__(self, accessors, animations, asset, buffers, buffer_views, cameras, extensions, extensions_required, + extensions_used, extras, images, materials, meshes, nodes, samplers, scene, scenes, skins, textures): + self.accessors = accessors + self.animations = animations + self.asset = asset + self.buffers = buffers + self.buffer_views = buffer_views + self.cameras = cameras + self.extensions = extensions + self.extensions_required = extensions_required + self.extensions_used = extensions_used + self.extras = extras + self.images = images + self.materials = materials + self.meshes = meshes + self.nodes = nodes + self.samplers = samplers + self.scene = scene + self.scenes = scenes + self.skins = skins + self.textures = textures + + @staticmethod + def from_dict(obj): + assert isinstance(obj, dict) + accessors = from_union([lambda x: from_list(Accessor.from_dict, x), from_none], obj.get("accessors")) + animations = from_union([lambda x: from_list(Animation.from_dict, x), from_none], obj.get("animations")) + asset = Asset.from_dict(obj.get("asset")) + buffers = from_union([lambda x: from_list(Buffer.from_dict, x), from_none], obj.get("buffers")) + buffer_views = from_union([lambda x: from_list(BufferView.from_dict, x), from_none], obj.get("bufferViews")) + cameras = from_union([lambda x: from_list(Camera.from_dict, x), from_none], obj.get("cameras")) + extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + obj.get("extensions")) + extensions_required = from_union([lambda x: from_list(from_str, x), from_none], obj.get("extensionsRequired")) + extensions_used = from_union([lambda x: from_list(from_str, x), from_none], obj.get("extensionsUsed")) + extras = obj.get("extras") + images = from_union([lambda x: from_list(Image.from_dict, x), from_none], obj.get("images")) + materials = from_union([lambda x: from_list(Material.from_dict, x), from_none], obj.get("materials")) + meshes = from_union([lambda x: from_list(Mesh.from_dict, x), from_none], obj.get("meshes")) + nodes = from_union([lambda x: from_list(Node.from_dict, x), from_none], obj.get("nodes")) + samplers = from_union([lambda x: from_list(Sampler.from_dict, x), from_none], obj.get("samplers")) + scene = from_union([from_int, from_none], obj.get("scene")) + scenes = from_union([lambda x: from_list(Scene.from_dict, x), from_none], obj.get("scenes")) + skins = from_union([lambda x: from_list(Skin.from_dict, x), from_none], obj.get("skins")) + textures = from_union([lambda x: from_list(Texture.from_dict, x), from_none], obj.get("textures")) + return Gltf(accessors, animations, asset, buffers, buffer_views, cameras, extensions, extensions_required, + extensions_used, extras, images, materials, meshes, nodes, samplers, scene, scenes, skins, textures) + + def to_dict(self): + result = {} + result["accessors"] = from_union([lambda x: from_list(lambda x: to_class(Accessor, x), x), from_none], + self.accessors) + result["animations"] = from_union([lambda x: from_list(lambda x: to_class(Animation, x), x), from_none], + self.animations) + result["asset"] = to_class(Asset, self.asset) + result["buffers"] = from_union([lambda x: from_list(lambda x: to_class(Buffer, x), x), from_none], self.buffers) + result["bufferViews"] = from_union([lambda x: from_list(lambda x: to_class(BufferView, x), x), from_none], + self.buffer_views) + result["cameras"] = from_union([lambda x: from_list(lambda x: to_class(Camera, x), x), from_none], self.cameras) + result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], + self.extensions) + result["extensionsRequired"] = from_union([lambda x: from_list(from_str, x), from_none], + self.extensions_required) + result["extensionsUsed"] = from_union([lambda x: from_list(from_str, x), from_none], self.extensions_used) + result["extras"] = self.extras + result["images"] = from_union([lambda x: from_list(lambda x: to_class(Image, x), x), from_none], self.images) + result["materials"] = from_union([lambda x: from_list(lambda x: to_class(Material, x), x), from_none], + self.materials) + result["meshes"] = from_union([lambda x: from_list(lambda x: to_class(Mesh, x), x), from_none], self.meshes) + result["nodes"] = from_union([lambda x: from_list(lambda x: to_class(Node, x), x), from_none], self.nodes) + result["samplers"] = from_union([lambda x: from_list(lambda x: to_class(Sampler, x), x), from_none], + self.samplers) + result["scene"] = from_union([from_int, from_none], self.scene) + result["scenes"] = from_union([lambda x: from_list(lambda x: to_class(Scene, x), x), from_none], self.scenes) + result["skins"] = from_union([lambda x: from_list(lambda x: to_class(Skin, x), x), from_none], self.skins) + result["textures"] = from_union([lambda x: from_list(lambda x: to_class(Texture, x), x), from_none], + self.textures) + return result + + +def gltf_from_dict(s): + return Gltf.from_dict(s) + + +def gltf_to_dict(x): + return to_class(Gltf, x) + diff --git a/io_scene_gltf2/io/com/gltf2_io_constants.py b/io_scene_gltf2/io/com/gltf2_io_constants.py new file mode 100755 index 00000000..c97908cd --- /dev/null +++ b/io_scene_gltf2/io/com/gltf2_io_constants.py @@ -0,0 +1,132 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import IntEnum + + +class ComponentType(IntEnum): + Byte = 5120 + UnsignedByte = 5121 + Short = 5122 + UnsignedShort = 5123 + UnsignedInt = 5125 + Float = 5126 + + @classmethod + def to_type_code(cls, component_type): + return { + ComponentType.Byte: 'b', + ComponentType.UnsignedByte: 'B', + ComponentType.Short: 'h', + ComponentType.UnsignedShort: 'H', + ComponentType.UnsignedInt: 'I', + ComponentType.Float: 'f' + }[component_type] + + @classmethod + def from_legacy_define(cls, type_define): + return { + GLTF_COMPONENT_TYPE_BYTE: ComponentType.Byte, + GLTF_COMPONENT_TYPE_UNSIGNED_BYTE: ComponentType.UnsignedByte, + GLTF_COMPONENT_TYPE_SHORT: ComponentType.Short, + GLTF_COMPONENT_TYPE_UNSIGNED_SHORT: ComponentType.UnsignedShort, + GLTF_COMPONENT_TYPE_UNSIGNED_INT: ComponentType.UnsignedInt, + GLTF_COMPONENT_TYPE_FLOAT: ComponentType.Float + }[type_define] + + @classmethod + def get_size(cls, component_type): + return { + ComponentType.Byte: 1, + ComponentType.UnsignedByte: 1, + ComponentType.Short: 2, + ComponentType.UnsignedShort: 2, + ComponentType.UnsignedInt: 4, + ComponentType.Float: 4 + }[component_type] + + +class DataType: + Scalar = "SCALAR" + Vec2 = "VEC2" + Vec3 = "VEC3" + Vec4 = "VEC4" + Mat2 = "MAT2" + Mat3 = "MAT3" + Mat4 = "MAT4" + + def __new__(cls, *args, **kwargs): + raise RuntimeError("{} should not be instantiated".format(cls.__name__)) + + @classmethod + def num_elements(cls, data_type): + return { + DataType.Scalar: 1, + DataType.Vec2: 2, + DataType.Vec3: 3, + DataType.Vec4: 4, + DataType.Mat2: 4, + DataType.Mat3: 9, + DataType.Mat4: 16 + }[data_type] + + @classmethod + def vec_type_from_num(cls, num_elems): + if not (0 < num_elems < 5): + raise ValueError("No vector type with {} elements".format(num_elems)) + return { + 1: DataType.Scalar, + 2: DataType.Vec2, + 3: DataType.Vec3, + 4: DataType.Vec4 + }[num_elems] + + @classmethod + def mat_type_from_num(cls, num_elems): + if not (4 <= num_elems <= 16): + raise ValueError("No matrix type with {} elements".format(num_elems)) + return { + 4: DataType.Mat2, + 9: DataType.Mat3, + 16: DataType.Mat4 + }[num_elems] + + +################# +# LEGACY DEFINES + +GLTF_VERSION = "2.0" + +# +# Component Types +# +GLTF_COMPONENT_TYPE_BYTE = "BYTE" +GLTF_COMPONENT_TYPE_UNSIGNED_BYTE = "UNSIGNED_BYTE" +GLTF_COMPONENT_TYPE_SHORT = "SHORT" +GLTF_COMPONENT_TYPE_UNSIGNED_SHORT = "UNSIGNED_SHORT" +GLTF_COMPONENT_TYPE_UNSIGNED_INT = "UNSIGNED_INT" +GLTF_COMPONENT_TYPE_FLOAT = "FLOAT" + + +# +# Data types +# +GLTF_DATA_TYPE_SCALAR = "SCALAR" +GLTF_DATA_TYPE_VEC2 = "VEC2" +GLTF_DATA_TYPE_VEC3 = "VEC3" +GLTF_DATA_TYPE_VEC4 = "VEC4" +GLTF_DATA_TYPE_MAT2 = "MAT2" +GLTF_DATA_TYPE_MAT3 = "MAT3" +GLTF_DATA_TYPE_MAT4 = "MAT4" + diff --git a/io_scene_gltf2/io/com/gltf2_io_debug.py b/io_scene_gltf2/io/com/gltf2_io_debug.py new file mode 100755 index 00000000..a7df8fed --- /dev/null +++ b/io_scene_gltf2/io/com/gltf2_io_debug.py @@ -0,0 +1,138 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Imports +# + +import time +import logging + +# +# Globals +# + +OUTPUT_LEVELS = ['ERROR', 'WARNING', 'INFO', 'PROFILE', 'DEBUG', 'VERBOSE'] + +g_current_output_level = 'DEBUG' +g_profile_started = False +g_profile_start = 0.0 +g_profile_end = 0.0 +g_profile_delta = 0.0 + +# +# Functions +# + + +def set_output_level(level): + """Set an output debug level.""" + global g_current_output_level + + if OUTPUT_LEVELS.index(level) < 0: + return + + g_current_output_level = level + + +def print_console(level, output): + """Print to Blender console with a given header and output.""" + global OUTPUT_LEVELS + global g_current_output_level + + if OUTPUT_LEVELS.index(level) > OUTPUT_LEVELS.index(g_current_output_level): + return + + print(level + ': ' + output) + + +def print_newline(): + """Print a new line to Blender console.""" + print() + + +def print_timestamp(label=None): + """Print a timestamp to Blender console.""" + output = 'Timestamp: ' + str(time.time()) + + if label is not None: + output = output + ' (' + label + ')' + + print_console('PROFILE', output) + + +def profile_start(): + """Start profiling by storing the current time.""" + global g_profile_start + global g_profile_started + + if g_profile_started: + print_console('ERROR', 'Profiling already started') + return + + g_profile_started = True + + g_profile_start = time.time() + + +def profile_end(label=None): + """Stop profiling and printing out the delta time since profile start.""" + global g_profile_end + global g_profile_delta + global g_profile_started + + if not g_profile_started: + print_console('ERROR', 'Profiling not started') + return + + g_profile_started = False + + g_profile_end = time.time() + g_profile_delta = g_profile_end - g_profile_start + + output = 'Delta time: ' + str(g_profile_delta) + + if label is not None: + output = output + ' (' + label + ')' + + print_console('PROFILE', output) + + +# TODO: need to have a unique system for logging importer/exporter +# TODO: this logger is used for importer, but in io and in blender part, but is written here in a _io_ file +class Log: + def __init__(self, loglevel): + self.logger = logging.getLogger('glTFImporter') + self.hdlr = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + self.hdlr.setFormatter(formatter) + self.logger.addHandler(self.hdlr) + self.logger.setLevel(int(loglevel)) + + @staticmethod + def get_levels(): + levels = [ + (str(logging.CRITICAL), "Critical", "", logging.CRITICAL), + (str(logging.ERROR), "Error", "", logging.ERROR), + (str(logging.WARNING), "Warning", "", logging.WARNING), + (str(logging.INFO), "Info", "", logging.INFO), + (str(logging.NOTSET), "NotSet", "", logging.NOTSET) + ] + + return levels + + @staticmethod + def default(): + return str(logging.ERROR) + diff --git a/io_scene_gltf2/io/com/gltf2_io_functional.py b/io_scene_gltf2/io/com/gltf2_io_functional.py new file mode 100755 index 00000000..eb65112f --- /dev/null +++ b/io_scene_gltf2/io/com/gltf2_io_functional.py @@ -0,0 +1,41 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing + + +def chunks(lst: typing.Sequence[typing.Any], n: int) -> typing.List[typing.Any]: + """ + Generator that yields successive n sized chunks of the list l + :param lst: the list to be split + :param n: the length of the chunks + :return: a sublist of at most length n + """ + result = [] + for i in range(0, len(lst), n): + result.append(lst[i:i + n]) + return result + + +def unzip(*args: typing.Iterable[typing.Any]) -> typing.Iterable[typing.Iterable[typing.Any]]: + """ + Unzip the list. Inverse of the builtin zip + :param args: a list of lists or multiple list arguments + :return: a list of unzipped lists + """ + if len(args) == 1: + args = args[0] + + return zip(*args) + diff --git a/io_scene_gltf2/io/com/gltf2_io_image.py b/io_scene_gltf2/io/com/gltf2_io_image.py new file mode 100755 index 00000000..af86daeb --- /dev/null +++ b/io_scene_gltf2/io/com/gltf2_io_image.py @@ -0,0 +1,154 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Imports +# + +import struct +import zlib + + +class Image: + """ + Image object class to represent a 4-channel RGBA image. + + Pixel values are expected to be floating point in the range of [0.0 to 1.0] + """ + + def __init__(self, width, height, pixels): + self.width = width + self.height = height + self.channels = 4 + self.pixels = pixels + self.name = "" + self.file_format = "PNG" + + def to_png_data(self): + buf = bytearray([int(channel * 255.0) for channel in self.pixels]) + + # + # Taken from 'blender-thumbnailer.py' in Blender. + # + + # reverse the vertical line order and add null bytes at the start + width_byte_4 = self.width * 4 + raw_data = b"".join( + b'\x00' + buf[span:span + width_byte_4] for span in range( + (self.height - 1) * self.width * 4, -1, - width_byte_4)) + + def png_pack(png_tag, data): + chunk_head = png_tag + data + return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head)) + + return b"".join([ + b'\x89PNG\r\n\x1a\n', + png_pack(b'IHDR', struct.pack("!2I5B", self.width, self.height, 8, 6, 0, 0, 0)), + png_pack(b'IDAT', zlib.compress(raw_data, 9)), + png_pack(b'IEND', b'')]) + + def to_image_data(self, mime_type): + if mime_type == 'image/png': + return self.to_png_data() + raise ValueError("Unsupported image file type {}".format(mime_type)) + + def save_png(self, dst_path): + data = self.to_png_data() + with open(dst_path, 'wb') as f: + f.write(data) + + +def create_img(width, height, r=0.0, g=0.0, b=0.0, a=1.0): + """ + Create a new image object with 4 channels and initialize it with the given default values. + + (if no arguments are given, these default to R=0, G=0, B=0, A=1.0) + Return the created image object. + """ + return Image(width, height, [r, g, b, a] * (width * height)) + + +def create_img_from_pixels(width, height, pixels): + """ + Create a new image object with 4 channels and initialize it using the given array of pixel data. + + Return the created image object. + """ + if pixels is None or len(pixels) != width * height * 4: + return None + + return Image(width, height, pixels) + + +def copy_img_channel(dst_image, dst_channel, src_image, src_channel): + """ + Copy a single channel (identified by src_channel) from src_image to dst_image (overwriting dst_channel). + + src_image and dst_image are expected to be image objects created using create_img. + Return True on success, False otherwise. + """ + if dst_image is None or src_image is None: + return False + + if dst_channel < 0 or dst_channel >= dst_image.channels or src_channel < 0 or src_channel >= src_image.channels: + return False + + if src_image.width != dst_image.width or \ + src_image.height != dst_image.height or \ + src_image.channels != dst_image.channels: + return False + + for i in range(0, len(dst_image.pixels), dst_image.channels): + dst_image.pixels[i + dst_channel] = src_image.pixels[i + src_channel] + + return True + + +def test_save_img(image, path): + """ + Save the given image to a PNG file (specified by path). + + Return True on success, False otherwise. + """ + if image is None or image.channels != 4: + return False + + width = image.width + height = image.height + + buf = bytearray([int(channel * 255.0) for channel in image.pixels]) + + # + # Taken from 'blender-thumbnailer.py' in Blender. + # + + # reverse the vertical line order and add null bytes at the start + width_byte_4 = width * 4 + raw_data = b"".join( + b'\x00' + buf[span:span + width_byte_4] for span in range((height - 1) * width * 4, -1, - width_byte_4)) + + def png_pack(png_tag, data): + chunk_head = png_tag + data + return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head)) + + data = b"".join([ + b'\x89PNG\r\n\x1a\n', + png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)), + png_pack(b'IDAT', zlib.compress(raw_data, 9)), + png_pack(b'IEND', b'')]) + + with open(path, 'wb') as f: + f.write(data) + return True + diff --git a/io_scene_gltf2/io/com/gltf2_io_trs.py b/io_scene_gltf2/io/com/gltf2_io_trs.py new file mode 100755 index 00000000..59f30830 --- /dev/null +++ b/io_scene_gltf2/io/com/gltf2_io_trs.py @@ -0,0 +1,68 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class TRS: + + def __new__(cls, *args, **kwargs): + raise RuntimeError("{} should not be instantiated".format(cls.__name__)) + + @staticmethod + def scale_to_matrix(scale): + # column major ! + return [scale[0], 0, 0, 0, + 0, scale[1], 0, 0, + 0, 0, scale[2], 0, + 0, 0, 0, 1] + + @staticmethod + def quaternion_to_matrix(q): + x, y, z, w = q + # TODO : is q normalized ? --> if not, multiply by 1/(w*w + x*x + y*y + z*z) + # column major ! + return [ + 1 - 2 * y * y - 2 * z * z, 2 * x * y + 2 * w * z, 2 * x * z - 2 * w * y, 0, + 2 * x * y - 2 * w * z, 1 - 2 * x * x - 2 * z * z, 2 * y * z + 2 * w * x, 0, + 2 * x * z + 2 * y * w, 2 * y * z - 2 * w * x, 1 - 2 * x * x - 2 * y * y, 0, + 0, 0, 0, 1] + + @staticmethod + def matrix_multiply(m, n): + # column major ! + + return [ + m[0] * n[0] + m[4] * n[1] + m[8] * n[2] + m[12] * n[3], + m[1] * n[0] + m[5] * n[1] + m[9] * n[2] + m[13] * n[3], + m[2] * n[0] + m[6] * n[1] + m[10] * n[2] + m[14] * n[3], + m[3] * n[0] + m[7] * n[1] + m[11] * n[2] + m[15] * n[3], + m[0] * n[4] + m[4] * n[5] + m[8] * n[6] + m[12] * n[7], + m[1] * n[4] + m[5] * n[5] + m[9] * n[6] + m[13] * n[7], + m[2] * n[4] + m[6] * n[5] + m[10] * n[6] + m[14] * n[7], + m[3] * n[4] + m[7] * n[5] + m[11] * n[6] + m[15] * n[7], + m[0] * n[8] + m[4] * n[9] + m[8] * n[10] + m[12] * n[11], + m[1] * n[8] + m[5] * n[9] + m[9] * n[10] + m[13] * n[11], + m[2] * n[8] + m[6] * n[9] + m[10] * n[10] + m[14] * n[11], + m[3] * n[8] + m[7] * n[9] + m[11] * n[10] + m[15] * n[11], + m[0] * n[12] + m[4] * n[13] + m[8] * n[14] + m[12] * n[15], + m[1] * n[12] + m[5] * n[13] + m[9] * n[14] + m[13] * n[15], + m[2] * n[12] + m[6] * n[13] + m[10] * n[14] + m[14] * n[15], + m[3] * n[12] + m[7] * n[13] + m[11] * n[14] + m[15] * n[15], + ] + + @staticmethod + def translation_to_matrix(translation): + # column major ! + return [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, + translation[0], translation[1], translation[2], 1.0] + diff --git a/io_scene_gltf2/io/exp/gltf2_io_binary_data.py b/io_scene_gltf2/io/exp/gltf2_io_binary_data.py new file mode 100755 index 00000000..42f6d5d7 --- /dev/null +++ b/io_scene_gltf2/io/exp/gltf2_io_binary_data.py @@ -0,0 +1,36 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing +import array +from io_scene_gltf2.io.com import gltf2_io_constants + + +class BinaryData: + """Store for gltf binary data that can later be stored in a buffer.""" + + def __init__(self, data: bytes): + if not isinstance(data, bytes): + raise TypeError("Data is not a bytes array") + self.data = data + + @classmethod + def from_list(cls, lst: typing.List[typing.Any], gltf_component_type: gltf2_io_constants.ComponentType): + format_char = gltf2_io_constants.ComponentType.to_type_code(gltf_component_type) + return BinaryData(array.array(format_char, lst).tobytes()) + + @property + def byte_length(self): + return len(self.data) + diff --git a/io_scene_gltf2/io/exp/gltf2_io_buffer.py b/io_scene_gltf2/io/exp/gltf2_io_buffer.py new file mode 100755 index 00000000..694be11e --- /dev/null +++ b/io_scene_gltf2/io/exp/gltf2_io_buffer.py @@ -0,0 +1,61 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 + +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.io.exp import gltf2_io_binary_data + + +class Buffer: + """Class representing binary data for use in a glTF file as 'buffer' property.""" + + def __init__(self, buffer_index=0): + self.__data = b"" + self.__buffer_index = buffer_index + + def add_and_get_view(self, binary_data: gltf2_io_binary_data.BinaryData) -> gltf2_io.BufferView: + """Add binary data to the buffer. Return a glTF BufferView.""" + offset = len(self.__data) + self.__data += binary_data.data + + # offsets should be a multiple of 4 --> therefore add padding if necessary + padding = (4 - (binary_data.byte_length % 4)) % 4 + self.__data += b"\x00" * padding + + buffer_view = gltf2_io.BufferView( + buffer=self.__buffer_index, + byte_length=binary_data.byte_length, + byte_offset=offset, + byte_stride=None, + extensions=None, + extras=None, + name=None, + target=None + ) + return buffer_view + + @property + def byte_length(self): + return len(self.__data) + + def to_bytes(self): + return self.__data + + def to_embed_string(self): + return 'data:application/octet-stream;base64,' + base64.b64encode(self.__data).decode('ascii') + + def clear(self): + self.__data = b"" + diff --git a/io_scene_gltf2/io/exp/gltf2_io_export.py b/io_scene_gltf2/io/exp/gltf2_io_export.py new file mode 100755 index 00000000..561b2ac1 --- /dev/null +++ b/io_scene_gltf2/io/exp/gltf2_io_export.py @@ -0,0 +1,97 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Imports +# + +import json +import struct + +# +# Globals +# + +# +# Functions +# + + +def save_gltf(glTF, export_settings, encoder, glb_buffer): + indent = None + separators = separators = (',', ':') + + if export_settings['gltf_format'] == 'ASCII' and not export_settings['gltf_strip']: + indent = 4 + # The comma is typically followed by a newline, so no trailing whitespace is needed on it. + separators = separators = (',', ' : ') + + glTF_encoded = json.dumps(glTF, indent=indent, separators=separators, sort_keys=True, cls=encoder, allow_nan=False) + + # + + if export_settings['gltf_format'] == 'ASCII': + file = open(export_settings['gltf_filepath'], "w", encoding="utf8", newline="\n") + file.write(glTF_encoded) + file.write("\n") + file.close() + + binary = export_settings['gltf_binary'] + if len(binary) > 0 and not export_settings['gltf_embed_buffers']: + file = open(export_settings['gltf_filedirectory'] + export_settings['gltf_binaryfilename'], "wb") + file.write(binary) + file.close() + + else: + file = open(export_settings['gltf_filepath'], "wb") + + glTF_data = glTF_encoded.encode() + binary = glb_buffer + + length_gtlf = len(glTF_data) + spaces_gltf = (4 - (length_gtlf & 3)) & 3 + length_gtlf += spaces_gltf + + length_bin = len(binary) + zeros_bin = (4 - (length_bin & 3)) & 3 + length_bin += zeros_bin + + length = 12 + 8 + length_gtlf + if length_bin > 0: + length += 8 + length_bin + + # Header (Version 2) + file.write('glTF'.encode()) + file.write(struct.pack("I", 2)) + file.write(struct.pack("I", length)) + + # Chunk 0 (JSON) + file.write(struct.pack("I", length_gtlf)) + file.write('JSON'.encode()) + file.write(glTF_data) + for i in range(0, spaces_gltf): + file.write(' '.encode()) + + # Chunk 1 (BIN) + if length_bin > 0: + file.write(struct.pack("I", length_bin)) + file.write('BIN\0'.encode()) + file.write(binary) + for i in range(0, zeros_bin): + file.write('\0'.encode()) + + file.close() + + return True + diff --git a/io_scene_gltf2/io/exp/gltf2_io_get.py b/io_scene_gltf2/io/exp/gltf2_io_get.py new file mode 100755 index 00000000..35c65615 --- /dev/null +++ b/io_scene_gltf2/io/exp/gltf2_io_get.py @@ -0,0 +1,316 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Imports +# + +import os + +# +# Globals +# + +# +# Functions +# + + +def get_material_requires_texcoords(glTF, index): + """Query function, if a material "needs" texture coordinates. This is the case, if a texture is present and used.""" + if glTF.materials is None: + return False + + materials = glTF.materials + + if index < 0 or index >= len(materials): + return False + + material = materials[index] + + # General + + if material.emissive_texture is not None: + return True + + if material.normal_texture is not None: + return True + + if material.occlusion_texture is not None: + return True + + # Metallic roughness + + if material.pbr_metallic_roughness is not None and \ + material.pbr_metallic_roughness.base_color_texture is not None: + return True + + if material.pbr_metallic_roughness is not None and \ + material.pbr_metallic_roughness.metallic_roughness_texture is not None: + return True + + return False + + +def get_material_requires_normals(glTF, index): + """ + Query function, if a material "needs" normals. This is the case, if a texture is present and used. + + At point of writing, same function as for texture coordinates. + """ + return get_material_requires_texcoords(glTF, index) + + +def get_material_index(glTF, name): + """Return the material index in the glTF array.""" + if name is None: + return -1 + + if glTF.materials is None: + return -1 + + index = 0 + for material in glTF.materials: + if material.name == name: + return index + + index += 1 + + return -1 + + +def get_mesh_index(glTF, name): + """Return the mesh index in the glTF array.""" + if glTF.meshes is None: + return -1 + + index = 0 + for mesh in glTF.meshes: + if mesh.name == name: + return index + + index += 1 + + return -1 + + +def get_skin_index(glTF, name, index_offset): + """Return the skin index in the glTF array.""" + if glTF.skins is None: + return -1 + + skeleton = get_node_index(glTF, name) + + index = 0 + for skin in glTF.skins: + if skin.skeleton == skeleton: + return index + index_offset + + index += 1 + + return -1 + + +def get_camera_index(glTF, name): + """Return the camera index in the glTF array.""" + if glTF.cameras is None: + return -1 + + index = 0 + for camera in glTF.cameras: + if camera.name == name: + return index + + index += 1 + + return -1 + + +def get_light_index(glTF, name): + """Return the light index in the glTF array.""" + if glTF.extensions is None: + return -1 + + extensions = glTF.extensions + + if extensions.get('KHR_lights_punctual') is None: + return -1 + + khr_lights_punctual = extensions['KHR_lights_punctual'] + + if khr_lights_punctual.get('lights') is None: + return -1 + + lights = khr_lights_punctual['lights'] + + index = 0 + for light in lights: + if light['name'] == name: + return index + + index += 1 + + return -1 + + +def get_node_index(glTF, name): + """Return the node index in the glTF array.""" + if glTF.nodes is None: + return -1 + + index = 0 + for node in glTF.nodes: + if node.name == name: + return index + + index += 1 + + return -1 + + +def get_scene_index(glTF, name): + """Return the scene index in the glTF array.""" + if glTF.scenes is None: + return -1 + + index = 0 + for scene in glTF.scenes: + if scene.name == name: + return index + + index += 1 + + return -1 + + +def get_texture_index(glTF, filename): + """Return the texture index in the glTF array by a given file path.""" + if glTF.textures is None: + return -1 + + image_index = get_image_index(glTF, filename) + + if image_index == -1: + return -1 + + for texture_index, texture in enumerate(glTF.textures): + if image_index == texture.source: + return texture_index + + return -1 + + +def get_image_index(glTF, filename): + """Return the image index in the glTF array.""" + if glTF.images is None: + return -1 + + image_name = get_image_name(filename) + + for index, current_image in enumerate(glTF.images): + if image_name == current_image.name: + return index + + return -1 + + +def get_image_name(filename): + """Return user-facing, extension-agnostic name for image.""" + return os.path.splitext(filename)[0] + + +def get_scalar(default_value, init_value=0.0): + """Return scalar with a given default/fallback value.""" + return_value = init_value + + if default_value is None: + return return_value + + return_value = default_value + + return return_value + + +def get_vec2(default_value, init_value=[0.0, 0.0]): + """Return vec2 with a given default/fallback value.""" + return_value = init_value + + if default_value is None or len(default_value) < 2: + return return_value + + index = 0 + for number in default_value: + return_value[index] = number + + index += 1 + if index == 2: + return return_value + + return return_value + + +def get_vec3(default_value, init_value=[0.0, 0.0, 0.0]): + """Return vec3 with a given default/fallback value.""" + return_value = init_value + + if default_value is None or len(default_value) < 3: + return return_value + + index = 0 + for number in default_value: + return_value[index] = number + + index += 1 + if index == 3: + return return_value + + return return_value + + +def get_vec4(default_value, init_value=[0.0, 0.0, 0.0, 1.0]): + """Return vec4 with a given default/fallback value.""" + return_value = init_value + + if default_value is None or len(default_value) < 4: + return return_value + + index = 0 + for number in default_value: + return_value[index] = number + + index += 1 + if index == 4: + return return_value + + return return_value + + +def get_index(elements, name): + """Return index of a glTF element by a given name.""" + if elements is None or name is None: + return -1 + + index = 0 + for element in elements: + if isinstance(element, dict): + if element.get('name') == name: + return index + else: + if element.name == name: + return index + + index += 1 + + return -1 + diff --git a/io_scene_gltf2/io/exp/gltf2_io_image_data.py b/io_scene_gltf2/io/exp/gltf2_io_image_data.py new file mode 100755 index 00000000..23a2843e --- /dev/null +++ b/io_scene_gltf2/io/exp/gltf2_io_image_data.py @@ -0,0 +1,106 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing +import struct +import zlib +import numpy as np + +class ImageData: + """Contains channels of an image with raw pixel data.""" + # TODO: refactor to only operate on numpy arrays + # FUTURE_WORK: as a method to allow the node graph to be better supported, we could model some of + # the node graph elements with numpy functions + + def __init__(self, name: str, width: int, height: int, channels: typing.Optional[typing.List[np.ndarray]] = []): + if width <= 0 or height <= 0: + raise ValueError("Image data can not have zero width or height") + self.name = name + self.channels = channels + self.width = width + self.height = height + + def add_to_image(self, image_data): + if self.width != image_data.width or self.height != image_data.height: + raise ValueError("Image dimensions do not match") + if len(self.channels) + len(image_data.channels) > 4: + raise ValueError("Can't append image: channels full") + self.name += image_data.name + self.channels += image_data.channels + + @property + def r(self): + if len(self.channels) <= 0: + return None + return self.channels[0] + + @property + def g(self): + if len(self.channels) <= 1: + return None + return self.channels[1] + + @property + def b(self): + if len(self.channels) <= 2: + return None + return self.channels[2] + + @property + def a(self): + if len(self.channels) <= 3: + return None + return self.channels[3] + + def to_image_data(self, mime_type: str) -> bytes: + if mime_type == 'image/png': + return self.to_png_data() + raise ValueError("Unsupported image file type {}".format(mime_type)) + + def to_png_data(self) -> bytes: + channels = self.channels + + # if there is no data, create a single pixel image + if not channels: + channels = np.zeros((1, 1)) + + # fill all channels of the png + for _ in range(4 - len(channels)): + channels.append(np.ones_like(channels[0])) + + image = np.concatenate(self.channels, axis=1) + image = image.flatten() + image = (image * 255.0).astype(np.uint8) + buf = image.tobytes() + + # + # Taken from 'blender-thumbnailer.py' in Blender. + # + + # reverse the vertical line order and add null bytes at the start + width_byte_4 = self.width * 4 + raw_data = b"".join( + b'\x00' + buf[span:span + width_byte_4] for span in range( + (self.height - 1) * self.width * 4, -1, - width_byte_4)) + + def png_pack(png_tag, data): + chunk_head = png_tag + data + return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head)) + + return b"".join([ + b'\x89PNG\r\n\x1a\n', + png_pack(b'IHDR', struct.pack("!2I5B", self.width, self.height, 8, 6, 0, 0, 0)), + png_pack(b'IDAT', zlib.compress(raw_data, 9)), + png_pack(b'IEND', b'')]) + diff --git a/io_scene_gltf2/io/imp/__init__.py b/io_scene_gltf2/io/imp/__init__.py new file mode 100755 index 00000000..d3c53771 --- /dev/null +++ b/io_scene_gltf2/io/imp/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""IO imp package.""" + diff --git a/io_scene_gltf2/io/imp/gltf2_io_binary.py b/io_scene_gltf2/io/imp/gltf2_io_binary.py new file mode 100755 index 00000000..5f51d95d --- /dev/null +++ b/io_scene_gltf2/io/imp/gltf2_io_binary.py @@ -0,0 +1,178 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import struct +import base64 +from os.path import dirname, join, isfile, basename + + +class BinaryData(): + """Binary reader.""" + def __new__(cls, *args, **kwargs): + raise RuntimeError("%s should not be instantiated" % cls) + + @staticmethod + def get_binary_from_accessor(gltf, accessor_idx): + """Get binary from accessor.""" + accessor = gltf.data.accessors[accessor_idx] + bufferView = gltf.data.buffer_views[accessor.buffer_view] # TODO initialize with 0 when not present! + if bufferView.buffer in gltf.buffers.keys(): + buffer = gltf.buffers[bufferView.buffer] + else: + # load buffer + gltf.load_buffer(bufferView.buffer) + buffer = gltf.buffers[bufferView.buffer] + + accessor_offset = accessor.byte_offset + bufferview_offset = bufferView.byte_offset + + if accessor_offset is None: + accessor_offset = 0 + if bufferview_offset is None: + bufferview_offset = 0 + + return buffer[accessor_offset + bufferview_offset:accessor_offset + bufferview_offset + bufferView.byte_length] + + @staticmethod + def get_data_from_accessor(gltf, accessor_idx): + """Get data from accessor.""" + accessor = gltf.data.accessors[accessor_idx] + + bufferView = gltf.data.buffer_views[accessor.buffer_view] # TODO initialize with 0 when not present! + buffer_data = BinaryData.get_binary_from_accessor(gltf, accessor_idx) + + fmt_char = gltf.fmt_char_dict[accessor.component_type] + component_nb = gltf.component_nb_dict[accessor.type] + fmt = '<' + (fmt_char * component_nb) + stride_ = struct.calcsize(fmt) + # TODO data alignment stuff + + if bufferView.byte_stride: + stride = bufferView.byte_stride + else: + stride = stride_ + + data = [] + offset = 0 + while len(data) < accessor.count: + element = struct.unpack_from(fmt, buffer_data, offset) + data.append(element) + offset += stride + + if accessor.sparse: + sparse_indices_data = BinaryData.get_data_from_sparse(gltf, accessor.sparse, "indices") + sparse_values_values = BinaryData.get_data_from_sparse( + gltf, + accessor.sparse, + "values", + accessor.type, + accessor.component_type + ) + + # apply sparse + for cpt_idx, idx in enumerate(sparse_indices_data): + data[idx[0]] = sparse_values_values[cpt_idx] + + # Normalization + if accessor.normalized: + for idx, tuple in enumerate(data): + new_tuple = () + for i in tuple: + new_tuple += (float(i),) + data[idx] = new_tuple + + return data + + @staticmethod + def get_data_from_sparse(gltf, sparse, type_, type_val=None, comp_type=None): + """Get data from sparse.""" + if type_ == "indices": + bufferView = gltf.data.buffer_views[sparse.indices.buffer_view] + offset = sparse.indices.byte_offset + component_nb = gltf.component_nb_dict['SCALAR'] + fmt_char = gltf.fmt_char_dict[sparse.indices.component_type] + elif type_ == "values": + bufferView = gltf.data.buffer_views[sparse.values.buffer_view] + offset = sparse.values.byte_offset + component_nb = gltf.component_nb_dict[type_val] + fmt_char = gltf.fmt_char_dict[comp_type] + + if bufferView.buffer in gltf.buffers.keys(): + buffer = gltf.buffers[bufferView.buffer] + else: + # load buffer + gltf.load_buffer(bufferView.buffer) + buffer = gltf.buffers[bufferView.buffer] + + bin_data = buffer[bufferView.byte_offset + offset:bufferView.byte_offset + offset + bufferView.byte_length] + + fmt = '<' + (fmt_char * component_nb) + stride_ = struct.calcsize(fmt) + # TODO data alignment stuff ? + + if bufferView.byte_stride: + stride = bufferView.byte_stride + else: + stride = stride_ + + data = [] + offset = 0 + while len(data) < sparse.count: + element = struct.unpack_from(fmt, bin_data, offset) + data.append(element) + offset += stride + + return data + + @staticmethod + def get_image_data(gltf, img_idx): + """Get data from image.""" + pyimage = gltf.data.images[img_idx] + + image_name = "Image_" + str(img_idx) + + if pyimage.uri: + sep = ';base64,' + if pyimage.uri[:5] == 'data:': + idx = pyimage.uri.find(sep) + if idx != -1: + data = pyimage.uri[idx + len(sep):] + return base64.b64decode(data), image_name + + if isfile(join(dirname(gltf.filename), pyimage.uri)): + with open(join(dirname(gltf.filename), pyimage.uri), 'rb') as f_: + return f_.read(), basename(join(dirname(gltf.filename), pyimage.uri)) + else: + pyimage.gltf.log.error("Missing file (index " + str(img_idx) + "): " + pyimage.uri) + return None, None + + if pyimage.buffer_view is None: + return None, None + + bufferView = gltf.data.buffer_views[pyimage.buffer_view] + + if bufferView.buffer in gltf.buffers.keys(): + buffer = gltf.buffers[bufferView.buffer] + else: + # load buffer + gltf.load_buffer(bufferView.buffer) + buffer = gltf.buffers[bufferView.buffer] + + bufferview_offset = bufferView.byte_offset + + if bufferview_offset is None: + bufferview_offset = 0 + + return buffer[bufferview_offset:bufferview_offset + bufferView.byte_length], image_name + diff --git a/io_scene_gltf2/io/imp/gltf2_io_gltf.py b/io_scene_gltf2/io/imp/gltf2_io_gltf.py new file mode 100755 index 00000000..1c9e67a2 --- /dev/null +++ b/io_scene_gltf2/io/imp/gltf2_io_gltf.py @@ -0,0 +1,199 @@ +# Copyright 2018 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..com.gltf2_io import gltf_from_dict +from ..com.gltf2_io_debug import Log +import logging +import json +import struct +import base64 +from os.path import dirname, join, getsize, isfile + + +class glTFImporter(): + """glTF Importer class.""" + + def __init__(self, filename, import_settings): + """initialization.""" + self.filename = filename + self.import_settings = import_settings + self.buffers = {} + + if 'loglevel' not in self.import_settings.keys(): + self.import_settings['loglevel'] = logging.ERROR + + log = Log(import_settings['loglevel']) + self.log = log.logger + self.log_handler = log.hdlr + + self.SIMPLE = 1 + self.TEXTURE = 2 + self.TEXTURE_FACTOR = 3 + + # TODO: move to a com place? + self.extensions_managed = [ + 'KHR_materials_pbrSpecularGlossiness' + ] + + # TODO : merge with io_constants + self.fmt_char_dict = {} + self.fmt_char_dict[5120] = 'b' # Byte + self.fmt_char_dict[5121] = 'B' # Unsigned Byte + self.fmt_char_dict[5122] = 'h' # Short + self.fmt_char_dict[5123] = 'H' # Unsigned Short + self.fmt_char_dict[5125] = 'I' # Unsigned Int + self.fmt_char_dict[5126] = 'f' # Float + + self.component_nb_dict = {} + self.component_nb_dict['SCALAR'] = 1 + self.component_nb_dict['VEC2'] = 2 + self.component_nb_dict['VEC3'] = 3 + self.component_nb_dict['VEC4'] = 4 + self.component_nb_dict['MAT2'] = 4 + self.component_nb_dict['MAT3'] = 9 + self.component_nb_dict['MAT4'] = 16 + + @staticmethod + def bad_json_value(val): + """Bad Json value.""" + raise ValueError('Json contains some unauthorized values') + + def checks(self): + """Some checks.""" + if self.data.asset.version != "2.0": + return False, "glTF version must be 2" + + if self.data.extensions_required is not None: + for extension in self.data.extensions_required: + if extension not in self.data.extensions_used: + return False, "Extension required must be in Extension Used too" + if extension not in self.extensions_managed: + return False, "Extension " + extension + " is not available on this addon version" + + if self.data.extensions_used is not None: + for extension in self.data.extensions_used: + if extension not in self.extensions_managed: + # Non blocking error #TODO log + pass + + return True, None + + def load_glb(self): + """Load binary glb.""" + header = struct.unpack_from('<4sII', self.content) + self.format = header[0] + self.version = header[1] + self.file_size = header[2] + + if self.format != b'glTF': + return False, "This file is not a glTF/glb file" + + if self.version != 2: + return False, "glTF version doesn't match to 2" + + if self.file_size != getsize(self.filename): + return False, "File size doesn't match" + + offset = 12 # header size = 12 + + # TODO check json type for chunk 0, and BIN type for next ones + + # json + type, len_, str_json, offset = self.load_chunk(offset) + if len_ != len(str_json): + return False, "Length of json part doesn't match" + try: + json_ = json.loads(str_json.decode('utf-8'), parse_constant=glTFImporter.bad_json_value) + self.data = gltf_from_dict(json_) + except ValueError as e: + return False, e.args[0] + + # binary data + chunk_cpt = 0 + while offset < len(self.content): + type, len_, data, offset = self.load_chunk(offset) + if len_ != len(data): + return False, "Length of bin buffer " + str(chunk_cpt) + " doesn't match" + + self.buffers[chunk_cpt] = data + chunk_cpt += 1 + + self.content = None + return True, None + + def load_chunk(self, offset): + """Load chunk.""" + chunk_header = struct.unpack_from('<I4s', self.content, offset) + data_length = chunk_header[0] + data_type = chunk_header[1] + data = self.content[offset + 8: offset + 8 + data_length] + + return data_type, data_length, data, offset + 8 + data_length + + def read(self): + """Read file.""" + # Check this is a file + if not isfile(self.filename): + return False, "Please select a file" + + # Check if file is gltf or glb + with open(self.filename, 'rb') as f: + self.content = f.read() + + self.is_glb_format = self.content[:4] == b'glTF' + + # glTF file + if not self.is_glb_format: + self.content = None + with open(self.filename, 'r') as f: + content = f.read() + try: + self.data = gltf_from_dict(json.loads(content, parse_constant=glTFImporter.bad_json_value)) + return True, None + except ValueError as e: + return False, e.args[0] + + # glb file + else: + # Parsing glb file + success, txt = self.load_glb() + return success, txt + + def is_node_joint(self, node_idx): + """Check if node is a joint.""" + if not self.data.skins: # if no skin in gltf file + return False, None + + for skin_idx, skin in enumerate(self.data.skins): + if node_idx in skin.joints: + return True, skin_idx + + return False, None + + def load_buffer(self, buffer_idx): + """Load buffer.""" + buffer = self.data.buffers[buffer_idx] + + if buffer.uri: + sep = ';base64,' + if buffer.uri[:5] == 'data:': + idx = buffer.uri.find(sep) + if idx != -1: + data = buffer.uri[idx + len(sep):] + self.buffers[buffer_idx] = base64.b64decode(data) + return + + with open(join(dirname(self.filename), buffer.uri), 'rb') as f_: + self.buffers[buffer_idx] = f_.read() + |