Files
plugin-library/Assets/99.imdk_unity/Runtime/Scripts/XR/MapManager.cs

869 lines
32 KiB
C#
Raw Normal View History

2025-06-19 10:56:43 +08:00
/*===============================================================================
Copyright (C) 2024 Immersal - Part of Hexagon. All Rights Reserved.
This file is part of the Immersal SDK.
The Immersal SDK cannot be copied, distributed, or made available to
third-parties for commercial purposes without written permission of Immersal Ltd.
Contact sales@immersal.com for licensing requests.
===============================================================================*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Immersal.REST;
using Unity.XR.CoreUtils;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
namespace Immersal.XR
{
public static class MapManager
{
public static UnityEvent<int> MapRegisteredAndLoaded;
private static Dictionary<int, MapEntry> m_MapEntries = new Dictionary<int, MapEntry>();
public static bool HasRegisteredMaps { get; private set; }
public static async Task RegisterAndLoadMap(XRMap map, ISceneUpdateable sceneParent)
{
if (!map.IsConfigured)
{
ImmersalLogger.LogError($"Trying to register and load unconfigured map: {map.name}");
return;
}
if (map.LocalizationMethod == null)
{
ImmersalLogger.LogError($"Map {map.mapName} has null localization method.");
return;
}
if (!TryGetMapEntry(map.mapId, out MapEntry entry))
{
RegisterMap(map, sceneParent);
}
await map.LocalizationMethod.OnMapRegistered(map);
bool loadSuccess = await LoadMap(map);
if (loadSuccess)
{
MapRegisteredAndLoaded?.Invoke(map.mapId);
}
UpdateHasRegisteredMaps();
ImmersalLogger.Log($"RegisterAndLoad for map {map.mapId} complete.");
}
// The LoadMap method checks if the map has a MapLoadingOption and loads the map
// into the Immersal plugin based on the configuration.
// Note: not all maps require loading (ServerLocalization for example)
public static async Task<bool> LoadMap(XRMap map)
{
MapLoadingOption mlo = map.MapOptions.FirstOrDefault(option => option.Name == "MapLoading") as MapLoadingOption;
if (mlo == null)
{
ImmersalLogger.Log($"No MapLoadingOption on map {map.mapId}, skipping loading.");
// No MapLoadingOption -> assume map does not need to be loaded
return true;
}
ImmersalLogger.Log($"Loading map {map.mapId} according to MapLoadingOption.");
// Download visualization?
if (mlo.DownloadVisualizationAtRuntime)
{
ImmersalLogger.Log($"Downloading visualization for map {map.mapId}");
JobLoadMapSparseAsync j = new JobLoadMapSparseAsync();
j.id = map.mapId;
SDKSparseDownloadResult plyResult = await j.RunJobAsync();
ImmersalLogger.Log($"Adding visualization for map {map.mapId}");
map.CreateVisualization(XRMapVisualization.RenderMode.EditorAndRuntime, true);
map.Visualization.LoadPly(plyResult.data, map.mapName);
}
// Only report failure if loading is attempted and failed
bool loadSuccess = true;
// Load raw bytes. This option is not exposed in the inspector, only for runtime configs.
if (mlo.Bytes is { Length: > 0 })
{
ImmersalLogger.Log($"Loading provided bytes for: {map.mapId}");
loadSuccess = await TryToLoadMap(mlo.Bytes, map.mapId);
}
// Download map data
else if (mlo.m_SerializedDataSource == (int)MapDataSource.Download)
{
ImmersalLogger.Log($"Map {map.mapId} configured to download data.");
ImmersalLogger.Log($"Downloading mapfile for map {map.mapId}");
JobLoadMapBinaryAsync j = new JobLoadMapBinaryAsync();
j.id = map.mapId;
SDKMapResult result = await j.RunJobAsync();
ImmersalLogger.Log($"Loading downloaded mapfile for map {map.mapId}");
loadSuccess = await TryToLoadMap(result.mapData, map.mapId);
}
// Use embedded mapfile
else if (mlo.m_SerializedDataSource == (int)MapDataSource.Embed)
{
if (map.mapFile == null)
{
ImmersalLogger.LogError($"Missing map file for: {map.mapId}.");
return false;
}
ImmersalLogger.Log($"Loading embedded mapfile for: {map.mapId}");
byte[] mapBytes = map.mapFile.bytes;
loadSuccess = await TryToLoadMap(mapBytes, map.mapId);
}
else
{
ImmersalLogger.Log("MapLoadingOption not configured to load any map data.");
}
return loadSuccess;
}
public static async Task<bool> TryToLoadMap(byte[] mapBytes, int mapId)
{
// Check if map is registered
if (TryGetMapEntry(mapId, out MapEntry entry))
{
if (mapBytes != null)
{
Task<int> t = Task.Run(() => { return Immersal.Core.LoadMap(mapId, mapBytes); });
await t;
return t.Result != -1;
}
}
else
{
ImmersalLogger.LogError($"Trying to load unregistered map ID: {mapId}");
}
return false;
}
public static async Task<MapCreationResult> TryCreateMap(MapCreationParameters parameters, bool unconfigured = false)
{
// Default to failure
MapCreationResult result = new MapCreationResult { Success = false };
// GameObject
parameters.Name = string.IsNullOrEmpty(parameters.Name) ? "Unnamed map" : parameters.Name;
GameObject go = new GameObject(parameters.Name);
ImmersalLogger.Log($"Creating a new map: '{parameters.Name}'");
// XRMap component
XRMap map = go.AddComponent<XRMap>();
result.Map = map;
// SceneParent
if (parameters.SceneParent == null)
{
GameObject newParent = new GameObject("New XR Space");
parameters.SceneParent = newParent.AddComponent<XRSpace>();
}
go.transform.SetParent(parameters.SceneParent.GetTransform());
result.SceneParent = parameters.SceneParent;
// Optionally return here and leave out configuration, registering and loading
if (unconfigured)
{
result.Success = true;
return result;
}
// Configure
if (parameters.MapId != null)
{
map.SetIdAndName(parameters.MapId.Value, parameters.Name, true);
map.Configure();
}
else
{
if (parameters.MetadataGetResult == null)
return result; // fail
map.Configure(parameters.MetadataGetResult.Value); // applies metadata
}
// Alignment
// Note: assumes alignment is already defined with metadata/custom data
if (parameters.ApplyMapAlignment)
{
map.ApplyAlignment();
}
// Localization method
// If null, try to find based on Type
if (parameters.LocalizationMethod == null)
{
ILocalizationMethod[] availableMethods = AvailableLocalizationMethods;
ILocalizationMethod method =
availableMethods?.FirstOrDefault(m => m.GetType() == parameters.LocalizationMethodType);
if (method == null)
{
ImmersalLogger.LogError("Requested LocalizationMethod is not available");
return result; // fail
}
parameters.LocalizationMethod = method;
}
// Try to configure the LocalizationMethod
DefaultLocalizerConfiguration config = new DefaultLocalizerConfiguration
{
ConfigurationsToAdd = new Dictionary<ILocalizationMethod, XRMap[]>
{
{ parameters.LocalizationMethod, new XRMap[] { map } }
}
};
ILocalizerConfigurationResult r = await ImmersalSDK.Instance.Localizer.ConfigureLocalizer(config);
//if (!await parameters.LocalizationMethod.Configure(new XRMap[] { map }))
if (!r.Success)
{
ImmersalLogger.LogError("Could not configure requested LocalizationMethod");
return result; // fail
}
map.LocalizationMethod = parameters.LocalizationMethod;
// Map options
if (parameters.MapOptions != null)
{
map.MapOptions = parameters.MapOptions.ToList();
}
// Register and load
await RegisterAndLoadMap(map, parameters.SceneParent);
result.Success = true;
return result;
}
public static List<XRMap> GetRegisteredMaps()
{
List<XRMap> maps = new List<XRMap>();
foreach (KeyValuePair<int,MapEntry> keyValuePair in m_MapEntries)
{
maps.Add(keyValuePair.Value.Map);
}
return maps;
}
public static int GetRegisteredMapCount()
{
return m_MapEntries.Count;
}
public static List<ISceneUpdateable> GetSceneUpdateablesInUse()
{
List<ISceneUpdateable> updateables = new List<ISceneUpdateable>();
foreach (KeyValuePair<int,MapEntry> keyValuePair in m_MapEntries)
{
ISceneUpdateable sceneUpdateable = keyValuePair.Value.SceneParent;
if (!updateables.Contains(sceneUpdateable ))
updateables.Add(sceneUpdateable);
}
return updateables;
}
private static void UpdateHasRegisteredMaps()
{
HasRegisteredMaps = m_MapEntries.Count > 0;
}
#region MapEntries
public static void RegisterMap(XRMap map, ISceneUpdateable sceneParent)
{
ImmersalLogger.Log($"Registering map: {map.mapId}");
if (m_MapEntries.ContainsKey(map.mapId))
{
ImmersalLogger.LogWarning("Map is already registered, aborting.");
return;
}
Transform tr = map.transform;
MapToSpaceRelation mtsr = new MapToSpaceRelation
{
Position = tr.localPosition,
Rotation = tr.localRotation,
Scale = tr.localScale
};
MapEntry me = new MapEntry
{
Map = map,
SceneParent = sceneParent,
Relation = mtsr
};
m_MapEntries.Add(map.mapId, me);
}
public static async void RemoveMap(int mapId, bool removeFromLocalizer = true, bool destroyObjects = false)
{
if (TryGetMapEntry(mapId, out MapEntry entry))
{
// Update localizer configuration
if (removeFromLocalizer)
{
ILocalizationMethod method = entry.Map.LocalizationMethod;
DefaultLocalizerConfiguration config = new DefaultLocalizerConfiguration
{
ConfigurationsToRemove = new Dictionary<ILocalizationMethod, XRMap[]>
{
{ method, new XRMap[] { entry.Map } }
}
};
await ImmersalSDK.Instance.Localizer.ConfigureLocalizer(config);
}
// Free & remove mapping (if it's loaded)
Immersal.Core.FreeMap(mapId);
// Destroy
if (destroyObjects)
{
XRMap map = entry.Map;
if (map.Visualization != null)
map.RemoveVisualization();
if (map.gameObject)
GameObject.Destroy(map.gameObject);
// Check if parent only had this one child
// Note: the XRMap destruction above happens later when Unity wants it to
Transform parent = entry.SceneParent.GetTransform();
if (parent != null && parent.childCount == 1)
{
GameObject.Destroy(parent.gameObject);
}
}
// Remove entry
m_MapEntries.Remove(mapId);
UpdateHasRegisteredMaps();
}
}
public static void RemoveAllMaps(bool removeFromLocalizer = true, bool destroyObjects = false)
{
if (removeFromLocalizer)
{
// Remove all from localizer configuration with one call
Dictionary<ILocalizationMethod, XRMap[]> allMaps = m_MapEntries.Values
.Select(entry => entry.Map)
.GroupBy(map => map.LocalizationMethod)
.ToDictionary(
group => group.Key,
group => group.ToArray());
DefaultLocalizerConfiguration config = new DefaultLocalizerConfiguration
{
ConfigurationsToRemove = allMaps
};
ImmersalSDK.Instance.Localizer.ConfigureLocalizer(config);
}
foreach (int id in m_MapEntries.Keys.ToList())
{
// removeFromLocalizer set to false since we already removed all
RemoveMap(id, false, destroyObjects);
}
}
public static bool TryGetMapEntry(int mapId, out MapEntry mapEntry)
{
return m_MapEntries.TryGetValue(mapId, out mapEntry);
}
public static bool HasMapEntry(int mapId)
{
return m_MapEntries.ContainsKey(mapId);
}
#endregion
#region Localization methods and map options
/*
* This section takes care of managing a collection of available localization methods.
*/
private static ILocalizationMethod[] m_CachedLocalizationMethods;
public static ILocalizationMethod[] AvailableLocalizationMethods
{
get
{
if (m_CachedLocalizationMethods == null)
{
RefreshLocalizationMethods();
}
return m_CachedLocalizationMethods;
}
}
// To ensure our cached collections are up to date and pointing to the correct instances,
// we need to react to both assembly reloads and scene changes.
#if UNITY_EDITOR
static MapManager()
{
EditorSceneManager.sceneOpened += OnSceneOpened;
}
private static void OnSceneOpened(Scene scene, OpenSceneMode mode)
{
InvalidateCache();
}
[InitializeOnLoadMethod]
public static void ReactToAssemblyReload()
{
InvalidateCache();
}
#endif
private static void InvalidateCache()
{
m_CachedLocalizationMethods = null;
}
public static void RefreshLocalizationMethods()
{
if (ImmersalSDK.Instance == null || ImmersalSDK.Instance.Localizer == null)
{
ImmersalLogger.LogError("Missing ImmersalSDK or Localizer, cant fetch localization methods.");
return;
}
ILocalizationMethod[] methods = ImmersalSDK.Instance.Localizer.AvailableLocalizationMethods;
if (methods == null)
{
ImmersalLogger.LogError("No localization methods defined in Localizer");
return;
}
// Check for duplicates
HashSet<ILocalizationMethod> uniqueMethods = new HashSet<ILocalizationMethod>();
foreach (ILocalizationMethod method in methods)
{
if (!uniqueMethods.Add(method))
{
// Duplicate found
ImmersalLogger.LogWarning("Localizer has duplicate Localization Method references.");
continue;
}
}
m_CachedLocalizationMethods = uniqueMethods.ToArray();
}
public static bool TryGetMapOptions(ILocalizationMethod localizationMethod, out List<IMapOption> options)
{
options = new List<IMapOption>();
if (localizationMethod.IsNullOrDead())
{
ImmersalLogger.LogError("Trying to fetch map options for null localization method.");
return false;
}
IMapOption[] mapOptionsFromMethod = localizationMethod.MapOptions;
if (mapOptionsFromMethod == null)
return false;
options = mapOptionsFromMethod.ToList();
return true;
}
#endregion
#region Downloading coroutines for edit time use
#if UNITY_EDITOR
public static string GetDirectoryPath(string inputPath = "", bool assetsRoot = false)
{
string result = "";
string root = assetsRoot ? "Assets/" : Application.dataPath;
string defaultPath = Path.Combine(root, ImmersalSDK.Instance.DownloadDirectory);
result = inputPath != "" ? Path.Combine(root, inputPath) : defaultPath;
return result;
}
public static bool CheckDirectory(string path)
{
if (!Directory.Exists(path))
{
ImmersalLogger.LogWarning("Requested directory does not exist, creating it now.");
Directory.CreateDirectory(path);
}
if (string.IsNullOrEmpty(path))
{
ImmersalLogger.LogError("Requested path is null or empty.");
return false;
}
try
{
string fullPath = Path.GetFullPath(path);
}
catch
{
ImmersalLogger.LogError($"Requested path is invalid: {path}");
return false;
}
return true;
}
public static IEnumerator DownloadMapMetadata(int mapId, Action<SDKMapMetadataGetResult> resultCallback, string jsonWritePath = "")
{
string targetFullPath = GetDirectoryPath(jsonWritePath);
if (!CheckDirectory(targetFullPath))
yield break;
// Load map metadata from Immersal Cloud Service
SDKMapMetadataGetRequest r = new SDKMapMetadataGetRequest();
r.token = ImmersalSDK.Instance.developerToken;
r.id = mapId;
if (r.token == "")
ImmersalLogger.LogWarning("Trying to download map data without developer token.");
string jsonString = JsonUtility.ToJson(r);
UnityWebRequest request =
UnityWebRequest.Put(
string.Format(ImmersalHttp.URL_FORMAT, ImmersalSDK.Instance.localizationServer,
SDKMapMetadataGetRequest.endpoint), jsonString);
request.method = UnityWebRequest.kHttpVerbPOST;
request.useHttpContinue = false;
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Accept", "application/json");
request.SendWebRequest();
while (!request.isDone)
{
yield return null;
}
#if UNITY_2020_1_OR_NEWER
if (request.result != UnityWebRequest.Result.Success)
#else
if (request.isNetworkError || request.isHttpError)
#endif
{
ImmersalLogger.LogError(request.error);
}
else
{
SDKMapMetadataGetResult result =
JsonUtility.FromJson<SDKMapMetadataGetResult>(request.downloadHandler.text);
if (result.error == "none")
{
resultCallback(result);
string fileName = $"{result.id}-{result.name}-metadata.json";
string jsonFilePath = Path.Combine(targetFullPath, fileName);
WriteJson(jsonFilePath, request.downloadHandler.text);
string assetPath = Path.Combine(GetDirectoryPath(jsonWritePath, true), fileName);
AssetDatabase.Refresh();
AssetDatabase.ImportAsset(assetPath);
}
}
}
public static IEnumerator DownloadMapFile(int mapId, string mapName, Action<SDKMapDownloadResult, TextAsset> resultCallback, string bytesWritePath = "")
{
string targetFullPath = GetDirectoryPath(bytesWritePath);
string targetAssetPath = GetDirectoryPath(bytesWritePath, true);
if (!CheckDirectory(targetFullPath))
yield break;
// Load map file from Immersal Cloud Service
SDKMapDownloadRequest r = new SDKMapDownloadRequest();
r.token = ImmersalSDK.Instance.developerToken;
r.id = mapId;
if (r.token == "")
ImmersalLogger.LogWarning("Trying to download map data without developer token.");
string jsonString = JsonUtility.ToJson(r);
UnityWebRequest request = UnityWebRequest.Put(string.Format(ImmersalHttp.URL_FORMAT, ImmersalSDK.Instance.localizationServer, SDKMapDownloadRequest.endpoint), jsonString);
request.method = UnityWebRequest.kHttpVerbPOST;
request.useHttpContinue = false;
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Accept", "application/json");
request.SendWebRequest();
while (!request.isDone)
{
yield return null;
}
#if UNITY_2020_1_OR_NEWER
if (request.result != UnityWebRequest.Result.Success)
#else
if (request.isNetworkError || request.isHttpError)
#endif
{
ImmersalLogger.LogError(request.error);
}
else
{
SDKMapDownloadResult mapDataResult = JsonUtility.FromJson<SDKMapDownloadResult>(request.downloadHandler.text);
if (mapDataResult.error == "none")
{
// Save map file on disk, overwrite existing file
string fileName = $"{mapId}-{mapName}.bytes";
string mapFileFullPath = Path.Combine(targetFullPath, fileName);
string mapFileAssetPath = Path.Combine(GetDirectoryPath(bytesWritePath, true), fileName);
WriteBytes(mapFileFullPath, mapDataResult.b64);
AssetDatabase.Refresh();
AssetDatabase.ImportAsset(mapFileAssetPath);
TextAsset mapFile =
(TextAsset)AssetDatabase.LoadAssetAtPath(mapFileAssetPath, typeof(TextAsset));
resultCallback(mapDataResult, mapFile);
}
}
}
public static IEnumerator DownloadSparseFile(int mapId, string mapName, Action<SDKSparseDownloadResult, string> resultCallback, string plyWritePath = "")
{
string targetFullPath = GetDirectoryPath(plyWritePath);
if (!CheckDirectory(targetFullPath))
yield break;
string uri =
$"{ImmersalSDK.Instance.localizationServer}/{SDKSparseDownloadRequest.endpoint}?token={ImmersalSDK.Instance.developerToken}&id={mapId}";
if (ImmersalSDK.Instance.developerToken == "")
ImmersalLogger.LogWarning("Trying to download map data without developer token.");
using (UnityWebRequest request = UnityWebRequest.Get(uri))
{
// Request and wait for completion
yield return request.SendWebRequest();
#if UNITY_2020_1_OR_NEWER
if (request.result != UnityWebRequest.Result.Success)
#else
if (request.isNetworkError || request.isHttpError)
#endif
{
ImmersalLogger.LogError(request.error);
}
else
{
SDKSparseDownloadResult plyDataResult = new SDKSparseDownloadResult();
plyDataResult.data = request.downloadHandler.data;
plyDataResult.error = request.error;
if (plyDataResult.error == null)
{
// Save map file on disk, overwrite existing file
string fileName = $"{mapId}-{mapName}-sparse.ply";
string plyFilePath = Path.Combine(targetFullPath, fileName);
WritePly(plyFilePath, plyDataResult.data);
resultCallback(plyDataResult, plyFilePath);
}
}
}
}
public static void WriteJson(string jsonFilepath, string data, bool overwrite = false)
{
if (File.Exists(jsonFilepath))
{
if (!overwrite) return;
File.Delete(jsonFilepath);
}
File.WriteAllText(jsonFilepath, data);
}
public static void WriteBytes(string mapFilepath, string b64, bool overwrite = false)
{
if (File.Exists(mapFilepath))
{
if (!overwrite) return;
File.Delete(mapFilepath);
}
byte[] data = Convert.FromBase64String(b64);
File.WriteAllBytes(mapFilepath, data);
}
public static void WritePly(string plyFilepath, byte[] data, bool overwrite = false)
{
if (File.Exists(plyFilepath))
{
if (!overwrite) return;
File.Delete(plyFilepath);
}
File.WriteAllBytes(plyFilepath, data);
AssetDatabase.Refresh();
AssetDatabase.ImportAsset(plyFilepath);
}
#endif
#endregion
}
public enum MapDataSource
{
Embed = 0, // use embedded mapfile
Download = 1 // let MapManager download mapfile at runtime
}
[Serializable]
public class MapLoadingOption : IMapOption
{
public string Name => "MapLoading";
[SerializeField]
public int m_SerializedDataSource = 0;
private MapDataSource m_MapDataSource = MapDataSource.Embed;
[SerializeField]
public bool DownloadVisualizationAtRuntime = false;
private TextAsset currentMapFile = null;
private TextAsset prevMapFile = null;
public byte[] Bytes;
public void DrawEditorGUI(XRMap map)
{
#if UNITY_EDITOR
m_MapDataSource = (MapDataSource)m_SerializedDataSource;
EditorGUI.BeginChangeCheck();
m_MapDataSource = (MapDataSource)EditorGUILayout.EnumPopup("Map data source", m_MapDataSource);
if (EditorGUI.EndChangeCheck())
{
m_SerializedDataSource = (int)m_MapDataSource;
}
if (m_SerializedDataSource == 0)
{
currentMapFile = prevMapFile = map.mapFile;
EditorGUI.BeginChangeCheck();
currentMapFile =
(TextAsset)EditorGUILayout.ObjectField("Map file", currentMapFile, typeof(TextAsset), false);
if (EditorGUI.EndChangeCheck())
{
if (currentMapFile == null)
return;
if (prevMapFile == null || currentMapFile != prevMapFile)
{
string bytesPath = AssetDatabase.GetAssetPath(currentMapFile);
if (bytesPath.EndsWith(".bytes"))
{
// Switch mapfile
prevMapFile = currentMapFile;
map.Uninitialize();
map.Configure(currentMapFile);
EditorUtility.SetDirty(map);
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
}
else
{
ImmersalLogger.LogError($"{AssetDatabase.GetAssetPath(currentMapFile)} is not a valid map file");
map.mapFile = prevMapFile;
}
}
}
}
else
{
DownloadVisualizationAtRuntime =
EditorGUILayout.Toggle("Download visualization", DownloadVisualizationAtRuntime);
}
#endif
}
}
public class MapEntry
{
public XRMap Map;
public ISceneUpdateable SceneParent;
public MapToSpaceRelation Relation;
}
public class MapToSpaceRelation
{
public Vector3 Position;
public Quaternion Rotation;
public Vector3 Scale;
public Matrix4x4 ApplyRelation(Matrix4x4 input)
{
Vector3 inPos = input.GetPosition();
Quaternion inRot = input.rotation;
Matrix4x4 offsetNoScale = Matrix4x4.TRS(Position, Rotation, Vector3.one);
Vector3 scaledPos = Vector3.Scale(inPos, Scale);
return offsetNoScale * Matrix4x4.TRS(scaledPos, inRot, Vector3.one);
}
public Matrix4x4 ApplyInverseRelation(Matrix4x4 input)
{
Matrix4x4 offsetNoScale = Matrix4x4.TRS(Position, Rotation, Vector3.one);
Matrix4x4 offsetNoScaleInverse = offsetNoScale.inverse;
Matrix4x4 intermediate = offsetNoScaleInverse * input;
Vector3 scaledPos = intermediate.GetPosition();
Quaternion rot = intermediate.rotation;
Vector3 unscaledPos = scaledPos.SafeDivide(Scale);
return Matrix4x4.TRS(unscaledPos, rot, Vector3.one);
}
}
public class MapCreationParameters
{
public int? MapId; // Either this
public SDKMapMetadataGetResult? MetadataGetResult; // or this is necessary
public String Name; // Optional
public ISceneUpdateable SceneParent; // Optional
public ILocalizationMethod LocalizationMethod; // Optional
public Type LocalizationMethodType = typeof(DeviceLocalization); // Necessary if above is null
public IMapOption[] MapOptions; // Optional
public bool ApplyMapAlignment;
}
public class MapCreationResult
{
public bool Success;
public XRMap Map;
public ISceneUpdateable SceneParent;
}
}