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

134 lines
4.3 KiB
C#

using UnityEngine;
namespace MyInfoMate.Vr.Scene
{
/// <summary>
/// Une phrase affichée dans le casque, lisible, à hauteur d'yeux.
///
/// <b>C'est un livrable, pas un raffinement.</b> Une scène qu'on ne sait pas
/// charger — décor en splats, manifeste d'une version inconnue, asset absent —
/// doit le dire. Sur site, personne ne lit une console : un écran noir devient
/// « le casque est cassé », et un message devient un appel au support qui sait
/// quoi corriger.
/// </summary>
public class SceneMessage : MonoBehaviour
{
// 3,50 m : à 2 m un texte de cette taille occupe trop de champ pour être lu
// confortablement, et l'oeil doit converger de près. Plus loin, il se lit
// d'un seul regard.
const float DistanceMeters = 3.5f;
const float HeightMeters = 1.5f;
/// <summary>Au-delà de cet écart, le visiteur ne regarde plus le message.</summary>
const float ReleaseAngleDegrees = 45f;
const float RecentringSpeed = 3f;
Transform _head;
bool _recentring;
public static SceneMessage Show(string message, Transform visitorHead)
{
var instance = new GameObject("SceneMessage").AddComponent<SceneMessage>();
instance.Build(message, visitorHead);
return instance;
}
void Build(string message, Transform visitorHead)
{
_head = visitorHead;
var label = Ui.Label.Create(
transform, "Text", Ui.Label.Anchor.MiddleCenter, 0.08f, Color.white);
label.Text = Wrap(message, 40);
if (_head != null) transform.position = TargetPosition();
FaceVisitor();
}
/// <summary>
/// Le message <b>reste posé</b> tant que le visiteur le regarde, et ne se
/// recentre que s'il lui tourne le dos. Le suivre à chaque image le rend
/// illisible — on ne peut pas fixer un texte qui fuit ; ne jamais le suivre
/// le laisserait dans le dos du visiteur, donc jamais lu.
/// </summary>
void LateUpdate()
{
if (_head == null) return;
if (LookingAway()) _recentring = true;
if (_recentring)
{
var target = TargetPosition();
transform.position = Vector3.Lerp(
transform.position, target, Time.deltaTime * RecentringSpeed);
if ((transform.position - target).sqrMagnitude < 0.0004f) _recentring = false;
}
FaceVisitor();
}
bool LookingAway()
{
var toMessage = transform.position - _head.position;
toMessage.y = 0f;
if (toMessage.sqrMagnitude < 0.01f) return true;
return Vector3.Angle(HeadForward(), toMessage.normalized) > ReleaseAngleDegrees;
}
Vector3 TargetPosition()
{
return new Vector3(_head.position.x, HeightMeters, _head.position.z)
+ HeadForward() * DistanceMeters;
}
/// <summary>Le texte fait face au visiteur, sans jamais s'incliner.</summary>
void FaceVisitor()
{
if (_head == null) return;
var away = transform.position - _head.position;
away.y = 0f;
if (away.sqrMagnitude < 0.01f) return;
transform.rotation = Quaternion.LookRotation(away.normalized);
}
Vector3 HeadForward()
{
var forward = _head.forward;
forward.y = 0f;
return forward.sqrMagnitude < 0.01f ? Vector3.forward : forward.normalized;
}
static string Wrap(string message, int columns)
{
var words = message.Split(' ');
var line = 0;
var wrapped = new System.Text.StringBuilder();
foreach (var word in words)
{
if (line + word.Length > columns)
{
wrapped.Append('\n');
line = 0;
}
else if (line > 0)
{
wrapped.Append(' ');
line++;
}
wrapped.Append(word);
line += word.Length;
}
return wrapped.ToString();
}
}
}