diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index b57c3bf46f82bbb0c174fe5e1115b89ec4821519..091b82332fa2f4ca11eab30590a59c7726e2bf7c 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -53,21 +53,6 @@ bl_info = { # -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 @@ -90,6 +75,16 @@ class ExportGLTF2_Base: default='GLB' ) + ui_tab: EnumProperty( + items=(('GENERAL', "General", "General settings"), + ('MESHES', "Meshes", "Mesh settings"), + ('OBJECTS', "Objects", "Object settings"), + ('MATERIALS', "Materials", "Material settings"), + ('ANIMATION', "Animation", "Animation settings")), + name="ui_tab", + description="Export setting categories", + ) + export_copyright: StringProperty( name='Copyright', description='Legal rights and conditions for the model', @@ -214,8 +209,7 @@ class ExportGLTF2_Base: export_all_influences: BoolProperty( name='Include All Bone Influences', - description='Allow >4 joint vertex influences. Models may appear' \ - ' incorrectly in many viewers', + description='Allow >4 joint vertex influences. Models may appear incorrectly in many viewers', default=False ) @@ -239,22 +233,22 @@ class ExportGLTF2_Base: export_lights: BoolProperty( name='Punctual Lights', - description='Export directional, point, and spot lights. Uses ' \ - ' "KHR_lights_punctual" glTF extension', + description='Export directional, point, and spot lights. ' + 'Uses "KHR_lights_punctual" glTF extension', default=False ) export_texture_transform: BoolProperty( name='Texture Transforms', - description='Export texture or UV position, rotation, and scale.' \ - ' Uses "KHR_texture_transform" glTF extension', + description='Export texture or UV position, rotation, and scale. ' + 'Uses "KHR_texture_transform" glTF extension', default=False ) export_displacement: BoolProperty( name='Displacement Textures (EXPERIMENTAL)', - description='EXPERIMENTAL: Export displacement textures. Uses' \ - ' incomplete "KHR_materials_displacement" glTF extension', + description='EXPERIMENTAL: Export displacement textures. ' + 'Uses incomplete "KHR_materials_displacement" glTF extension', default=False ) @@ -360,40 +354,47 @@ class ExportGLTF2_Base: return gltf2_blender_export.save(context, export_settings) def draw(self, context): - layout = self.layout - - # - - col = layout.box().column() - col.label(text='General:', icon='PREFERENCES') + self.layout.prop(self, 'ui_tab', expand=True) + if self.ui_tab == 'GENERAL': + self.draw_general_settings() + elif self.ui_tab == 'MESHES': + self.draw_mesh_settings() + elif self.ui_tab == 'OBJECTS': + self.draw_object_settings() + elif self.ui_tab == 'MATERIALS': + self.draw_material_settings() + elif self.ui_tab == 'ANIMATION': + self.draw_animation_settings() + + def draw_general_settings(self): + col = self.layout.box().column() col.prop(self, 'export_format') col.prop(self, 'export_selected') - #col.prop(self, 'export_layers') col.prop(self, 'export_apply') col.prop(self, 'export_yup') col.prop(self, 'export_extras') col.prop(self, 'export_copyright') - col = layout.box().column() - col.label(text='Meshes:', icon='MESH_DATA') + def draw_mesh_settings(self): + col = self.layout.box().column() 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') + def draw_object_settings(self): + col = self.layout.box().column() col.prop(self, 'export_cameras') col.prop(self, 'export_lights') - col = layout.box().column() - col.label(text='Materials:', icon='MATERIAL_DATA') + def draw_material_settings(self): + col = self.layout.box().column() col.prop(self, 'export_materials') col.prop(self, 'export_texture_transform') - col = layout.box().column() - col.label(text='Animation:', icon='ARMATURE_DATA') + def draw_animation_settings(self): + col = self.layout.box().column() col.prop(self, 'export_animations') if self.export_animations: col.prop(self, 'export_frame_range') @@ -412,12 +413,6 @@ class ExportGLTF2_Base: if self.export_morph_normal: col.prop(self, 'export_morph_tangent') - row = layout.row() - row.operator( - GLTF2ExportSettings.bl_idname, - text=GLTF2ExportSettings.bl_label, - icon="%s" % "PINNED" if self.will_save_settings else "UNPINNED") - class ExportGLTF2(bpy.types.Operator, ExportGLTF2_Base, ExportHelper): """Export scene as glTF 2.0 file""" @@ -497,7 +492,6 @@ def menu_func_import(self, context): classes = ( - GLTF2ExportSettings, ExportGLTF2, ImportGLTF2 ) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export.py b/io_scene_gltf2/blender/exp/gltf2_blender_export.py index f2cc9fa528d5c1550e0867cedc28c74a699cbe45..1ddeb6a5020890d384533f8e56db0819a3b7ccb6 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_export.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_export.py @@ -33,6 +33,7 @@ def save(context, export_settings): def __export(export_settings): + export_settings['gltf_channelcache'] = dict() exporter = GlTF2Exporter(__get_copyright(export_settings)) __add_root_objects(exporter, export_settings) buffer = __create_buffer(exporter, export_settings) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index c26429d362959002c029bfd27483b33c8075d3b5..8762a90fc7f58c98dd39e7e126d37d0a79717676 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -975,10 +975,10 @@ def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, exp if max_index >= range_indices: # - # Spliting result_primitives. + # Splitting result_primitives. # - # At start, all indicees are pending. + # At start, all indices are pending. pending_attributes = { POSITION_ATTRIBUTE: [], NORMAL_ATTRIBUTE: [] @@ -1038,6 +1038,7 @@ def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, exp while len(pending_indices) > 0: process_indices = pending_primitive[INDICES_ID] + max_index = max(process_indices) pending_indices = [] @@ -1046,7 +1047,7 @@ def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, exp all_local_indices = [] - for i in range(0, (max(process_indices) // range_indices) + 1): + for i in range(0, (max_index // range_indices) + 1): all_local_indices.append([]) # @@ -1063,7 +1064,7 @@ def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, exp 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): + for i in range(0, (max_index // range_indices) + 1): offset = i * range_indices # Yes, so store the primitive with its indices. @@ -1075,7 +1076,7 @@ def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, exp written = True break - # If not written, the triangel face has indices from different ranges. + # If not written, the triangle 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]]) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py index d9583d080697aae15734d6b5a86270fe7817b7b7..b6131a594f7dde7e8d0cdb3a30cd17ac1f2f6c8f 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py @@ -49,7 +49,7 @@ def __filter_image(sockets_or_slots, export_settings): def __gather_buffer_view(sockets_or_slots, export_settings): if export_settings[gltf2_blender_export_keys.FORMAT] != 'GLTF_SEPARATE': - image = __get_image_data(sockets_or_slots) + image = __get_image_data(sockets_or_slots, export_settings) return gltf2_io_binary_data.BinaryData( data=image.to_image_data(__gather_mime_type(sockets_or_slots, export_settings))) return None @@ -81,7 +81,7 @@ def __gather_name(sockets_or_slots, export_settings): def __gather_uri(sockets_or_slots, export_settings): if export_settings[gltf2_blender_export_keys.FORMAT] == 'GLTF_SEPARATE': # as usual we just store the data in place instead of already resolving the references - return __get_image_data(sockets_or_slots) + return __get_image_data(sockets_or_slots, export_settings) return None @@ -93,14 +93,21 @@ def __is_slot(sockets_or_slots): return isinstance(sockets_or_slots[0], bpy.types.MaterialTextureSlot) -def __get_image_data(sockets_or_slots): +def __get_image_data(sockets_or_slots, export_settings): # 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.List[typing.List[float]]: + def split_pixels_by_channels(image: bpy.types.Image, export_settings) -> typing.List[typing.List[float]]: + channelcache = export_settings['gltf_channelcache'] + if image.name in channelcache: + return channelcache[image.name] + pixels = np.array(image.pixels) pixels = pixels.reshape((pixels.shape[0] // image.channels, image.channels)) channels = np.split(pixels, pixels.shape[1], axis=1) + + channelcache[image.name] = channels + return channels if __is_socket(sockets_or_slots): @@ -118,15 +125,16 @@ def __get_image_data(sockets_or_slots): }[elem.from_socket.name] if channel is not None: - pixels = [split_pixels_by_channels(result.shader_node.image)[channel]] + pixels = [split_pixels_by_channels(result.shader_node.image, export_settings)[channel]] else: - pixels = split_pixels_by_channels(result.shader_node.image) + pixels = split_pixels_by_channels(result.shader_node.image, export_settings) channel = 0 file_name = os.path.splitext(result.shader_node.image.name)[0] image_data = gltf2_io_image_data.ImageData( file_name, + result.shader_node.image.filepath, result.shader_node.image.size[0], result.shader_node.image.size[1], channel, @@ -140,10 +148,11 @@ def __get_image_data(sockets_or_slots): return image elif __is_slot(sockets_or_slots): texture = __get_tex_from_slot(sockets_or_slots[0]) - pixels = split_pixels_by_channels(texture.image) + pixels = split_pixels_by_channels(texture.image, export_settings) image_data = gltf2_io_image_data.ImageData( texture.name, + texture.image.filepath, texture.image.size[0], texture.image.size[1], 0, diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py index b1408b62b0d9ed70664a4facbc7c919fe3f28a09..6561567e284efb506ba53972ab07fcae63cba247 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py @@ -20,6 +20,9 @@ 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 +import bpy +import os +from shutil import copyfile class GlTF2Exporter: """ @@ -141,9 +144,18 @@ class GlTF2Exporter: :return: """ for image in self.__images: - uri = output_path + image.name + ".png" - with open(uri, 'wb') as f: - f.write(image.to_png_data()) + dst_path = output_path + image.name + ".png" + + src_path = bpy.path.abspath(image.filepath) + if os.path.isfile(src_path): + # Source file exists. + if os.path.abspath(dst_path) != os.path.abspath(src_path): + # Only copy, if source and destination are not the same. + copyfile(src_path, dst_path) + else: + # Source file does not exist e.g. it is packed or has been generated. + with open(dst_path, 'wb') as f: + f.write(image.to_png_data()) def add_scene(self, scene: gltf2_io.Scene, active: bool = True): """ diff --git a/io_scene_gltf2/io/com/gltf2_io_debug.py b/io_scene_gltf2/io/com/gltf2_io_debug.py index a7df8fed6d615147537b410927e847fb869950d0..b9098ebadf7580fc0b057e78b66dd016ab6ed369 100755 --- a/io_scene_gltf2/io/com/gltf2_io_debug.py +++ b/io_scene_gltf2/io/com/gltf2_io_debug.py @@ -54,7 +54,7 @@ def print_console(level, output): if OUTPUT_LEVELS.index(level) > OUTPUT_LEVELS.index(g_current_output_level): return - print(level + ': ' + output) + print(get_timestamp() + " | " + level + ': ' + output) def print_newline(): @@ -62,9 +62,14 @@ def print_newline(): print() +def get_timestamp(): + current_time = time.gmtime() + return time.strftime("%H:%M:%S", current_time) + + def print_timestamp(label=None): """Print a timestamp to Blender console.""" - output = 'Timestamp: ' + str(time.time()) + output = 'Timestamp: ' + get_timestamp() if label is not None: output = output + ' (' + label + ')' diff --git a/io_scene_gltf2/io/exp/gltf2_io_image_data.py b/io_scene_gltf2/io/exp/gltf2_io_image_data.py index c69dacb290dd6088b470c559fa54efe87ed8778f..5f547f7567127797d06ac93c2287cb29a86ae24b 100755 --- a/io_scene_gltf2/io/exp/gltf2_io_image_data.py +++ b/io_scene_gltf2/io/exp/gltf2_io_image_data.py @@ -23,23 +23,17 @@ class ImageData: # 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, offset: int, channels: typing.Optional[typing.List[np.ndarray]] = []): + def __init__(self, name: str, filepath: str, width: int, height: int, offset: 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") if offset + len(channels) > 4: raise ValueError("Image data can not have more than 4 channels") - - self.channels = [] - for fill in range(offset): - # Fill before. - self.channels.append(np.ones_like(channels[0])) - self.channels += channels - total_channels = len(self.channels) - for fill in range(total_channels, 4): - # Fill after. - self.channels.append(np.ones_like(channels[0])) - + self.channels = [None, None, None, None] + channels_length = len(channels) + for index in range(offset, offset + channels_length): + self.channels[index] = channels[index - offset] self.name = name + self.filepath = filepath self.width = width self.height = height @@ -51,7 +45,9 @@ class ImageData: if len(image_data.channels) != 4: raise ValueError("Can't append image: incomplete image") - self.name += image_data.name + if self.name != image_data.name: + self.name += image_data.name + self.filepath = "" # Replace channel. self.channels[channel] = image_data.channels[channel] @@ -91,10 +87,18 @@ class ImageData: # if there is no data, create a single pixel image if not channels: channels = np.ones((1, 1)) - # fill all channels of the png for _ in range(4 - len(channels)): channels.append(np.ones_like(channels[0])) + else: + template_index = None + for index in range(0, 4): + if channels[index] is not None: + template_index = index + break + for index in range(0, 4): + if channels[index] is None: + channels[index] = np.ones_like(channels[template_index]) image = np.concatenate(channels, axis=1) image = image.flatten()