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

113 lines
4.2 KiB
C#

using System.Collections;
using UnityEngine;
namespace MyInfoMate.Vr.Menu
{
/// <summary>
/// Le retour au survol et à la sélection : un son court, et une vibration quand il y
/// a une manette pour la sentir.
///
/// <b>Ce que ça règle.</b> En visée à la tête, rien ne confirme qu'on a bien accroché
/// un panneau avant que l'anneau ne commence à se remplir — et l'anneau est en
/// périphérie du regard au moment où on l'attend. Un son de 40 ms dit « tu es
/// dessus » avant même que l'œil ne l'ait vu.
///
/// <b>Les sons sont synthétisés</b>, pas chargés. Deux raisons : aucun fichier à
/// gérer dans un repo qui n'a pas d'assets, et un clic généré est identique sur tous
/// les casques. Ils sont volontairement discrets — une borne de musée qui claque à
/// chaque regard devient insupportable en une heure.
/// </summary>
public class MenuFeedback : MonoBehaviour
{
const int SampleRate = 44100;
static MenuFeedback _instance;
AudioSource _source;
AudioClip _hover;
AudioClip _select;
static MenuFeedback Instance
{
get
{
if (_instance != null) return _instance;
var root = new GameObject("MenuFeedback");
DontDestroyOnLoad(root);
_instance = root.AddComponent<MenuFeedback>();
_instance.Build();
return _instance;
}
}
void Build()
{
_source = gameObject.AddComponent<AudioSource>();
_source.playOnAwake = false;
// 2D : le retour n'appartient pas à un endroit de la pièce, il appartient au
// geste. Le spatialiser le ferait venir d'un panneau qu'on est en train de
// quitter.
_source.spatialBlend = 0f;
_hover = Tone("hover", 880f, 0.045f, 0.18f);
_select = Tone("select", 1320f, 0.09f, 0.32f, 660f);
}
/// <summary>
/// Une sinusoïde avec une enveloppe qui décroît. <paramref name="startFrequency"/>
/// non nul fait un glissando : c'est ce qui distingue « choisi » de « survolé »
/// sans avoir à monter le volume.
/// </summary>
static AudioClip Tone(string name, float frequency, float seconds, float volume,
float startFrequency = 0f)
{
var samples = Mathf.RoundToInt(SampleRate * seconds);
var data = new float[samples];
var phase = 0f;
for (var i = 0; i < samples; i++)
{
var t = i / (float)samples;
var f = startFrequency > 0f ? Mathf.Lerp(startFrequency, frequency, t) : frequency;
phase += 2f * Mathf.PI * f / SampleRate;
// Attaque très courte pour éviter le claquement d'un début abrupt, puis
// décroissance exponentielle.
var envelope = Mathf.Min(1f, t * 40f) * Mathf.Exp(-5f * t);
data[i] = Mathf.Sin(phase) * envelope * volume;
}
var clip = AudioClip.Create(name, samples, 1, SampleRate, false);
clip.SetData(data, 0);
return clip;
}
public static void Hover() => Instance.Play(Instance._hover, 0.12f, 0.05f);
public static void Select() => Instance.Play(Instance._select, 0.5f, 0.12f);
void Play(AudioClip clip, float vibrationAmplitude, float vibrationSeconds)
{
if (clip != null) _source.PlayOneShot(clip);
var controller = OVRInput.GetActiveController();
if (controller != OVRInput.Controller.RTouch && controller != OVRInput.Controller.LTouch)
return;
StopAllCoroutines();
StartCoroutine(Vibrate(controller, vibrationAmplitude, vibrationSeconds));
}
IEnumerator Vibrate(OVRInput.Controller controller, float amplitude, float seconds)
{
OVRInput.SetControllerVibration(0.4f, amplitude, controller);
yield return new WaitForSeconds(seconds);
OVRInput.SetControllerVibration(0f, 0f, controller);
}
}
}