99.imdk_unity 上传

This commit is contained in:
2025-06-19 10:56:43 +08:00
parent d48c1f1f7b
commit 820c663ab8
651 changed files with 123674 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fb6de87a81def4d49b6cafe60de749b7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,136 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Immersal.XR
{
public class AVTFilter : IImmersalFilter
{
public bool m_UseConfidence = false;
public float m_ConfidenceMax = 50f;
public float m_ConfidenceImpact = 0.5f;
private readonly int m_HistorySize = 8;
private Vector3[] m_P;
private Vector3[] m_X;
private Vector3[] m_Z;
private float[] m_C;
private uint m_Samples = 0;
public AVTFilter()
{
m_P = new Vector3[m_HistorySize];
m_X = new Vector3[m_HistorySize];
m_Z = new Vector3[m_HistorySize];
m_C = new float[m_HistorySize];
}
public AVTFilter(int historySize, bool useConfidence, float confidenceMax, float confidenceImpact)
{
m_HistorySize = historySize;
m_P = new Vector3[m_HistorySize];
m_X = new Vector3[m_HistorySize];
m_Z = new Vector3[m_HistorySize];
m_C = new float[m_HistorySize];
m_UseConfidence = useConfidence;
m_ConfidenceMax = confidenceMax;
m_ConfidenceImpact = confidenceImpact;
}
public void InvalidateHistory()
{
m_Samples = 0;
}
public bool IsValid()
{
return m_Samples > 1;
}
public Matrix4x4 Filter(Matrix4x4 pose, SceneUpdateData data)
{
float confidence = data.LocalizeInfo.confidence;
int idx = (int)(m_Samples % m_HistorySize);
m_P[idx] = pose.GetColumn(3);
m_X[idx] = pose.GetColumn(0);
m_Z[idx] = pose.GetColumn(2);
float c = 1f;
if (m_UseConfidence)
{
float impact = Mathf.Clamp01(m_ConfidenceImpact);
float cc = Mathf.Clamp01(confidence / m_ConfidenceMax);
c = 1f - impact + (cc * impact * 2f);
}
m_C[idx] = c;
m_Samples++;
uint n = m_Samples > m_HistorySize ? (uint)m_HistorySize : m_Samples;
Vector3 position = FilterAVT(m_P, n, m_C, idx);
Vector3 x = Vector3.Normalize(FilterAVT(m_X, n, m_C, idx));
Vector3 z = Vector3.Normalize(FilterAVT(m_Z, n, m_C, idx));
Vector3 up = Vector3.Normalize(Vector3.Cross(z, x));
Quaternion rotation = Quaternion.LookRotation(z, up);
Matrix4x4 filteredPose = Matrix4x4.TRS(position, rotation, Vector3.one);
return filteredPose;
}
private Vector3 FilterAVT(Vector3[] buf, uint n, float[] confidence, int idx)
{
Vector3 mean = Vector3.zero;
float totalWeight = 0f;
// Calculate weighted mean
for (uint i = 0; i < n; i++)
{
mean += buf[i] * confidence[i];
totalWeight += confidence[i];
}
mean /= totalWeight;
// Return mean when sample count is low
if (n <= 2)
return mean;
// Calculate standard deviation / variance
float s = 0;
for (uint i = 0; i < n; i++)
{
Vector3 value = buf[i] * confidence[i];
s += Vector3.SqrMagnitude(value - mean);
}
s /= totalWeight;
// Calculate a mean of samples with error less than or equal to st dev
Vector3 avg = Vector3.zero;
totalWeight = 0f;
for (uint i = 0; i < n; i++)
{
// For each sample, get error
Vector3 value = buf[i] * confidence[i];
float d = Vector3.SqrMagnitude(value - mean);
// If error <= st dev, count it in
if (d <= s)
{
avg += buf[i] * confidence[i];
totalWeight += confidence[i];
}
}
if (totalWeight > 0)
{
avg /= totalWeight;
return avg;
}
return mean;
}
public void Reset()
{
InvalidateHistory();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e10dd620b341549ce9b987a5533d4db0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,109 @@
/*===============================================================================
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.Generic;
using System.Threading.Tasks;
using UnityEngine;
namespace Immersal.XR
{
public class DataProcessingChain<T> : IDataProcessingChain<T> where T : class
{
//private IDataProcessor<T>[] m_DataProcessors;
private List<IDataProcessor<T>> m_DataProcessors;
private T m_CurrentDataInChain = null;
private bool m_IsProcessing = false;
public DataProcessingChain()
{
m_DataProcessors = new List<IDataProcessor<T>>();
}
public DataProcessingChain(IDataProcessor<T>[] dataProcessors)
{
//m_DataProcessors = dataProcessors;
m_DataProcessors = new List<IDataProcessor<T>>(dataProcessors);
}
public void AddProcessor(IDataProcessor<T> processor)
{
m_DataProcessors.Add(processor);
}
public void RemoveProcessor(IDataProcessor<T> processor)
{
m_DataProcessors.Remove(processor);
}
public async Task ProcessNewData(T inputData)
{
await ProcessChain(inputData, DataProcessorTrigger.NewData);
}
public async Task UpdateChain()
{
if (m_CurrentDataInChain == null)
{
return;
}
await ProcessChain(m_CurrentDataInChain, DataProcessorTrigger.Update);
}
private async Task ProcessChain(T inputData, DataProcessorTrigger trigger)
{
if (m_IsProcessing)
return;
if (inputData == null)
return;
m_IsProcessing = true;
T dataBeingProcessed = inputData;
foreach (IDataProcessor<T> processor in m_DataProcessors)
{
dataBeingProcessed = await processor.ProcessData(dataBeingProcessed, trigger);
}
m_CurrentDataInChain = dataBeingProcessed;
m_IsProcessing = false;
}
public T GetCurrentData()
{
return m_CurrentDataInChain;
}
public async Task ResetProcessors()
{
foreach (IDataProcessor<T> processor in m_DataProcessors)
{
await processor.ResetProcessor();
}
}
}
public interface IDataProcessor<T>
{
Task<T> ProcessData(T data, DataProcessorTrigger trigger);
Task ResetProcessor();
}
public enum DataProcessorTrigger
{
NewData,
Update
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2eb7759c7f3504d458f9aa90cf16254b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,189 @@
/*===============================================================================
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.Threading.Tasks;
using TMPro;
using UnityEngine;
namespace Immersal.XR
{
public struct VisualGeoPose
{
public double Latitude;
public double Longitude;
public double Altitude;
public float Bearing;
public Pose Pose;
}
public class GeoPoseProcessor : MonoBehaviour, IDataProcessor<SessionData>
{
[SerializeField]
private TextMeshProUGUI m_GeoPoseText;
public VisualGeoPose LatestGeoPose { get; private set; }
private Camera m_MainCamera = null;
private Matrix4x4 m_LatestTrackerSpacePose;
private double[] m_LatestMapEcef;
public Camera MainCamera
{
get
{
if (m_MainCamera == null)
{
m_MainCamera = Camera.main;
if (m_MainCamera == null)
ImmersalLogger.LogError("No Camera found");
}
return m_MainCamera;
}
}
public virtual void Update()
{
if (m_LatestMapEcef != null)
UpdateLocation();
}
private void UpdateLocation()
{
VisualGeoPose newPose = new VisualGeoPose();
Vector2 cd = CompassDir(MainCamera, m_LatestTrackerSpacePose.inverse, m_LatestMapEcef);
float bearing = Mathf.Atan2(-cd.x, cd.y) * (180f / (float)Math.PI);
if(bearing >= 0f)
{
newPose.Bearing = bearing;
}
else
{
newPose.Bearing = 360f - Mathf.Abs(bearing);
}
Vector3 pos = m_LatestTrackerSpacePose.GetColumn(3);
double[] wgs84 = new double[3];
int r = Immersal.Core.PosMapToWgs84(wgs84, pos.SwitchHandedness(), m_LatestMapEcef);
newPose.Latitude = wgs84[0];
newPose.Longitude = wgs84[1];
newPose.Altitude = wgs84[2];
newPose.Pose = new Pose(pos, m_LatestTrackerSpacePose.rotation);
LatestGeoPose = newPose;
string vgpsString = string.Format("VLat: {0}, VLon: {1}, VAlt: {2}, VBRG: {3}",
newPose.Latitude.ToString("0.000000"),
newPose.Longitude.ToString("0.000000"),
newPose.Altitude.ToString("0.0"),
newPose.Bearing.ToString("0.0"));
if (m_GeoPoseText != null)
m_GeoPoseText.text = vgpsString;
}
public Task<SessionData> ProcessData(SessionData data, DataProcessorTrigger trigger)
{
if (trigger == DataProcessorTrigger.NewData)
{
Vector3 capturePos = data.PlatformResult.CameraData.CameraPositionOnCapture;
Quaternion captureRot = data.PlatformResult.CameraData.CameraRotationOnCapture;
Vector3 pos = data.LocalizationResult.LocalizeInfo.position;
Quaternion rot = data.LocalizationResult.LocalizeInfo.rotation;
rot *= data.PlatformResult.CameraData.Orientation;
pos.SwitchHandedness();
rot.SwitchHandedness();
// Bring pose to tracker space
MapToSpaceRelation mo = data.Entry.Relation;
Matrix4x4 offsetNoScale = Matrix4x4.TRS(mo.Position, mo.Rotation, Vector3.one);
Vector3 scaledPos = Vector3.Scale(pos, mo.Scale);
Matrix4x4 cloudSpace = offsetNoScale * Matrix4x4.TRS(scaledPos, rot, Vector3.one);
Matrix4x4 trackerSpace = Matrix4x4.TRS(capturePos, captureRot, Vector3.one);
m_LatestTrackerSpacePose = trackerSpace * (cloudSpace.inverse);
// Cache ecef as well
m_LatestMapEcef = data.Entry.Map.MapToEcefGet();
}
return Task.FromResult(data);
}
public Task ResetProcessor()
{
LatestGeoPose = new VisualGeoPose();
m_LatestTrackerSpacePose = Matrix4x4.identity;
m_LatestMapEcef = Array.Empty<double>();
return Task.CompletedTask;
}
Matrix4x4 RotX(double angle)
{
float c = (float)System.Math.Cos(angle * System.Math.PI / 180.0);
float s = (float)System.Math.Sin(angle * System.Math.PI / 180.0);
Matrix4x4 r = Matrix4x4.identity;
r.m11 = c;
r.m22 = c;
r.m12 = s;
r.m21 = -s;
return r;
}
Matrix4x4 RotZ(double angle)
{
float c = (float)System.Math.Cos(angle * System.Math.PI / 180.0);
float s = (float)System.Math.Sin(angle * System.Math.PI / 180.0);
Matrix4x4 r = Matrix4x4.identity;
r.m00 = c;
r.m11 = c;
r.m10 = -s;
r.m01 = s;
return r;
}
Matrix4x4 Rot3d(double lat, double lon)
{
Matrix4x4 rz = RotZ(90 + lon);
Matrix4x4 rx = RotX(90 - lat);
return rx * rz;
}
Vector2 CompassDir(Camera cam, Matrix4x4 trackerToMap, double[] mapToEcef)
{
Vector3 a = trackerToMap.MultiplyPoint(cam.transform.position);
Vector3 b = trackerToMap.MultiplyPoint(cam.transform.position + cam.transform.forward);
double[] aEcef = new double[3];
int ra = Immersal.Core.PosMapToEcef(aEcef, a.SwitchHandedness(), mapToEcef);
double[] bEcef = new double[3];
int rb = Immersal.Core.PosMapToEcef(bEcef, b.SwitchHandedness(), mapToEcef);
double[] wgs84 = new double[3];
int rw = Immersal.Core.PosMapToWgs84(wgs84, a.SwitchHandedness(), mapToEcef);
Matrix4x4 R = Rot3d(wgs84[0], wgs84[1]);
Vector3 v = new Vector3((float)(bEcef[0] - aEcef[0]), (float)(bEcef[1] - aEcef[1]), (float)(bEcef[2] - aEcef[2]));
Vector3 vt = R.MultiplyVector(v.normalized);
Vector2 d = new Vector2(vt.x, vt.y);
return d.normalized;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 00a8834cd55604382a55a5f566f65651
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,50 @@
/*===============================================================================
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.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;
namespace Immersal.XR
{
public class MapChangeDetector : MonoBehaviour, IDataProcessor<SessionData>
{
public UnityEvent<int, int> OnMapChanged;
public bool InvokeOnFirstLocalization = false;
private int m_LastLocalizedMapId = -1;
public Task<SessionData> ProcessData(SessionData data, DataProcessorTrigger trigger)
{
if (trigger == DataProcessorTrigger.NewData)
{
int mapId = data.Entry.Map.mapId;
if (mapId != m_LastLocalizedMapId)
{
if (m_LastLocalizedMapId == -1 && !InvokeOnFirstLocalization)
{
m_LastLocalizedMapId = mapId;
return Task.FromResult(data);
}
OnMapChanged?.Invoke(m_LastLocalizedMapId, mapId);
m_LastLocalizedMapId = mapId;
}
}
return Task.FromResult(data);
}
public Task ResetProcessor()
{
m_LastLocalizedMapId = -1;
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 70058f23eb73e4e1fa8168423a939176
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,194 @@
/*===============================================================================
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.Generic;
using System.Threading.Tasks;
using UnityEngine;
namespace Immersal.XR
{
public enum FilterMethod
{
Default,
Advanced,
Legacy,
}
public class PoseFilter : MonoBehaviour, IDataProcessor<SceneUpdateData>
{
// Note:
// Custom editor does not draw default inspector
[SerializeField] private FilterMethod m_FilterMethod = FilterMethod.Default;
[SerializeField] private int m_HistorySize = 8;
[SerializeField] private bool m_UseConfidence = false;
[SerializeField] private float m_ConfidenceMax = 50f;
[SerializeField] private float m_ConfidenceImpact = 0.5f;
private Dictionary<int, IImmersalFilter> m_MapFilters;
private IImmersalFilter m_LegacyFilter = new AVTFilter();
private bool m_HasProcessedData = false;
private SceneUpdateData m_CurrentData;
public FilterMethod FilterMethod
{
get => m_FilterMethod;
set => m_FilterMethod = value;
}
public int HistorySize
{
get => m_HistorySize;
set => m_HistorySize = value;
}
public bool UseConfidence
{
get => m_UseConfidence;
set => m_UseConfidence = value;
}
public float ConfidenceMax
{
get => m_ConfidenceMax;
set => m_ConfidenceMax = value;
}
public float ConfidenceImpact
{
get => m_ConfidenceImpact;
set => m_ConfidenceImpact = value;
}
private void OnValidate()
{
m_HistorySize = m_FilterMethod == FilterMethod.Advanced ? Mathf.Max(2, m_HistorySize) : 8;
m_UseConfidence = m_UseConfidence && m_FilterMethod == FilterMethod.Advanced;
}
private void Awake()
{
m_MapFilters = new Dictionary<int, IImmersalFilter>();
}
private void ProcessData(SceneUpdateData data)
{
// Legacy method
if (m_FilterMethod == FilterMethod.Legacy)
{
ProcessDataLegacy(data);
return;
}
// We want to do filtering in a specific relative space
Matrix4x4 pose = PreFilterTransform(data);
int mapID = data.MapEntry.Map.mapId;
// Use existing filter or create new
if (m_MapFilters.TryGetValue(mapID, out IImmersalFilter filter))
{
pose = filter.Filter(pose, data);
}
else
{
m_MapFilters.Add(mapID, CreateFilter());
pose = m_MapFilters[mapID].Filter(pose, data);
}
// Filtered pose must be transformed to correct space
pose = PostFilterTransform(pose, data);
data.Pose = pose;
m_CurrentData = data;
m_HasProcessedData = true;
}
private IImmersalFilter CreateFilter()
{
return new AVTFilter(m_HistorySize, m_UseConfidence, m_ConfidenceMax, m_ConfidenceImpact);
}
private void ProcessDataLegacy(SceneUpdateData data)
{
Matrix4x4 pose = m_LegacyFilter.Filter(data.Pose, data);
data.Pose = pose;
m_CurrentData = data;
m_HasProcessedData = true;
}
// Gets localized pose in tracker space without MapRelation
private Matrix4x4 PreFilterTransform(SceneUpdateData data)
{
Vector3 pos = data.LocalizeInfo.position;
Quaternion rot = data.LocalizeInfo.rotation;
rot *= data.CameraData.Orientation;
pos.SwitchHandedness();
rot.SwitchHandedness();
Matrix4x4 imSpacePose = Matrix4x4.TRS(pos, rot, Vector3.one);
return data.TrackerSpace * imSpacePose.inverse;
}
// Applies MapRelation in cloud space and transforms back to tracker space
private Matrix4x4 PostFilterTransform(Matrix4x4 pose, SceneUpdateData data)
{
Matrix4x4 imSpace = pose.inverse * data.TrackerSpace;
Vector3 imPos = imSpace.GetColumn(3);
Quaternion imRot = imSpace.rotation;
MapToSpaceRelation mo = data.MapEntry.Relation;
Matrix4x4 offsetNoScale = Matrix4x4.TRS(mo.Position, mo.Rotation, Vector3.one);
Vector3 scaledPos = Vector3.Scale(imPos, mo.Scale);
Matrix4x4 result = offsetNoScale * Matrix4x4.TRS(scaledPos, imRot, Vector3.one);
return data.TrackerSpace * result.inverse;
}
public Task<SceneUpdateData> ProcessData(SceneUpdateData data, DataProcessorTrigger trigger)
{
// skip on updates with no new data
if (trigger == DataProcessorTrigger.Update)
{
if (m_HasProcessedData)
return Task.FromResult(m_CurrentData);
return Task.FromResult(data);
}
ProcessData(data);
return Task.FromResult(m_CurrentData);
}
public Task ResetProcessor()
{
foreach (KeyValuePair<int,IImmersalFilter> keyValuePair in m_MapFilters)
{
keyValuePair.Value.Reset();
}
m_LegacyFilter.Reset();
return Task.CompletedTask;
}
public void ForgetIndividualFilters()
{
m_MapFilters = new Dictionary<int, IImmersalFilter>();
}
}
public interface IImmersalFilter
{
public Matrix4x4 Filter(Matrix4x4 pose, SceneUpdateData data);
public void Reset();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4ace14c723c1842578283a60dffe9bb2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,144 @@
/*===============================================================================
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.Threading.Tasks;
using UnityEngine;
using UnityEngine.Serialization;
namespace Immersal.XR
{
public enum SmoothingMode
{
SlowApproach,
Linear,
Sinusoidal,
Cubic
}
public class PoseSmoother : MonoBehaviour, IDataProcessor<SceneUpdateData>
{
[Header("Smoothing")]
[SerializeField, Tooltip("Classic slow approach method or linear, sinusoidal or cubic timing function.")]
private SmoothingMode m_Mode = SmoothingMode.SlowApproach;
[SerializeField, Tooltip("Smoothing factor for the slow approach mode.")]
private float m_SlowApproachSmoothing = 0.025f;
[SerializeField, Tooltip("Interpolation time for linear, sinusoidal and cubic modes.")]
private float m_SmoothTimeSpan = 0.5f;
[Header("Warping")]
[SerializeField, Tooltip("Enable to warp to target instantly when distance or angle gets too large.")]
private bool m_WarpOutsideThreshold = true;
[SerializeField, Tooltip("Warp if distance is larger than this.")]
private float m_WarpThresholdDist = 5.0f;
[SerializeField, Tooltip("Warp if angle is larger than this.")]
private float m_WarpThresholdAngle = 20.0f;
private Vector3 targetPosition = Vector3.zero;
private Quaternion targetRotation = Quaternion.identity;
private Vector3 startPosition = Vector3.zero;
private Quaternion startRotation = Quaternion.identity;
private Vector3 currentPosition = Vector3.zero;
private Quaternion currentRotation = Quaternion.identity;
private float m_WarpThresholdDistSq;
private float m_WarpThresholdCosAngle;
private float elapsedTime = 0f;
private bool m_HasUpdated = false;
private void Awake()
{
m_WarpThresholdDistSq = m_WarpThresholdDist * m_WarpThresholdDist;
m_WarpThresholdCosAngle = Mathf.Cos(m_WarpThresholdAngle * Mathf.PI / 180f);
}
private void Update()
{
UpdatePose();
}
private void UpdatePose()
{
float distSq = (currentPosition - targetPosition).sqrMagnitude;
float cosAngle = Quaternion.Dot(currentRotation, targetRotation);
bool warpCondition = m_WarpOutsideThreshold &&
(distSq > m_WarpThresholdDistSq || cosAngle < m_WarpThresholdCosAngle);
if (!m_HasUpdated || warpCondition)
{
currentPosition = targetPosition;
currentRotation = targetRotation;
}
else
{
float alpha = 0f;
elapsedTime += Time.deltaTime;
float t = Mathf.Clamp01(elapsedTime / m_SmoothTimeSpan);
switch (m_Mode)
{
case SmoothingMode.SlowApproach:
float s = Time.deltaTime / (1.0f / 60.0f);
float steps = Mathf.Min(Mathf.Max(s, 1f), 6f);
alpha = 1.0f - Mathf.Pow(1.0f - m_SlowApproachSmoothing, steps);
startPosition = currentPosition;
startRotation = currentRotation;
break;
case SmoothingMode.Linear:
alpha = t;
break;
case SmoothingMode.Sinusoidal:
alpha = Mathf.Sin(t * Mathf.PI * 0.5f);
break;
case SmoothingMode.Cubic:
alpha = t < 0.5f ? 4 * t * t * t : 1 - Mathf.Pow(-2 * t + 2, 3) / 2;
break;
default:
throw new ArgumentOutOfRangeException();
}
currentPosition = Vector3.Lerp(startPosition, targetPosition, alpha);
currentRotation = Quaternion.Slerp(startRotation, targetRotation, alpha);
}
m_HasUpdated = true;
}
public Task<SceneUpdateData> ProcessData(SceneUpdateData data, DataProcessorTrigger trigger)
{
if (trigger == DataProcessorTrigger.NewData)
{
startPosition = currentPosition;
startRotation = currentRotation;
targetPosition = data.Pose.GetPosition();
targetRotation = data.Pose.rotation;
elapsedTime = 0f;
}
UpdatePose();
data.Pose = Matrix4x4.TRS(currentPosition, currentRotation, Vector3.one);
return Task.FromResult(data);
}
public Task ResetProcessor()
{
targetPosition = Vector3.zero;
targetRotation = Quaternion.identity;
currentPosition = Vector3.zero;
currentRotation = Quaternion.identity;
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9edd45749c5964a5bbbdec0791c43d79
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,374 @@
/*===============================================================================
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.Linq;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
using Object = UnityEngine.Object;
namespace Immersal.XR
{
public class ImmersalSession : MonoBehaviour, IImmersalSession
{
[Tooltip("Start Session at app startup")] [SerializeField]
private bool m_AutoStart = true;
[Tooltip("Seconds between continuous updates")] [SerializeField]
private float m_SessionUpdateInterval = 2.0f;
[Tooltip("Try to localize at maximum speed at app startup / resume")] [SerializeField]
private bool m_BurstMode = true;
[Tooltip("Number of successful localizations to turn off burst mode")] [SerializeField]
private int m_BurstSuccessCount = 10;
[Tooltip("Time limit for burst mode in seconds")] [SerializeField]
private float m_BurstTimeLimit = 15f;
[Tooltip("Reset stats and ARSpace filters on application pause")] [SerializeField]
private bool m_ResetOnPause = true;
[Tooltip("Restart session automatically after a reset")] [SerializeField]
private bool m_RestartOnReset = true;
[Tooltip("Process component results in a data processing chain")] [SerializeField]
private bool m_ProcessData = false;
[SerializeField]
[Interface(typeof(IDataProcessor<SessionData>))]
private Object[] m_SessionDataProcessors;
public IDataProcessor<SessionData>[] SessionDataProcessors =>
m_SessionDataProcessors.OfType<IDataProcessor<SessionData>>().ToArray();
public bool AutoStart
{
get => m_AutoStart;
set => m_AutoStart = value;
}
public bool BurstMode
{
get { return m_BurstMode; }
set { SetBurstMode(value); }
}
public bool ProcessData
{
get => m_ProcessData;
set => m_ProcessData = value;
}
public float SessionUpdateInterval
{
get => m_SessionUpdateInterval;
set => m_SessionUpdateInterval = value;
}
public int BurstSuccessCount
{
get => m_BurstSuccessCount;
set => m_BurstSuccessCount = value;
}
public float BurstTimeLimit
{
get => m_BurstTimeLimit;
set => m_BurstTimeLimit = value;
}
public bool RestartOnReset
{
get => m_RestartOnReset;
set => m_RestartOnReset = value;
}
private void SetBurstMode(bool on)
{
m_BurstStartTime = Time.unscaledTime;
m_BurstModeActive = on;
}
#region Events
public UnityEvent OnPause;
public UnityEvent OnResume;
public UnityEvent OnReset;
#endregion
private ImmersalSDK sdk;
private float m_BurstStartTime = 0.0f;
private bool m_BurstModeActive = false;
private float m_LastUpdateTime;
private bool m_SessionIsRunning = false;
private bool m_RunningTasks = false;
private bool m_Paused = false;
private Task m_CurrentRunningTask;
private CancellationTokenSource m_CTS;
private IPlatformUpdateResult m_LatestPlatformUpdateResult = null;
private ILocalizationResults m_LatestLocalizationResults = null;
private DataProcessingChain<SessionData> m_SessionDataProcessingChain;
private void Start()
{
sdk = ImmersalSDK.Instance;
m_SessionDataProcessingChain = new DataProcessingChain<SessionData>(SessionDataProcessors);
if (!m_AutoStart)
return;
if (sdk.IsReady)
{
StartSession();
}
else
{
sdk.OnInitializationComplete.AddListener(StartSession);
}
}
public void StartSession()
{
if (m_SessionIsRunning)
{
if (m_Paused)
ResumeSession(); // Resume if paused
return;
}
if (!sdk.IsReady)
return;
ImmersalLogger.Log("Session starting", ImmersalLogger.LoggingLevel.Verbose);
m_SessionIsRunning = true;
SetBurstMode(BurstMode);
}
private void Update()
{
// update data processing chain
if (m_ProcessData)
m_SessionDataProcessingChain.UpdateChain();
// bail out conditionals
if (!m_SessionIsRunning || !sdk.IsReady || !MapManager.HasRegisteredMaps)
return;
float curTime = Time.unscaledTime;
// deactivate burst after enough success or certain time
if (sdk.TrackingStatus?.LocalizationSuccessCount >= m_BurstSuccessCount || curTime - m_BurstStartTime >= m_BurstTimeLimit)
{
SetBurstMode(false);
}
// check if update interval has passed or if we are bursting
bool updateIntervalOrBurst = curTime - m_LastUpdateTime >= m_SessionUpdateInterval
|| m_BurstModeActive;
// update conditional
bool doUpdate = updateIntervalOrBurst
&& !m_RunningTasks
&& !m_Paused;
if (doUpdate)
{
m_LastUpdateTime = curTime;
m_RunningTasks = true;
m_CTS = new CancellationTokenSource();
m_CurrentRunningTask = RunTasksAsync(m_ProcessData, m_CTS.Token);
}
}
private async Task RunTasksAsync(bool processData, CancellationToken cancellationToken)
{
try
{
// Update platform
m_LatestPlatformUpdateResult = await sdk.PlatformSupport.UpdatePlatform();
if (cancellationToken.IsCancellationRequested) { m_RunningTasks = false; return; }
if (m_LatestPlatformUpdateResult.Success)
{
// Localize
m_LatestLocalizationResults = await sdk.Localizer.Localize(m_LatestPlatformUpdateResult.CameraData);
if (cancellationToken.IsCancellationRequested) { m_RunningTasks = false; return; }
foreach (ILocalizationResult result in m_LatestLocalizationResults.Results)
{
if (result.Success)
{
// Check if map entry exists
if (MapManager.TryGetMapEntry(result.MapId, out MapEntry entry))
{
ImmersalLogger.Log($"Localized to map {entry.Map.mapId}-{entry.Map.mapName}", ImmersalLogger.LoggingLevel.Verbose);
ILocalizationResult r = result;
IPlatformUpdateResult p = m_LatestPlatformUpdateResult;
if (processData)
{
// Push new data to data processing chain
await m_SessionDataProcessingChain.ProcessNewData(new SessionData()
{
Entry = entry,
PlatformResult = p,
LocalizationResult = r
});
SessionData processed = m_SessionDataProcessingChain.GetCurrentData();
entry = processed.Entry;
r = processed.LocalizationResult;
p = processed.PlatformResult;
}
// Update SceneUpdater
if (cancellationToken.IsCancellationRequested) { m_RunningTasks = false; return; }
await sdk.SceneUpdater.UpdateScene(entry, p.CameraData, r);
}
else
{
ImmersalLogger.LogError("Localization result does not match with any registered map.");
}
}
}
}
else
{
// Localization never ran due to failed platform update
// so we need to invalidate previous results.
m_LatestLocalizationResults = new LocalizationResults
{
Results = Array.Empty<ILocalizationResult>()
};
}
// Update tracking status
sdk.TrackingAnalyzer.Analyze(m_LatestPlatformUpdateResult.Status, m_LatestLocalizationResults);
}
catch (Exception e)
{
ImmersalLogger.LogError($"Session task error: {e.Message}. Stopping session.");
m_SessionIsRunning = false;
}
finally
{
m_RunningTasks = false;
}
}
// Convenience method
public async Task LocalizeOnce()
{
if (m_CurrentRunningTask != null)
await m_CurrentRunningTask;
m_LastUpdateTime = Time.unscaledTime;
m_RunningTasks = true;
m_CTS = new CancellationTokenSource();
try
{
m_CurrentRunningTask = RunTasksAsync(m_ProcessData, m_CTS.Token);
}
catch (Exception e)
{
ImmersalLogger.LogError(e.Message);
m_SessionIsRunning = false;
}
}
// Note: MonoBehaviour.OnApplicationPause is called as a GameObject starts. The call is made after Awake.
private void OnApplicationPause(bool paused)
{
if (paused)
{
PauseSession();
}
else
{
ResumeSession();
}
}
public async void PauseSession()
{
if (!m_SessionIsRunning)
return;
if (m_ResetOnPause)
await ResetSession();
m_Paused = true;
OnPause?.Invoke();
}
public void ResumeSession()
{
if (!m_SessionIsRunning)
return;
m_Paused = false;
SetBurstMode(BurstMode);
OnResume?.Invoke();
}
public void TriggerResetSession()
{
ResetSession();
}
public async Task ResetSession()
{
ImmersalLogger.Log("Resetting session");
await StopSession();
await m_SessionDataProcessingChain.ResetProcessors();
OnReset?.Invoke();
if (m_RestartOnReset)
StartSession();
}
public async Task StopSession(bool cancelRunningTask = true)
{
// Send cancellation token to abort running task
if (cancelRunningTask)
m_CTS?.Cancel();
// Wait for running task to finish
if (m_CurrentRunningTask != null)
await m_CurrentRunningTask;
m_SessionIsRunning = false;
m_RunningTasks = false;
m_LatestLocalizationResults = null;
m_LatestPlatformUpdateResult = null;
m_BurstModeActive = false;
m_BurstStartTime = 0.0f;
m_LastUpdateTime = 0f;
m_Paused = false;
ImmersalLogger.Log("Session stopped");
}
}
public class SessionData
{
public MapEntry Entry;
public IPlatformUpdateResult PlatformResult;
public ILocalizationResult LocalizationResult;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1998a90ed503d46ffa5a88d3c7505c28
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 83bf888582b264eaba3baa3b7101e02e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,188 @@
/*===============================================================================
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.Runtime.InteropServices;
using System.Threading;
using UnityEngine;
namespace Immersal.XR
{
public interface ICameraData
{
IImageData GetImageData();
byte[] GetBytes();
CameraData Copy(IImageData imageData);
public void ReleaseReference();
public void CheckReferences();
int Width { get; }
int Height { get; }
int Channels { get; }
CameraDataFormat Format { get; }
Vector4 Intrinsics { get; }
Vector3 CameraPositionOnCapture { get; }
Quaternion CameraRotationOnCapture { get; }
double[] Distortion { get; }
Quaternion Orientation { get; }
}
public class CameraData : ICameraData
{
public int Width { get; set; }
public int Height { get; set; }
public int Channels { get; set; }
public CameraDataFormat Format { get; set; }
public Vector4 Intrinsics { get; set; } // x = principal point x, y = principal point y, z = focal length x, w = focal length y
public Vector3 CameraPositionOnCapture { get; set; }
public Quaternion CameraRotationOnCapture { get; set; }
public double[] Distortion { get; set; } // not yet used
public Quaternion Orientation { get; set; }
private readonly IImageData m_ImageData;
private int m_ReferenceCount;
private bool m_IsDisposed;
public CameraData(IImageData imageData)
{
m_ImageData = imageData;
m_ImageData.SetCameraDataReference(this);
}
public IImageData GetImageData()
{
if (m_IsDisposed) throw new ObjectDisposedException("Immersal.XR.CameraData");
Interlocked.Increment(ref m_ReferenceCount);
return m_ImageData;
}
public byte[] GetBytes()
{
if (m_IsDisposed) throw new ObjectDisposedException("Immersal.XR.CameraData");
return m_ImageData.ManagedBytes;
}
public void ReleaseReference()
{
Interlocked.Decrement(ref m_ReferenceCount);
CheckReferences();
}
public void CheckReferences()
{
if (m_ReferenceCount <= 0)
{
Dispose();
}
}
private void Dispose()
{
if (m_IsDisposed)
{
ImmersalLogger.LogWarning("Attempting to dispose already disposed CameraData");
return;
}
if (m_ImageData != null)
{
m_ImageData.DisposeData();
}
else
{
ImmersalLogger.LogWarning("Attempting to dispose null ImageData");
}
m_IsDisposed = true;
}
public CameraData Copy(IImageData imageData)
{
CameraData data = new CameraData(imageData)
{
Width = this.Width,
Height = this.Height,
Intrinsics = this.Intrinsics,
Format = this.Format,
Channels = this.Channels,
CameraPositionOnCapture = this.CameraPositionOnCapture,
CameraRotationOnCapture = this.CameraRotationOnCapture,
Orientation = this.Orientation,
Distortion = this.Distortion
};
return data;
}
}
public interface IImageData : IDisposable
{
public IntPtr UnmanagedDataPointer { get; }
public byte[] ManagedBytes { get; }
void SetCameraDataReference(ICameraData cameraData);
void DisposeData();
}
public abstract class ImageData: IImageData
{
public abstract IntPtr UnmanagedDataPointer { get; }
public abstract byte[] ManagedBytes { get; }
public abstract void DisposeData();
private ICameraData m_CameraData;
private bool m_CameraDataReferenceSet = false;
public void Dispose()
{
if (m_CameraData == null)
{
ImmersalLogger.LogWarning("Disposing ImageData with no CameraData reference.");
DisposeData();
return;
}
m_CameraData.ReleaseReference();
}
public void SetCameraDataReference(ICameraData cameraData)
{
if (m_CameraDataReferenceSet)
{
ImmersalLogger.LogError("CameraData reference already set.");
return;
}
m_CameraData = cameraData;
m_CameraDataReferenceSet = true;
}
}
public sealed class SimpleImageData : ImageData
{
public override IntPtr UnmanagedDataPointer => m_UnmanagedDataPointer;
public override byte[] ManagedBytes { get; }
private IntPtr m_UnmanagedDataPointer;
private GCHandle m_managedDataHandle;
public SimpleImageData(byte[] bytes)
{
ManagedBytes = bytes;
m_managedDataHandle = GCHandle.Alloc(ManagedBytes, GCHandleType.Pinned);
m_UnmanagedDataPointer = m_managedDataHandle.AddrOfPinnedObject();
}
public override void DisposeData()
{
m_managedDataHandle.Free();
m_UnmanagedDataPointer = IntPtr.Zero;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 03a98a5ff49164b08addce88bff9bfed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
/*===============================================================================
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.Threading.Tasks;
namespace Immersal.XR
{
public interface IDataProcessingChain<T>
{
public Task ProcessNewData(T inputData);
public Task UpdateChain();
public T GetCurrentData();
public Task ResetProcessors();
public void AddProcessor(IDataProcessor<T> processor);
public void RemoveProcessor(IDataProcessor<T> processor);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52c0b4f597e8a4721a94e07ba8c7719a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
/*===============================================================================
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.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
public interface IImmersalSession
{
void PauseSession();
void ResumeSession();
Task ResetSession();
Task StopSession(bool cancelRunningTask = true);
void StartSession();
Task LocalizeOnce();
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ffba8fb68d0ca444ca8e3a96cb3d32bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
/*===============================================================================
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.Threading;
using System.Threading.Tasks;
using Immersal.REST;
using UnityEngine;
namespace Immersal.XR
{
public interface ILocalizationResult
{
bool Success { get; }
int MapId { get; }
LocalizeInfo LocalizeInfo { get; }
}
public interface ILocalizationMethodConfiguration
{
XRMap[] MapsToAdd { get; }
XRMap[] MapsToRemove { get; }
SolverType? SolverType { get; }
int? PriorNNCountMin { get; }
int? PriorNNCountMax { get; }
Vector3? PriorScale { get; }
float? PriorRadius { get; }
}
public enum ConfigurationMode
{
WhenNecessary,
Always
}
public interface ILocalizationMethod : IHasNullOrDeadCheck
{
ConfigurationMode ConfigurationMode { get; }
IMapOption[] MapOptions { get; }
Task<bool> Configure(ILocalizationMethodConfiguration configuration);
Task<ILocalizationResult> Localize(ICameraData cameraData, CancellationToken cancellationToken);
Task StopAndCleanUp();
Task OnMapRegistered(XRMap map);
}
// Utility interface and extension method for checking if an interface is null.
// Directly comparing an interface to null sidesteps the custom null-checks used with Unity objects.
// This means the interface reference might not be null in the C# sense, even if the object it is
// referencing has been destroyed in the Unity context.
// This is only an issue if the class implementing the interface is inheriting from Unity objects.
public interface IHasNullOrDeadCheck {}
public static class NullOrDeadCheckExtension
{
public static bool IsNullOrDead(this IHasNullOrDeadCheck obj)
{
// Casting to UnityEngine.Object will force the check to utilize Unity's custom null-checking
if (obj is UnityEngine.Object o)
return o == null;
return obj == null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 138bfe17044ec4f6e95318d640e0c4c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,60 @@
/*===============================================================================
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.Generic;
using System.Threading;
using System.Threading.Tasks;
using Immersal.REST;
namespace Immersal.XR
{
public interface ILocalizationResults
{
ILocalizationResult[] Results { get; }
}
public interface ILocalizerConfiguration
{
Dictionary<ILocalizationMethod, XRMap[]> ConfigurationsToAdd { get; }
Dictionary<ILocalizationMethod, XRMap[]> ConfigurationsToRemove { get; }
bool StopRunningTasks { get; }
}
public interface ILocalizerConfigurationResult
{
bool Success { get; set; }
}
public interface ILocalizer
{
ILocalizationMethod[] AvailableLocalizationMethods { get; }
Task<ILocalizerConfigurationResult> ConfigureLocalizer(ILocalizerConfiguration configuration);
Task<ILocalizationResults> Localize(ICameraData cameraData);
Task<List<LocalizationTask>> CreateLocalizationTasks(ICameraData cameraData);
Task<ILocalizationResults> LocalizeAllMethods(ICameraData cameraData);
Task StopAndCleanUp();
Task StopLocalizationForMethod(ILocalizationMethod localizationMethod);
bool TryGetLocalizationTask(ILocalizationMethod localizationMethod, out LocalizationTask task);
}
public class LocalizationTask
{
public Task<ILocalizationResult> LocalizationMethodTask { get; private set; }
public CancellationTokenSource CancellationTokenSource { get; private set; }
public LocalizationTask(Task<ILocalizationResult> task, CancellationTokenSource cancellationTokenSource)
{
LocalizationMethodTask = task;
CancellationTokenSource = cancellationTokenSource;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 17af27cd6aa4e429186d011bbb43b70f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,76 @@
/*===============================================================================
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 UnityEditor;
using UnityEngine;
namespace Immersal.XR
{
public interface IMapOption
{
string Name { get; }
void DrawEditorGUI(XRMap map);
}
// Example IMapOption implementation
[Serializable]
public class IntOption : IMapOption
{
public string Name { get; private set; }
[SerializeField]
public int Value;
public IntOption(string name, int initialValue)
{
Name = name;
Value = initialValue;
}
public void DrawEditorGUI(XRMap map)
{
#if UNITY_EDITOR
Value = EditorGUILayout.IntField(Name, Value);
#endif
}
}
[Serializable]
public class SerializableMapOption
{
public string TypeName;
public string Data;
// Serialize an IMapOption instance
public static SerializableMapOption Serialize(IMapOption option)
{
var serializableOption = new SerializableMapOption
{
TypeName = option.GetType().AssemblyQualifiedName,
Data = JsonUtility.ToJson(option)
};
return serializableOption;
}
// Deserialize into an IMapOption instance
public IMapOption Deserialize()
{
var type = Type.GetType(TypeName);
if (type != null)
{
return (IMapOption)JsonUtility.FromJson(Data, type);
}
return null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6a4ee57b73bcb4fe5bf2d6c043df6930
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,80 @@
/*===============================================================================
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.Threading.Tasks;
using UnityEngine;
namespace Immersal.XR
{
public interface IPlatformUpdateResult
{
bool Success { get; }
IPlatformStatus Status { get; }
ICameraData CameraData { get; }
}
public interface IPlatformStatus
{
int TrackingQuality { get; }
}
public enum CameraDataFormat
{
SingleChannel,
RGB
}
public interface IPlatformConfigureResult
{
bool Success { get; }
}
public interface IPlatformSupport
{
Task<IPlatformUpdateResult> UpdatePlatform();
Task<IPlatformUpdateResult> UpdatePlatform(IPlatformConfiguration oneShotConfiguration);
Task<IPlatformConfigureResult> ConfigurePlatform();
Task<IPlatformConfigureResult> ConfigurePlatform(IPlatformConfiguration configuration);
Task StopAndCleanUp();
}
public interface IPlatformConfiguration
{
CameraDataFormat CameraDataFormat { get; }
}
#region Simple implementations
public struct SimplePlatformConfigureResult : IPlatformConfigureResult
{
public bool Success { get; set; }
}
public struct SimplePlatformUpdateResult : IPlatformUpdateResult
{
public bool Success { get; set; }
public IPlatformStatus Status { get; set; }
public ICameraData CameraData { get; set; }
}
public struct SimplePlatformStatus : IPlatformStatus
{
public int TrackingQuality { get; set; }
}
public struct PlatformConfiguration : IPlatformConfiguration
{
public CameraDataFormat CameraDataFormat { get; set; }
}
#endregion
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bb89d50895fb74e67a68ee7887a68478
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
/*===============================================================================
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 sdk@immersal.com for licensing requests.
===============================================================================*/
using System.Threading.Tasks;
using UnityEngine;
namespace Immersal.XR
{
public interface ISceneUpdateable
{
Task SceneUpdate(SceneUpdateData data);
Transform GetTransform();
Task ResetScene();
}
public static class SceneUpdateableExtensions
{
public static Matrix4x4 ToMapSpace(this ISceneUpdateable sceneUpdateable, Vector3 pos, Quaternion rot)
{
Transform spaceTransform = sceneUpdateable.GetTransform();
Matrix4x4 pose = Matrix4x4.TRS(pos, rot, Vector3.one);
Matrix4x4 spacePose = Matrix4x4.TRS(spaceTransform.position, spaceTransform.rotation, Vector3.one);
return spacePose.inverse * pose;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1470387a4d318459a91c6a30cb5ef9b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
/*===============================================================================
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.Threading.Tasks;
namespace Immersal.XR
{
public interface ISceneUpdater
{
Task UpdateScene(MapEntry entry, ICameraData cameraData, ILocalizationResult localizationResult);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39b89922d087d4f66824b1389aa59801
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,27 @@
/*===============================================================================
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.
===============================================================================*/
namespace Immersal.XR
{
public interface ITrackingStatus
{
int LocalizationAttemptCount { get; }
int LocalizationSuccessCount { get; }
int TrackingQuality { get; }
}
public interface ITrackingAnalyzer
{
ITrackingStatus TrackingStatus { get; }
void Analyze(IPlatformStatus platformStatus, ILocalizationResults localizationResults);
void Reset();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fea0f6762065741059e6568ded85996f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 065c941179f654789b30a560a80ac8ba
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,172 @@
/*===============================================================================
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.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Immersal.REST;
using UnityEditor;
using UnityEngine;
namespace Immersal.XR
{
public enum SolverType
{
Default = 0,
Lean = 1,
Prior = 2
};
[Serializable]
public class DeviceLocalization : MonoBehaviour, ILocalizationMethod
{
// Note:
// Custom editor does not draw default inspector
[SerializeField]
private ConfigurationMode m_ConfigurationMode = ConfigurationMode.Always;
[SerializeField]
private SolverType m_SolverType = SolverType.Default;
[SerializeField]
private int m_PriorNNCount = 0;
[SerializeField]
private float m_PriorRadius = 0f;
public ConfigurationMode ConfigurationMode => m_ConfigurationMode;
public IMapOption[] MapOptions => new IMapOption[]
{
new MapLoadingOption()
};
private SDKMapId[] m_MapIds;
private int m_previouslyLocalizedMapId = 0;
public Task<bool> Configure(ILocalizationMethodConfiguration configuration)
{
m_SolverType = configuration.SolverType ?? m_SolverType;
m_PriorNNCount = configuration.PriorNNCountMin ?? m_PriorNNCount;
m_PriorRadius = configuration.PriorRadius ?? m_PriorRadius;
List<SDKMapId> mapList = m_MapIds != null ? m_MapIds.ToList() : new List<SDKMapId>();
// Add maps
if (configuration.MapsToAdd != null)
{
foreach (XRMap map in configuration.MapsToAdd)
{
mapList.Add(new SDKMapId {id = map.mapId});
}
}
// Remove maps
if (configuration.MapsToRemove != null)
{
foreach (XRMap map in configuration.MapsToRemove)
{
mapList.Remove(new SDKMapId {id = map.mapId});
}
// Check if there are no configured maps left
if (mapList.Count == 0)
{
m_MapIds = Array.Empty<SDKMapId>();
return Task.FromResult(false);
}
}
m_MapIds = mapList.ToArray();
return Task.FromResult(true);
}
public async Task<ILocalizationResult> Localize(ICameraData cameraData, CancellationToken cancellationToken)
{
LocalizationResult r = new LocalizationResult();
using IImageData imageData = cameraData.GetImageData();
float startTime = Time.realtimeSinceStartup;
LocalizeInfo locInfo;
if (m_SolverType == SolverType.Prior &&
m_previouslyLocalizedMapId != 0 &&
MapManager.TryGetMapEntry(m_previouslyLocalizedMapId, out MapEntry entry))
{
Vector3 pos = cameraData.CameraPositionOnCapture;
Matrix4x4 mapPoseWithRelation = entry.SceneParent.ToMapSpace(pos, Quaternion.identity);
Vector3 priorPos = entry.Relation.ApplyInverseRelation(mapPoseWithRelation).GetPosition();
priorPos.SwitchHandedness();
locInfo = await Task.Run(() => Immersal.Core.icvLocalizeImageWithPrior(cameraData, imageData.UnmanagedDataPointer, ref priorPos, m_PriorNNCount, m_PriorRadius), cancellationToken);
}
else
{
// previously localized map not found, reset
m_previouslyLocalizedMapId = 0;
locInfo = await Task.Run(() => Immersal.Core.LocalizeImage(cameraData, imageData.UnmanagedDataPointer,(int)m_SolverType), cancellationToken);
}
float elapsedTime = Time.realtimeSinceStartup - startTime;
if (locInfo.mapId > 0)
{
r.Success = true;
r.MapId = locInfo.mapId;
r.LocalizeInfo = locInfo;
m_previouslyLocalizedMapId = locInfo.mapId;
ImmersalLogger.Log($"Relocalized in {elapsedTime} seconds");
}
else
{
r.Success = false;
ImmersalLogger.Log($"Localization attempt failed after {elapsedTime} seconds");
}
return r;
}
public Task StopAndCleanUp()
{
// This implementation has nothing to clean up
return Task.CompletedTask;
}
public Task OnMapRegistered(XRMap map)
{
// Ensure there is some map loading configuration
MapLoadingOption mlo = map.MapOptions.FirstOrDefault(option => option.Name == "MapLoading") as MapLoadingOption;
if (mlo == null)
{
ImmersalLogger.LogWarning($"Map {map.mapName} is missing DataSource option, attempting to deduce intended configuration.");
mlo = new MapLoadingOption
{
m_SerializedDataSource = map.mapFile == null ? 1 : 0, // Download mapfile if not found
DownloadVisualizationAtRuntime = map.Visualization == null // Download vis if not found
};
map.MapOptions.Add(mlo);
}
return Task.CompletedTask;
}
public void SetSolverType(SolverType newSolverType)
{
m_SolverType = newSolverType;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d8be42d7c34a64923aa637c8abcaeb7d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,231 @@
/*===============================================================================
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.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Immersal.REST;
using UnityEngine;
using UnityEngine.Events;
namespace Immersal.XR
{
public class GeoLocalization : MonoBehaviour, ILocalizationMethod
{
// Note:
// Custom editor does not draw default inspector
[SerializeField]
private ConfigurationMode m_ConfigurationMode = ConfigurationMode.WhenNecessary;
[SerializeField]
private SolverType m_SolverType = SolverType.Default;
[SerializeField]
private int m_PriorNNCountMin = 60;
[SerializeField]
private int m_PriorNNCountMax = 720;
[SerializeField]
private Vector3 m_PriorScale = Vector3.one;
[SerializeField]
private float m_PriorRadius = 0f;
public ConfigurationMode ConfigurationMode => m_ConfigurationMode;
public UnityEvent<float> OnProgress;
private int m_previouslyLocalizedMapId = 0;
// No options
public IMapOption[] MapOptions => null;
private SDKMapId[] m_MapIds;
public Task<bool> Configure(ILocalizationMethodConfiguration configuration)
{
m_SolverType = configuration.SolverType ?? m_SolverType;
m_PriorNNCountMin = configuration.PriorNNCountMin ?? m_PriorNNCountMin;
m_PriorNNCountMax = configuration.PriorNNCountMax ?? m_PriorNNCountMax;
m_PriorScale = configuration.PriorScale ?? m_PriorScale;
m_PriorRadius = configuration.PriorRadius ?? m_PriorRadius;
List<SDKMapId> mapList = m_MapIds != null ? m_MapIds.ToList() : new List<SDKMapId>();
// Add maps
if (configuration.MapsToAdd != null)
{
foreach (XRMap map in configuration.MapsToAdd)
{
mapList.Add(new SDKMapId {id = map.mapId});
}
}
// Remove maps
if (configuration.MapsToRemove != null)
{
foreach (XRMap map in configuration.MapsToRemove)
{
mapList.Remove(new SDKMapId {id = map.mapId});
}
// Check if there are no configured maps left
if (mapList.Count == 0)
{
m_MapIds = Array.Empty<SDKMapId>();
return Task.FromResult(false);
}
}
m_MapIds = mapList.ToArray();
return Task.FromResult(true);
}
public async Task<ILocalizationResult> Localize(ICameraData cameraData, CancellationToken cancellationToken)
{
LocalizationResult r = new LocalizationResult
{
Success = false
};
if (m_MapIds == null || m_MapIds.Length == 0)
return r;
JobGeoPoseAsync j = new JobGeoPoseAsync();
j.Progress.ProgressChanged += OnCurrentJobProgress;
Vector4 intrinsics = cameraData.Intrinsics;
int channels = cameraData.Channels;
int width = cameraData.Width;
int height = cameraData.Height;
byte[] capture = new byte[channels * width * height + 8192];
float startTime = Time.realtimeSinceStartup;
using (IImageData imageData = cameraData.GetImageData())
{
int size = width * height * channels;
byte[] pixels = new byte[size];
Marshal.Copy(imageData.UnmanagedDataPointer, pixels, 0, size);
Task<(byte[], CaptureInfo)> t = Task.Run(() =>
{
CaptureInfo info =
Immersal.Core.CaptureImage(capture, capture.Length, pixels, width, height, channels);
Array.Resize(ref capture, info.captureSize);
return (capture, info);
});
await t;
}
j.image = capture; //t.Result.Item1;
j.intrinsics = intrinsics;
j.mapIds = m_MapIds;
j.solverType = m_SolverType == SolverType.Prior ? 4 : 0;
if (m_SolverType == SolverType.Prior &&
m_previouslyLocalizedMapId != 0 &&
MapManager.TryGetMapEntry(m_previouslyLocalizedMapId, out MapEntry previousMapEntry))
{
Vector3 pos = cameraData.CameraPositionOnCapture;
Matrix4x4 mapPoseWithRelation = previousMapEntry.SceneParent.ToMapSpace(pos, Quaternion.identity);
Vector3 priorPos = previousMapEntry.Relation.ApplyInverseRelation(mapPoseWithRelation).GetPosition();
priorPos.SwitchHandedness();
j.priorPos = priorPos;
j.priorNNCountMin = m_PriorNNCountMin;
j.priorNNCountMax = m_PriorNNCountMax;
j.priorScale = m_PriorScale;
j.priorRadius = m_PriorRadius;
}
else
{
// previously localized map not found, reset
m_previouslyLocalizedMapId = 0;
}
Quaternion rot = cameraData.CameraRotationOnCapture * cameraData.Orientation;
rot.SwitchHandedness();
j.rotation = rot;
SDKGeoPoseResult result = await j.RunJobAsync(cancellationToken);
float elapsedTime = Time.realtimeSinceStartup - startTime;
if (result.success)
{
ImmersalLogger.Log($"Relocalized in {elapsedTime} seconds");
int mapId = result.map;
double latitude = result.latitude;
double longitude = result.longitude;
double ellipsoidHeight = result.ellipsoidHeight;
Quaternion quat = new Quaternion(result.quaternion[1], result.quaternion[2], result.quaternion[3], result.quaternion[0]);
ImmersalLogger.Log($"GeoPose returned latitude: {latitude}, longitude: {longitude}, ellipsoidHeight: {ellipsoidHeight}, quaternion: {quat}");
double[] ecef = new double[3];
double[] wgs84 = new double[3] { latitude, longitude, ellipsoidHeight };
Core.PosWgs84ToEcef(ecef, wgs84);
if (MapManager.TryGetMapEntry(mapId, out MapEntry entry))
{
double[] mapToEcef = entry.Map.MapToEcefGet();
Core.PosEcefToMap(out Vector3 mapPos, ecef, mapToEcef);
Core.RotEcefToMap(out Quaternion mapRot, quat, mapToEcef);
LocalizeInfo locInfo = new LocalizeInfo
{
mapId = mapId,
position = mapPos,
rotation = mapRot,
confidence = 0
};
r.Success = true;
r.MapId = mapId;
r.LocalizeInfo = locInfo;
m_previouslyLocalizedMapId = mapId;
}
}
else
{
ImmersalLogger.Log($"Localization attempt failed after {elapsedTime} seconds");
}
j.Progress.ProgressChanged -= OnCurrentJobProgress;
return r;
}
public Task StopAndCleanUp()
{
m_MapIds = Array.Empty<SDKMapId>();
return Task.CompletedTask;
}
public Task OnMapRegistered(XRMap map)
{
return Task.CompletedTask;
}
private void OnCurrentJobProgress(object sender, float value)
{
OnProgress?.Invoke(value);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: abe9919d22e3a41a8ad36c34e59a0f7e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,449 @@
/*===============================================================================
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.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Immersal.REST;
using UnityEditor;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
using Object = UnityEngine.Object;
namespace Immersal.XR
{
public struct LocalizationResults : ILocalizationResults
{
public ILocalizationResult[] Results { get; set; }
}
public struct LocalizationResult : ILocalizationResult
{
public bool Success { get; set; }
public int MapId { get; set; }
public LocalizeInfo LocalizeInfo { get; set; }
}
public class Localizer : MonoBehaviour, ILocalizer
{
// Localization Methods
// Serialized as Objects like the components in ImmersalSDK
[SerializeField, Interface(typeof(ILocalizationMethod))]
private Object[] m_LocalizationMethodObjects;
private ILocalizationMethod[] m_CachedLocalizationMethods; // cache deserialized methods
public ILocalizationMethod[] AvailableLocalizationMethods
{
get
{
// Return if already cached
if (m_CachedLocalizationMethods != null) return m_CachedLocalizationMethods;
if (m_LocalizationMethodObjects == null) return Array.Empty<ILocalizationMethod>();
// Deserialize and cache the localization methods
m_CachedLocalizationMethods = m_LocalizationMethodObjects.OfType<ILocalizationMethod>().ToArray();
return m_CachedLocalizationMethods;
}
}
private List<ILocalizationMethod> m_ConfiguredLocalizationMethods = new List<ILocalizationMethod>();
// Keep references to running LocalizationTasks in a dictionary
// Each type of ILocalizationMethod has it's own LocalizationTask
private Dictionary<ILocalizationMethod, LocalizationTask> m_RunningLocalizationTasks;
// Events
[Header("Events"), Space]
// Invoked when localization is successful for the first time (since start/reset)
public UnityEvent OnFirstSuccessfulLocalization;
// Invoked once per Localize call if any localization was successful
// int[] includes mapIds of successful localizations
public UnityEvent<int[]> OnSuccessfulLocalizations;
public UnityEvent<ILocalizationResults> OnLocalizationResult;
// Invoked once per Localize call if all localization attempts failed
public UnityEvent OnFailedLocalizations;
// Other
private bool m_IsLocalizing = false;
private bool m_HasLocalizedSuccessfully = false;
// Configuration
// Note: this can get called again after the initial configuration if new LocalizationMethods need to be added
public async Task<ILocalizerConfigurationResult> ConfigureLocalizer(ILocalizerConfiguration configuration)
{
ImmersalLogger.Log("Configuring Localizer");
// Check if tasks are running and stop if requested
if (configuration.StopRunningTasks && m_RunningLocalizationTasks is { Count: > 0 })
await StopRunningLocalizationTasks();
// Default to failure
ILocalizerConfigurationResult r = new LocalizerConfigurationResult { Success = false };
List<ILocalizationMethod> configuredMethods = new List<ILocalizationMethod>();
// Add new configurations if requested
if (configuration.ConfigurationsToAdd != null)
{
foreach (ILocalizationMethod localizationMethod in AvailableLocalizationMethods)
{
bool isNecessary = configuration.ConfigurationsToAdd.TryGetValue(localizationMethod, out XRMap[] maps);
bool configureMethod = localizationMethod.ConfigurationMode switch
{
ConfigurationMode.WhenNecessary => isNecessary,
ConfigurationMode.Always => true,
_ => false
};
if (configureMethod)
{
// Try to configure method with associated mapIds
if (!await ConfigureLocalizationMethod(localizationMethod, maps))
{
// Configure failed, bail out
return r;
}
configuredMethods.Add(localizationMethod);
}
}
}
// Refresh our localization method cache to only include configured methods
m_ConfiguredLocalizationMethods.AddRange(configuredMethods);
// Remove configurations if requested
if (configuration.ConfigurationsToRemove != null)
{
foreach (KeyValuePair<ILocalizationMethod,XRMap[]> keyValuePair in configuration.ConfigurationsToRemove)
{
await RemoveLocalizationMethodConfiguration(keyValuePair.Key, keyValuePair.Value);
}
}
// Initialize running tasks Dictionary
if (m_RunningLocalizationTasks == null)
m_RunningLocalizationTasks = new Dictionary<ILocalizationMethod, LocalizationTask>();
r.Success = true;
return r;
}
private async Task<bool> ConfigureLocalizationMethod(ILocalizationMethod localizationMethod, XRMap[] maps)
{
ImmersalLogger.Log($"Configuring localization method: {localizationMethod.GetType().Name}");
// Ensure we have the requested localization method available
if (!AvailableLocalizationMethods.Contains(localizationMethod))
{
ImmersalLogger.LogError("Trying to configure unavailable localization method.");
return false;
}
DefaultLocalizationMethodConfiguration config = new DefaultLocalizationMethodConfiguration
{
MapsToAdd = maps
};
if (await localizationMethod.Configure(config))
{
return true;
}
ImmersalLogger.LogError($"Could not configure localization method: {localizationMethod.GetType().Name}.");
return false;
}
private async Task<bool> RemoveLocalizationMethodConfiguration(ILocalizationMethod localizationMethod, XRMap[] maps)
{
// Check if configured
if (!m_ConfiguredLocalizationMethods.Contains(localizationMethod))
{
ImmersalLogger.LogError("Trying to remove configurations from a non-configured localization method.");
return false;
}
DefaultLocalizationMethodConfiguration config = new DefaultLocalizationMethodConfiguration
{
MapsToRemove = maps
};
ImmersalLogger.Log($"Removing {maps.Length} maps from {localizationMethod.GetType().Name} configuration");
// Configure will return false if the method does not have any maps configured after removal
// => should remove the configuration entirely if set to WhenNecessary
if (!await localizationMethod.Configure(config) && localizationMethod.ConfigurationMode == ConfigurationMode.WhenNecessary)
{
ImmersalLogger.Log($"Removing {localizationMethod.GetType().Name} configuration");
// Cancel possible running task
if (m_RunningLocalizationTasks.TryGetValue(localizationMethod, out LocalizationTask task))
{
task.CancellationTokenSource.Cancel();
await task.LocalizationMethodTask;
}
m_ConfiguredLocalizationMethods.Remove(localizationMethod);
}
return true;
}
// This ILocalizer implementation can run multiple asynchronous localization tasks (one per method)
// The Localize task itself always awaits for one of the internal localization tasks to finish
// before providing results. Remaining localization tasks will propagate to the next cycle.
public async Task<ILocalizationResults> Localize(ICameraData cameraData)
{
// Localization is already running -> bail out
if (m_IsLocalizing)
{
cameraData.CheckReferences();
return new LocalizationResults { Results = Array.Empty<ILocalizationResult>() };
}
m_IsLocalizing = true;
List<ILocalizationResult> results = new List<ILocalizationResult>();
// Make sure all LocalizationTasks are running
foreach (ILocalizationMethod localizationMethod in m_ConfiguredLocalizationMethods)
{
// If a task is already running, we check if has completed since last localization cycle
if (m_RunningLocalizationTasks.TryGetValue(localizationMethod, out LocalizationTask task))
{
if (task.LocalizationMethodTask.IsCompleted)
{
// Add results and remove so we can start again
results.Add(task.LocalizationMethodTask.Result);
m_RunningLocalizationTasks.Remove(localizationMethod);
task.LocalizationMethodTask.Dispose();
}
else
{
// Skip unfinished tasks
continue;
}
}
// Start new localization task
StartNewLocalizationTask(localizationMethod, cameraData);
}
// Wait for any of the currently running localization tasks to finish
Task anyTask = Task.WhenAny(
m_RunningLocalizationTasks.Values.Select(runningTask => runningTask.LocalizationMethodTask));
try
{
await anyTask;
}
catch (OperationCanceledException)
{
CleanUpLocalizationTasks();
return new LocalizationResults { Results = Array.Empty<ILocalizationResult>() };
}
ImmersalLogger.Log("Localization task completed");
// Collect results for this cycle and combine with previous
results.AddRange(CollectLocalizationResults());
CheckForEvents(results);
LocalizationResults localizationResults = new LocalizationResults
{
Results = results.ToArray()
};
m_IsLocalizing = false;
OnLocalizationResult?.Invoke(localizationResults);
return localizationResults;
}
public async Task<List<LocalizationTask>> CreateLocalizationTasks(ICameraData cameraData)
{
List<LocalizationTask> tasks = new List<LocalizationTask>();
foreach (ILocalizationMethod localizationMethod in m_ConfiguredLocalizationMethods)
{
// Create new localization task
tasks.Add(CreateNewLocalizationTask(localizationMethod, cameraData));
}
return tasks;
}
public async Task<ILocalizationResults> LocalizeAllMethods(ICameraData cameraData)
{
List<ILocalizationResult> results = new List<ILocalizationResult>();
List<LocalizationTask> tasks = await CreateLocalizationTasks(cameraData);
await Task.WhenAll(tasks.Select(t => t.LocalizationMethodTask));
foreach (Task<ILocalizationResult> t in tasks.Select(t => t.LocalizationMethodTask))
{
if (t.Status != TaskStatus.RanToCompletion) continue;
results.Add(t.Result);
t.Dispose();
}
return new LocalizationResults { Results = results.ToArray() };
}
private void StartNewLocalizationTask(ILocalizationMethod localizationMethod, ICameraData cameraData)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task<ILocalizationResult> localizationMethodTask = localizationMethod.Localize(cameraData, cts.Token);
LocalizationTask task = new LocalizationTask(localizationMethodTask, cts);
m_RunningLocalizationTasks.Add(localizationMethod, task);
}
private LocalizationTask CreateNewLocalizationTask(ILocalizationMethod localizationMethod, ICameraData cameraData)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task<ILocalizationResult> localizationMethodTask = localizationMethod.Localize(cameraData, cts.Token);
LocalizationTask task = new LocalizationTask(localizationMethodTask, cts);
return task;
}
private ILocalizationResult[] CollectLocalizationResults()
{
// Combine all currently finished results
List<ILocalizationResult> resultList = new List<ILocalizationResult>();
foreach (ILocalizationMethod localizationMethod in m_ConfiguredLocalizationMethods)
{
if (m_RunningLocalizationTasks.TryGetValue(localizationMethod, out LocalizationTask task))
{
if (task.LocalizationMethodTask.IsCompleted)
{
resultList.Add(task.LocalizationMethodTask.Result);
m_RunningLocalizationTasks.Remove(localizationMethod);
task.LocalizationMethodTask.Dispose();
}
}
}
return resultList.ToArray();
}
private void CleanUpLocalizationTasks()
{
// remove cancelled tasks
foreach (ILocalizationMethod localizationMethod in m_ConfiguredLocalizationMethods)
{
if (m_RunningLocalizationTasks.TryGetValue(localizationMethod, out LocalizationTask task))
{
if (task.LocalizationMethodTask.IsCanceled)
{
m_RunningLocalizationTasks.Remove(localizationMethod);
}
}
}
}
// Event firing logic
private void CheckForEvents(List<ILocalizationResult> results)
{
int[] ids = results.Where(r => r.Success).Select(r => r.MapId).ToArray();
if (ids.Length > 0)
{
if (!m_HasLocalizedSuccessfully)
{
OnFirstSuccessfulLocalization?.Invoke();
}
OnSuccessfulLocalizations?.Invoke(ids);
m_HasLocalizedSuccessfully = true;
}
else
{
OnFailedLocalizations?.Invoke();
}
}
private async Task StopRunningLocalizationTasks()
{
if (m_RunningLocalizationTasks is not { Count: > 0 }) return;
List<Task<ILocalizationResult>> tasks = new List<Task<ILocalizationResult>>();
foreach (LocalizationTask localizationTask in m_RunningLocalizationTasks.Values)
{
localizationTask.CancellationTokenSource.Cancel();
tasks.Add(localizationTask.LocalizationMethodTask);
}
// Wait for task to finish
await Task.WhenAll(tasks);
// Clean up tasks
m_RunningLocalizationTasks.Clear();
}
public async Task StopLocalizationForMethod(ILocalizationMethod localizationMethod)
{
if (m_RunningLocalizationTasks is not { Count: > 0 }) return;
if (m_RunningLocalizationTasks.TryGetValue(localizationMethod, out LocalizationTask task))
{
task.CancellationTokenSource.Cancel();
await task.LocalizationMethodTask;
m_RunningLocalizationTasks.Remove(localizationMethod);
}
}
public bool TryGetLocalizationTask(ILocalizationMethod localizationMethod, out LocalizationTask task)
{
return m_RunningLocalizationTasks.TryGetValue(localizationMethod, out task);
}
public async Task StopAndCleanUp()
{
// Cancel all running tasks
await StopRunningLocalizationTasks();
// Clean up methods
await Task.WhenAll(m_ConfiguredLocalizationMethods.Select(method => method.StopAndCleanUp()));
m_ConfiguredLocalizationMethods.Clear();
m_HasLocalizedSuccessfully = false;
}
}
public struct LocalizerConfigurationResult : ILocalizerConfigurationResult
{
public bool Success { get; set; }
}
public struct DefaultLocalizerConfiguration : ILocalizerConfiguration
{
public Dictionary<ILocalizationMethod, XRMap[]> ConfigurationsToAdd { get; set; }
public Dictionary<ILocalizationMethod, XRMap[]> ConfigurationsToRemove { get; set; }
public bool StopRunningTasks { get; set; }
}
public struct DefaultLocalizationMethodConfiguration : ILocalizationMethodConfiguration
{
public XRMap[] MapsToAdd { get; set; }
public XRMap[] MapsToRemove { get; set; }
public SolverType? SolverType { get; set; }
[ObsoleteAttribute("PriorNNCount is obsolete. Use PriorNNCountMin/Max instead.", false)]
public int? PriorNNCount { get; set; }
public int? PriorNNCountMin { get; set; }
public int? PriorNNCountMax { get; set; }
public Vector3? PriorScale { get; set; }
public float? PriorRadius { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0113bf92df9fa4e24bd92cedc0f00ec6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,221 @@
/*===============================================================================
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.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Immersal.REST;
using UnityEngine;
using UnityEngine.Events;
namespace Immersal.XR
{
public class ServerLocalization : MonoBehaviour, ILocalizationMethod
{
// Note:
// Custom editor does not draw default inspector
[SerializeField]
private ConfigurationMode m_ConfigurationMode = ConfigurationMode.WhenNecessary;
[SerializeField]
private SolverType m_SolverType = SolverType.Default;
[SerializeField]
private int m_PriorNNCountMin = 60;
[SerializeField]
private int m_PriorNNCountMax = 720;
[SerializeField]
private Vector3 m_PriorScale = Vector3.one;
[SerializeField]
private float m_PriorRadius = 0f;
public ConfigurationMode ConfigurationMode => m_ConfigurationMode;
public UnityEvent<float> OnProgress;
private int m_previouslyLocalizedMapId = 0;
// No options
public IMapOption[] MapOptions => null;
private SDKMapId[] m_MapIds;
public Task<bool> Configure(ILocalizationMethodConfiguration configuration)
{
m_SolverType = configuration.SolverType ?? m_SolverType;
m_PriorNNCountMin = configuration.PriorNNCountMin ?? m_PriorNNCountMin;
m_PriorNNCountMax = configuration.PriorNNCountMax ?? m_PriorNNCountMax;
m_PriorScale = configuration.PriorScale ?? m_PriorScale;
m_PriorRadius = configuration.PriorRadius ?? m_PriorRadius;
List<SDKMapId> mapList = m_MapIds != null ? m_MapIds.ToList() : new List<SDKMapId>();
// Add maps
if (configuration.MapsToAdd != null)
{
foreach (XRMap map in configuration.MapsToAdd)
{
mapList.Add(new SDKMapId {id = map.mapId});
}
}
// Remove maps
if (configuration.MapsToRemove != null)
{
foreach (XRMap map in configuration.MapsToRemove)
{
mapList.Remove(new SDKMapId {id = map.mapId});
}
// Check if there are no configured maps left
if (mapList.Count == 0)
{
m_MapIds = Array.Empty<SDKMapId>();
return Task.FromResult(false);
}
}
m_MapIds = mapList.ToArray();
return Task.FromResult(true);
}
public async Task<ILocalizationResult> Localize(ICameraData cameraData, CancellationToken cancellationToken)
{
LocalizationResult r = new LocalizationResult
{
Success = false
};
if (m_MapIds == null || m_MapIds.Length == 0)
return r;
JobLocalizeServerAsync j = new JobLocalizeServerAsync();
j.Progress.ProgressChanged += OnCurrentJobProgress;
Vector4 intrinsics = cameraData.Intrinsics;
int channels = cameraData.Channels;
int width = cameraData.Width;
int height = cameraData.Height;
byte[] capture = new byte[channels * width * height + 8192];
float startTime = Time.realtimeSinceStartup;
using (IImageData imageData = cameraData.GetImageData())
{
int size = width * height * channels;
byte[] pixels = new byte[size];
Marshal.Copy(imageData.UnmanagedDataPointer, pixels, 0, size);
Task<(byte[], CaptureInfo)> t = Task.Run(() =>
{
CaptureInfo info =
Immersal.Core.CaptureImage(capture, capture.Length, pixels, width, height, channels);
Array.Resize(ref capture, info.captureSize);
return (capture, info);
});
await t;
}
j.image = capture; //t.Result.Item1;
j.intrinsics = intrinsics;
j.mapIds = m_MapIds;
j.solverType = m_SolverType == SolverType.Prior ? 4 : 0;
if (m_SolverType == SolverType.Prior &&
m_previouslyLocalizedMapId != 0 &&
MapManager.TryGetMapEntry(m_previouslyLocalizedMapId, out MapEntry entry))
{
Vector3 pos = cameraData.CameraPositionOnCapture;
Matrix4x4 mapPoseWithRelation = entry.SceneParent.ToMapSpace(pos, Quaternion.identity);
Vector3 priorPos = entry.Relation.ApplyInverseRelation(mapPoseWithRelation).GetPosition();
priorPos.SwitchHandedness();
j.priorPos = priorPos;
j.priorNNCountMin = m_PriorNNCountMin;
j.priorNNCountMax = m_PriorNNCountMax;
j.priorScale = m_PriorScale;
j.priorRadius = m_PriorRadius;
}
else
{
// previously localized map not found, reset
m_previouslyLocalizedMapId = 0;
}
Quaternion rot = cameraData.CameraRotationOnCapture * cameraData.Orientation;
rot.SwitchHandedness();
j.rotation = rot;
SDKLocalizeResult result = await j.RunJobAsync(cancellationToken);
float elapsedTime = Time.realtimeSinceStartup - startTime;
if (result.success)
{
ImmersalLogger.Log($"Relocalized to mapId {result.map} in {elapsedTime} seconds");
int mapId = result.map;
if (mapId > 0)
{
Matrix4x4 m = Matrix4x4.identity;
m.m00 = result.r00; m.m01 = result.r01; m.m02 = result.r02;
m.m10 = result.r10; m.m11 = result.r11; m.m12 = result.r12;
m.m20 = result.r20; m.m21 = result.r21; m.m22 = result.r22;
LocalizeInfo locInfo = new LocalizeInfo
{
mapId = mapId,
position = new Vector3(result.px, result.py, result.pz),
rotation = m.rotation,
confidence = result.confidence
};
r.Success = true;
r.MapId = mapId;
r.LocalizeInfo = locInfo;
m_previouslyLocalizedMapId = mapId;
}
}
else
{
ImmersalLogger.Log($"Localization attempt failed after {elapsedTime} seconds");
}
j.Progress.ProgressChanged -= OnCurrentJobProgress;
return r;
}
public Task StopAndCleanUp()
{
m_MapIds = Array.Empty<SDKMapId>();
return Task.CompletedTask;
}
public Task OnMapRegistered(XRMap map)
{
return Task.CompletedTask;
}
private void OnCurrentJobProgress(object sender, float value)
{
OnProgress?.Invoke(value);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 82ac8f41066bd43cca7a925e70116602
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,868 @@
/*===============================================================================
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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0c7cd7d260b4d42f1924089f286b6736
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 560e94b4b18d54326a7612a1281a20cf
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,486 @@
/*===============================================================================
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.Runtime.InteropServices;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
namespace Immersal.XR
{
public class ARFoundationSupport : MonoBehaviour, IPlatformSupport
{
[SerializeField, Tooltip("Maximum configuration attempts")]
private int m_MaxConfigurationAttempts = 10;
[SerializeField, Tooltip("Milliseconds to wait between configuration attempts")]
private int m_MsBetweenConfigurationAttempts = 100;
private ARCameraManager m_CameraManager;
private ARSession m_ARSession;
private Transform m_CameraTransform;
private XRCameraConfiguration? m_InitialConfig;
private IPlatformConfiguration m_Configuration;
private bool m_ConfigDone = false;
private bool m_OverrideScreenOrientation = false;
private ScreenOrientation m_ScreenOrientationOverride = ScreenOrientation.Portrait;
public ARCameraManager cameraManager
{
get
{
if (m_CameraManager == null)
{
m_CameraManager = UnityEngine.Object.FindObjectOfType<ARCameraManager>();
}
return m_CameraManager;
}
}
public ARSession arSession
{
get
{
if (m_ARSession == null)
{
m_ARSession = UnityEngine.Object.FindObjectOfType<ARSession>();
}
return m_ARSession;
}
}
public enum CameraResolution { Default, HD, FullHD, Max }; // With Huawei AR Engine SDK, only Default (640x480) and Max (1440x1080) are supported.
[SerializeField]
[Tooltip("Android resolution")]
private CameraResolution m_AndroidResolution = CameraResolution.FullHD;
[SerializeField]
[Tooltip("iOS resolution")]
private CameraResolution m_iOSResolution = CameraResolution.Default;
[SerializeField]
private CameraDataFormat m_CameraDataFormat = CameraDataFormat.SingleChannel;
public CameraResolution androidResolution
{
get { return m_AndroidResolution; }
set
{
m_AndroidResolution = value;
ConfigureCamera();
}
}
public CameraResolution iOSResolution
{
get { return m_iOSResolution; }
set
{
m_iOSResolution = value;
ConfigureCamera();
}
}
private Task<(bool, CameraData)> m_CurrentCameraDataTask;
private bool m_isTracking = false;
public async Task<IPlatformConfigureResult> ConfigurePlatform()
{
PlatformConfiguration config = new PlatformConfiguration
{
CameraDataFormat = m_CameraDataFormat
};
return await ConfigurePlatform(config);
}
public async Task<IPlatformConfigureResult> ConfigurePlatform(IPlatformConfiguration configuration)
{
ImmersalLogger.Log("Configuring ARF Platform");
#if UNITY_EDITOR
ImmersalLogger.LogWarning("Running AR Foundation Platform in Unity Editor will result in failed updates.");
#endif
m_CameraManager = UnityEngine.Object.FindObjectOfType<ARCameraManager>();
if (!m_CameraManager)
{
throw new ComponentTaskCriticalException("Could not find ARCameraManager.");
}
m_ARSession = UnityEngine.Object.FindObjectOfType<ARSession>();
if (!m_ARSession)
{
throw new ComponentTaskCriticalException("Could not find ARSession.");
}
m_Configuration = configuration;
if (Camera.main != null) m_CameraTransform = Camera.main.transform;
for (int i = 0; i < m_MaxConfigurationAttempts; i++)
{
m_ConfigDone = ConfigureCamera();
if (m_ConfigDone)
break;
await Task.Delay(m_MsBetweenConfigurationAttempts);
}
IPlatformConfigureResult r = new SimplePlatformConfigureResult
{
Success = m_ConfigDone
};
return r;
}
private bool ConfigureCamera()
{
#if !UNITY_EDITOR && (UNITY_ANDROID || UNITY_IOS)
var cameraSubsystem = cameraManager.subsystem;
if (cameraSubsystem == null || !cameraSubsystem.running)
return false;
var configurations = cameraSubsystem.GetConfigurations(Allocator.Temp);
if (!configurations.IsCreated || (configurations.Length <= 0))
return false;
int bestError = int.MaxValue;
var currentConfig = cameraSubsystem.currentConfiguration;
int dw = (int)currentConfig?.width;
int dh = (int)currentConfig?.height;
if (dw == 0 && dh == 0)
return false;
#if UNITY_ANDROID
CameraResolution reso = androidResolution;
#else
CameraResolution reso = iOSResolution;
#endif
if (!m_ConfigDone)
{
m_InitialConfig = currentConfig;
}
switch (reso)
{
case CameraResolution.Default:
dw = (int)currentConfig?.width;
dh = (int)currentConfig?.height;
break;
case CameraResolution.HD:
dw = 1280;
dh = 720;
break;
case CameraResolution.FullHD:
dw = 1920;
dh = 1080;
break;
case CameraResolution.Max:
dw = 80000;
dh = 80000;
break;
}
foreach (var config in configurations)
{
int perror = config.width * config.height - dw * dh;
if (Math.Abs(perror) < bestError)
{
bestError = Math.Abs(perror);
currentConfig = config;
}
}
if (reso != CameraResolution.Default) {
ImmersalLogger.Log($"resolution = {(int)currentConfig?.width}x{(int)currentConfig?.height}");
cameraSubsystem.currentConfiguration = currentConfig;
}
else
{
cameraSubsystem.currentConfiguration = m_InitialConfig;
}
#endif
return true;
}
public async Task<IPlatformUpdateResult> UpdatePlatform()
{
return await UpdateWithConfiguration(m_Configuration);
}
public async Task<IPlatformUpdateResult> UpdatePlatform(IPlatformConfiguration oneShotConfiguration)
{
return await UpdateWithConfiguration(oneShotConfiguration);
}
private async Task<IPlatformUpdateResult> UpdateWithConfiguration(IPlatformConfiguration configuration)
{
ImmersalLogger.Log("Updating ARF Platform");
if (!m_ConfigDone)
throw new ComponentTaskCriticalException("Trying to update platform before configuration.");
// Status
SimplePlatformStatus platformStatus = new SimplePlatformStatus
{
TrackingQuality = m_isTracking ? 1 : 0
};
m_CurrentCameraDataTask = GetCameraData(configuration.CameraDataFormat);
(bool success, CameraData data) = await m_CurrentCameraDataTask;
// UpdateResult
SimplePlatformUpdateResult r = new SimplePlatformUpdateResult
{
Success = success,
Status = platformStatus,
CameraData = (ICameraData)data
};
return r;
}
private async Task<(bool, CameraData)> GetCameraData(CameraDataFormat cameraDataFormat)
{
if (!GetIntrinsics(out Vector4 intrinsics))
{
ImmersalLogger.LogError("Could not acquire camera intrinsics.");
return (false, null);
}
if (m_CameraTransform == null)
{
ImmersalLogger.LogError("Could not acquire camera pose.");
return (false, null);
}
bool imageAcquired = false;
Task<XRCpuImage> t = Task.Run(() =>
{
// XRCpuImage lifecycle will be managed by CameraData/ImageData
imageAcquired = m_CameraManager.TryAcquireLatestCpuImage(out XRCpuImage image);
return image;
});
XRCpuImage image = await t;
if (!imageAcquired)
{
ImmersalLogger.LogError("Could not acquire camera image.");
return (false, null);
}
ARFImageData imageData = new ARFImageData(image, cameraDataFormat);
CameraData data = new CameraData(imageData)
{
Width = image.width,
Height = image.height,
Intrinsics = intrinsics,
Format = cameraDataFormat,
Channels = cameraDataFormat == CameraDataFormat.SingleChannel ? 1 : 3,
CameraPositionOnCapture = m_CameraTransform.position,
CameraRotationOnCapture = m_CameraTransform.rotation,
Orientation = GetOrientation()
};
return (true, data);
}
public bool GetIntrinsics(out Vector4 intrinsics)
{
intrinsics = Vector4.zero;
XRCameraIntrinsics intr = default;
bool success = m_CameraManager != null && m_CameraManager.TryGetIntrinsics(out intr);
if (success)
{
intrinsics.x = intr.focalLength.x;
intrinsics.y = intr.focalLength.y;
intrinsics.z = intr.principalPoint.x;
intrinsics.w = intr.principalPoint.y;
}
return success;
}
public void SetOrientationOverride(ScreenOrientation newOrientation)
{
m_OverrideScreenOrientation = true;
m_ScreenOrientationOverride = newOrientation;
}
public void DisableOrientationOverride()
{
m_OverrideScreenOrientation = false;
}
public Quaternion GetOrientation()
{
ScreenOrientation orientation =
m_OverrideScreenOrientation ? m_ScreenOrientationOverride : Screen.orientation;
float angle = orientation switch
{
ScreenOrientation.Portrait => 90f,
ScreenOrientation.LandscapeLeft => 180f,
ScreenOrientation.LandscapeRight => 0f,
ScreenOrientation.PortraitUpsideDown => -90f,
_ => 0f
};
return Quaternion.Euler(0f, 0f, angle);
}
private void OnEnable()
{
#if !UNITY_EDITOR
m_isTracking = ARSession.state == ARSessionState.SessionTracking;
ARSession.stateChanged += ARSessionStateChanged;
#endif
}
private void OnDisable()
{
#if !UNITY_EDITOR
ARSession.stateChanged -= ARSessionStateChanged;
#endif
m_isTracking = false;
}
private void ARSessionStateChanged(ARSessionStateChangedEventArgs args)
{
m_isTracking = args.state == ARSessionState.SessionTracking;
}
public async Task StopAndCleanUp()
{
// there is no cancellation token for the update procedure here, just wait
await m_CurrentCameraDataTask;
m_ConfigDone = false;
m_isTracking = false;
}
}
public class ARFImageData : ImageData
{
public XRCpuImage Image;
private IntPtr m_unmanagedDataPointer;
private byte[] m_managedBytes;
public override IntPtr UnmanagedDataPointer => m_unmanagedDataPointer;
public override byte[] ManagedBytes
{
get
{
if (m_managedBytes == null || m_managedBytes.Length == 0)
{
m_managedBytes = CopyBytes();
}
return m_managedBytes;
}
}
private CameraDataFormat m_Format;
public ARFImageData(XRCpuImage image, CameraDataFormat format)
{
Image = image;
m_Format = format;
switch (format)
{
case CameraDataFormat.RGB:
GetPointerToRGB(ref m_unmanagedDataPointer, Image);
break;
default:
case CameraDataFormat.SingleChannel:
GetPointerFast(ref m_unmanagedDataPointer, Image);
break;
}
}
public override void DisposeData()
{
Image.Dispose();
m_unmanagedDataPointer = IntPtr.Zero;
}
private void GetPointerFast(ref IntPtr unmanagedPointer, XRCpuImage image)
{
XRCpuImage.Plane plane = image.GetPlane(0); // use the Y plane
int width = image.width, height = image.height;
if (width == plane.rowStride)
{
unsafe
{
unmanagedPointer = (IntPtr)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(plane.data);
}
}
else
{
byte[] data = new byte[width * height];
unsafe
{
fixed (byte* dstPtr = data)
{
byte* srcPtr = (byte*)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(plane.data);
if (width > 0 && height > 0)
{
UnsafeUtility.MemCpyStride(dstPtr, width, srcPtr, plane.rowStride, width, height);
}
unmanagedPointer = (IntPtr)dstPtr;
}
}
}
}
private static void GetPointerToRGB(ref IntPtr unmanagedPointer, XRCpuImage image)
{
var conversionParams = new XRCpuImage.ConversionParams
{
inputRect = new RectInt(0, 0, image.width, image.height),
outputDimensions = new Vector2Int(image.width, image.height),
outputFormat = TextureFormat.RGB24,
transformation = XRCpuImage.Transformation.None
};
int size = image.GetConvertedDataSize(conversionParams);
byte[] data = new byte[size];
unsafe
{
fixed (byte* dstPtr = data)
{
unmanagedPointer = (IntPtr)dstPtr;
image.Convert(conversionParams, unmanagedPointer, data.Length);
}
}
}
private byte[] CopyBytes()
{
int pixelSize = m_Format == CameraDataFormat.SingleChannel ? 1 : 3;
int size = Image.width * Image.height * pixelSize;
byte[] bytes = new byte[size];
Marshal.Copy(m_unmanagedDataPointer, bytes, 0, size);
return bytes;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ff1599bedf7cb4f2bb03770be4930ce8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,75 @@
/*===============================================================================
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.Linq;
using System.Threading.Tasks;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Immersal.XR
{
public class SceneUpdateData
{
public Matrix4x4 Pose;
public Matrix4x4 TrackerSpace;
public Matrix4x4 MapSpacePose;
public ICameraData CameraData;
public LocalizeInfo LocalizeInfo;
public MapEntry MapEntry;
public bool Ignore;
}
public class SceneUpdater : MonoBehaviour, ISceneUpdater
{
public async Task UpdateScene(MapEntry entry, ICameraData cameraData, ILocalizationResult localizationResult)
{
ImmersalLogger.Log("Updating scene", ImmersalLogger.LoggingLevel.Verbose);
LocalizeInfo locInfo = localizationResult.LocalizeInfo;
// Immersal pose relative to the map
Vector3 localizedPos = locInfo.position;
Quaternion localizedRot = locInfo.rotation;
// Apply device specific orientation and switch handedness to align with Unity
localizedRot *= cameraData.Orientation;
localizedPos.SwitchHandedness();
localizedRot.SwitchHandedness();
// Apply map to space relative transform (map pose in the scene)
MapToSpaceRelation mo = entry.Relation;
Matrix4x4 offsetNoScale = Matrix4x4.TRS(mo.Position, mo.Rotation, Vector3.one);
Vector3 scaledPos = Vector3.Scale(localizedPos, mo.Scale);
Matrix4x4 mapSpace = offsetNoScale * Matrix4x4.TRS(scaledPos, localizedRot, Vector3.one);
// Tracker space
Vector3 capturePos = cameraData.CameraPositionOnCapture;
Quaternion captureRot = cameraData.CameraRotationOnCapture;
Matrix4x4 trackerSpace = Matrix4x4.TRS(capturePos, captureRot, Vector3.one);
// Tracker relative pose
Matrix4x4 m = trackerSpace * (mapSpace.inverse);
SceneUpdateData data = new SceneUpdateData
{
Pose = m,
TrackerSpace = trackerSpace,
MapSpacePose = mapSpace,
CameraData = cameraData,
LocalizeInfo = locInfo,
MapEntry = entry,
Ignore = false
};
await entry.SceneParent.SceneUpdate(data);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 10b6f3f5d6c78474d867465b74b8a539
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,157 @@
/*===============================================================================
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 UnityEngine;
using UnityEngine.Events;
namespace Immersal.XR
{
public class TrackingAnalyzer : MonoBehaviour, ITrackingAnalyzer
{
[SerializeField]
private float m_SecondsToDecayPose = 10f;
// LocalizationResults might contain multiple fails / success results.
// By default these are combined to one attempt and one fail/success.
// This options allows counting each result separately.
[SerializeField]
private bool m_SeparateLocalizationResults = false;
// This is invoked the first time we notice platform tracking quality drops to 0.
// After platform tracking quality goes up again, we enable a new invoke on the next quality drop.
// Platform tracking must go above 0 for this to ever be invoked.
public UnityEvent OnPlatformTrackingLost;
// These actions are invoked when tracking quality is high enough or drops to 0.
// Only invoked once when the change happens. Invoking one resets the other.
// OnTrackingLost will not be invoked before TrackingWell has occured.
// The bool value is true on the first ever invocation, otherwise false.
public UnityEvent<bool> OnTrackingWell;
public UnityEvent<bool> OnTrackingLost;
public ITrackingStatus TrackingStatus => m_CurrentTrackingStatus;
private TrackingStatus m_CurrentTrackingStatus = new TrackingStatus();
private bool m_HasPose = false;
private int m_CurrentResult = 0;
private int m_PreviousResult = 0;
private float m_LatestPoseUpdateTime = 0f;
private bool m_AllowPlatformTrackingLostInvoke = false;
private bool m_TrackingWell = false;
private bool m_NeverLostTracking = true;
private bool m_NeverTrackedWell = true;
// Analyze is run after other tasks have been completed / attempted.
// Frequency depends on ImmersalSDK session update interval.
public void Analyze(IPlatformStatus platformStatus, ILocalizationResults localizationResults)
{
if (platformStatus.TrackingQuality == 0 && m_AllowPlatformTrackingLostInvoke)
{
OnPlatformTrackingLost?.Invoke();
m_AllowPlatformTrackingLostInvoke = false;
}
else if (platformStatus.TrackingQuality > 0 && !m_AllowPlatformTrackingLostInvoke)
{
m_AllowPlatformTrackingLostInvoke = true;
}
if (!m_SeparateLocalizationResults)
m_CurrentTrackingStatus.LocalizationAttemptCount++;
bool hadSuccess = false;
foreach (ILocalizationResult result in localizationResults.Results)
{
if (m_SeparateLocalizationResults)
m_CurrentTrackingStatus.LocalizationAttemptCount++;
if (result.Success)
{
hadSuccess = true;
if (m_SeparateLocalizationResults)
m_CurrentTrackingStatus.LocalizationSuccessCount++;
if (!m_HasPose)
{
m_HasPose = true;
}
}
}
if (!m_SeparateLocalizationResults && hadSuccess)
m_CurrentTrackingStatus.LocalizationSuccessCount++;
}
public void Reset()
{
ImmersalLogger.Log("Resetting TrackingAnalyzer");
m_CurrentTrackingStatus = new TrackingStatus();
m_HasPose = false;
m_CurrentResult = 0;
m_PreviousResult = 0;
m_LatestPoseUpdateTime = 0f;
m_TrackingWell = false;
m_NeverLostTracking = true;
m_NeverTrackedWell = true;
}
private void Update()
{
if (m_CurrentTrackingStatus.LocalizationAttemptCount == 0) return;
// Update cumulative result tracking
int diffResults = m_CurrentTrackingStatus.LocalizationSuccessCount - m_PreviousResult;
m_PreviousResult = m_CurrentTrackingStatus.LocalizationSuccessCount;
if (diffResults > 0)
{
m_LatestPoseUpdateTime = Time.time;
m_CurrentResult = Mathf.Min(m_CurrentResult + diffResults, 3);
}
else if (Time.time - m_LatestPoseUpdateTime > m_SecondsToDecayPose)
{
m_LatestPoseUpdateTime = Time.time;
m_CurrentResult = Mathf.Max(m_CurrentResult - 1, 0);
}
// Tracking lost
if (m_HasPose && m_CurrentResult < 1)
{
m_HasPose = false;
if (m_TrackingWell)
{
OnTrackingLost?.Invoke(m_NeverLostTracking);
m_NeverLostTracking = false;
m_TrackingWell = false;
}
}
// Tracking well
else if (m_HasPose && diffResults > 0 && m_CurrentResult > 2 && !m_TrackingWell)
{
OnTrackingWell?.Invoke(m_NeverTrackedWell);
m_NeverTrackedWell = false;
m_TrackingWell = true;
}
m_CurrentTrackingStatus.TrackingQuality = m_CurrentResult;
}
}
public class TrackingStatus : ITrackingStatus
{
public int LocalizationAttemptCount { get; set; }
public int LocalizationSuccessCount { get; set; }
public int TrackingQuality { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d3670ede57ef84f68845b3bb411d4a87
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,435 @@
/*===============================================================================
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.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Immersal.REST;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using UnityEngine.Serialization;
using Object = UnityEngine.Object;
namespace Immersal.XR
{
public class XRMap : MonoBehaviour, ISerializationCallbackReceiver
{
// Note: All properties are configured via the custom editor
// The editor does not render the default inspector view,
// so none of these will show up without explicitly adding to the editor
[SerializeField]
public TextAsset mapFile;
[SerializeField]
private int m_MapId = -1;
[SerializeField]
private string m_MapName = null;
public int privacy;
public MapAlignment mapAlignment;
public WGS84 wgs84;
[System.Serializable]
public struct MetadataFile
{
public string error;
public int id;
public int type;
public string created;
public string version;
public int user;
public int creator;
public string name;
public int size;
public string status;
public int privacy;
public double latitude;
public double longitude;
public double altitude;
public double tx;
public double ty;
public double tz;
public double qw;
public double qx;
public double qy;
public double qz;
public double scale;
public string sha256_al;
public string sha256_sparse;
public string sha256_dense;
public string sha256_tex;
}
[System.Serializable]
public struct MapAlignment
{
public double tx;
public double ty;
public double tz;
public double qx;
public double qy;
public double qz;
public double qw;
public double scale;
}
[System.Serializable]
public struct WGS84
{
public double latitude;
public double longitude;
public double altitude;
}
[SerializeField]
public bool IsConfigured = false;
[SerializeField]
public XRMapVisualization Visualization;
public int mapId
{
get => m_MapId;
private set => m_MapId = value;
}
public string mapName
{
get => m_MapName;
set => m_MapName = value;
}
// Localization method
[SerializeField]
private Object m_LocalizationMethodObject;
public ILocalizationMethod LocalizationMethod
{
get => m_LocalizationMethodObject as ILocalizationMethod;
set => m_LocalizationMethodObject = value as Object;
}
#region Map options
/*
* Map options are custom configurations specific to individual ILocalizationMethods.
* They are defined in the ILocalizationMethod implementation and registered in MapManager.
* They require custom serialization via the SerializableMapOption class.
*/
[SerializeField]
private List<SerializableMapOption> m_SerializedMapOptions = new List<SerializableMapOption>();
public List<IMapOption> MapOptions = new List<IMapOption>();
#if UNITY_EDITOR
public void UpdateMapOptions()
{
MapOptions = new List<IMapOption>();
if (MapManager.TryGetMapOptions(LocalizationMethod, out List<IMapOption> options))
{
MapOptions = options;
}
// force serialization
SerializeMapOptions();
}
#endif
public void SerializeMapOptions()
{
m_SerializedMapOptions.Clear();
foreach (IMapOption mapOption in MapOptions)
{
m_SerializedMapOptions.Add(SerializableMapOption.Serialize(mapOption));
}
}
public void DeserializeMapOptions()
{
MapOptions.Clear();
foreach (SerializableMapOption serializedOption in m_SerializedMapOptions)
{
IMapOption option = serializedOption.Deserialize();
if (option != null)
{
MapOptions.Add(option);
}
}
}
// Unity serialization callbacks
public void OnBeforeSerialize()
{
SerializeMapOptions();
}
public void OnAfterDeserialize()
{
DeserializeMapOptions();
}
private void Awake()
{
// Ensure we have deserialized options available at runtime
DeserializeMapOptions();
}
#endregion
public double[] MapToEcefGet()
{
double[] m = mapAlignment.QuaternionsToDoubleMatrix3x3();
double[] mapToEcef = new double[] {this.mapAlignment.tx, this.mapAlignment.ty, this.mapAlignment.tz, m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], this.mapAlignment.scale};
return mapToEcef;
}
public void Uninitialize()
{
ClearBytesReference();
if (Visualization != null)
RemoveVisualization();
IsConfigured = false;
}
public void Configure(SDKMapMetadataGetResult result, bool setIdAndName = true)
{
SetMetadata(result, setIdAndName);
Configure();
}
public void Configure(TextAsset mapFile = null)
{
if (mapFile != null)
{
this.mapFile = mapFile;
ParseMapFiles();
}
IsConfigured = true;
}
public void CreateVisualization(XRMapVisualization.RenderMode renderMode = XRMapVisualization.RenderMode.EditorOnly, bool randomColor = false)
{
Color c = randomColor ? XRMapVisualization.pointCloudColors[UnityEngine.Random.Range(0, XRMapVisualization.pointCloudColors.Length)]
: new Color(0.57f, 0.93f, 0.12f);
CreateVisualization(c, renderMode);
}
public void CreateVisualization(Color pointColor, XRMapVisualization.RenderMode renderMode)
{
if (Visualization != null)
{
RemoveVisualization();
}
GameObject go = new GameObject($"{mapId}-{mapName}-vis");
go.transform.SetParent(transform, false);
Visualization = go.AddComponent<XRMapVisualization>();
Visualization.Initialize(this, renderMode, pointColor);
}
public void RemoveVisualization()
{
Visualization.ClearVisualization();
DestroyImmediate(Visualization.gameObject);
Visualization = null;
}
private void ClearBytesReference()
{
mapFile = null;
}
public void ApplyAlignment()
{
Vector3 posMetadata = new Vector3((float)mapAlignment.tx, (float)mapAlignment.ty, (float)mapAlignment.tz);
Quaternion rotMetadata = new Quaternion((float)mapAlignment.qx, (float)mapAlignment.qy, (float)mapAlignment.qz, (float)mapAlignment.qw);
float scaleMetadata = (float)mapAlignment.scale; // Only uniform scale metadata is supported
// IMPORTANT
// Switch coordinate system handedness back from Immersal Cloud Service's default right-handed system to Unity's left-handed system
Matrix4x4 b = Matrix4x4.TRS(posMetadata, rotMetadata, new Vector3(scaleMetadata, scaleMetadata, scaleMetadata));
Matrix4x4 a = b.SwitchHandedness();
Vector3 pos = a.GetColumn(3);
Quaternion rot = a.rotation;
Vector3 scl = new Vector3(scaleMetadata, scaleMetadata, scaleMetadata); // Only uniform scale metadata is supported
// Set XR Map local transform from the converted metadata
transform.localPosition = pos;
transform.localRotation = rot;
transform.localScale = scl;
}
public void ParseMapFiles()
{
int id = -1;
if (GetMapId(out id))
{
SetIdAndName(id, mapFile.name.Substring(id.ToString().Length + 1), true);
}
#if UNITY_EDITOR
if (Application.isEditor)
{
if (mapFile != null)
{
try
{
string mapFilePath = AssetDatabase.GetAssetPath(mapFile);
string mapFileDir = Path.GetDirectoryName(mapFilePath);
string jsonFilePath = Path.Combine(mapFileDir, string.Format("{0}-metadata.json", mapFile.name));
MetadataFile metadataFile = JsonUtility.FromJson<MetadataFile>(File.ReadAllText(jsonFilePath));
SetMetadata(metadataFile);
}
catch (FileNotFoundException e)
{
ImmersalLogger.LogWarning($"{e.Message}\nCould not find {mapFile.name}-metadata.json");
// set default values in case metadata is not available
mapAlignment.tx = 0.0;
mapAlignment.ty = 0.0;
mapAlignment.tz = 0.0;
mapAlignment.qx = 0.0;
mapAlignment.qy = 0.0;
mapAlignment.qz = 0.0;
mapAlignment.qw = 1.0;
mapAlignment.scale = 1.0;
wgs84.latitude = 0.0;
wgs84.longitude = 0.0;
wgs84.altitude = 0.0;
privacy = 0;
}
}
}
#endif
}
public void SetMetadata(SDKMapMetadataGetResult result, bool setIdAndName = false)
{
mapAlignment.tx = result.tx;
mapAlignment.ty = result.ty;
mapAlignment.tz = result.tz;
mapAlignment.qx = result.qx;
mapAlignment.qy = result.qy;
mapAlignment.qz = result.qz;
mapAlignment.qw = result.qw;
mapAlignment.scale = result.scale;
wgs84.latitude = result.latitude;
wgs84.longitude = result.longitude;
wgs84.altitude = result.altitude;
privacy = result.privacy;
if (setIdAndName)
{
SetIdAndName(result.id, result.name, true);
}
}
public void SetMetadata(MetadataFile metadataFile, bool setIdAndName = false)
{
mapAlignment.tx = metadataFile.tx;
mapAlignment.ty = metadataFile.ty;
mapAlignment.tz = metadataFile.tz;
mapAlignment.qx = metadataFile.qx;
mapAlignment.qy = metadataFile.qy;
mapAlignment.qz = metadataFile.qz;
mapAlignment.qw = metadataFile.qw;
mapAlignment.scale = metadataFile.scale;
wgs84.latitude = metadataFile.latitude;
wgs84.longitude = metadataFile.longitude;
wgs84.altitude = metadataFile.altitude;
privacy = metadataFile.privacy;
if (setIdAndName)
{
SetIdAndName(metadataFile.id, metadataFile.name, true);
}
}
public void SetIdAndName(int mapId, string mapName, bool applyToGameObject = false)
{
this.mapId = mapId;
this.mapName = mapName;
if (applyToGameObject)
{
gameObject.name = string.Format("XR Map {0}-{1}", mapId, mapName);
}
}
private bool GetMapId(out int parsedMapId)
{
if (mapFile == null)
{
parsedMapId = -1;
return false;
}
string mapFileName = mapFile.name;
Regex rx = new Regex(@"^\d+");
Match match = rx.Match(mapFileName);
if (match.Success)
{
parsedMapId = Int32.Parse(match.Value);
return true;
}
else
{
parsedMapId = -1;
return false;
}
}
public bool PreBuildCheck(out string message)
{
message = "";
if (!gameObject.activeInHierarchy || !enabled)
return false;
string logName = mapName;
if (!IsConfigured)
{
ImmersalLogger.LogWarning($"XRMap on object {name} is unconfigured.");
logName = name;
}
if (LocalizationMethod == null)
{
message = $"XRMap {logName} has null LocalizationMethod.";
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c31058a003661418e8c53c458b1c0790
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,311 @@
/*===============================================================================
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 UnityEngine;
using UnityEngine.Rendering;
namespace Immersal.XR
{
[ExecuteAlways]
public class XRMapVisualization : MonoBehaviour
{
private const int MAX_VERTICES = 65535;
[SerializeField, HideInInspector] public bool IsVisualized = false;
public static readonly Color[] pointCloudColors = new Color[]
{
new Color(0.22f, 1f, 0.46f),
new Color(0.96f, 0.14f, 0.14f),
new Color(0.16f, 0.69f, 0.95f),
new Color(0.93f, 0.84f, 0.12f),
new Color(0.57f, 0.93f, 0.12f),
new Color(1f, 0.38f, 0.78f),
new Color(0.4f, 0f, 0.9f),
new Color(0.89f, 0.4f, 0f)
};
public enum RenderMode
{
DoNotRender,
EditorOnly,
EditorAndRuntime
}
public static bool pointCloudVisible = true;
[SerializeField, HideInInspector] public RenderMode renderMode = RenderMode.EditorOnly;
[SerializeField, HideInInspector] public Color m_PointColor = new Color(0.57f, 0.93f, 0.12f);
public Color pointColor
{
get { return m_PointColor; }
set { m_PointColor = value; }
}
public static float pointSize = 0.33f;
// public static bool isRenderable = true;
public static bool renderAs3dPoints = true;
[SerializeField, HideInInspector]
public XRMap Map;
[SerializeField, HideInInspector]
private Shader m_Shader;
[SerializeField, HideInInspector]
private Material m_Material;
[SerializeField, HideInInspector]
private Mesh m_Mesh;
[SerializeField, HideInInspector]
private MeshFilter m_MeshFilter;
[SerializeField, HideInInspector]
private MeshRenderer m_MeshRenderer;
public Mesh Mesh => m_Mesh;
public void Initialize(XRMap map, RenderMode renderMode, Color pointColor)
{
Map = map;
this.renderMode = renderMode;
this.pointColor = pointColor;
}
public void LoadPly(string filePath)
{
Mesh plyMesh = PlyImporter.PlyToMesh(filePath);
SetMesh(plyMesh);
}
public void LoadPly(byte[] bytes, string plyName)
{
Mesh plyMesh = PlyImporter.PlyToMesh(bytes, plyName);
SetMesh(plyMesh);
}
public void LoadFromPlugin()
{
if (Map == null)
{
ImmersalLogger.LogError("Cant load point cloud from plugin, map is null");
return;
}
int numPoints = Immersal.Core.GetPointCloudSize(Map.mapId);
Vector3[] points = new Vector3[numPoints];
Immersal.Core.GetPointCloud(Map.mapId, points);
for (int i = 0; i < numPoints; i++)
points[i] = points[i].SwitchHandedness();
Mesh mesh = PointsToMesh(points, numPoints, Matrix4x4.identity);
SetMesh(mesh);
}
private Mesh PointsToMesh(Vector3[] points, int totalPoints, Matrix4x4 offset)
{
Mesh m = new Mesh();
int numPoints = totalPoints >= MAX_VERTICES ? MAX_VERTICES : totalPoints;
int[] indices = new int[numPoints];
Vector3[] pts = new Vector3[numPoints];
Color32[] col = new Color32[numPoints];
for (int i = 0; i < numPoints; ++i)
{
indices[i] = i;
pts[i] = offset.MultiplyPoint3x4(points[i]);
}
m.Clear();
m.vertices = pts;
m.colors32 = col;
m.SetIndices(indices, MeshTopology.Points, 0);
m.bounds = new Bounds(transform.position, new Vector3(float.MaxValue, float.MaxValue, float.MaxValue));
return m;
}
public void SetMesh(Mesh mesh)
{
if (this == null) return;
ClearVisualization();
if (m_Shader == null)
{
m_Shader = Shader.Find("Immersal/Point Cloud");
}
if (m_Material == null)
{
m_Material = new Material(m_Shader);
//m_Material.hideFlags = HideFlags.DontSave;
}
m_Mesh = mesh;
if (m_MeshFilter == null)
{
m_MeshFilter = gameObject.GetComponent<MeshFilter>();
if (m_MeshFilter == null)
{
m_MeshFilter = gameObject.AddComponent<MeshFilter>();
}
}
if (m_MeshRenderer == null)
{
m_MeshRenderer = gameObject.GetComponent<MeshRenderer>();
if (m_MeshRenderer == null)
{
m_MeshRenderer = gameObject.AddComponent<MeshRenderer>();
}
}
m_MeshFilter.mesh = m_Mesh;
m_MeshRenderer.material = m_Material;
m_MeshRenderer.shadowCastingMode = ShadowCastingMode.Off;
m_MeshRenderer.lightProbeUsage = LightProbeUsage.Off;
m_MeshRenderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
IsVisualized = true;
UpdateMaterial();
}
public void ClearVisualization()
{
if (m_MeshFilter != null)
{
DestroyImmediate(m_MeshFilter);
}
if (m_MeshRenderer != null)
{
DestroyImmediate(m_MeshRenderer);
}
if (m_Material != null)
{
DestroyImmediate(m_Material);
}
m_Material = null;
m_Mesh = null;
IsVisualized = false;
}
private void ClearMesh()
{
if (m_Mesh != null)
{
m_Mesh.Clear();
}
}
private bool IsRenderable()
{
if (pointCloudVisible)
{
switch (renderMode)
{
case RenderMode.DoNotRender:
return false;
case RenderMode.EditorOnly:
if (Application.isEditor)
{
return true;
}
else
{
return false;
}
case RenderMode.EditorAndRuntime:
return true;
default:
return false;
}
}
return false;
}
public void Start()
{
UpdateMaterial();
}
public void OnRenderObject()
{
UpdateMaterial();
}
public void UpdateMaterial()
{
if (m_MeshRenderer == null)
{
m_MeshRenderer = GetComponent<MeshRenderer>();
if (m_MeshRenderer == null) return;
}
if (m_Material == null)
{
m_Material = m_MeshRenderer.sharedMaterial;
if (m_Material == null) return;
}
if (!IsRenderable())
{
m_MeshRenderer.enabled = false;
return;
}
m_MeshRenderer.enabled = true;
if (renderAs3dPoints)
{
m_Material.SetFloat("_PerspectiveEnabled", 1f);
m_Material.SetFloat("_PointSize", Mathf.Lerp(0.002f, 0.14f, Mathf.Max(0, Mathf.Pow(pointSize, 3f))));
}
else
{
m_Material.SetFloat("_PerspectiveEnabled", 0f);
m_Material.SetFloat("_PointSize", Mathf.Lerp(1.5f, 40f, Mathf.Max(0, pointSize)));
}
m_Material.SetColor("_PointColor", m_PointColor);
}
private void OnDestroy()
{
if (m_Material != null)
{
if (Application.isPlaying)
{
Destroy(m_Mesh);
Destroy(m_Material);
}
else
{
DestroyImmediate(m_Mesh);
DestroyImmediate(m_Material);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0db6108358afe4533aa9736d47b6b095
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,133 @@
/*===============================================================================
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.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Serialization;
using Object = UnityEngine.Object;
namespace Immersal.XR
{
public class XRSpace : MonoBehaviour, ISceneUpdateable
{
[SerializeField]
private bool m_ProcessPoses = false;
[SerializeField] [Interface(typeof(IDataProcessor<SceneUpdateData>))]
private Object[] m_DataProcessors;
public IDataProcessor<SceneUpdateData>[] SceneDataProcessors =>
m_DataProcessors.OfType<IDataProcessor<SceneUpdateData>>().ToArray();
public bool ProcessPoses
{
get => m_ProcessPoses;
set => m_ProcessPoses = value;
}
public Matrix4x4 InitialPose => m_InitialPose;
private Transform m_TransformToUpdate;
private IDataProcessingChain<SceneUpdateData> m_DataProcessingChain;
private Matrix4x4 m_InitialPose = Matrix4x4.identity;
private SceneUpdateData m_CurrentData;
private void Awake()
{
m_TransformToUpdate = transform;
m_InitialPose = Matrix4x4.TRS(m_TransformToUpdate.position, m_TransformToUpdate.rotation, Vector3.one);
m_CurrentData = null;
if (m_DataProcessors != null)
{
m_DataProcessingChain = new DataProcessingChain<SceneUpdateData>(SceneDataProcessors);
}
else
{
m_DataProcessingChain =
new DataProcessingChain<SceneUpdateData>();
}
}
private void Start()
{
}
private async void Update()
{
if (m_ProcessPoses && m_DataProcessingChain != null)
{
await m_DataProcessingChain.UpdateChain();
SceneUpdateData result = m_DataProcessingChain.GetCurrentData();
m_CurrentData = result;
UpdateSpace(m_CurrentData);
}
}
public async Task SceneUpdate(SceneUpdateData data)
{
if (!m_TransformToUpdate)
return;
if (m_ProcessPoses && m_DataProcessingChain != null)
{
await m_DataProcessingChain.ProcessNewData(data);
}
else
{
m_CurrentData = data;
UpdateSpace(m_CurrentData);
}
}
private void UpdateSpace(SceneUpdateData data)
{
if (data == null || data.Ignore || !data.Pose.ValidTRS()) return;
Matrix4x4 pose = data.Pose;
m_TransformToUpdate.SetPositionAndRotation(pose.GetPosition(), pose.rotation);
}
public Transform GetTransform()
{
return transform;
}
public void TriggerResetScene()
{
ResetScene();
}
public async Task ResetScene()
{
if (m_ProcessPoses && m_DataProcessingChain != null)
{
await m_DataProcessingChain.ResetProcessors();
}
}
public void AddSceneDataProcessor(IDataProcessor<SceneUpdateData>
processor)
{
m_DataProcessingChain.AddProcessor(processor);
}
public void RemoveSceneDataProcessor(IDataProcessor<SceneUpdateData>
processor)
{
m_DataProcessingChain.RemoveProcessor(processor);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7e870a96aef1a4fb0b543d281fcb163e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: