Skip to content

Commit 6b72bf7

Browse files
authored
Release: Extensions (#2497)
- capture STDERR to fix #2495 - release #2492 - release #2493 - moves `trimesh/exchange/gltf.py` to `trimesh/exchange/gltf/__init__.py` which is identical for import users, but lets us add the additional `extensions.py` next to it.
2 parents d7c9985 + 15b8446 commit 6b72bf7

File tree

5 files changed

+387
-51
lines changed

5 files changed

+387
-51
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"]
55
[project]
66
name = "trimesh"
77
requires-python = ">=3.8"
8-
version = "4.10.1"
8+
version = "4.11.0"
99
authors = [{name = "Michael Dawson-Haggerty", email = "[email protected]"}]
1010
license = {file = "LICENSE.md"}
1111
description = "Import, export, process, analyze and view triangular meshes."
@@ -48,6 +48,7 @@ packages = [
4848
"trimesh.visual",
4949
"trimesh.viewer",
5050
"trimesh.exchange",
51+
"trimesh.exchange.gltf",
5152
"trimesh.resources",
5253
"trimesh.interfaces",
5354
]

trimesh/exchange/binvox.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ def __call__(self, path, overwrite=False):
553553

554554
# generalizes to python2 and python3
555555
# will capture terminal output into variable rather than printing
556-
verbosity = subprocess.check_output(self._args)
556+
verbosity = subprocess.check_output(self._args, stderr=subprocess.STDOUT)
557557

558558
# if requested print ourselves
559559
if self.verbose:
@@ -598,6 +598,7 @@ def voxelize_mesh(mesh, binvoxer=None, export_type="off", **binvoxer_kwargs):
598598
out_path = binvoxer(model_path)
599599
with open(out_path, "rb") as fp:
600600
out_model = load_binvox(fp)
601+
601602
return out_model
602603

603604

Lines changed: 107 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
gltf.py
2+
gltf/__init__.py
33
------------
44
55
Provides GLTF 2.0 exports of trimesh.Trimesh objects
@@ -13,14 +13,14 @@
1313

1414
import 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

Comments
 (0)