Thomas Fransolet 0310d28b5e Canal VR Meta Quest : projet Unity, viewer web et documentation
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>
2026-09-16 15:26:07 +02:00

83 lines
3.2 KiB
C#

using System.Threading.Tasks;
using MyInfoMate.Vr.Scene;
using UnityEngine;
namespace MyInfoMate.Vr.Boot
{
/// <summary>
/// Orchestration de l'étape S1 : décor, calibration, personnage, zone de navigation.
///
/// <b>Provisoire par construction.</b> Les chemins sont ici en dur, parce que S1
/// n'a ni manifeste ni réseau — c'est exactement son intérêt : prouver la chaîne
/// technique sans backend. S2 remplace ce script par <c>SceneBuilder</c>, qui lit
/// les mêmes assets depuis un manifeste. Ne rien construire au-dessus de celui-ci.
/// </summary>
public class S1Bootstrap : MonoBehaviour
{
[Header("Fichiers dans Assets/StreamingAssets")]
[SerializeField] string worldFile = "world.glb";
[SerializeField] string personaFile = "persona.glb";
[Header("Placement du personnage — coordonnées glTF, en mètres")]
[SerializeField] float[] personaPosition = { -1.5f, 0f, -1f };
[Header("Navigation")]
[SerializeField] float navigationRadiusMeters = 3f;
[SerializeField] bool showBoundary = true;
[SerializeField] bool runCalibration = true;
Transform _sceneRoot;
async void Start()
{
_sceneRoot = new GameObject("SceneRoot").transform;
_sceneRoot.SetParent(transform, false);
var head = Camera.main != null ? Camera.main.transform : null;
if (head == null)
Debug.LogError("[S1] Aucune caméra active : le rig VR n'est pas dans la scène. " +
"Meta → Tools → Building Blocks → Camera Rig.");
await LoadWorld();
if (runCalibration)
await gameObject.AddComponent<CalibrationCheck>().RunAsync(_sceneRoot);
await LoadPersona(head);
SetUpBounds(head);
}
async Task LoadWorld()
{
var world = await GltfLoader.LoadAsync(
GltfLoader.StreamingAssetsUrl(worldFile), _sceneRoot, "World");
if (world == null)
Debug.LogError($"[S1] {worldFile} absent de StreamingAssets — " +
"exporte un monde Marble en mesh GLB et dépose-le là.");
}
async Task LoadPersona(Transform head)
{
var holder = new GameObject("Persona");
holder.transform.SetParent(_sceneRoot, false);
holder.transform.localPosition = GltfSpace.Position(personaPosition);
var persona = holder.AddComponent<PersonaInstance>();
if (!await persona.LoadAsync(GltfLoader.StreamingAssetsUrl(personaFile), head))
Debug.LogError($"[S1] {personaFile} absent de StreamingAssets.");
}
void SetUpBounds(Transform head)
{
// La zone suit le rig, pas la tête : c'est la position au sol du visiteur
// qui compte, et la tête bouge de 60 cm quand on se penche.
var rig = head != null ? head.root : null;
var bounds = new GameObject("NavigationBounds").AddComponent<NavigationBounds>();
bounds.transform.SetParent(_sceneRoot, false);
bounds.Configure(rig, head, navigationRadiusMeters, showBoundary);
}
}
}