1 # SPDX-FileCopyrightText: 2018-2021 The glTF-Blender-IO authors
3 # SPDX-License-Identifier: Apache-2.0
9 from ....io
.com
import gltf2_io
10 from ....io
.com
.gltf2_io_path
import path_to_uri
11 from ....io
.exp
import gltf2_io_binary_data
, gltf2_io_image_data
12 from ....io
.com
import gltf2_io_debug
13 from ....io
.exp
.gltf2_io_user_extensions
import export_user_extensions
14 from ..gltf2_blender_gather_cache
import cached
15 from .extensions
.gltf2_blender_image
import Channel
, ExportImage
, FillImage
16 from ..gltf2_blender_get
import get_tex_from_socket
20 blender_shader_sockets
: typing
.Tuple
[bpy
.types
.NodeSocket
],
21 default_sockets
: typing
.Tuple
[bpy
.types
.NodeSocket
],
23 if not __filter_image(blender_shader_sockets
, export_settings
):
24 return None, None, None
26 image_data
= __get_image_data(blender_shader_sockets
, default_sockets
, export_settings
)
27 if image_data
.empty():
28 # The export image has no data
29 return None, None, None
31 mime_type
= __gather_mime_type(blender_shader_sockets
, image_data
, export_settings
)
32 name
= __gather_name(image_data
, export_settings
)
36 if image_data
.original
is None:
37 uri
, factor_uri
= __gather_uri(image_data
, mime_type
, name
, export_settings
)
39 # Retrieve URI relative to exported glTF files
40 uri
= __gather_original_uri(image_data
.original
.filepath
, export_settings
)
41 # In case we can't retrieve image (for example packed images, with original moved)
42 # We don't create invalid image without uri
44 if uri
is None: return None, None, None
46 buffer_view
, factor_buffer_view
= __gather_buffer_view(image_data
, mime_type
, name
, export_settings
)
48 factor
= factor_uri
if uri
is not None else factor_buffer_view
52 __gather_extensions(blender_shader_sockets
, export_settings
),
53 __gather_extras(blender_shader_sockets
, export_settings
),
60 export_user_extensions('gather_image_hook', export_settings
, image
, blender_shader_sockets
)
62 # We also return image_data, as it can be used to generate same file with another extension for webp management
63 return image
, image_data
, factor
65 def __gather_original_uri(original_uri
, export_settings
):
67 path_to_image
= bpy
.path
.abspath(original_uri
)
68 if not os
.path
.exists(path_to_image
): return None
70 rel_path
= os
.path
.relpath(
72 start
=export_settings
['gltf_filedirectory'],
75 # eg. because no relative path between C:\ and D:\ on Windows
77 return path_to_uri(rel_path
)
81 def __make_image(buffer_view
, extensions
, extras
, mime_type
, name
, uri
, export_settings
):
82 return gltf2_io
.Image(
83 buffer_view
=buffer_view
,
84 extensions
=extensions
,
92 def __filter_image(sockets
, export_settings
):
99 def __gather_buffer_view(image_data
, mime_type
, name
, export_settings
):
100 if export_settings
['gltf_format'] != 'GLTF_SEPARATE':
101 data
, factor
= image_data
.encode(mime_type
, export_settings
)
102 return gltf2_io_binary_data
.BinaryData(data
=data
), factor
106 def __gather_extensions(sockets
, export_settings
):
110 def __gather_extras(sockets
, export_settings
):
114 def __gather_mime_type(sockets
, export_image
, export_settings
):
115 # force png or webp if Alpha contained so we can export alpha
116 for socket
in sockets
:
117 if socket
.name
== "Alpha":
118 if export_settings
["gltf_image_format"] == "WEBP":
121 # If we keep image as is (no channel composition), we need to keep original format (for webp)
122 image
= export_image
.blender_image()
123 if image
is not None and __is_blender_image_a_webp(image
):
127 if export_settings
["gltf_image_format"] == "AUTO":
128 if export_image
.original
is None: # We are going to create a new image
129 image
= export_image
.blender_image()
131 # Using original image
132 image
= export_image
.original
134 if image
is not None and __is_blender_image_a_jpeg(image
):
136 elif image
is not None and __is_blender_image_a_webp(image
):
140 elif export_settings
["gltf_image_format"] == "WEBP":
142 elif export_settings
["gltf_image_format"] == "JPEG":
146 def __gather_name(export_image
, export_settings
):
147 if export_image
.original
is None:
148 # Find all Blender images used in the ExportImage
150 for fill
in export_image
.fills
.values():
151 if isinstance(fill
, FillImage
):
156 # If all the images have the same path, use the common filename
157 filepaths
= set(img
.filepath
for img
in imgs
)
158 if len(filepaths
) == 1:
159 filename
= os
.path
.basename(list(filepaths
)[0])
160 name
, extension
= os
.path
.splitext(filename
)
161 if extension
.lower() in ['.png', '.jpg', '.jpeg']:
165 # Combine the image names: img1-img2-img3
168 name
, extension
= os
.path
.splitext(img
.name
)
170 name
= '-'.join(names
)
171 return name
or 'Image'
173 return export_image
.original
.name
177 def __gather_uri(image_data
, mime_type
, name
, export_settings
):
178 if export_settings
['gltf_format'] == 'GLTF_SEPARATE':
179 # as usual we just store the data in place instead of already resolving the references
180 data
, factor
= image_data
.encode(mime_type
, export_settings
)
181 return gltf2_io_image_data
.ImageData(
190 def __get_image_data(sockets
, default_sockets
, export_settings
) -> ExportImage
:
191 # For shared resources, such as images, we just store the portion of data that is needed in the glTF property
192 # in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary
194 results
= [get_tex_from_socket(socket
) for socket
in sockets
]
196 # Check if we need a simple mapping or more complex calculation
197 if any([socket
.name
== "Specular" and socket
.node
.type == "BSDF_PRINCIPLED" for socket
in sockets
]):
198 return __get_image_data_specular(sockets
, results
, export_settings
)
200 return __get_image_data_mapping(sockets
, default_sockets
, results
, export_settings
)
202 def __get_image_data_mapping(sockets
, default_sockets
, results
, export_settings
) -> ExportImage
:
205 Will fit for most of exported textures : RoughnessMetallic, Basecolor, normal, ...
207 composed_image
= ExportImage()
209 default_metallic
= None
210 default_roughness
= None
211 if "Metallic" in [s
.name
for s
in default_sockets
]:
212 default_metallic
= [s
for s
in default_sockets
if s
.name
== "Metallic"][0].default_value
213 if "Roughness" in [s
.name
for s
in default_sockets
]:
214 default_roughness
= [s
for s
in default_sockets
if s
.name
== "Roughness"][0].default_value
216 for result
, socket
in zip(results
, sockets
):
217 # Assume that user know what he does, and that channels/images are already combined correctly for pbr
218 # If not, we are going to keep only the first texture found
219 # Example : If user set up 2 or 3 different textures for Metallic / Roughness / Occlusion
220 # Only 1 will be used at export
221 # This Warning is displayed in UI of this option
222 if export_settings
['gltf_keep_original_textures']:
223 composed_image
= ExportImage
.from_original(result
.shader_node
.image
)
226 # rudimentarily try follow the node tree to find the correct image data.
228 for elem
in result
.path
:
229 if isinstance(elem
.from_node
, bpy
.types
.ShaderNodeSeparateColor
):
234 }[elem
.from_socket
.name
]
235 if elem
.from_socket
.name
== 'Alpha':
240 # some sockets need channel rewriting (gltf pbr defines fixed channels for some attributes)
241 if socket
.name
== 'Metallic':
243 elif socket
.name
== 'Roughness' and socket
.node
.type == "BSDF_PRINCIPLED":
245 elif socket
.name
== 'Occlusion':
247 elif socket
.name
== 'Alpha':
249 elif socket
.name
== 'Coat':
251 elif socket
.name
== 'Coat Roughness':
253 elif socket
.name
== 'Thickness': # For KHR_materials_volume
255 elif socket
.name
== "Specular": # For original KHR_material_specular
257 elif socket
.name
== "Roughness" and socket
.node
.type == "BSDF_SHEEN": # For KHR_materials_sheen
260 if dst_chan
is not None:
261 composed_image
.fill_image(result
.shader_node
.image
, dst_chan
, src_chan
)
263 # Since metal/roughness are always used together, make sure
264 # the other channel is filled.
265 if socket
.name
== 'Metallic' and not composed_image
.is_filled(Channel
.G
):
266 if default_roughness
is not None:
267 composed_image
.fill_with(Channel
.G
, default_roughness
)
269 composed_image
.fill_white(Channel
.G
)
270 elif socket
.name
== 'Roughness' and not composed_image
.is_filled(Channel
.B
):
271 if default_metallic
is not None:
272 composed_image
.fill_with(Channel
.B
, default_metallic
)
274 composed_image
.fill_white(Channel
.B
)
276 # copy full image...eventually following sockets might overwrite things
277 composed_image
= ExportImage
.from_blender_image(result
.shader_node
.image
)
279 # Check that we don't have some empty channels (based on weird images without any size for example)
280 keys
= list(composed_image
.fills
.keys()) # do not loop on dict, we may have to delete an element
281 for k
in [k
for k
in keys
if isinstance(composed_image
.fills
[k
], FillImage
)]:
282 if composed_image
.fills
[k
].image
.size
[0] == 0 or composed_image
.fills
[k
].image
.size
[1] == 0:
283 gltf2_io_debug
.print_console("WARNING",
284 "Image '{}' has no size and cannot be exported.".format(
285 composed_image
.fills
[k
].image
))
286 del composed_image
.fills
[k
]
288 return composed_image
291 def __get_image_data_specular(sockets
, results
, export_settings
) -> ExportImage
:
293 calculating Specular Texture, settings needed data
295 from .extensions
.gltf2_blender_texture_specular
import specular_calculation
296 composed_image
= ExportImage()
297 composed_image
.set_calc(specular_calculation
)
299 composed_image
.store_data("ior", sockets
[4].default_value
, type="Data")
301 results
= [get_tex_from_socket(socket
) for socket
in sockets
[:-1]] #Do not retrieve IOR --> No texture allowed
310 for idx
, result
in enumerate(results
):
311 if get_tex_from_socket(sockets
[idx
]):
313 composed_image
.store_data(mapping
[idx
], result
.shader_node
.image
, type="Image")
315 # rudimentarily try follow the node tree to find the correct image data.
316 src_chan
= None if idx
== 2 else Channel
.R
317 for elem
in result
.path
:
318 if isinstance(elem
.from_node
, bpy
.types
.ShaderNodeSeparateColor
):
323 }[elem
.from_socket
.name
]
324 if elem
.from_socket
.name
== 'Alpha':
326 # For base_color, keep all channels, as this is a Vec, not scalar
328 composed_image
.store_data(mapping
[idx
] + "_channel", src_chan
, type="Data")
330 if src_chan
is not None:
331 composed_image
.store_data(mapping
[idx
] + "_channel", src_chan
, type="Data")
334 composed_image
.store_data(mapping
[idx
], sockets
[idx
].default_value
, type="Data")
336 return composed_image
339 def __is_blender_image_a_jpeg(image
: bpy
.types
.Image
) -> bool:
340 if image
.source
!= 'FILE':
342 if image
.filepath_raw
== '' and image
.packed_file
:
343 return image
.packed_file
.data
[:3] == b
'\xff\xd8\xff'
345 path
= image
.filepath_raw
.lower()
346 return path
.endswith('.jpg') or path
.endswith('.jpeg') or path
.endswith('.jpe')
348 def __is_blender_image_a_webp(image
: bpy
.types
.Image
) -> bool:
349 if image
.source
!= 'FILE':
351 if image
.filepath_raw
== '' and image
.packed_file
:
352 return image
.packed_file
.data
[8:12] == b
'WEBP'
354 path
= image
.filepath_raw
.lower()
355 return path
.endswith('.webp')