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>
134 lines
4.3 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|