You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
494 lines
16 KiB
494 lines
16 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine.XR.ARSubsystems;
|
|
|
|
#if UNITY_2019_3_OR_NEWER
|
|
using LegacyMeshId = UnityEngine.XR.MeshId;
|
|
#else
|
|
using LegacyMeshId = UnityEngine.Experimental.XR.TrackableId;
|
|
using MeshInfo = UnityEngine.Experimental.XR.MeshInfo;
|
|
using XRMeshSubsystem = UnityEngine.Experimental.XR.XRMeshSubsystem;
|
|
using XRMeshSubsystemDescriptor = UnityEngine.Experimental.XR.XRMeshSubsystemDescriptor;
|
|
using MeshChangeState = UnityEngine.Experimental.XR.MeshChangeState;
|
|
using MeshVertexAttributes = UnityEngine.Experimental.XR.MeshVertexAttributes;
|
|
using MeshGenerationStatus = UnityEngine.Experimental.XR.MeshGenerationStatus;
|
|
using MeshGenerationResult = UnityEngine.Experimental.XR.MeshGenerationResult;
|
|
#endif
|
|
|
|
#if !UNITY_2019_2_OR_NEWER
|
|
using SubsystemManager = UnityEngine.Experimental.SubsystemManager;
|
|
#endif
|
|
|
|
namespace UnityEngine.XR.ARFoundation
|
|
{
|
|
/// <summary>
|
|
/// A manager for triangle meshes generated by an AR device.
|
|
/// Creates, updates, and removes <c>GameObject</c>s in response to
|
|
/// the environment. For each mesh, a <see cref="meshPrefab"/> is
|
|
/// instantiated which must contain at least a <c>MeshFilter</c>.
|
|
/// If the <see cref="meshPrefab"/>'s <c>GameObject</c> also has a
|
|
/// <c>MeshCollider</c>, then a physics mesh is generated asynchronously,
|
|
/// without blocking the main thread.
|
|
/// </summary>
|
|
[DefaultExecutionOrder(ARUpdateOrder.k_MeshManager)]
|
|
[HelpURL("https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@3.0/api/UnityEngine.XR.ARFoundation.ARMeshManager.html")]
|
|
[DisallowMultipleComponent]
|
|
public class ARMeshManager : MonoBehaviour
|
|
{
|
|
[SerializeField]
|
|
[Tooltip("The prefab to be instantiated for each generated mesh. MeshColliders are processed asynchronously and do not block the main thread.")]
|
|
MeshFilter m_MeshPrefab;
|
|
|
|
/// <summary>
|
|
/// A prefab to be instantiated for each generated mesh. The prefab must have at least a
|
|
/// <c>MeshFilter</c> component on it. If it also has a <c>MeshCollider</c> component, the
|
|
/// physics bounding volume data will be generated asynchronously. This does not block the
|
|
/// main thread, but may take longer to process.
|
|
/// </summary>
|
|
public MeshFilter meshPrefab
|
|
{
|
|
get { return m_MeshPrefab; }
|
|
set { m_MeshPrefab = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("The density of the generated mesh [0..1]. 1 will be highly tesselated while 0 will be very low.")]
|
|
[Range(0, 1)]
|
|
float m_Density = 0.5f;
|
|
|
|
/// <summary>
|
|
/// The density of the generated mesh [0..1]. 1 will be densely tesselated,
|
|
/// while 0 will have the lowest supported tesselation.
|
|
/// </summary>
|
|
public float density
|
|
{
|
|
get { return m_Density; }
|
|
set
|
|
{
|
|
if (value < 0f || value > 1f)
|
|
throw new ArgumentOutOfRangeException(nameof(value), value, "Mesh density must be between 0 and 1, inclusive.");
|
|
|
|
if (m_Density == value)
|
|
return;
|
|
|
|
m_Density = value;
|
|
if (m_Subsystem != null)
|
|
m_Subsystem.meshDensity = m_Density;
|
|
}
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("If enabled, a normal is requested for each vertex.")]
|
|
bool m_Normals = true;
|
|
|
|
/// <summary>
|
|
/// If <c>True</c>, requests a normal for each vertex in generated meshes.
|
|
/// </summary>
|
|
public bool normals
|
|
{
|
|
get { return m_Normals; }
|
|
set { m_Normals = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("If enabled, a tangent is requested for each vertex.")]
|
|
bool m_Tangents;
|
|
|
|
/// <summary>
|
|
/// If <c>True</c>, requests a tangent for each vertex in generated meshes.
|
|
/// </summary>
|
|
public bool tangents
|
|
{
|
|
get { return m_Tangents; }
|
|
set { m_Tangents = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("If enabled, a UV texture coordinate is requested for each vertex.")]
|
|
bool m_TextureCoordinates;
|
|
|
|
/// <summary>
|
|
/// If <c>True</c>, requests a texture coordinate for each vertex in generated meshes.
|
|
/// </summary>
|
|
public bool textureCoordinates
|
|
{
|
|
get { return m_TextureCoordinates; }
|
|
set { m_TextureCoordinates = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("If enabled, a color value is requested for each vertex.")]
|
|
bool m_Colors;
|
|
|
|
/// <summary>
|
|
/// If <c>True</c>, requests a color value for each vertex in generated meshes.
|
|
/// </summary>
|
|
public bool colors
|
|
{
|
|
get { return m_Colors; }
|
|
set { m_Colors = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("The number of meshes to process concurrently. Higher values require more CPU time.")]
|
|
int m_ConcurrentQueueSize = 4;
|
|
|
|
/// <summary>
|
|
/// The number of meshes to process concurrently. Meshes are processed on a background
|
|
/// thread. Higher numbers will require additional CPU time.
|
|
/// </summary>
|
|
public int concurrentQueueSize
|
|
{
|
|
get { return m_ConcurrentQueueSize; }
|
|
set { m_ConcurrentQueueSize = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked whenever meshes have changed (been added, updated, or removed).
|
|
/// </summary>
|
|
public event Action<ARMeshesChangedEventArgs> meshesChanged;
|
|
|
|
/// <summary>
|
|
/// The <c>XRMeshSubsystem</c> used by this component to generate meshes.
|
|
/// </summary>
|
|
public XRMeshSubsystem subsystem
|
|
{
|
|
get { return m_Subsystem; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a collection of <c>MeshFilter</c>s representing meshes generated by this component.
|
|
/// </summary>
|
|
public IList<MeshFilter> meshes
|
|
{
|
|
get { return m_Meshes.Values; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Destroys all generated meshes and ignores any pending meshes.
|
|
/// </summary>
|
|
public void DestroyAllMeshes()
|
|
{
|
|
m_Pending.Clear();
|
|
m_Generating.Clear();
|
|
foreach (var meshFilter in meshes)
|
|
{
|
|
if (meshFilter != null)
|
|
Destroy(meshFilter.gameObject);
|
|
}
|
|
m_Meshes.Clear();
|
|
}
|
|
|
|
ARSessionOrigin GetSessionOrigin()
|
|
{
|
|
if (transform.parent == null)
|
|
return null;
|
|
|
|
return transform.parent.GetComponent<ARSessionOrigin>();
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
void Reset()
|
|
{
|
|
if (GetSessionOrigin() != null)
|
|
transform.localScale = Vector3.one * 10f;
|
|
}
|
|
|
|
void OnValidate()
|
|
{
|
|
if (GetSessionOrigin() == null)
|
|
{
|
|
UnityEditor.EditorUtility.DisplayDialog(
|
|
"Hierarchy not allowed",
|
|
$"An {nameof(ARMeshManager)} must be a child of an {nameof(ARSessionOrigin)}.",
|
|
"Remove Component");
|
|
UnityEditor.EditorApplication.delayCall += ()=>
|
|
{
|
|
DestroyImmediate(this);
|
|
};
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void SetBoundingVolume()
|
|
{
|
|
m_Subsystem.SetBoundingVolume(transform.localPosition, transform.localScale);
|
|
transform.hasChanged = false;
|
|
}
|
|
|
|
void OnEnable()
|
|
{
|
|
if (GetSessionOrigin() == null)
|
|
{
|
|
enabled = false;
|
|
throw new InvalidOperationException($"An {nameof(ARMeshManager)} must be a child of an {nameof(ARSessionOrigin)}.");
|
|
}
|
|
|
|
if (m_Subsystem == null)
|
|
m_Subsystem = CreateSubsystem();
|
|
|
|
if (m_Subsystem != null)
|
|
{
|
|
m_Subsystem.meshDensity = m_Density;
|
|
SetBoundingVolume();
|
|
m_Subsystem.Start();
|
|
}
|
|
else
|
|
{
|
|
enabled = false;
|
|
}
|
|
}
|
|
|
|
void OnDrawGizmosSelected()
|
|
{
|
|
Gizmos.color = new Color(0, .5f, 0, .35f);
|
|
Gizmos.matrix = transform.localToWorldMatrix;
|
|
Gizmos.DrawCube(Vector3.zero, Vector3.one);
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (m_Subsystem != null)
|
|
{
|
|
if (transform.hasChanged)
|
|
SetBoundingVolume();
|
|
|
|
UpdateMeshInfos();
|
|
|
|
if (m_MeshPrefab != null)
|
|
Generate();
|
|
}
|
|
|
|
// Invoke user callbacks
|
|
try
|
|
{
|
|
if (meshesChanged != null && (m_Added.Count + m_Updated.Count + m_Removed.Count > 0))
|
|
{
|
|
meshesChanged(new ARMeshesChangedEventArgs(m_Added, m_Updated, m_Removed));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// Make sure we clear the internal lists if user code throws an exception
|
|
m_Added.Clear();
|
|
m_Updated.Clear();
|
|
|
|
foreach (var meshFilter in m_Removed)
|
|
{
|
|
if (meshFilter != null)
|
|
Destroy(meshFilter.gameObject);
|
|
}
|
|
|
|
m_Removed.Clear();
|
|
}
|
|
}
|
|
|
|
void Generate()
|
|
{
|
|
var vertexAttributes = MeshVertexAttributes.None;
|
|
if (m_Normals)
|
|
vertexAttributes |= MeshVertexAttributes.Normals;
|
|
if (m_Tangents)
|
|
vertexAttributes |= MeshVertexAttributes.Tangents;
|
|
if (m_TextureCoordinates)
|
|
vertexAttributes |= MeshVertexAttributes.UVs;
|
|
if (m_Colors)
|
|
vertexAttributes |= MeshVertexAttributes.Colors;
|
|
|
|
while ((m_Generating.Count < m_ConcurrentQueueSize) &&
|
|
m_Pending.TryDequeue(m_Generating, out MeshInfo meshInfo))
|
|
{
|
|
var meshId = meshInfo.MeshId;
|
|
var meshFilter = GetOrCreateMeshFilter(GetTrackableId(meshId));
|
|
var meshCollider = meshFilter.GetComponent<MeshCollider>();
|
|
var mesh = (meshFilter.sharedMesh != null) ? meshFilter.sharedMesh : meshFilter.mesh;
|
|
|
|
m_Generating.Add(meshId, meshInfo);
|
|
m_Subsystem.GenerateMeshAsync(
|
|
meshInfo.MeshId,
|
|
mesh,
|
|
meshCollider,
|
|
vertexAttributes,
|
|
m_OnMeshGeneratedDelegate);
|
|
}
|
|
}
|
|
|
|
void OnMeshGenerated(MeshGenerationResult result)
|
|
{
|
|
if (!m_Generating.TryGetValue(result.MeshId, out MeshInfo meshInfo))
|
|
return;
|
|
|
|
m_Generating.Remove(result.MeshId);
|
|
|
|
if (result.Status != MeshGenerationStatus.Success)
|
|
return;
|
|
|
|
if (!m_Meshes.TryGetValue(GetTrackableId(result.MeshId), out MeshFilter meshFilter) || (meshFilter == null))
|
|
return;
|
|
|
|
meshFilter.gameObject.SetActive(true);
|
|
|
|
switch (meshInfo.ChangeState)
|
|
{
|
|
case MeshChangeState.Added:
|
|
m_Added.Add(meshFilter);
|
|
break;
|
|
case MeshChangeState.Updated:
|
|
m_Updated.Add(meshFilter);
|
|
break;
|
|
|
|
// Removed/unchanged meshes don't get generated.
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void UpdateMeshInfos()
|
|
{
|
|
if (m_Subsystem.TryGetMeshInfos(s_MeshInfos))
|
|
{
|
|
foreach (var meshInfo in s_MeshInfos)
|
|
{
|
|
switch (meshInfo.ChangeState)
|
|
{
|
|
case MeshChangeState.Added:
|
|
case MeshChangeState.Updated:
|
|
m_Pending.EnqueueUnique(meshInfo);
|
|
break;
|
|
|
|
case MeshChangeState.Removed:
|
|
// Remove from processing queues
|
|
m_Pending.Remove(meshInfo.MeshId);
|
|
m_Generating.Remove(meshInfo.MeshId);
|
|
|
|
// Add to list of removed meshes
|
|
var trackableId = GetTrackableId(meshInfo.MeshId);
|
|
if (m_Meshes.TryGetValue(trackableId, out MeshFilter meshFilter))
|
|
{
|
|
m_Meshes.Remove(trackableId);
|
|
if (meshFilter != null)
|
|
m_Removed.Add(meshFilter);
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnDisable()
|
|
{
|
|
if (m_Subsystem != null)
|
|
m_Subsystem.Stop();
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
if (m_Subsystem != null)
|
|
m_Subsystem.Destroy();
|
|
|
|
m_Subsystem = null;
|
|
}
|
|
|
|
MeshFilter GetOrCreateMeshFilter(TrackableId trackableId)
|
|
{
|
|
// If the mesh filter is Destroyed by user code, then meshFilter will compare
|
|
// equal with null. In that case, we want to recreate it.
|
|
if (m_Meshes.TryGetValue(trackableId, out MeshFilter meshFilter) && (meshFilter != null))
|
|
return meshFilter;
|
|
|
|
var sessionOrigin = GetSessionOrigin();
|
|
meshFilter = (sessionOrigin == null) ?
|
|
Instantiate(m_MeshPrefab) :
|
|
Instantiate(m_MeshPrefab, sessionOrigin.trackablesParent);
|
|
|
|
meshFilter.gameObject.name = $"Mesh {trackableId.ToString()}";
|
|
|
|
// The GameObject should start life inactive until we've populated it
|
|
meshFilter.gameObject.SetActive(false);
|
|
|
|
m_Meshes[trackableId] = meshFilter;
|
|
|
|
return meshFilter;
|
|
}
|
|
|
|
internal static unsafe TrackableId GetTrackableId(LegacyMeshId trackableId)
|
|
{
|
|
return *(TrackableId*)&trackableId;
|
|
}
|
|
|
|
internal static unsafe LegacyMeshId GetLegacyMeshId(TrackableId trackableId)
|
|
{
|
|
return *(LegacyMeshId*)&trackableId;
|
|
}
|
|
|
|
void Awake()
|
|
{
|
|
m_Added = new List<MeshFilter>();
|
|
m_Updated = new List<MeshFilter>();
|
|
m_Removed = new List<MeshFilter>();
|
|
m_Pending = new MeshQueue();
|
|
m_Generating = new Dictionary<LegacyMeshId, MeshInfo>();
|
|
m_Meshes = new SortedList<TrackableId, MeshFilter>(s_TrackableIdComparer);
|
|
m_OnMeshGeneratedDelegate = new Action<MeshGenerationResult>(OnMeshGenerated);
|
|
}
|
|
|
|
XRMeshSubsystem CreateSubsystem()
|
|
{
|
|
SubsystemManager.GetSubsystemDescriptors(s_SubsystemDescriptors);
|
|
if (s_SubsystemDescriptors.Count > 0)
|
|
{
|
|
var descriptor = s_SubsystemDescriptors[0];
|
|
if (s_SubsystemDescriptors.Count > 1)
|
|
Debug.LogWarning($"Multiple {nameof(XRMeshSubsystem)} found. Using {descriptor.id}");
|
|
|
|
return descriptor.Create();
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class TrackableIdComparer : IComparer<TrackableId>
|
|
{
|
|
public int Compare(TrackableId trackableIdA, TrackableId trackableIdB)
|
|
{
|
|
if (trackableIdA.subId1 == trackableIdB.subId1)
|
|
{
|
|
return trackableIdA.subId2.CompareTo(trackableIdB.subId2);
|
|
}
|
|
else
|
|
{
|
|
return trackableIdA.subId1.CompareTo(trackableIdB.subId1);
|
|
}
|
|
}
|
|
}
|
|
|
|
List<MeshFilter> m_Added;
|
|
|
|
List<MeshFilter> m_Updated;
|
|
|
|
List<MeshFilter> m_Removed;
|
|
|
|
MeshQueue m_Pending;
|
|
|
|
Dictionary<LegacyMeshId, MeshInfo> m_Generating;
|
|
|
|
SortedList<TrackableId, MeshFilter> m_Meshes;
|
|
|
|
Action<MeshGenerationResult> m_OnMeshGeneratedDelegate;
|
|
|
|
XRMeshSubsystem m_Subsystem;
|
|
|
|
static TrackableIdComparer s_TrackableIdComparer = new TrackableIdComparer();
|
|
|
|
static List<MeshInfo> s_MeshInfos = new List<MeshInfo>();
|
|
|
|
static List<XRMeshSubsystemDescriptor> s_SubsystemDescriptors =
|
|
new List<XRMeshSubsystemDescriptor>();
|
|
}
|
|
}
|