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>
85 lines
3.2 KiB
C#
85 lines
3.2 KiB
C#
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using UnityEngine;
|
|
|
|
namespace MyInfoMate.Vr.Scene
|
|
{
|
|
/// <summary>
|
|
/// Un personnage posé dans la scène : GLB chargé au runtime, animation idle en
|
|
/// boucle, orientation vers le visiteur.
|
|
///
|
|
/// Pas de lip sync, jamais — écarté au niveau produit. Un personnage stylisé qui
|
|
/// respire et regarde dans la bonne direction porte l'essentiel de la présence ;
|
|
/// une bouche mal synchronisée la détruit.
|
|
/// </summary>
|
|
public class PersonaInstance : MonoBehaviour
|
|
{
|
|
[SerializeField] Transform lookAtTarget;
|
|
[SerializeField] bool gazeAtVisitor = true;
|
|
[SerializeField] float gazeDegreesPerSecond = 90f;
|
|
|
|
Quaternion _restRotation;
|
|
|
|
public async Task<bool> LoadAsync(string url, Transform visitorHead)
|
|
{
|
|
lookAtTarget = visitorHead;
|
|
_restRotation = transform.localRotation;
|
|
|
|
var model = await GltfLoader.LoadAsync(url, transform, "Model");
|
|
if (model == null) return false;
|
|
|
|
PlayIdle(model);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ⚠️ glTFast expose les animations de deux façons selon
|
|
/// <c>GltfImportSettings.AnimationMethod</c> : <c>Legacy</c> (composant
|
|
/// <see cref="Animation"/>, le défaut) ou <c>Mecanim</c> (un <c>Animator</c> et
|
|
/// son controller). Ce code suit le défaut ; si le personnage reste figé alors
|
|
/// que le GLB contient bien des clips, c'est le premier endroit à regarder.
|
|
///
|
|
/// On joue le clip nommé
|
|
/// « idle » s'il existe, sinon le premier — un pack d'animations tiers ne
|
|
/// respecte aucune convention de nommage, et refuser de jouer parce que le
|
|
/// nom ne colle pas donnerait un personnage figé sans message d'erreur.
|
|
/// </summary>
|
|
void PlayIdle(GameObject model)
|
|
{
|
|
var animation = model.GetComponentInChildren<Animation>();
|
|
if (animation == null) return;
|
|
|
|
var clip = animation.Cast<AnimationState>()
|
|
.FirstOrDefault(s => s.name.ToLowerInvariant().Contains("idle"))
|
|
?? animation.Cast<AnimationState>().FirstOrDefault();
|
|
|
|
if (clip == null)
|
|
{
|
|
Debug.LogWarning($"[Persona] {name} : aucune animation dans le GLB");
|
|
return;
|
|
}
|
|
|
|
clip.wrapMode = WrapMode.Loop;
|
|
animation.Play(clip.name);
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (!gazeAtVisitor || lookAtTarget == null) return;
|
|
|
|
// Rotation sur Y seulement : un personnage qui bascule la tête vers un
|
|
// visiteur assis ou grand donne un effet de pantin.
|
|
var direction = lookAtTarget.position - transform.position;
|
|
direction.y = 0f;
|
|
if (direction.sqrMagnitude < 0.01f) return;
|
|
|
|
transform.rotation = Quaternion.RotateTowards(
|
|
transform.rotation,
|
|
Quaternion.LookRotation(direction),
|
|
gazeDegreesPerSecond * Time.deltaTime);
|
|
}
|
|
|
|
public void ReturnToRest() => transform.localRotation = _restRotation;
|
|
}
|
|
}
|