vr-app/tools/make_calibration_glb.py
Thomas Fransolet 0310d28b5e Canal VR Meta Quest : projet Unity, viewer web et documentation
Premier commit du cinquième front. Trois parties :

- `unity/MyInfoMateVR/` : le projet Unity (6000.0.83f1, URP, Meta XR SDK 205),
  un APK unique pour tous les clients. Menu flottant à sélection au regard,
  appairage, chargement de scène GLB, POI, cache de contenu, télémétrie.
- `unity-overlay/` : les mêmes scripts à recopier sur un projet Unity neuf, avec
  les pièges rencontrés consignés dans son README.
- `viewer/` : viewer et éditeur de scène web autonome (Vite, TypeScript,
  three.js), partagé avec les autres fronts.
- `docs/` : état des lieux, setup Unity, décisions d'architecture et plan
  d'exécution en 9 étapes.

La scène est décrite par un `scene.json` poussé par `adb push` : l'app le
préfère à celui embarqué dans l'APK.

Les binaires (GLB, textures de l'échantillon Sponza, DLL Meta XR) passent par
Git LFS dès ce premier commit — les y faire entrer après coup demanderait de
réécrire l'historique. Les artefacts régénérés par l'éditeur et par CMake
(`Library/`, `.utmp/`, Burst debug) sont ignorés.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 15:26:07 +02:00

136 lines
4.9 KiB
Python

"""Génère calibration.glb — le repère de calibration de l'étape E1.
Trois branches de longueurs DIFFÉRENTES, aux couleurs de la convention habituelle :
+X rouge (1.00 m) +Y vert (0.60 m) +Z bleu (0.30 m)
Pourquoi trois longueurs différentes : un repère symétrique ne permet pas de voir
une inversion d'axe. Ici, si la conversion glTF -> Unity se trompe de signe, la
branche rouge part du mauvais côté par rapport à la bleue, et ça se voit sans
mesurer.
Le modèle est écrit en coordonnées glTF (main droite, Y-up, mètres) — c'est-à-dire
exactement la convention du manifeste. C'est ce qui en fait un test valable :
l'app place un marqueur AUX MÊMES coordonnées, sans passer par l'importeur.
Usage : python3 make_calibration_glb.py [chemin/de/sortie.glb]
"""
import base64
import json
import struct
import sys
from pathlib import Path
AXES = [
# (nom, dimensions du pavé, couleur RGBA)
("AxisX", (1.00, 0.04, 0.04), (0.85, 0.15, 0.15, 1.0)),
("AxisY", (0.04, 0.60, 0.04), (0.15, 0.75, 0.20, 1.0)),
("AxisZ", (0.04, 0.04, 0.30), (0.15, 0.35, 0.90, 1.0)),
]
FACES = [
((0, 0, 1), [(0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1)]),
((0, 0, -1), [(1, 0, 0), (0, 0, 0), (0, 1, 0), (1, 1, 0)]),
((1, 0, 0), [(1, 0, 1), (1, 0, 0), (1, 1, 0), (1, 1, 1)]),
((-1, 0, 0), [(0, 0, 0), (0, 0, 1), (0, 1, 1), (0, 1, 0)]),
((0, 1, 0), [(0, 1, 1), (1, 1, 1), (1, 1, 0), (0, 1, 0)]),
((0, -1, 0), [(0, 0, 0), (1, 0, 0), (1, 0, 1), (0, 0, 1)]),
]
def box(size):
"""Pavé dont un coin est à l'origine et qui s'étend vers +X, +Y, +Z."""
positions, normals, indices = [], [], []
for normal, corners in FACES:
base = len(positions)
for cx, cy, cz in corners:
positions.append((cx * size[0], cy * size[1], cz * size[2]))
normals.append(normal)
indices += [base, base + 1, base + 2, base, base + 2, base + 3]
return positions, normals, indices
def main(out_path):
buffer = bytearray()
buffer_views, accessors, meshes, materials, nodes = [], [], [], [], []
def add_view(data, target):
while len(buffer) % 4:
buffer.append(0)
offset = len(buffer)
buffer.extend(data)
buffer_views.append(
{"buffer": 0, "byteOffset": offset, "byteLength": len(data), "target": target}
)
return len(buffer_views) - 1
for name, size, color in AXES:
positions, normals, indices = box(size)
pos_view = add_view(b"".join(struct.pack("<3f", *p) for p in positions), 34962)
nrm_view = add_view(b"".join(struct.pack("<3f", *n) for n in normals), 34962)
idx_view = add_view(b"".join(struct.pack("<H", i) for i in indices), 34963)
accessors.append({
"bufferView": pos_view, "componentType": 5126, "count": len(positions),
"type": "VEC3",
"min": [min(p[i] for p in positions) for i in range(3)],
"max": [max(p[i] for p in positions) for i in range(3)],
})
accessors.append({
"bufferView": nrm_view, "componentType": 5126, "count": len(normals),
"type": "VEC3",
})
accessors.append({
"bufferView": idx_view, "componentType": 5123, "count": len(indices),
"type": "SCALAR",
})
base = len(accessors) - 3
materials.append({
"name": name + "Mat",
"pbrMetallicRoughness": {
"baseColorFactor": list(color), "metallicFactor": 0.0, "roughnessFactor": 0.8,
},
})
meshes.append({
"name": name,
"primitives": [{
"attributes": {"POSITION": base, "NORMAL": base + 1},
"indices": base + 2,
"material": len(materials) - 1,
}],
})
nodes.append({"name": name, "mesh": len(meshes) - 1})
gltf = {
"asset": {
"version": "2.0",
"generator": "MyInfoMate VR — make_calibration_glb.py",
},
"scene": 0,
"scenes": [{"name": "Calibration", "nodes": list(range(len(nodes)))}],
"nodes": nodes,
"meshes": meshes,
"materials": materials,
"accessors": accessors,
"bufferViews": buffer_views,
"buffers": [{"byteLength": len(buffer)}],
}
json_chunk = json.dumps(gltf, separators=(",", ":")).encode("utf-8")
json_chunk += b" " * (-len(json_chunk) % 4)
bin_chunk = bytes(buffer) + b"\0" * (-len(buffer) % 4)
glb = struct.pack("<III", 0x46546C67, 2, 12 + 8 + len(json_chunk) + 8 + len(bin_chunk))
glb += struct.pack("<II", len(json_chunk), 0x4E4F534A) + json_chunk
glb += struct.pack("<II", len(bin_chunk), 0x004E4942) + bin_chunk
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
Path(out_path).write_bytes(glb)
print(f"{out_path}{len(glb)} octets, {len(nodes)} branches")
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "calibration.glb")