11"""
2- gltf.py
2+ gltf/__init__ .py
33------------
44
55Provides GLTF 2.0 exports of trimesh.Trimesh objects
1313
1414import numpy as np
1515
16- from .. import rendering , resources , transformations , util , visual
17- from ..caching import hash_fast
18- from ..constants import log , tol
19- from ..resolvers import ResolverLike , ZipResolver
20- from ..scene .cameras import Camera
21- from ..typed import Dict , List , NDArray , Optional , Stream
22- from ..util import triangle_strips_to_faces , unique_name
23- from .. visual . gloss import specular_to_pbr
16+ from ... import rendering , resources , transformations , util , visual
17+ from ... caching import hash_fast
18+ from ... constants import log , tol
19+ from ... resolvers import ResolverLike , ZipResolver
20+ from ... scene .cameras import Camera
21+ from ... typed import Dict , List , NDArray , Optional , Stream
22+ from ... util import triangle_strips_to_faces , unique_name
23+ from .extensions import handle_extensions
2424
2525# magic numbers which have meaning in GLTF
2626# most are uint32's of UTF-8 text
@@ -75,6 +75,7 @@ def export_gltf(
7575 tree_postprocessor = None ,
7676 embed_buffers = False ,
7777 extension_webp = False ,
78+ extension_draco = False ,
7879):
7980 """
8081 Export a scene object as a GLTF directory.
@@ -101,6 +102,9 @@ def export_gltf(
101102 Embed the buffer into JSON file as a base64 string in the URI
102103 extension_webp : bool
103104 Export textures as webP (using glTF's EXT_texture_webp extension).
105+ extension_draco : bool
106+ Compress mesh data using Draco (KHR_draco_mesh_compression).
107+ Requires the `dracox` package to be installed.
104108
105109 Returns
106110 ----------
@@ -117,6 +121,7 @@ def export_gltf(
117121 unitize_normals = unitize_normals ,
118122 include_normals = include_normals ,
119123 extension_webp = extension_webp ,
124+ extension_draco = extension_draco ,
120125 )
121126
122127 # allow custom postprocessing
@@ -172,6 +177,7 @@ def export_glb(
172177 tree_postprocessor = None ,
173178 buffer_postprocessor = None ,
174179 extension_webp = False ,
180+ extension_draco = False ,
175181):
176182 """
177183 Export a scene as a binary GLTF (GLB) file.
@@ -189,6 +195,9 @@ def export_glb(
189195 before exporting.
190196 extension_webp : bool
191197 Export textures as webP using EXT_texture_webp extension.
198+ extension_draco : bool
199+ Compress mesh data using Draco (KHR_draco_mesh_compression).
200+ Requires the `dracox` package to be installed.
192201
193202 Returns
194203 ----------
@@ -206,6 +215,7 @@ def export_glb(
206215 include_normals = include_normals ,
207216 buffer_postprocessor = buffer_postprocessor ,
208217 extension_webp = extension_webp ,
218+ extension_draco = extension_draco ,
209219 )
210220
211221 # A bufferView is a slice of a file
@@ -610,6 +620,7 @@ def _create_gltf_structure(
610620 unitize_normals = None ,
611621 buffer_postprocessor = None ,
612622 extension_webp = False ,
623+ extension_draco = False ,
613624):
614625 """
615626 Generate a GLTF header.
@@ -626,6 +637,8 @@ def _create_gltf_structure(
626637 Unitize all exported normals so as to pass GLTF validation
627638 extension_webp : bool
628639 Export textures as webP using EXT_texture_webp extension.
640+ extension_draco : bool
641+ Compress mesh data using Draco (KHR_draco_mesh_compression).
629642
630643 Returns
631644 ---------------
@@ -683,6 +696,7 @@ def _create_gltf_structure(
683696 unitize_normals = unitize_normals ,
684697 mat_hashes = mat_hashes ,
685698 extension_webp = extension_webp ,
699+ extension_draco = extension_draco ,
686700 )
687701 elif util .is_instance_named (geometry , "Path" ):
688702 # add Path2D and Path3D objects
@@ -703,26 +717,33 @@ def _create_gltf_structure(
703717 tree .update (nodes )
704718
705719 extensions_used = set ()
720+ extensions_required = set ()
706721 # Add any scene extensions used
707722 if "extensions" in tree :
708723 extensions_used = extensions_used .union (set (tree ["extensions" ].keys ()))
709724 # Add any mesh extensions used
710725 for mesh in tree ["meshes" ]:
711726 if "extensions" in mesh :
712727 extensions_used = extensions_used .union (set (mesh ["extensions" ].keys ()))
728+ # Check primitives for extensions too
729+ for prim in mesh .get ("primitives" , []):
730+ if "extensions" in prim :
731+ extensions_used = extensions_used .union (set (prim ["extensions" ].keys ()))
713732 # Add any extensions already in the tree (e.g. node extensions)
714733 if "extensionsUsed" in tree :
715734 extensions_used = extensions_used .union (set (tree ["extensionsUsed" ]))
716735 # Add WebP if used
717736 if extension_webp :
718737 extensions_used .add ("EXT_texture_webp" )
738+ extensions_required .add ("EXT_texture_webp" )
739+ # Add Draco if used (no fallback, so required)
740+ if extension_draco :
741+ extensions_used .add ("KHR_draco_mesh_compression" )
742+ extensions_required .add ("KHR_draco_mesh_compression" )
719743 if len (extensions_used ) > 0 :
720744 tree ["extensionsUsed" ] = list (extensions_used )
721-
722- # Also add WebP to required (no fallback currently implemented)
723- # 'extensionsRequired' aren't currently used so this doesn't overwrite
724- if extension_webp :
725- tree ["extensionsRequired" ] = ["EXT_texture_webp" ]
745+ if len (extensions_required ) > 0 :
746+ tree ["extensionsRequired" ] = list (extensions_required )
726747
727748 if buffer_postprocessor is not None :
728749 buffer_postprocessor (buffer_items , tree )
@@ -748,6 +769,7 @@ def _append_mesh(
748769 unitize_normals : bool ,
749770 mat_hashes : dict ,
750771 extension_webp : bool ,
772+ extension_draco : bool = False ,
751773):
752774 """
753775 Append a mesh to the scene structure and put the
@@ -773,6 +795,8 @@ def _append_mesh(
773795 Which materials have already been added
774796 extension_webp : bool
775797 Export textures as webP (using glTF's EXT_texture_webp extension).
798+ extension_draco : bool
799+ Compress mesh data using Draco (KHR_draco_mesh_compression).
776800 """
777801 # return early from empty meshes to avoid crashing later
778802 if len (mesh .faces ) == 0 or len (mesh .vertices ) == 0 :
@@ -963,6 +987,24 @@ def _append_mesh(
963987 data = data ,
964988 )
965989
990+ # Handle Draco compression via extension handler
991+ if extension_draco :
992+ # Determine if normals should be included
993+ should_include_normals = include_normals or (
994+ include_normals is None and "vertex_normals" in mesh ._cache .cache
995+ )
996+ # Call primitive_export handlers
997+ handle_extensions (
998+ extensions = {"KHR_draco_mesh_compression" : {}},
999+ scope = "primitive_export" ,
1000+ mesh = mesh ,
1001+ name = name ,
1002+ tree = tree ,
1003+ buffer_items = buffer_items ,
1004+ primitive = current ["primitives" ][0 ],
1005+ include_normals = should_include_normals ,
1006+ )
1007+
9661008 tree ["meshes" ].append (current )
9671009
9681010
@@ -1318,35 +1360,29 @@ def _parse_materials(header, views, resolver=None):
13181360 List of trimesh.visual.texture.Material objects
13191361 """
13201362
1321- def parse_values_and_textures ( input_dict ):
1363+ def parse_textures ( * , data ):
13221364 result = {}
1323- for k , v in input_dict .items ():
1365+ for k , v in data .items ():
13241366 if isinstance (v , (list , tuple )):
13251367 # colors are always float 0.0 - 1.0 in GLTF
13261368 result [k ] = np .array (v , dtype = np .float64 )
13271369 elif not isinstance (v , dict ):
13281370 result [k ] = v
13291371 elif images is not None and "index" in v :
13301372 try :
1331- # get the index of image for texture
1373+ index = None
13321374 texture = header ["textures" ][v ["index" ]]
1333- # check to see if this is using a webp extension texture
1334- # should this be case sensitive?
1335- webp = (
1336- texture .get ("extensions" , {})
1337- .get ("EXT_texture_webp" , {})
1338- .get ("source" )
1339- )
1340- if webp is not None :
1341- idx = webp
1342- elif "source" in texture :
1343- # fallback (or primary, if extensions are not present)
1344- idx = texture ["source" ]
1345- else :
1346- # no source available
1347- continue
1348- # store the actual image as the value
1349- result [k ] = images [idx ]
1375+ # Handle texture extensions through registry
1376+ if tex_ext := texture .get ("extensions" ):
1377+ index = handle_extensions (
1378+ extensions = tex_ext , scope = "texture_source"
1379+ )
1380+
1381+ if index is None :
1382+ # fall back to standard source key
1383+ index = texture .get ("source" )
1384+ if index is not None :
1385+ result [k ] = images [index ]
13501386 except BaseException :
13511387 log .debug ("unable to store texture" , exc_info = True )
13521388 return result
@@ -1364,15 +1400,21 @@ def parse_values_and_textures(input_dict):
13641400 # add keys of keys to top level dict
13651401 loopable .update (loopable .pop ("pbrMetallicRoughness" ))
13661402
1367- ext = mat .get ("extensions" , {}).get (
1368- "KHR_materials_pbrSpecularGlossiness" , None
1369- )
1370- if isinstance (ext , dict ):
1371- ext_params = parse_values_and_textures (ext )
1372- loopable .update (specular_to_pbr (** ext_params ))
1403+ # Handle material extensions through registry
1404+ if mat_extensions := mat .get ("extensions" ):
1405+ ext_results = handle_extensions (
1406+ extensions = mat_extensions ,
1407+ scope = "material" ,
1408+ parse_textures = parse_textures ,
1409+ images = images ,
1410+ )
1411+ # Flatten extension results into the material parameters
1412+ for ext_result in ext_results .values ():
1413+ if isinstance (ext_result , dict ):
1414+ loopable .update (ext_result )
13731415
13741416 # save flattened keys we can use for kwargs
1375- pbr = parse_values_and_textures ( loopable )
1417+ pbr = parse_textures ( data = loopable )
13761418 # create a PBR material object for the GLTF material
13771419 materials .append (visual .material .PBRMaterial (** pbr ))
13781420
@@ -1523,6 +1565,17 @@ def _read_buffers(
15231565 metadata ["gltf_extensions" ] = m ["extensions" ]
15241566
15251567 for p in m ["primitives" ]:
1568+ # Handle primitive preprocessing extensions (e.g. Draco decompression)
1569+ # These run before reading accessors since they may modify them
1570+ if prim_extensions := p .get ("extensions" ):
1571+ handle_extensions (
1572+ extensions = prim_extensions ,
1573+ scope = "primitive_preprocess" ,
1574+ primitive = p ,
1575+ accessors = access ,
1576+ views = views ,
1577+ )
1578+
15261579 # if we don't have a triangular mesh continue
15271580 # if not specified assume it is a mesh
15281581 kwargs = deepcopy (mesh_kwargs )
@@ -1543,7 +1596,7 @@ def _read_buffers(
15431596
15441597 if mode == _GL_LINES :
15451598 # load GL_LINES into a Path object
1546- from ..path .entities import Line
1599+ from ... path .entities import Line
15471600
15481601 kwargs ["vertices" ] = access [attr ["POSITION" ]]
15491602 kwargs ["entities" ] = [Line (points = np .arange (len (kwargs ["vertices" ])))]
@@ -1642,6 +1695,16 @@ def _read_buffers(
16421695 }
16431696 if len (custom ) > 0 :
16441697 kwargs ["vertex_attributes" ] = custom
1698+
1699+ # Process primitive-level extensions through registry
1700+ if prim_extensions := p .get ("extensions" ):
1701+ handle_extensions (
1702+ extensions = prim_extensions ,
1703+ scope = "primitive" ,
1704+ primitive = p ,
1705+ mesh_kwargs = kwargs ,
1706+ accessors = access ,
1707+ )
16451708 else :
16461709 log .debug ("skipping primitive with mode %s!" , mode )
16471710 continue
@@ -2098,7 +2161,7 @@ def _append_material(mat, tree, buffer_items, mat_hashes, extension_webp):
20982161 # add a reference to the base color texture
20992162 result [key ] = {"index" : len (tree ["textures" ])}
21002163
2101- # add an object for the texture according to the WebP extension
2164+ # add texture object, optionally using EXT_texture_webp
21022165 if extension_webp :
21032166 tree ["textures" ].append (
21042167 {"extensions" : {"EXT_texture_webp" : {"source" : index }}}
@@ -2174,7 +2237,7 @@ def get_schema():
21742237 """
21752238 # replace references
21762239 # get zip resolver to access referenced assets
2177- from ..schemas import resolve
2240+ from ... schemas import resolve
21782241
21792242 # get a blob of a zip file including the GLTF 2.0 schema
21802243 stream = resources .get_stream ("schema/gltf2.schema.zip" )
0 commit comments