Skip to content
Snippets Groups Projects
Commit 0cdaac6f authored by Julien Duroure's avatar Julien Duroure
Browse files

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
parent 0aa618c8
No related branches found
No related tags found
No related merge requests found
......@@ -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')
......
......@@ -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
......
......@@ -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,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment