glTF exporter: add glTF Embedded format option back - with option
[blender-addons.git] / io_scene_gltf2 / blender / exp / gltf2_blender_gltf2_exporter.py
blobbae3da9e16c2f9671f28f7a42717303f3f3a87d9
1 # SPDX-FileCopyrightText: 2018-2021 The glTF-Blender-IO authors
3 # SPDX-License-Identifier: Apache-2.0
5 import bpy
6 import re
7 import os
8 from typing import List
10 from ... import get_version_string
11 from ...io.com import gltf2_io, gltf2_io_extensions
12 from ...io.com.gltf2_io_path import path_to_uri, uri_to_path
13 from ...io.com.gltf2_io_constants import ComponentType, DataType
14 from ...io.exp import gltf2_io_binary_data, gltf2_io_buffer, gltf2_io_image_data
15 from ...io.exp.gltf2_io_user_extensions import export_user_extensions
16 from .gltf2_blender_gather_accessors import gather_accessor
17 from .material.gltf2_blender_gather_image import get_gltf_image_from_blender_image
19 class AdditionalData:
20 def __init__(self):
21 additional_textures = []
23 class GlTF2Exporter:
24 """
25 The glTF exporter flattens a scene graph to a glTF serializable format.
27 Any child properties are replaced with references where necessary
28 """
30 def __init__(self, export_settings):
31 self.export_settings = export_settings
32 self.__finalized = False
34 copyright = export_settings['gltf_copyright'] or None
35 asset = gltf2_io.Asset(
36 copyright=copyright,
37 extensions=None,
38 extras=None,
39 generator='Khronos glTF Blender I/O v' + get_version_string(),
40 min_version=None,
41 version='2.0')
43 export_user_extensions('gather_asset_hook', export_settings, asset)
45 self.__gltf = gltf2_io.Gltf(
46 accessors=[],
47 animations=[],
48 asset=asset,
49 buffers=[],
50 buffer_views=[],
51 cameras=[],
52 extensions={},
53 extensions_required=[],
54 extensions_used=[],
55 extras=None,
56 images=[],
57 materials=[],
58 meshes=[],
59 nodes=[],
60 samplers=[],
61 scene=-1,
62 scenes=[],
63 skins=[],
64 textures=[]
68 self.additional_data = AdditionalData()
70 self.__buffer = gltf2_io_buffer.Buffer()
71 self.__images = {}
73 # mapping of all glTFChildOfRootProperty types to their corresponding root level arrays
74 self.__childOfRootPropertyTypeLookup = {
75 gltf2_io.Accessor: self.__gltf.accessors,
76 gltf2_io.Animation: self.__gltf.animations,
77 gltf2_io.Buffer: self.__gltf.buffers,
78 gltf2_io.BufferView: self.__gltf.buffer_views,
79 gltf2_io.Camera: self.__gltf.cameras,
80 gltf2_io.Image: self.__gltf.images,
81 gltf2_io.Material: self.__gltf.materials,
82 gltf2_io.Mesh: self.__gltf.meshes,
83 gltf2_io.Node: self.__gltf.nodes,
84 gltf2_io.Sampler: self.__gltf.samplers,
85 gltf2_io.Scene: self.__gltf.scenes,
86 gltf2_io.Skin: self.__gltf.skins,
87 gltf2_io.Texture: self.__gltf.textures
90 self.__propertyTypeLookup = [
91 gltf2_io.AccessorSparseIndices,
92 gltf2_io.AccessorSparse,
93 gltf2_io.AccessorSparseValues,
94 gltf2_io.AnimationChannel,
95 gltf2_io.AnimationChannelTarget,
96 gltf2_io.AnimationSampler,
97 gltf2_io.Asset,
98 gltf2_io.CameraOrthographic,
99 gltf2_io.CameraPerspective,
100 gltf2_io.MeshPrimitive,
101 gltf2_io.TextureInfo,
102 gltf2_io.MaterialPBRMetallicRoughness,
103 gltf2_io.MaterialNormalTextureInfoClass,
104 gltf2_io.MaterialOcclusionTextureInfoClass
107 self.__traverse(asset)
109 @property
110 def glTF(self):
111 if not self.__finalized:
112 raise RuntimeError("glTF requested, but buffers are not finalized yet")
113 return self.__gltf
115 def finalize_buffer(self, output_path=None, buffer_name=None, is_glb=False):
116 """Finalize the glTF and write buffers."""
117 if self.__finalized:
118 raise RuntimeError("Tried to finalize buffers for finalized glTF file")
120 if self.__buffer.byte_length > 0:
121 if is_glb:
122 uri = None
123 elif output_path and buffer_name:
124 with open(output_path + uri_to_path(buffer_name), 'wb') as f:
125 f.write(self.__buffer.to_bytes())
126 uri = buffer_name
127 else:
128 uri = self.__buffer.to_embed_string()
130 buffer = gltf2_io.Buffer(
131 byte_length=self.__buffer.byte_length,
132 extensions=None,
133 extras=None,
134 name=None,
135 uri=uri
137 self.__gltf.buffers.append(buffer)
139 self.__finalized = True
141 if is_glb:
142 return self.__buffer.to_bytes()
144 def add_draco_extension(self):
146 Register Draco extension as *used* and *required*.
148 :return:
150 self.__gltf.extensions_required.append('KHR_draco_mesh_compression')
151 self.__gltf.extensions_used.append('KHR_draco_mesh_compression')
153 def finalize_images(self):
155 Write all images.
157 output_path = self.export_settings['gltf_texturedirectory']
159 if self.__images:
160 os.makedirs(output_path, exist_ok=True)
162 for name, image in self.__images.items():
163 dst_path = output_path + "/" + name
164 with open(dst_path, 'wb') as f:
165 f.write(image.data)
168 def manage_gpu_instancing(self, node, also_mesh=False):
169 instances = {}
170 for child_idx in node.children:
171 child = self.__gltf.nodes[child_idx]
172 if child.children:
173 continue
174 if child.mesh is not None and child.mesh not in instances.keys():
175 instances[child.mesh] = []
176 if child.mesh is not None:
177 instances[child.mesh].append(child_idx)
179 # For now, manage instances only if there are all children of same object
180 # And this instances don't have any children
181 instances = {k:v for k, v in instances.items() if len(v) > 1}
183 holders = []
184 if len(instances.keys()) == 1 and also_mesh is False:
185 # There is only 1 set of instances. So using the parent as instance holder
186 holder = node
187 holders = [node]
188 elif len(instances.keys()) > 1 or (len(instances.keys()) == 1 and also_mesh is True):
189 for h in range(len(instances.keys())):
190 # Create a new node
191 n = gltf2_io.Node(
192 camera=None,
193 children=[],
194 extensions=None,
195 extras=None,
196 matrix=None,
197 mesh=None,
198 name=node.name + "." + str(h),
199 rotation=None,
200 scale=None,
201 skin=None,
202 translation=None,
203 weights=None,
205 n = self.__traverse_property(n)
206 idx = self.__to_reference(n)
208 # Add it to original empty
209 node.children.append(idx)
210 holders.append(self.__gltf.nodes[idx])
212 for idx, inst_key in enumerate(instances.keys()):
213 insts = instances[inst_key]
214 holder = holders[idx]
216 # Let's retrieve TRS of instances
217 translation = []
218 rotation = []
219 scale = []
220 for inst_node_idx in insts:
221 inst_node = self.__gltf.nodes[inst_node_idx]
222 t = inst_node.translation if inst_node.translation is not None else [0,0,0]
223 r = inst_node.rotation if inst_node.rotation is not None else [0,0,0,1]
224 s = inst_node.scale if inst_node.scale is not None else [1,1,1]
225 for i in t:
226 translation.append(i)
227 for i in r:
228 rotation.append(i)
229 for i in s:
230 scale.append(i)
232 # Create Accessors for the extension
233 ext = {}
234 ext['attributes'] = {}
235 ext['attributes']['TRANSLATION'] = gather_accessor(
236 gltf2_io_binary_data.BinaryData.from_list(translation, ComponentType.Float),
237 ComponentType.Float,
238 len(translation) // 3,
239 None,
240 None,
241 DataType.Vec3,
242 None
244 ext['attributes']['ROTATION'] = gather_accessor(
245 gltf2_io_binary_data.BinaryData.from_list(rotation, ComponentType.Float),
246 ComponentType.Float,
247 len(rotation) // 4,
248 None,
249 None,
250 DataType.Vec4,
251 None
253 ext['attributes']['SCALE'] = gather_accessor(
254 gltf2_io_binary_data.BinaryData.from_list(scale, ComponentType.Float),
255 ComponentType.Float,
256 len(scale) // 3,
257 None,
258 None,
259 DataType.Vec3,
260 None
263 # Add extension to the Node, and traverse it
264 if not holder.extensions:
265 holder.extensions = {}
266 holder.extensions["EXT_mesh_gpu_instancing"] = gltf2_io_extensions.Extension('EXT_mesh_gpu_instancing', ext, False)
267 holder.mesh = inst_key
268 self.__traverse(holder.extensions)
270 # Remove children from original Empty
271 new_children = []
272 for child_idx in node.children:
273 if child_idx not in insts:
274 new_children.append(child_idx)
275 node.children = new_children
277 self.nodes_idx_to_remove.extend(insts)
280 def manage_gpu_instancing_nodes(self, export_settings):
281 if export_settings['gltf_gpu_instances'] is True:
282 for scene_num in range(len(self.__gltf.scenes)):
283 # Modify the scene data in case of EXT_mesh_gpu_instancing export
285 self.nodes_idx_to_remove = []
286 for node_idx in self.__gltf.scenes[scene_num].nodes:
287 node = self.__gltf.nodes[node_idx]
288 if node.mesh is None:
289 self.manage_gpu_instancing(node)
290 else:
291 self.manage_gpu_instancing(node, also_mesh=True)
292 for child_idx in node.children:
293 child = self.__gltf.nodes[child_idx]
294 self.manage_gpu_instancing(child, also_mesh=child.mesh is not None)
296 # Slides other nodes index
298 self.nodes_idx_to_remove.sort()
299 for node_idx in self.__gltf.scenes[scene_num].nodes:
300 self.recursive_slide_node_idx(node_idx)
302 new_node_list = []
303 for node_idx in self.__gltf.scenes[scene_num].nodes:
304 len_ = len([i for i in self.nodes_idx_to_remove if i < node_idx])
305 new_node_list.append(node_idx - len_)
306 self.__gltf.scenes[scene_num].nodes = new_node_list
308 for skin in self.__gltf.skins:
309 new_joint_list = []
310 for node_idx in skin.joints:
311 len_ = len([i for i in self.nodes_idx_to_remove if i < node_idx])
312 new_joint_list.append(node_idx - len_)
313 skin.joints = new_joint_list
314 if skin.skeleton is not None:
315 len_ = len([i for i in self.nodes_idx_to_remove if i < skin.skeleton])
316 skin.skeleton = skin.skeleton - len_
318 # Remove animation channels that was targeting a node that will be removed
319 new_animation_list = []
320 for animation in self.__gltf.animations:
321 new_channel_list = []
322 for channel in animation.channels:
323 if channel.target.node not in self.nodes_idx_to_remove:
324 new_channel_list.append(channel)
325 animation.channels = new_channel_list
326 if len(animation.channels) > 0:
327 new_animation_list.append(animation)
328 self.__gltf.animations = new_animation_list
330 #TODO: remove unused animation accessors?
332 # And now really remove nodes
333 self.__gltf.nodes = [node for idx, node in enumerate(self.__gltf.nodes) if idx not in self.nodes_idx_to_remove]
336 def add_scene(self, scene: gltf2_io.Scene, active: bool = False, export_settings=None):
338 Add a scene to the glTF.
340 The scene should be built up with the generated glTF classes
341 :param scene: gltf2_io.Scene type. Root node of the scene graph
342 :param active: If true, sets the glTD.scene index to the added scene
343 :return: nothing
345 if self.__finalized:
346 raise RuntimeError("Tried to add scene to finalized glTF file")
348 scene_num = self.__traverse(scene)
349 if active:
350 self.__gltf.scene = scene_num
352 def recursive_slide_node_idx(self, node_idx):
353 node = self.__gltf.nodes[node_idx]
355 new_node_children = []
356 for child_idx in node.children:
357 len_ = len([i for i in self.nodes_idx_to_remove if i < child_idx])
358 new_node_children.append(child_idx - len_)
361 for child_idx in node.children:
362 self.recursive_slide_node_idx(child_idx)
364 node.children = new_node_children
366 def traverse_unused_skins(self, skins):
367 for s in skins:
368 self.__traverse(s)
370 def traverse_additional_textures(self):
371 if self.export_settings['gltf_unused_textures'] is True:
372 tab = []
373 for tex in self.export_settings['additional_texture_export']:
374 res = self.__traverse(tex)
375 tab.append(res)
377 self.additional_data.additional_textures = tab
379 def traverse_additional_images(self):
380 if self.export_settings['gltf_unused_images']:
381 for img in [img for img in bpy.data.images if img.source != "VIEWER"]:
382 # TODO manage full / partial / custom via hook ...
383 if img.name not in self.export_settings['exported_images'].keys():
384 self.__traverse(get_gltf_image_from_blender_image(img.name, self.export_settings))
386 def add_animation(self, animation: gltf2_io.Animation):
388 Add an animation to the glTF.
390 :param animation: glTF animation, with python style references (names)
391 :return: nothing
393 if self.__finalized:
394 raise RuntimeError("Tried to add animation to finalized glTF file")
396 self.__traverse(animation)
398 def __to_reference(self, property):
400 Append a child of root property to its respective list and return a reference into said list.
402 If the property is not child of root, the property itself is returned.
403 :param property: A property type object that should be converted to a reference
404 :return: a reference or the object itself if it is not child or root
406 gltf_list = self.__childOfRootPropertyTypeLookup.get(type(property), None)
407 if gltf_list is None:
408 # The object is not of a child of root --> don't convert to reference
409 return property
411 return self.__append_unique_and_get_index(gltf_list, property)
413 @staticmethod
414 def __append_unique_and_get_index(target: list, obj):
415 if obj in target:
416 return target.index(obj)
417 else:
418 index = len(target)
419 target.append(obj)
420 return index
422 def __add_image(self, image: gltf2_io_image_data.ImageData):
423 name = image.adjusted_name()
424 count = 1
425 regex = re.compile(r"-\d+$")
426 while name + image.file_extension in self.__images.keys():
427 regex_found = re.findall(regex, name)
428 if regex_found:
429 name = re.sub(regex, "-" + str(count), name)
430 else:
431 name += "-" + str(count)
433 count += 1
434 # TODO: allow embedding of images (base64)
436 self.__images[name + image.file_extension] = image
438 texture_dir = self.export_settings['gltf_texturedirectory']
439 abs_path = os.path.join(texture_dir, name + image.file_extension)
440 rel_path = os.path.relpath(
441 abs_path,
442 start=self.export_settings['gltf_filedirectory'],
444 return path_to_uri(rel_path)
446 @classmethod
447 def __get_key_path(cls, d: dict, keypath: List[str], default):
448 """Create if necessary and get the element at key path from a dict"""
449 key = keypath.pop(0)
451 if len(keypath) == 0:
452 v = d.get(key, default)
453 d[key] = v
454 return v
456 d_key = d.get(key, {})
457 d[key] = d_key
458 return cls.__get_key_path(d[key], keypath, default)
461 def traverse_extensions(self):
462 self.__traverse(self.__gltf.extensions)
464 def __traverse_property(self, node):
465 for member_name in [a for a in dir(node) if not a.startswith('__') and not callable(getattr(node, a))]:
466 new_value = self.__traverse(getattr(node, member_name))
467 setattr(node, member_name, new_value) # usually this is the same as before
469 # # TODO: maybe with extensions hooks we can find a more elegant solution
470 # if member_name == "extensions" and new_value is not None:
471 # for extension_name in new_value.keys():
472 # self.__append_unique_and_get_index(self.__gltf.extensions_used, extension_name)
473 # self.__append_unique_and_get_index(self.__gltf.extensions_required, extension_name)
474 return node
477 def __traverse(self, node):
479 Recursively traverse a scene graph consisting of gltf compatible elements.
481 The tree is traversed downwards until a primitive is reached. Then any ChildOfRoot property
482 is stored in the according list in the glTF and replaced with a index reference in the upper level.
484 # traverse nodes of a child of root property type and add them to the glTF root
485 if type(node) in self.__childOfRootPropertyTypeLookup:
486 node = self.__traverse_property(node)
487 idx = self.__to_reference(node)
488 # child of root properties are only present at root level --> replace with index in upper level
489 return idx
491 # traverse lists, such as children and replace them with indices
492 if isinstance(node, list):
493 for i in range(len(node)):
494 node[i] = self.__traverse(node[i])
495 return node
497 if isinstance(node, dict):
498 for key in node.keys():
499 node[key] = self.__traverse(node[key])
500 return node
502 # traverse into any other property
503 if type(node) in self.__propertyTypeLookup:
504 return self.__traverse_property(node)
506 # binary data needs to be moved to a buffer and referenced with a buffer view
507 if isinstance(node, gltf2_io_binary_data.BinaryData):
508 buffer_view = self.__buffer.add_and_get_view(node)
509 return self.__to_reference(buffer_view)
511 # image data needs to be saved to file
512 if isinstance(node, gltf2_io_image_data.ImageData):
513 image = self.__add_image(node)
514 return image
516 # extensions
517 # I don't know why, but after reloading script, this condition failed
518 # So using name comparison, instead of isinstance
519 # if isinstance(node, gltf2_io_extensions.Extension):
520 if isinstance(node, gltf2_io_extensions.Extension) \
521 or (node and hasattr(type(node), "extension")):
522 extension = self.__traverse(node.extension)
523 self.__append_unique_and_get_index(self.__gltf.extensions_used, node.name)
524 if node.required:
525 self.__append_unique_and_get_index(self.__gltf.extensions_required, node.name)
527 # extensions that lie in the root of the glTF.
528 # They need to be converted to a reference at place of occurrence
529 if isinstance(node, gltf2_io_extensions.ChildOfRootExtension):
530 root_extension_list = self.__get_key_path(self.__gltf.extensions, [node.name] + node.path, [])
531 idx = self.__append_unique_and_get_index(root_extension_list, extension)
532 return idx
534 return extension
536 # do nothing for any type that does not match a glTF schema (primitives)
537 return node
539 def fix_json(obj):
540 # TODO: move to custom JSON encoder
541 fixed = obj
542 if isinstance(obj, dict):
543 fixed = {}
544 for key, value in obj.items():
545 if key == 'extras' and value is not None:
546 fixed[key] = value
547 continue
548 if not __should_include_json_value(key, value):
549 continue
550 fixed[key] = fix_json(value)
551 elif isinstance(obj, list):
552 fixed = []
553 for value in obj:
554 fixed.append(fix_json(value))
555 elif isinstance(obj, float):
556 # force floats to int, if they are integers (prevent INTEGER_WRITTEN_AS_FLOAT validator warnings)
557 if int(obj) == obj:
558 return int(obj)
559 return fixed
561 def __should_include_json_value(key, value):
562 allowed_empty_collections = ["KHR_materials_unlit", "KHR_materials_specular"]
564 if value is None:
565 return False
566 elif __is_empty_collection(value) and key not in allowed_empty_collections:
567 return False
568 return True
571 def __is_empty_collection(value):
572 return (isinstance(value, dict) or isinstance(value, list)) and len(value) == 0