99.imdk_unity 上传
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb6de87a81def4d49b6cafe60de749b7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e10dd620b341549ce9b987a5533d4db0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2eb7759c7f3504d458f9aa90cf16254b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00a8834cd55604382a55a5f566f65651
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70058f23eb73e4e1fa8168423a939176
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ace14c723c1842578283a60dffe9bb2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9edd45749c5964a5bbbdec0791c43d79
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
374
Assets/99.imdk_unity/Runtime/Scripts/XR/ImmersalSession.cs
Normal file
374
Assets/99.imdk_unity/Runtime/Scripts/XR/ImmersalSession.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1998a90ed503d46ffa5a88d3c7505c28
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/99.imdk_unity/Runtime/Scripts/XR/Interfaces.meta
Normal file
8
Assets/99.imdk_unity/Runtime/Scripts/XR/Interfaces.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83bf888582b264eaba3baa3b7101e02e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03a98a5ff49164b08addce88bff9bfed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52c0b4f597e8a4721a94e07ba8c7719a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ffba8fb68d0ca444ca8e3a96cb3d32bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 138bfe17044ec4f6e95318d640e0c4c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 17af27cd6aa4e429186d011bbb43b70f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a4ee57b73bcb4fe5bf2d6c043df6930
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb89d50895fb74e67a68ee7887a68478
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1470387a4d318459a91c6a30cb5ef9b5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39b89922d087d4f66824b1389aa59801
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fea0f6762065741059e6568ded85996f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 065c941179f654789b30a560a80ac8ba
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d8be42d7c34a64923aa637c8abcaeb7d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abe9919d22e3a41a8ad36c34e59a0f7e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0113bf92df9fa4e24bd92cedc0f00ec6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82ac8f41066bd43cca7a925e70116602
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
868
Assets/99.imdk_unity/Runtime/Scripts/XR/MapManager.cs
Normal file
868
Assets/99.imdk_unity/Runtime/Scripts/XR/MapManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
11
Assets/99.imdk_unity/Runtime/Scripts/XR/MapManager.cs.meta
Normal file
11
Assets/99.imdk_unity/Runtime/Scripts/XR/MapManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c7cd7d260b4d42f1924089f286b6736
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/99.imdk_unity/Runtime/Scripts/XR/Platforms.meta
Normal file
8
Assets/99.imdk_unity/Runtime/Scripts/XR/Platforms.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 560e94b4b18d54326a7612a1281a20cf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff1599bedf7cb4f2bb03770be4930ce8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
75
Assets/99.imdk_unity/Runtime/Scripts/XR/SceneUpdater.cs
Normal file
75
Assets/99.imdk_unity/Runtime/Scripts/XR/SceneUpdater.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/99.imdk_unity/Runtime/Scripts/XR/SceneUpdater.cs.meta
Normal file
11
Assets/99.imdk_unity/Runtime/Scripts/XR/SceneUpdater.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10b6f3f5d6c78474d867465b74b8a539
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
157
Assets/99.imdk_unity/Runtime/Scripts/XR/TrackingAnalyzer.cs
Normal file
157
Assets/99.imdk_unity/Runtime/Scripts/XR/TrackingAnalyzer.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3670ede57ef84f68845b3bb411d4a87
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
435
Assets/99.imdk_unity/Runtime/Scripts/XR/XRMap.cs
Normal file
435
Assets/99.imdk_unity/Runtime/Scripts/XR/XRMap.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/99.imdk_unity/Runtime/Scripts/XR/XRMap.cs.meta
Normal file
11
Assets/99.imdk_unity/Runtime/Scripts/XR/XRMap.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c31058a003661418e8c53c458b1c0790
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
311
Assets/99.imdk_unity/Runtime/Scripts/XR/XRMapVisualization.cs
Normal file
311
Assets/99.imdk_unity/Runtime/Scripts/XR/XRMapVisualization.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0db6108358afe4533aa9736d47b6b095
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
133
Assets/99.imdk_unity/Runtime/Scripts/XR/XRSpace.cs
Normal file
133
Assets/99.imdk_unity/Runtime/Scripts/XR/XRSpace.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/99.imdk_unity/Runtime/Scripts/XR/XRSpace.cs.meta
Normal file
11
Assets/99.imdk_unity/Runtime/Scripts/XR/XRSpace.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e870a96aef1a4fb0b543d281fcb163e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user