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