From 0cdaac6f9a3e318b1d5db04ade2838d004cd500d Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Sun, 4 Jul 2021 17:54:15 +0200 Subject: glTF exporter: Add option to keep original texture files WARNING: if you use more than one texture, where pbr standard requires only one, only one texture will be used. This can lead to unexpected results --- io_scene_gltf2/__init__.py | 18 ++- .../blender/exp/gltf2_blender_gather_image.py | 173 +++++++++++++-------- io_scene_gltf2/blender/exp/gltf2_blender_image.py | 14 +- 3 files changed, 136 insertions(+), 69 deletions(-) (limited to 'io_scene_gltf2') diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 4c758229..c2c028b9 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 7, 15), + "version": (1, 7, 16), 'blender': (2, 91, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', @@ -173,6 +173,16 @@ class ExportGLTF2_Base: default='', ) + export_keep_originals: BoolProperty( + name='Keep original', + description=('Keep original textures files if possible. ' + 'WARNING: if you use more than one texture, ' + 'where pbr standard requires only one, only one texture will be used.' + 'This can lead to unexpected results' + ), + default=False, + ) + export_texcoords: BoolProperty( name='UVs', description='Export UVs (texture coordinates) with meshes', @@ -517,6 +527,7 @@ class ExportGLTF2_Base: export_settings['gltf_filedirectory'], self.export_texture_dir, ) + export_settings['gltf_keep_original_textures'] = self.export_keep_originals export_settings['gltf_format'] = self.export_format export_settings['gltf_image_format'] = self.export_image_format @@ -653,7 +664,10 @@ class GLTF_PT_export_main(bpy.types.Panel): layout.prop(operator, 'export_format') if operator.export_format == 'GLTF_SEPARATE': - layout.prop(operator, 'export_texture_dir', icon='FILE_FOLDER') + layout.prop(operator, 'export_keep_originals') + if operator.export_keep_originals is False: + layout.prop(operator, 'export_texture_dir', icon='FILE_FOLDER') + layout.prop(operator, 'export_copyright') layout.prop(operator, 'will_save_settings') 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 b0fb2c25..8e441f9c 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py @@ -42,7 +42,12 @@ def gather_image( mime_type = __gather_mime_type(blender_shader_sockets, image_data, export_settings) name = __gather_name(image_data, export_settings) - uri = __gather_uri(image_data, mime_type, name, export_settings) + if image_data.original is None: + uri = __gather_uri(image_data, mime_type, name, export_settings) + else: + # Retrieve URI relative to exported glTF files + uri = __gather_original_uri(image_data.original.filepath, export_settings) + buffer_view = __gather_buffer_view(image_data, mime_type, name, export_settings) image = __make_image( @@ -59,6 +64,27 @@ def gather_image( return image +def __gather_original_uri(original_uri, export_settings): + + def _path_to_uri(path): + import urllib + path = os.path.normpath(path) + path = path.replace(os.sep, '/') + return urllib.parse.quote(path) + + path_to_image = bpy.path.abspath(original_uri) + if not os.path.exists(path_to_image): return None + try: + rel_path = os.path.relpath( + path_to_image, + start=export_settings[gltf2_blender_export_keys.FILE_DIRECTORY], + ) + except ValueError: + # eg. because no relative path between C:\ and D:\ on Windows + return None + return _path_to_uri(rel_path) + + @cached def __make_image(buffer_view, extensions, extras, mime_type, name, uri, export_settings): return gltf2_io.Image( @@ -99,7 +125,12 @@ def __gather_mime_type(sockets, export_image, export_settings): return "image/png" if export_settings["gltf_image_format"] == "AUTO": - image = export_image.blender_image() + if export_image.original is None: # We are going to create a new image + image = export_image.blender_image() + else: + # Using original image + image = export_image.original + if image is not None and __is_blender_image_a_jpeg(image): return "image/jpeg" return "image/png" @@ -109,30 +140,33 @@ def __gather_mime_type(sockets, export_image, export_settings): def __gather_name(export_image, export_settings): - # Find all Blender images used in the ExportImage - imgs = [] - for fill in export_image.fills.values(): - if isinstance(fill, FillImage): - img = fill.image - if img not in imgs: - imgs.append(img) - - # If all the images have the same path, use the common filename - filepaths = set(img.filepath for img in imgs) - if len(filepaths) == 1: - filename = os.path.basename(list(filepaths)[0]) - name, extension = os.path.splitext(filename) - if extension.lower() in ['.png', '.jpg', '.jpeg']: - if name: - return name - - # Combine the image names: img1-img2-img3 - names = [] - for img in imgs: - name, extension = os.path.splitext(img.name) - names.append(name) - name = '-'.join(names) - return name or 'Image' + if export_image.original is None: + # Find all Blender images used in the ExportImage + imgs = [] + for fill in export_image.fills.values(): + if isinstance(fill, FillImage): + img = fill.image + if img not in imgs: + imgs.append(img) + + # If all the images have the same path, use the common filename + filepaths = set(img.filepath for img in imgs) + if len(filepaths) == 1: + filename = os.path.basename(list(filepaths)[0]) + name, extension = os.path.splitext(filename) + if extension.lower() in ['.png', '.jpg', '.jpeg']: + if name: + return name + + # Combine the image names: img1-img2-img3 + names = [] + for img in imgs: + name, extension = os.path.splitext(img.name) + names.append(name) + name = '-'.join(names) + return name or 'Image' + else: + return export_image.original.name @cached @@ -161,46 +195,55 @@ def __get_image_data(sockets, export_settings) -> ExportImage: result.shader_node.image)) continue - # rudimentarily try follow the node tree to find the correct image data. - src_chan = Channel.R - for elem in result.path: - if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateRGB): - src_chan = { - 'R': Channel.R, - 'G': Channel.G, - 'B': Channel.B, - }[elem.from_socket.name] - if elem.from_socket.name == 'Alpha': - src_chan = Channel.A - - dst_chan = None - - # some sockets need channel rewriting (gltf pbr defines fixed channels for some attributes) - if socket.name == 'Metallic': - dst_chan = Channel.B - elif socket.name == 'Roughness': - dst_chan = Channel.G - elif socket.name == 'Occlusion': - dst_chan = Channel.R - elif socket.name == 'Alpha': - dst_chan = Channel.A - elif socket.name == 'Clearcoat': - dst_chan = Channel.R - elif socket.name == 'Clearcoat Roughness': - dst_chan = Channel.G - - if dst_chan is not None: - composed_image.fill_image(result.shader_node.image, dst_chan, src_chan) - - # Since metal/roughness are always used together, make sure - # the other channel is filled. - if socket.name == 'Metallic' and not composed_image.is_filled(Channel.G): - composed_image.fill_white(Channel.G) - elif socket.name == 'Roughness' and not composed_image.is_filled(Channel.B): - composed_image.fill_white(Channel.B) + # Assume that user know what he does, and that channels/images are already combined correctly for pbr + # If not, we are going to keep only the first texture found + # Example : If user set up 2 or 3 different textures for Metallic / Roughness / Occlusion + # Only 1 will be used at export + # This Warning is displayed in UI of this option + if export_settings['gltf_keep_original_textures']: + composed_image = ExportImage.from_original(result.shader_node.image) + else: - # copy full image...eventually following sockets might overwrite things - composed_image = ExportImage.from_blender_image(result.shader_node.image) + # rudimentarily try follow the node tree to find the correct image data. + src_chan = Channel.R + for elem in result.path: + if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateRGB): + src_chan = { + 'R': Channel.R, + 'G': Channel.G, + 'B': Channel.B, + }[elem.from_socket.name] + if elem.from_socket.name == 'Alpha': + src_chan = Channel.A + + dst_chan = None + + # some sockets need channel rewriting (gltf pbr defines fixed channels for some attributes) + if socket.name == 'Metallic': + dst_chan = Channel.B + elif socket.name == 'Roughness': + dst_chan = Channel.G + elif socket.name == 'Occlusion': + dst_chan = Channel.R + elif socket.name == 'Alpha': + dst_chan = Channel.A + elif socket.name == 'Clearcoat': + dst_chan = Channel.R + elif socket.name == 'Clearcoat Roughness': + dst_chan = Channel.G + + if dst_chan is not None: + composed_image.fill_image(result.shader_node.image, dst_chan, src_chan) + + # Since metal/roughness are always used together, make sure + # the other channel is filled. + if socket.name == 'Metallic' and not composed_image.is_filled(Channel.G): + composed_image.fill_white(Channel.G) + elif socket.name == 'Roughness' and not composed_image.is_filled(Channel.B): + composed_image.fill_white(Channel.B) + else: + # copy full image...eventually following sockets might overwrite things + composed_image = ExportImage.from_blender_image(result.shader_node.image) return composed_image diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_image.py index a0952bb5..8ac272d8 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_image.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_image.py @@ -64,9 +64,12 @@ class ExportImage: intelligent decisions about how to encode the image. """ - def __init__(self): + def __init__(self, original=None): self.fills = {} + # In case of keeping original texture images + self.original = original + @staticmethod def from_blender_image(image: bpy.types.Image): export_image = ExportImage() @@ -74,6 +77,10 @@ class ExportImage: export_image.fill_image(image, dst_chan=chan, src_chan=chan) return export_image + @staticmethod + def from_original(image: bpy.types.Image): + return ExportImage(image) + def fill_image(self, image: bpy.types.Image, dst_chan: Channel, src_chan: Channel): self.fills[dst_chan] = FillImage(image, src_chan) @@ -84,7 +91,10 @@ class ExportImage: return chan in self.fills def empty(self) -> bool: - return not self.fills + if self.original is None: + return not self.fills + else: + return False def blender_image(self) -> Optional[bpy.types.Image]: """If there's an existing Blender image we can use, -- cgit v1.2.3