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>
193 lines
11 KiB
Markdown
193 lines
11 KiB
Markdown
# 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`](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 URL `http` — elle ne part pas hors ligne ;
|
|
- `SectionWeb` ne 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.
|
|
|
|
1. **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.
|
|
2. **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.
|
|
3. **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.AiProvenance` est 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 `IGenerationProvider`
|
|
de 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 **`Resource` de type `Model3D` dans 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.Model3DResourceId` pointe 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 |
|