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>
253 lines
16 KiB
Markdown
253 lines
16 KiB
Markdown
# unity-overlay — les fichiers à déposer sur le projet Unity
|
|
|
|
Ce dossier **n'est pas un projet Unity**. C'est l'arborescence des fichiers que je génère,
|
|
calquée sur celle du projet, à recopier par-dessus une fois le projet créé par Unity Hub.
|
|
|
|
**Pourquoi un dossier séparé** : Unity Hub refuse de créer un projet dans un dossier non vide.
|
|
Il faut donc que le projet naisse d'abord, et que mes fichiers arrivent ensuite. Ça a un
|
|
avantage secondaire — à chaque étape je remets à jour cet overlay, tu recopies, rien ne se perd.
|
|
|
|
## Ordre des opérations
|
|
|
|
1. **Créer le projet** — Unity Hub → New project → **Universal 3D (URP)** → nom `MyInfoMateVR`,
|
|
emplacement `vr-app/unity/`. Détail : [../docs/01-phase0-setup-unity.md](../docs/01-phase0-setup-unity.md).
|
|
|
|
2. **Importer le Meta XR SDK** (Asset Store → *Meta XR All-in-One SDK*) et lancer
|
|
*Edit → Project Settings → Meta XR* → **Fix All**.
|
|
|
|
3. **Recopier cet overlay** :
|
|
|
|
```powershell
|
|
Copy-Item -Recurse -Force vr-app\unity-overlay\* vr-app\unity\MyInfoMateVR\
|
|
```
|
|
|
|
⚠️ `Packages/manifest.json` **écrase** celui du projet. C'est voulu — il porte glTFast, KTX2,
|
|
Draco et OpenXR. Si tu as déjà importé le SDK Meta, ses lignes `com.meta.xr.*` sont
|
|
dans le fichier du projet et seront perdues : dans ce cas, fusionne les deux blocs
|
|
`dependencies` à la main plutôt que d'écraser. Unity recompile tout seul au retour dans l'éditeur.
|
|
|
|
4. **Déposer les deux GLB** dans `Assets/StreamingAssets/` :
|
|
- `world.glb` — ton monde Marble exporté en mesh
|
|
- `persona.glb` — un personnage stylisé avec une animation idle
|
|
|
|
`calibration.glb` y est déjà, il vient de cet overlay.
|
|
|
|
5. **MyInfoMate → Construire la scène Boot** (menu ajouté par `BuildBootScene.cs`).
|
|
|
|
6. **Meta → Tools → Building Blocks → Camera Rig**, à glisser dans la scène. C'est la seule
|
|
chose que je ne peux pas générer : référencer le rig depuis un script d'éditeur créerait une
|
|
dépendance de compilation sur un package importé à la main.
|
|
|
|
7. **File → Build Profiles → Build And Run.**
|
|
|
|
## Regénérer le repère de calibration
|
|
|
|
```bash
|
|
python3 vr-app/tools/make_calibration_glb.py vr-app/unity-overlay/Assets/StreamingAssets/calibration.glb
|
|
```
|
|
|
|
## ⚠️ Deux numérotations, et elles ne se mélangent pas
|
|
|
|
| Préfixe | Plan | Ce que ça couvre |
|
|
|---|---|---|
|
|
| **S0-S8** | [`../docs/04-phase3-plan.md`](../docs/04-phase3-plan.md) | La piste **Scène 3D** : monde GLB, manifeste, viewer web |
|
|
| **E0-E11** | [`../../DOCS/v2/vr-quest-unity-plan.md`](../../DOCS/v2/vr-quest-unity-plan.md) §XR-4 | L'**app Unity du canal VR** : appairage, export, menu, 360° |
|
|
|
|
Les scripts de bootstrap parlaient `E1`/`E2` en voulant dire `S1`/`S2` — **renommés le 12/09**,
|
|
alors que la doc l'était depuis le 11/09. Si tu croises un `E1Bootstrap` quelque part, il est périmé.
|
|
|
|
## Ce que contient l'overlay aujourd'hui (S1-S2 · E2-E5, E8-E10)
|
|
|
|
| Fichier | Rôle |
|
|
|---|---|
|
|
| `Packages/manifest.json` | glTFast + KTX2 + Draco + OpenXR |
|
|
| `Assets/Scripts/Scene/GltfSpace.cs` | **La** conversion glTF → Unity. Le seul endroit |
|
|
| `Assets/Scripts/Scene/GltfLoader.cs` | Chargement GLB au runtime, URI StreamingAssets Android comprise |
|
|
| `Assets/Scripts/Scene/CalibrationCheck.cs` | Le test de la conversion — critère de validation n°2 de S1 |
|
|
| `Assets/Scripts/Scene/NavigationBounds.cs` | Zone circulaire, plafond 3 m, repousse sans téléporter |
|
|
| `Assets/Scripts/Scene/PersonaInstance.cs` | GLB + idle en boucle + regard vers le visiteur |
|
|
| `Assets/Scripts/Boot/S1Bootstrap.cs` | Orchestration de S1 — **provisoire**, remplacé par `SceneBuilder` en S2 |
|
|
| `Assets/Scripts/Boot/S2Bootstrap.cs` | S2 — la scène vient de `scene.json` |
|
|
| `Assets/Scripts/Net/ApiClient.cs` | **E2-E3** — le seul endroit qui parle à manager-service (`X-Api-Key`, erreurs en phrases) |
|
|
| `Assets/Scripts/Net/PairingService.cs` | **E2** — code PIN → clé d'API → `POST /api/device` avec `appType = 3` |
|
|
| `Assets/Scripts/Net/ConfigurationExport.cs` | **E3** — `GET /api/configuration/{id}/export`, un appel pour tout le contenu |
|
|
| `Assets/Scripts/Net/ContentCache.cs` | **E4** — cache disque, écriture atomique, médias nommés par hachage d'URL |
|
|
| `Assets/Scripts/Net/ContentService.cs` | **E4** — cache d'abord, rafraîchissement de fond silencieux |
|
|
| `Assets/Scripts/Net/Telemetry.cs` | **E9** — `POST /api/stats/event` en « tire et oublie », `appType = "VR"` |
|
|
| `Assets/Scripts/Boot/PairingBootstrap.cs` | **E2-E4** — appaire, sert le cache, rafraîchit, affiche les sections dans le casque |
|
|
| `Assets/Scripts/Ui/Label.cs` | **Tout le texte du casque.** TextMeshPro, avec repli sur `TextMesh` si les ressources TMP manquent |
|
|
| `Assets/Scripts/Menu/MenuFeedback.cs` | Son de survol et de sélection (**synthétisés**, aucun fichier) + vibration de la manette |
|
|
| `Assets/Scripts/Menu/AimSelector.cs` | **E5** — viser à la **manette**, à la **main** ou à la **tête** ; temporisation 1,2 s à la tête seulement |
|
|
| `Assets/Scripts/Menu/MenuItemPanel.cs` | **E5** — un panneau : vignette, titre, anneau de progression |
|
|
| `Assets/Scripts/Menu/FloatingMenu.cs` | **E5** — l'arc de panneaux, posé une fois, qui ne suit pas la tête |
|
|
| `Assets/Scripts/Menu/PagedView.cs` | **E8** — la vue paginée commune : image de 1,6 m, texte, flèches, retour |
|
|
| `Assets/Scripts/Menu/SectionPages.cs` | **E8** — ce qui distingue Slider, Map/Parcours et Event |
|
|
| `Assets/Scripts/Menu/SkyboxView.cs` | **E6** — image et vidéo 360° en skybox équirectangulaire |
|
|
| `Assets/Scripts/Kiosk/KioskSession.cs` | **E10** — fin de visite à la repose du casque ou après 90 s, menu recentré |
|
|
| `Assets/Scripts/Boot/MenuBootstrap.cs` | **E5** — l'app : appairage, cache, menu, sections, borne, télémétrie |
|
|
| `Assets/Scripts/Editor/BuildBootScene.cs` | Construit `Boot.unity` par code — quatre variantes (S1, S2, E2-E3, E5) |
|
|
| `Assets/StreamingAssets/calibration.glb` | Repère RGB asymétrique (X 1,00 m · Y 0,60 m · Z 0,30 m) |
|
|
|
|
### Essayer le menu (E5) — c'est l'app
|
|
|
|
⚠️ **Une fois pour le projet : _Window → TextMeshPro → Import TMP Essential Resources_.** Tout le
|
|
texte du casque passe par TMP depuis le 14/09 (`Ui/Label.cs`). Sans ces ressources, `TMP_Settings`
|
|
n'a pas de police par défaut : `Label` le dit dans la console et retombe sur `TextMesh`, donc le
|
|
texte reste lisible — mais flou, ce qu'on voulait justement corriger.
|
|
|
|
1. **MyInfoMate → Construire la scène Boot (E5 — menu flottant)**.
|
|
2. Renseigner `baseUrl`, le **code PIN**, `headsetName`, la langue sur le `Bootstrap`.
|
|
3. Build And Run. Un arc de panneaux apparaît à 2,2 m. Trois façons d'en choisir un :
|
|
|
|
| Moyen | Geste | Remarque |
|
|
|---|---|---|
|
|
| **Manette** | Gâchette d'index | Prioritaire dès qu'une manette est active |
|
|
| **Main** | Pincement pouce-index | Demande le building block *Hand Tracking* dans la scène |
|
|
| **Tête** | Viser **1,2 s**, l'anneau se remplit | Le repli universel, et le mode d'une borne publique |
|
|
|
|
⚠️ **« À la tête », pas « aux yeux ».** Le Quest 2 n'a pas d'eye tracking (seul le Quest Pro en a) :
|
|
on suit la direction de la tête, ce qui marche sur tous les casques et ne demande aucun SDK.
|
|
|
|
⚠️ **La visée a besoin d'un collider et d'un rig.** Si rien ne réagit : vérifier que le Camera Rig
|
|
des Building Blocks est dans la scène (sans lui `Camera.main` est nul) et que le module Physics
|
|
n'a pas été exclu du build. La manette et la main passent par `OVRInput` / `OVRHand`, du **Core
|
|
SDK** — pas de l'Interaction SDK, qui reste non configuré.
|
|
|
|
**Quatre types de section s'ouvrent** : Slider (galerie), Map et Parcours (liste de POI), Event
|
|
(programme du jour). Les autres affichent leur titre et le disent. ⚠️ La Map n'est **pas** la
|
|
maquette 3D promise par le plan — ça demande `SectionModel3D` ; une carte sur panneau flottant
|
|
serait moins bonne qu'un téléphone, donc on montre les lieux plutôt que le plan.
|
|
|
|
**La borne se réinitialise seule** : casque reposé, ou 90 s sans rien → retour au menu, recentré
|
|
devant le visiteur suivant, nouvelle session de stats. Dans l'éditeur et le simulateur, le casque
|
|
est considéré porté — sinon la scène se réinitialiserait toutes les 90 s pendant qu'on travaille.
|
|
|
|
### Essayer l'appairage seul (E2-E3)
|
|
|
|
La variante réseau existe encore, et elle sert : elle isole un problème d'appairage d'un problème
|
|
de rendu, ce qu'on ne peut plus faire une fois que tout est dans le menu.
|
|
|
|
1. **MyInfoMate → Construire la scène Boot (E2-E3 — appairage et export)**.
|
|
2. Sur le `Bootstrap` de la scène : renseigner `baseUrl`, le **code PIN** de l'instance, la langue.
|
|
Le PIN se saisit dans l'inspecteur tant que l'Interaction SDK (E1) n'a pas donné de pointeur.
|
|
3. Build And Run. Le casque doit afficher le nom de la configuration et ses sections.
|
|
|
|
⚠️ **Le canal VR doit être activé sur l'instance** : `isVR` **et** une `ApplicationInstance` de
|
|
type VR, sinon le `POST /api/device` répond 404 et le casque affiche « Le canal VR n'est pas activé
|
|
pour ce lieu ». Il n'y a pas encore d'écran pour ça — c'est deux appels d'API, décrits au §24.0 de
|
|
[`../../DOCS/test-plan.md`](../../DOCS/test-plan.md).
|
|
|
|
L'appairage est **persistant** (`PlayerPrefs`) : au deuxième lancement le PIN n'est plus lu. Pour
|
|
réappairer, cocher `forgetPairing`.
|
|
|
|
## Le seul résultat qui compte
|
|
|
|
Les **trois sphères doivent coiffer le bout de la branche de leur couleur**.
|
|
|
|
Si l'une part du côté opposé, la conversion est en miroir : dans `GltfSpace.cs`, passer
|
|
`Axis` à `Convention.NegateZ` et relancer. C'est la seule ligne à changer, et c'est pour ça
|
|
qu'elle est seule.
|
|
|
|
---
|
|
|
|
## Pièges rencontrés au premier build (11/09/2026, Quest 2)
|
|
|
|
Tous constatés sur le vrai matériel, tous coûteux à rechercher. Dans l'ordre où ils tombent.
|
|
|
|
**`com.unity.meshopt.decompress` n'existe pas au registre.** Il figurait dans le manifest de
|
|
l'overlay et faisait échouer la résolution entière. Retiré : glTFast gère Draco et KTX2, la
|
|
décompression meshopt n'est pas nécessaire pour nos GLB. Ne pas le remettre.
|
|
|
|
**OpenXR n'est activé que pour Standalone après le Fix All.** Les réglages XR sont *par
|
|
plateforme*. Tant que la plateforme active est Windows, le Fix All de Meta ne touche pas la
|
|
colonne Android, et `XRGeneralSettingsPerBuildTarget.asset` ne contient qu'une entrée.
|
|
Ordre correct : **File → Build Profiles → Android → Switch Platform**, *puis* Project Settings
|
|
→ Meta XR → **Fix All**, *puis* vérifier XR Plug-in Management → onglet Android → OpenXR coché
|
|
+ groupe de features **Meta Quest**.
|
|
|
|
**`INSTALL_FAILED_MISSING_SHARED_LIBRARY` — le build réussit, l'install échoue.**
|
|
|
|
```
|
|
Supplement horizonos-supplement-hzplatformclientcore does not exist.
|
|
```
|
|
|
|
Le manifeste généré par le SDK Meta déclare `horizonos:targetSdkVersion="205"`. Un casque sur
|
|
une version d'Horizon OS plus ancienne (v74 dans notre cas) n'a pas le composant système
|
|
correspondant et refuse le paquet. Deux sorties : mettre le casque à jour (la bonne), ou
|
|
abaisser `targetSdkVersion` dans `Assets/Plugins/Android/AndroidManifest.xml` pour débloquer
|
|
(contournement — à remettre à 205 une fois le casque à jour). Cette déclaration ne sert qu'au
|
|
Platform SDK Meta, que nos scripts n'utilisent pas.
|
|
|
|
**Deux appareils branchés = Unity déploie au hasard.** Un téléphone Android connecté en USB
|
|
apparaît dans `adb devices` au même titre que le casque. Fixer **Run Device** sur le Quest dans
|
|
Build Profiles, ou débrancher le téléphone.
|
|
|
|
**Le premier build prend 20 minutes, les suivants 1 à 3.** Mesuré : 1245 s pour un APK de
|
|
66 Mo. C'est IL2CPP. Ne pas conclure à un plantage.
|
|
|
|
**Package name et Company Name restent ceux du template URP** si on ne les change pas
|
|
(`com.UnityTechnologies.com.unity.template.urpblank`). À corriger avant le premier sideload :
|
|
changer l'identifiant plus tard fait apparaître une *deuxième* app dans le casque.
|
|
|
|
## Tester sans casque — le simulateur
|
|
|
|
Le **Meta XR Simulator** (panneau Meta XR Tools) lance la scène en mode Play dans l'éditeur,
|
|
sans casque. `E1Bootstrap` tolère l'absence de `world.glb` et `persona.glb` — il se contente de
|
|
logger une erreur — donc **le critère de validation n°2 de S1, le test de miroir de
|
|
`GltfSpace`, est jouable dès maintenant** avec le seul `calibration.glb`, sans aucun asset
|
|
Marble. C'est le bug le plus cher du lot : autant le lever tôt.
|
|
|
|
Quand le simulateur est activé, la barre de statut l'affiche et ▶ ne va plus au casque. La
|
|
bascule est dans **Meta → Tools → Meta XR Simulator**.
|
|
## Shaders glTFast : magenta sur le casque, correct dans l'éditeur
|
|
|
|
Le piège le plus coûteux du lot, et il ne se voit **que** sur le device.
|
|
|
|
glTFast construit ses matériaux au runtime et trouve ses shaders par
|
|
`Shader.Find("Shader Graphs/glTF-pbrMetallicRoughness")`. Dans l'éditeur, tous les assets du
|
|
projet sont disponibles, donc ça marche toujours. Dans un build, `Shader.Find` ne voit que ce qui
|
|
a été explicitement embarqué — et un shader vivant dans un package n'est référencé par aucune
|
|
scène, donc il est éliminé. Tout ce qui vient d'un glTF sort en magenta, et
|
|
`new Material(null)` lève `ArgumentNullException: Parameter name: shader`.
|
|
|
|
**Un ShaderVariantCollection dans *Preloaded Shaders* ne suffit pas.** Vérifié sur le casque : la
|
|
collection capturée contenait bien `Universal Render Pipeline/Unlit`, et `Shader.Find` renvoyait
|
|
quand même `null`. Cette liste précharge les variantes de shaders **déjà embarqués** ; elle
|
|
n'embarque rien. Seul *Always Included Shaders* fait entrer un shader dans le build.
|
|
|
|
Le minimum qui marche, dans Project Settings → Graphics → **Always Included Shaders** :
|
|
|
|
| Shader | Référence YAML |
|
|
|---|---|
|
|
| `Universal Render Pipeline/Unlit` (l'anneau de `NavigationBounds`, les panneaux du menu) | `{fileID: 4800000, guid: 650dd9526735d5b46b79224bc6e94025, type: 3}` |
|
|
| `glTF-pbrMetallicRoughness` | `{fileID: -6465566751694194690, guid: b9d29dfa1474148e792ac720cbd45122, type: 3}` |
|
|
| **`Skybox/Panoramic`** — la 360° (E6). Même piège, même conséquence : sans lui le ciel sort **magenta sur le casque et correct dans l'éditeur** | ajouté par `MyInfoMate → Embarquer les shaders du runtime` |
|
|
|
|
⚠️ **`Universal Render Pipeline/Lit` ne doit PAS être dans cette liste.** Il pèse **2 359 296
|
|
variantes**, plus que ce qu'Unity accepte d'embarquer : le build échoue sur *« has too many Shader
|
|
variants »* — et il échoue **à la fin**, après 35 minutes de compilation. Constaté le 14/09. Aucun
|
|
`Shader.Find` du projet ne le demande : tout ce qui est dessiné par code est en Unlit, et les GLB
|
|
passent par les graphes glTFast. `IncludeRuntimeShaders.cs` ne l'ajoute plus ; s'il est déjà dans
|
|
la liste, **le retirer à la main** — le script ajoute, il ne retire jamais.
|
|
|
|
⚠️ **`GUI/Text Shader` non plus**, et pour une autre raison. Il vit dans
|
|
`Library/unity default resources`, marqué `HideFlags.DontSave` : dans la liste, il fait échouer le
|
|
build sur *« An asset is marked with HideFlags.DontSave but is included in the build »*, suivi d'une
|
|
assertion `m_LockCount == 0` et de *« Failed to write file:
|
|
Library/PlayerDataCache/Android/Data/Resources/unity_builtin_extra »*. Constaté le 14/09, juste
|
|
après avoir réglé le cas Lit. Le `TextMesh` de `SceneMessage` n'en a pas besoin : son matériau vient
|
|
de la police référencée par le composant, embarquée par le chemin normal.
|
|
|
|
⚠️ **N'ajoute que les shaders réellement utilisés.** Un Shader Graph dans cette liste fait compiler
|
|
**toutes** ses combinaisons de mots-clés : les trois graphes glTFast d'un coup, c'est 16 384
|
|
variantes et ~50 minutes de build. Pour savoir lesquels comptent, joue la scène dans l'éditeur puis
|
|
lis la capture (Graphics → Shader Loading → *Save to asset…*) : les shaders qu'elle liste sont
|
|
exactement ceux dont ton contenu a besoin. Le cache de shaders est conservé, la facture ne se paie
|
|
qu'une fois.
|
|
|
|
## NavigationBounds : mesurer la tête, déplacer le rig
|
|
|
|
En VR le rig **ne bouge pas** quand le visiteur marche — le tracking applique le déplacement à la
|
|
caméra, à l'intérieur du rig. Une contrainte de zone qui teste `rig.position` ne se déclenche donc
|
|
jamais, quelle que soit la distance parcourue : on sort du cercle sans rien sentir. Il faut mesurer
|
|
`Camera.main.transform` et retrancher l'excédent au rig. |