# 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.