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>
251 lines
7.9 KiB
C#
251 lines
7.9 KiB
C#
using System.Collections.Generic;
|
|
using Newtonsoft.Json;
|
|
using UnityEngine;
|
|
|
|
namespace MyInfoMate.Vr.Manifest
|
|
{
|
|
/// <summary>
|
|
/// Miroir exact du schéma §2.2 de la conception. <b>Ce fichier et
|
|
/// <c>viewer/src/manifest.ts</c> décrivent la même chose</b> : modifier l'un
|
|
/// sans l'autre casse le test croisé de S3.
|
|
///
|
|
/// Les coordonnées sont ici <b>telles qu'écrites dans le manifeste</b>, donc en
|
|
/// convention glTF. Elles ne deviennent des coordonnées Unity qu'en passant par
|
|
/// <see cref="Scene.GltfSpace"/> — nulle part ailleurs.
|
|
/// </summary>
|
|
public class SceneManifest
|
|
{
|
|
public const int SupportedManifestVersion = 1;
|
|
public const string ExpectedCoordinateSystem = "gltf/y-up/right-handed/meters";
|
|
|
|
/// <summary>Version du <b>format</b>. Distincte de <see cref="Version"/>.</summary>
|
|
public int ManifestVersion = 1;
|
|
|
|
public string SceneId;
|
|
public string InstanceId;
|
|
public string ConfigurationId;
|
|
|
|
/// <summary>Version du <b>contenu</b> : +1 à chaque publication. C'est elle que le
|
|
/// cache compare pour savoir qu'il y a du nouveau.</summary>
|
|
public int Version;
|
|
|
|
public string PublishedAt;
|
|
|
|
public List<string> Languages = new List<string>();
|
|
public string DefaultLanguage;
|
|
|
|
public string CoordinateSystem;
|
|
|
|
public NavigationManifest Navigation = new NavigationManifest();
|
|
public EnvironmentManifest Environment = new EnvironmentManifest();
|
|
public WorldManifest World;
|
|
|
|
public List<SceneObject> Objects = new List<SceneObject>();
|
|
public List<ScenePersona> Personas = new List<ScenePersona>();
|
|
public List<SceneHotspot> Hotspots = new List<SceneHotspot>();
|
|
public List<SceneAsset> Assets = new List<SceneAsset>();
|
|
|
|
public BudgetManifest Budget;
|
|
public ProvenanceManifest Provenance;
|
|
|
|
[JsonIgnore] Dictionary<string, SceneAsset> _assetsById;
|
|
|
|
/// <summary>
|
|
/// <c>assets[]</c> est la seule liste que le téléchargeur parcourt (§2.3) : tout
|
|
/// le reste ne porte que des identifiants. D'où cet index.
|
|
/// </summary>
|
|
public SceneAsset FindAsset(string assetId)
|
|
{
|
|
if (string.IsNullOrEmpty(assetId)) return null;
|
|
|
|
if (_assetsById == null)
|
|
{
|
|
_assetsById = new Dictionary<string, SceneAsset>();
|
|
foreach (var asset in Assets)
|
|
if (asset != null && !string.IsNullOrEmpty(asset.Id))
|
|
_assetsById[asset.Id] = asset;
|
|
}
|
|
|
|
return _assetsById.TryGetValue(assetId, out var found) ? found : null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Position + rotation + échelle en convention glTF. Les accesseurs Unity passent
|
|
/// tous par <see cref="Scene.GltfSpace"/> : c'est ce qui garantit qu'il n'existe
|
|
/// qu'une conversion dans le projet.
|
|
/// </summary>
|
|
public class TransformManifest
|
|
{
|
|
public float[] Position = { 0f, 0f, 0f };
|
|
public float[] Rotation = { 0f, 0f, 0f, 1f };
|
|
public float[] Scale = { 1f, 1f, 1f };
|
|
|
|
public Vector3 UnityPosition => Scene.GltfSpace.Position(Position ?? new[] { 0f, 0f, 0f });
|
|
|
|
public Quaternion UnityRotation =>
|
|
Scene.GltfSpace.Rotation(Rotation ?? new[] { 0f, 0f, 0f, 1f });
|
|
|
|
public Vector3 UnityScale => Scene.GltfSpace.Scale(Scale ?? new[] { 1f, 1f, 1f });
|
|
|
|
public void ApplyTo(Transform target)
|
|
{
|
|
target.localPosition = UnityPosition;
|
|
target.localRotation = UnityRotation;
|
|
target.localScale = UnityScale;
|
|
}
|
|
}
|
|
|
|
public class NavigationManifest
|
|
{
|
|
/// <summary>Le plafond dur du §2.2, appliqué serveur <b>et</b> client.</summary>
|
|
public const float MaxRadiusMeters = 3f;
|
|
|
|
public TransformManifest Spawn = new TransformManifest();
|
|
public float RadiusMeters = MaxRadiusMeters;
|
|
public bool ShowBoundary = true;
|
|
|
|
public float ClampedRadiusMeters => Mathf.Clamp(RadiusMeters, 0.5f, MaxRadiusMeters);
|
|
}
|
|
|
|
public class EnvironmentManifest
|
|
{
|
|
public string LightingPreset = "neutral-indoor";
|
|
public string HdriAssetId;
|
|
public float Exposure = 1f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// <c>kind</c> est la couture V2 : la V1 ne lit que <see cref="World3DKind.Mesh"/> et
|
|
/// refuse le reste <b>explicitement</b>. Un refus lisible vaut mieux qu'une scène
|
|
/// vide (§2.3).
|
|
/// </summary>
|
|
public class WorldManifest
|
|
{
|
|
public string Kind = "mesh";
|
|
public string AssetId;
|
|
public TransformManifest Transform = new TransformManifest();
|
|
|
|
public World3DKind ParsedKind =>
|
|
Kind switch
|
|
{
|
|
"mesh" => World3DKind.Mesh,
|
|
"splat" => World3DKind.Splat,
|
|
"panorama" => World3DKind.Panorama,
|
|
_ => World3DKind.Unknown
|
|
};
|
|
}
|
|
|
|
public enum World3DKind
|
|
{
|
|
Mesh,
|
|
Splat,
|
|
Panorama,
|
|
Unknown
|
|
}
|
|
|
|
public class SceneObject
|
|
{
|
|
public string Id;
|
|
public string Label;
|
|
public string AssetId;
|
|
public TransformManifest Transform = new TransformManifest();
|
|
}
|
|
|
|
public class ScenePersona
|
|
{
|
|
public string Id;
|
|
|
|
/// <summary>Null en V1 : la couture vers l'entité <c>Persona</c> du lot 7 Studio.</summary>
|
|
public string PersonaId;
|
|
|
|
public string Name;
|
|
public string AssetId;
|
|
public TransformManifest Transform = new TransformManifest();
|
|
public AnimationManifest Animation = new AnimationManifest();
|
|
public bool GazeAtVisitor = true;
|
|
public List<LocalizedAsset> Audio = new List<LocalizedAsset>();
|
|
public List<LocalizedText> Script = new List<LocalizedText>();
|
|
}
|
|
|
|
public class AnimationManifest
|
|
{
|
|
public string IdleClip = "Idle";
|
|
public bool Loop = true;
|
|
}
|
|
|
|
public class SceneHotspot
|
|
{
|
|
public string Id;
|
|
|
|
/// <summary>Un hotspot <b>est</b> un <c>GeoPoint</c> côté serveur (§3.2).</summary>
|
|
public int GeoPointId;
|
|
|
|
public TransformManifest Transform = new TransformManifest();
|
|
public List<LocalizedText> Title = new List<LocalizedText>();
|
|
public List<LocalizedText> Description = new List<LocalizedText>();
|
|
public List<HotspotContent> Contents = new List<HotspotContent>();
|
|
}
|
|
|
|
public class HotspotContent
|
|
{
|
|
public int Order;
|
|
public List<LocalizedText> Title = new List<LocalizedText>();
|
|
public List<LocalizedText> Description = new List<LocalizedText>();
|
|
public string AssetId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toutes les langues arrivent d'un coup : un casque en borne change de langue à
|
|
/// chaud, l'export mono-langue existant ne suffit pas (§2.3).
|
|
/// </summary>
|
|
public class LocalizedText
|
|
{
|
|
public string Language;
|
|
public string Value;
|
|
}
|
|
|
|
public class LocalizedAsset
|
|
{
|
|
public string Language;
|
|
public string AssetId;
|
|
}
|
|
|
|
public class SceneAsset
|
|
{
|
|
public string Id;
|
|
public string ResourceId;
|
|
public string Url;
|
|
public string MimeType;
|
|
public long SizeBytes;
|
|
|
|
/// <summary>Le seul moyen d'un delta réel côté casque (§2.4).</summary>
|
|
public string Sha256;
|
|
|
|
/// <summary>KTX2 / Meshopt viendront ici, sans casser le contrat.</summary>
|
|
public List<AssetVariant> Variants = new List<AssetVariant>();
|
|
}
|
|
|
|
public class AssetVariant
|
|
{
|
|
public string Kind;
|
|
public string Url;
|
|
public string MimeType;
|
|
public long SizeBytes;
|
|
public string Sha256;
|
|
}
|
|
|
|
public class BudgetManifest
|
|
{
|
|
public long TotalBytes;
|
|
public long LimitBytes;
|
|
}
|
|
|
|
public class ProvenanceManifest
|
|
{
|
|
public bool AiGenerated;
|
|
public string RightsHolder;
|
|
public List<string> Sources = new List<string>();
|
|
}
|
|
}
|