1 # SPDX-FileCopyrightText: 2018-2021 The glTF-Blender-IO authors
3 # SPDX-License-Identifier: Apache-2.0
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
21 additional_textures
= []
25 The glTF exporter flattens a scene graph to a glTF serializable format.
27 Any child properties are replaced with references where necessary
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(
39 generator
='Khronos glTF Blender I/O v' + get_version_string(),
43 export_user_extensions('gather_asset_hook', export_settings
, asset
)
45 self
.__gltf
= gltf2_io
.Gltf(
53 extensions_required
=[],
68 self
.additional_data
= AdditionalData()
70 self
.__buffer
= gltf2_io_buffer
.Buffer()
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
,
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
)
111 if not self
.__finalized
:
112 raise RuntimeError("glTF requested, but buffers are not finalized yet")
115 def finalize_buffer(self
, output_path
=None, buffer_name
=None, is_glb
=False):
116 """Finalize the glTF and write buffers."""
118 raise RuntimeError("Tried to finalize buffers for finalized glTF file")
120 if self
.__buffer
.byte_length
> 0:
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())
128 uri
= self
.__buffer
.to_embed_string()
130 buffer = gltf2_io
.Buffer(
131 byte_length
=self
.__buffer
.byte_length
,
137 self
.__gltf
.buffers
.append(buffer)
139 self
.__finalized
= True
142 return self
.__buffer
.to_bytes()
144 def add_draco_extension(self
):
146 Register Draco extension as *used* and *required*.
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
):
157 output_path
= self
.export_settings
['gltf_texturedirectory']
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
:
168 def manage_gpu_instancing(self
, node
, also_mesh
=False):
170 for child_idx
in node
.children
:
171 child
= self
.__gltf
.nodes
[child_idx
]
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}
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
188 elif len(instances
.keys()) > 1 or (len(instances
.keys()) == 1 and also_mesh
is True):
189 for h
in range(len(instances
.keys())):
198 name
=node
.name
+ "." + str(h
),
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
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]
226 translation
.append(i
)
232 # Create Accessors for the extension
234 ext
['attributes'] = {}
235 ext
['attributes']['TRANSLATION'] = gather_accessor(
236 gltf2_io_binary_data
.BinaryData
.from_list(translation
, ComponentType
.Float
),
238 len(translation
) // 3,
244 ext
['attributes']['ROTATION'] = gather_accessor(
245 gltf2_io_binary_data
.BinaryData
.from_list(rotation
, ComponentType
.Float
),
253 ext
['attributes']['SCALE'] = gather_accessor(
254 gltf2_io_binary_data
.BinaryData
.from_list(scale
, ComponentType
.Float
),
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
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
)
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
)
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
:
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
346 raise RuntimeError("Tried to add scene to finalized glTF file")
348 scene_num
= self
.__traverse
(scene
)
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
):
370 def traverse_additional_textures(self
):
371 if self
.export_settings
['gltf_unused_textures'] is True:
373 for tex
in self
.export_settings
['additional_texture_export']:
374 res
= self
.__traverse
(tex
)
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)
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
411 return self
.__append
_unique
_and
_get
_index
(gltf_list
, property)
414 def __append_unique_and_get_index(target
: list, obj
):
416 return target
.index(obj
)
422 def __add_image(self
, image
: gltf2_io_image_data
.ImageData
):
423 name
= image
.adjusted_name()
425 regex
= re
.compile(r
"-\d+$")
426 while name
+ image
.file_extension
in self
.__images
.keys():
427 regex_found
= re
.findall(regex
, name
)
429 name
= re
.sub(regex
, "-" + str(count
), name
)
431 name
+= "-" + str(count
)
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(
442 start
=self
.export_settings
['gltf_filedirectory'],
444 return path_to_uri(rel_path
)
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"""
451 if len(keypath
) == 0:
452 v
= d
.get(key
, default
)
456 d_key
= d
.get(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)
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
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
])
497 if isinstance(node
, dict):
498 for key
in node
.keys():
499 node
[key
] = self
.__traverse
(node
[key
])
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
)
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
)
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
)
536 # do nothing for any type that does not match a glTF schema (primitives)
540 # TODO: move to custom JSON encoder
542 if isinstance(obj
, dict):
544 for key
, value
in obj
.items():
545 if key
== 'extras' and value
is not None:
548 if not __should_include_json_value(key
, value
):
550 fixed
[key
] = fix_json(value
)
551 elif isinstance(obj
, list):
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)
561 def __should_include_json_value(key
, value
):
562 allowed_empty_collections
= ["KHR_materials_unlit", "KHR_materials_specular"]
566 elif __is_empty_collection(value
) and key
not in allowed_empty_collections
:
571 def __is_empty_collection(value
):
572 return (isinstance(value
, dict) or isinstance(value
, list)) and len(value
) == 0