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
This commit is contained in:
Julien Duroure 2021-07-04 17:54:15 +02:00
parent 0aa618c849
commit 0cdaac6f9a
3 changed files with 133 additions and 66 deletions

View File

@ -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')

View File

@ -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 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
# 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'
# 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
# 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)
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)
# 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

View File

@ -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,