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

84 lines
3.4 KiB
C#

using UnityEngine;
namespace MyInfoMate.Vr.Scene
{
/// <summary>
/// Zone de navigation circulaire, plafonnée à 3 m de rayon (§3.2 du cahier des
/// charges). Le plafond est appliqué ici <i>et</i> côté serveur : un client qui
/// fait confiance à la donnée est un client qu'on casse avec une donnée fausse.
///
/// Deux rôles : matérialiser la limite comme le garde-fou natif du Quest, et
/// ramener le visiteur à l'intérieur s'il en sort. On ne téléporte pas — on
/// repousse : une téléportation surprise dans un casque donne la nausée.
/// </summary>
public class NavigationBounds : MonoBehaviour
{
public const float MaxRadiusMeters = 3f;
[SerializeField] float radiusMeters = 3f;
[SerializeField] Transform visitor;
[SerializeField] Transform head;
[SerializeField] float fadeStartMeters = 0.4f;
LineRenderer _ring;
public void Configure(Transform visitorRig, Transform visitorHead, float radius, bool showBoundary)
{
visitor = visitorRig;
head = visitorHead;
radiusMeters = Mathf.Min(radius, MaxRadiusMeters);
if (showBoundary) DrawRing();
}
void DrawRing()
{
const int segments = 96;
_ring = gameObject.AddComponent<LineRenderer>();
_ring.useWorldSpace = false;
_ring.loop = true;
_ring.positionCount = segments;
_ring.widthMultiplier = 0.015f;
_ring.material = new Material(Shader.Find("Universal Render Pipeline/Unlit"));
_ring.material.color = new Color(0.4f, 0.8f, 1f, 0.6f);
for (var i = 0; i < segments; i++)
{
var angle = i * Mathf.PI * 2f / segments;
_ring.SetPosition(i, new Vector3(
Mathf.Cos(angle) * radiusMeters, 0.01f, Mathf.Sin(angle) * radiusMeters));
}
}
/// <summary>
/// On mesure la <b>tête</b> et on déplace le <b>rig</b>. 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. Mesurer le rig, c'est ne jamais rien mesurer —
/// la contrainte ne se déclenche alors pour aucune distance parcourue.
///
/// On retranche l'excédent au rig plutôt que de poser la tête sur le cercle :
/// la tête suit le corps, on ne peut que déplacer le repère sous elle.
/// </summary>
void LateUpdate()
{
if (visitor == null || head == null) return;
var local = transform.InverseTransformPoint(head.position);
var flat = new Vector2(local.x, local.z);
if (flat.magnitude <= radiusMeters) return;
var excess = flat - flat.normalized * radiusMeters;
visitor.position -= transform.TransformVector(new Vector3(excess.x, 0f, excess.y));
}
/// <summary>Distance à la limite, pour l'assombrissement progressif du bord.</summary>
public float ProximityToEdge()
{
if (head == null) return 0f;
var local = transform.InverseTransformPoint(head.position);
var distance = new Vector2(local.x, local.z).magnitude;
return Mathf.InverseLerp(radiusMeters - fadeStartMeters, radiusMeters, distance);
}
}
}