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>
135 lines
5.6 KiB
C#
135 lines
5.6 KiB
C#
using System;
|
|
using System.Threading.Tasks;
|
|
using MyInfoMate.Vr.Net;
|
|
using UnityEngine;
|
|
using UnityEngine.Video;
|
|
|
|
namespace MyInfoMate.Vr.Menu
|
|
{
|
|
/// <summary>
|
|
/// Le fond immersif d'une visite — le reste de l'item <b>E5</b>, spécifié au §4 de
|
|
/// <c>DOCS/v2/immersif-frontiere-plan.md</c>.
|
|
///
|
|
/// <b>Sans lui, le menu flotte dans le noir.</b> C'est le défaut d'Unity : une scène
|
|
/// vide est un vide, et un visiteur qui met le casque se retrouve devant six panneaux
|
|
/// suspendus dans le néant. Avec le panorama du lieu derrière, il est <i>dans</i> le
|
|
/// musée avant d'avoir choisi quoi que ce soit.
|
|
///
|
|
/// Trois différences avec <see cref="SkyboxView"/>, et elles tiennent toutes à la même
|
|
/// chose — <b>un fond n'est pas un contenu</b> :
|
|
/// <list type="bullet">
|
|
/// <item>on ne le choisit pas et on n'en sort pas : aucun panneau de retour ;</item>
|
|
/// <item>il reste en place tant que la visite dure, y compris entre deux sections ;</item>
|
|
/// <item>il s'efface devant une 360 ouverte, puis revient — <c>SkyboxView</c> restaure
|
|
/// le ciel précédent en se détruisant, et le ciel précédent, c'est celui-ci.</item>
|
|
/// </list>
|
|
///
|
|
/// Son absence est un cas nominal : un lieu sans fond garde le noir, sans un mot.
|
|
/// </summary>
|
|
public class ImmersiveBackdrop : MonoBehaviour
|
|
{
|
|
const int VideoWidth = 4096;
|
|
const int VideoHeight = 2048;
|
|
|
|
Material _sky;
|
|
Texture2D _image;
|
|
VideoPlayer _player;
|
|
RenderTexture _target;
|
|
|
|
/// <summary>
|
|
/// Pose le fond de cette visite, s'il y en a un. Rend le composant créé, ou null —
|
|
/// pas de fond n'est pas une erreur.
|
|
/// </summary>
|
|
public static async Task<ImmersiveBackdrop> ApplyAsync(
|
|
ConfigurationExport export, ApiClient client)
|
|
{
|
|
var background = export?.ImmersiveBackground;
|
|
if (background == null || string.IsNullOrEmpty(background.ResourceUrl)) return null;
|
|
|
|
// Une scène 3D en fond est du ressort du viewer de scène, pas du ciel : la
|
|
// rendre ici donnerait un décor plaqué sur une sphère. Le §4 la prévoit, E7
|
|
// sait la charger — le jour où on les branche, c'est ici que ça se décide.
|
|
if (background.Kind == ConfigurationExport.ImmersiveBackgroundKind.Scene3D)
|
|
{
|
|
Debug.Log("[Backdrop] Fond de type Scène 3D : pas encore rendu en fond, " +
|
|
"le menu reste sur le ciel par défaut.");
|
|
return null;
|
|
}
|
|
|
|
var root = new GameObject("ImmersiveBackdrop");
|
|
var backdrop = root.AddComponent<ImmersiveBackdrop>();
|
|
|
|
await backdrop.LoadAsync(background, client);
|
|
return backdrop;
|
|
}
|
|
|
|
async Task LoadAsync(ConfigurationExport.Backdrop background, ApiClient client)
|
|
{
|
|
_sky = PanoramicSky.CreateMaterial();
|
|
if (_sky == null) return;
|
|
|
|
if (background.Kind == ConfigurationExport.ImmersiveBackgroundKind.Video360)
|
|
PlayVideo(background, client);
|
|
else
|
|
await ShowImage(background, client);
|
|
}
|
|
|
|
async Task ShowImage(ConfigurationExport.Backdrop background, ApiClient client)
|
|
{
|
|
var texture = await PanoramicSky.LoadEquirectangularAsync(client, background.ResourceUrl);
|
|
if (texture == null || this == null) return;
|
|
|
|
_image = texture;
|
|
_sky.SetTexture("_MainTex", texture);
|
|
RenderSettings.skybox = _sky;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ⚠️ <b>Une vidéo en fond tourne en permanence.</b> Sur une borne qui fonctionne
|
|
/// huit heures, c'est un décodeur matériel occupé toute la journée et une batterie
|
|
/// qui descend plus vite — à mesurer avant de la vendre comme le cas normal. Le son
|
|
/// est coupé : un fond qui parle par-dessus le commentaire d'une section serait
|
|
/// intenable.
|
|
/// </summary>
|
|
void PlayVideo(ConfigurationExport.Backdrop background, ApiClient client)
|
|
{
|
|
_target = new RenderTexture(VideoWidth, VideoHeight, 0);
|
|
|
|
_player = gameObject.AddComponent<VideoPlayer>();
|
|
_player.source = VideoSource.Url;
|
|
_player.renderMode = VideoRenderMode.RenderTexture;
|
|
_player.targetTexture = _target;
|
|
_player.isLooping = true;
|
|
_player.audioOutputMode = VideoAudioOutputMode.None;
|
|
_player.errorReceived += (_, message) =>
|
|
Debug.LogError($"[Backdrop] Lecture vidéo impossible : {message}");
|
|
|
|
_sky.SetTexture("_MainTex", _target);
|
|
RenderSettings.skybox = _sky;
|
|
|
|
StartPlayback(background.ResourceUrl, client);
|
|
}
|
|
|
|
async void StartPlayback(string url, ApiClient client)
|
|
{
|
|
var path = await ContentCache.MediaPathAsync(client, url);
|
|
if (path == null || this == null || _player == null) return;
|
|
|
|
_player.url = new Uri(path).AbsoluteUri;
|
|
_player.Play();
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
// Le ciel est un réglage global. On ne le remet à null que s'il est encore le
|
|
// nôtre : une 360 ouverte par-dessus a déjà pris la place, et lui arracher son
|
|
// ciel en repassant par ici la laisserait dans le noir.
|
|
if (RenderSettings.skybox == _sky) RenderSettings.skybox = null;
|
|
|
|
if (_player != null) _player.Stop();
|
|
if (_target != null) _target.Release();
|
|
if (_image != null) Destroy(_image);
|
|
}
|
|
}
|
|
}
|