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:
parent
0aa618c849
commit
0cdaac6f9a
|
@ -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 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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue