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>
11 KiB
Décisions d'architecture du module VR
Arbitrées le 2026-09-03, à partir du relevé de code de
00-phase1-etat-des-lieux.md. Ne pas rouvrir sans raison technique forte. Les décisions antérieures (Unity vs WebXR, pricing add-on, MDM, types de section) restent dans../../DOCS/v2/vr-quest-unity-plan.md§6 à §10.
D1 — Le contenu VR est un type de section, pas une entité parallèle
Retenu : SectionScene3D, dans une Configuration existante.
Le manifeste de scène est une projection de la section, générée par le serveur, jamais saisie à la main. Conséquences, toutes acquises sans code neuf :
| On hérite de | Parce que |
|---|---|
| La publication | c'est une section, elle part dans /configuration/{id}/export |
| L'offline | GetReferencedResourceIds(language) est implémenté par sous-type — on l'implémente une fois |
| Les stats par canal | VisitEvent porte déjà AppType |
| Le multilingue | List<TranslationDTO> jsonb, comme le reste |
| Le quota stockage | SUM(SizeBytes) sur les Resource référencées |
| L'affichage web / mobile | une section est rendue par les 3 clients visiteurs — c'est ce qui rend l'add-on « contenu immersif » vendable sans casque |
Corollaire : GeoPoint est l'objet hotspot unique. Il porte déjà Title, Description,
ImageResource et Contents (titre + description + Resource audio, multilingue). Il lui manque
une position locale (x, y, z) + rotation à côté de son Geometry PostGIS. Pas de nouvel objet POI.
Écarté : une entité VrScene autonome — elle aurait obligé à réécrire publication,
versionnement, cache offline, quotas et stats.
Écarté aussi, pour la V1 : rendre les 13 types de section en immersif. Le §8 du plan du 31/08 garde sa validité comme cap (Menu / Video 360 / Slider / Map gagnent vraiment en VR), mais ce n'est pas le périmètre de la V1 : la V1 rend une scène, pas un CMS flottant.
D2 — Le placement se fait dans le viewer, au clic
Retenu : le viewer THREE.js est aussi l'éditeur de placement.
Clic dans la scène → raycast sur le sol → position. Poignée de rotation sur l'objet sélectionné, panneau numérique à côté pour ajuster au centimètre. Pas d'éditeur 3D complet, pas de gizmo de translation sur 3 axes.
Coût marginal quasi nul : le viewer doit exister de toute façon pour la preview (§3.6 du cahier des charges). C'est le seul mécanisme où le client voit ce qu'il compose.
⚠️ Ce que ça engage : le viewer cesse d'être en lecture seule. Il lui faut un état d'édition, un undo, et un enregistrement explicite. À ne pas sous-estimer — c'est la moitié de son coût.
Écartés : la mini-carte 2D vue de dessus (il aurait fallu produire un rendu orthographique du monde, et le client place à l'aveugle sur la hauteur) ; les ancrages prédéfinis (composition pauvre, sensation de gabarit — mauvais en démo commerciale).
D3 — Un seul viewer, app web autonome, partagé par les quatre fronts
Retenu : le viewer est une application web autonome nourrie par une URL de manifeste. Précisé le 2026-09-03 : il ne sert pas deux fronts mais quatre.
| Front | Comment il l'héberge | Mode |
|---|---|---|
manager-app (Flutter Web) |
iframe (HtmlElementView) + postMessage |
édition |
visitapp-web (Next.js) |
composant, ou iframe si plus simple | lecture |
mymuseum-visitapp (Flutter mobile) |
WebView servant le viewer empaqueté dans l'app | lecture, hors ligne |
vr-app (Unity/Quest) |
❌ pas concerné — Unity a son propre rendu natif | — |
Un seul moteur de rendu 3D pour tout l'écosystème. C'est ce qui attaque de front le risque n°2 du plan d'août (« nouvelle stack, zéro mutualisation ») : le rendu 3D n'est pas un quatrième front, c'est un composant partagé.
Sur Flutter mobile, il n'y a pas d'alternative sérieuse :
model_viewer_plus, la solution habituelle, est elle-même un WebView autour de<model-viewer>de Google. Autant que ce soit notre viewer dans ce WebView, avec nos hotspots et notre manifeste, qu'un composant tiers qu'on ne contrôle pas.
L'offline mobile : souhaitable, pas bloquant — arbitré le 2026-09-03
mymuseum-visitapp est offline-first, et un WebView pointant sur une URL distante casserait ça.
Thomas a tranché : si la 3D hors ligne s'avère trop coûteuse, on l'assume et on le dit au client
dans manager-app. C'est acceptable, et surtout c'est déjà la convention de la maison :
SectionVideo.GetReferencedResourceIds(SectionVideo.cs:25-27) exclut explicitement une vidéo dont la source est une URLhttp— elle ne part pas hors ligne ;SectionWebne collecte que sa vignette.
Une scène 3D non disponible hors ligne serait donc le troisième cas d'une règle existante, pas une exception. Rien à inventer, juste à signaler dans l'éditeur.
Ceci dit, c'est probablement faisable, et le coût n'est pas dans le rendu mais dans la plomberie : viewer empaqueté dans les assets Flutter, servi depuis une origine locale, GLB téléchargés dans le dossier documents, URL du manifeste réécrites en chemins locaux. Le pipeline offline fait déjà tout ça pour les médias 2D. À évaluer au moment du portage mobile, pas maintenant.
⚠️ Les trois règles à poser dès la V1 — indépendamment de l'offline
Elles ne coûtent rien maintenant, elles coûtent une réécriture plus tard, et elles sont de toute façon du bon design : un viewer qui ne sait pas d'où viennent ses données est un viewer réutilisable.
- Le viewer est un jeu de fichiers statiques, embarquable dans les assets d'une app Flutter et servable depuis une origine locale. Pas de build qui suppose un serveur.
- Aucune URL d'API en dur dans le viewer. Il ne connaît que le manifeste qu'on lui donne —
toutes les URL d'assets viennent de
assets[].url. C'est ce qui permet de lui passer un manifeste dont les URL ont été réécrites en chemins locaux par l'app, exactement comme le fait déjà le pipeline offline pour les médias. - Le manifeste est une entrée, pas un fetch. Le viewer accepte un manifeste injecté
(
postMessage/ paramètre), pas seulement une URL à charger.
Ce que ça débloque au passage
Le 360 et le GLB s'affichent alors en web et en mobile, pas seulement au casque — c'est
exactement la part de l'add-on « contenu immersif » qui se vend sans casque, et sur laquelle
repose son pricing (vr-quest-unity-plan.md §6).
⚠️ Rappel de contrat : le viewer est un aperçu indicatif — placement, échelle, composition. Pas un rendu final. Alignement à tenir : même tone mapping, même HDRI d'environnement, PBR core glTF uniquement. La validation réelle passe par une publication sur canal de test et un essai au casque.
D4 — La borne se réinitialise à la repose du casque
Retenu : détection du retrait (capteur de proximité) → fondu au noir, reset de la scène, retour au menu de sélection dans la langue par défaut.
Précédent direct dans l'écosystème : le retour à l'accueil de la borne tablette après 5 min
(DOCS/kanban/done/140-*). Un agent n'a rien à faire, ce qui est cohérent avec l'argument qui avait
fait retenir Unity — l'exploitation sans surveillance.
Le timeout d'inactivité reste utile en filet secondaire (casque posé sans être retiré proprement), pas comme mécanisme principal.
D5 — La V1 consomme des mondes uploadés ; la génération vient juste après
Retenu : upload manuel d'abord, génération intégrée ensuite.
En V1, le monde est un GLB uploadé dans la médiathèque — produit hors ligne, sur le site de World Labs, par nous, pour les clients pilotes. Le module VR devient alors indépendant du socle Studio et livrable seul, sans attendre les lots 0 / 1 / 3.
La génération in-app (le bouton « Générer un monde » du §3.1 du cahier des charges) arrive quand ces
lots existent, sans rien casser : au bout du pipeline, c'est la même Resource GLB référencée
par la même section. La seule chose à faire dès maintenant, c'est de ne rien coder qui empêche cette
greffe — donc :
- le monde est toujours un
ResourceId, jamais un chemin ou une URL en dur ; Resource.AiProvenanceest prévu dans le manifeste dès la V1, même vide ;- Marble n'est jamais appelé depuis le code VR : le jour venu, ce sera un
IGenerationProviderde plus (ProviderKey = "worldlabs",GenerationKind.World), pas un client HTTP maison.
Écarté : la génération dès la V1 (elle mettait plusieurs semaines de backend V2 devant la
première scène visible) et le compromis « lot 0 seul + appel Marble en dur » (il recrée exactement
la dette que studio-plan.md cherche à éviter : modèle codé en dur, pas de journal, pas de
réservation de crédits).
D6 — Personnages 3D : catalogue externe sous licence, mais branché sur la médiathèque dès le jour 1
Retenu : une petite bibliothèque de personnages stylisés sous licence (modèles type Quaternius / Kenney ou un pack payant, animations idle type Mixamo), livrée comme catalogue de départ. Le client choisit, il ne modélise pas.
⚠️ Condition posée par Thomas, et elle est structurante : « il faut quand même prévoir vite d'intégrer ce qu'on génère dans le Studio ». Donc le catalogue n'est pas une liste en dur dans l'APK. Dès la V1 :
- un personnage 3D est un
Resourcede typeModel3Ddans la médiathèque, comme n'importe quel autre asset — le catalogue de départ est juste un jeu de ressources pré-chargées, marquées comme telles ; Persona.Model3DResourceIdpointe dessus. Le jour où le Studio produit un GLB, il remplace la valeur du champ, il ne demande aucune refonte ;- la licence de chaque modèle du catalogue de départ est tracée dans
AiProvenance.rightsHolder(ou son équivalent pour un asset non généré) — sujet droits, pas seulement technique, pour des institutions publiques.
Écartés : le studio 3D sur devis en V1 (aucun pilote ne pourrait composer une scène sans payer d'abord une prestation) et l'absence de personnage en V1 (c'est l'élément le plus démonstratif).
Ce qui reste ouvert
| Sujet | Où c'est décrit | Ce qui manque pour trancher |
|---|---|---|
| KTX2 / Meshopt : où s'exécute la transformation ? | état des lieux F5 | Binaires natifs dans l'image Docker vs GLB servi tel quel en V1. Défaut retenu faute d'objection : tel quel en V1, avec le bloc variants par asset posé dans le manifeste dès le départ pour que l'ajout n'y touche pas. À rouvrir dès qu'on aura un vrai poids de monde Marble mesuré |
| Go/no-go client pilote | vr-quest-unity-plan.md §5 |
Décision produit, inchangée : ne pas engager l'app Unity complète sans un lieu pilote identifié. D5 réduit l'enjeu — la chaîne technique se prouve sans client |