File size: 3,886 Bytes
1fc2cc9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
"""Bounded GLB metadata preflight; does not load textures or allocate mesh arrays."""
import json
import struct
from pathlib import Path

MAX_FACES = 500_000
MAX_VERTICES = 500_000
MAX_JSON = 16 * 1024 * 1024

def inspect_glb(path):
    path = Path(path)
    size = path.stat().st_size
    with path.open("rb") as stream:
        header = stream.read(20)
        if len(header) != 20:
            raise ValueError("Truncated GLB header")
        magic, version, length, json_length, chunk = struct.unpack("<4sIIII", header)
        if magic != b"glTF" or version != 2 or length != size or chunk != 0x4E4F534A:
            raise ValueError("Invalid GLB 2.0 header or file length")
        if json_length > MAX_JSON or json_length > size - 20:
            raise ValueError("GLB metadata is too large or truncated")
        doc = json.loads(stream.read(json_length))
    meshes = doc.get("meshes", [])
    accessors = doc.get("accessors", [])
    vertices = faces = 0
    unsupported = False
    for mesh in meshes:
        for primitive in mesh.get("primitives", []):
            if primitive.get("mode", 4) != 4:
                unsupported = True
                continue
            position = primitive.get("attributes", {}).get("POSITION")
            if position is None:
                raise ValueError("Mesh primitive has no POSITION accessor")
            count = int(accessors[position]["count"])
            index = primitive.get("indices")
            indices = int(accessors[index]["count"]) if index is not None else count
            if count < 0 or indices < 0 or indices % 3:
                raise ValueError("Invalid vertex or triangle count")
            vertices += count
            faces += indices // 3
    # Count node instances because upstream merges transformed scene geometry.
    nodes = doc.get("nodes", [])
    instances = {}
    for node in nodes:
        if "mesh" in node:
            instances[node["mesh"]] = instances.get(node["mesh"], 0) + 1
    instantiated_faces = 0
    instantiated_vertices = 0
    for i, mesh in enumerate(meshes):
        multiplicity = max(1, instances.get(i, 0))
        for primitive in mesh.get("primitives", []):
            pos = primitive.get("attributes", {}).get("POSITION")
            if pos is None:
                continue
            count = int(accessors[pos]["count"])
            index = primitive.get("indices")
            n = int(accessors[index]["count"]) if index is not None else count
            instantiated_vertices += count * multiplicity
            if primitive.get("mode", 4) == 4:
                instantiated_faces += n // 3 * multiplicity
    required_extensions = doc.get("extensionsRequired", [])
    unsupported = unsupported or any(x in required_extensions for x in
                                     ("KHR_draco_mesh_compression", "EXT_meshopt_compression"))
    dense = instantiated_faces > MAX_FACES or instantiated_vertices > MAX_VERTICES or size > 512 * 1024**2
    warning = ("Preprocessing/decimation required: this input exceeds the conservative "
               "500,000-face or 500,000-vertex limit. The original is preserved; "
               "inference has not started.") if dense else ""
    if unsupported:
        warning = "Preprocessing required: export uncompressed triangle geometry before inference."
    return dict(file_size_bytes=size, mesh_count=len(meshes), vertices=vertices,
                triangles=faces, instantiated_vertices=instantiated_vertices,
                instantiated_triangles=instantiated_faces,
                materials=len(doc.get("materials", [])),
                textures_present=bool(doc.get("textures") or doc.get("images")),
                density_risk="high" if dense else "within conservative limit",
                inference_allowed=bool(meshes) and faces > 0 and not dense and not unsupported,
                warning=warning)