using System; using System.Collections.Generic; using Unity.Collections; using UnityEngine.XR.ARSubsystems; using UnityEngine.XR.Management; using Unity.XR.CoreUtils; using LegacyMeshId = UnityEngine.XR.MeshId; namespace UnityEngine.XR.ARFoundation { /// /// A manager for triangle meshes generated by an AR device. /// Creates, updates, and removes GameObjects in /// response to the environment. For each mesh, a is instantiated which must contain at /// least a [MeshFilter](https://docs.unity3d.com/ScriptReference/MeshFilter.html). If the /// 's GameObject also has a /// [MeshCollider](https://docs.unity3d.com/ScriptReference/MeshCollider.html), then a physics mesh is generated /// asynchronously, without blocking the main thread. /// /// /// Related information: AR Mesh Manager component /// [DefaultExecutionOrder(ARUpdateOrder.k_MeshManager)] [DisallowMultipleComponent] [AddComponentMenu("XR/AR Foundation/AR Mesh Manager")] [HelpURL("features/meshing")] public class ARMeshManager : MonoBehaviour { List m_Added; List m_Updated; List m_Removed; MeshQueue m_Pending; Dictionary m_Generating; SortedList m_Meshes; Dictionary m_Transforms = new(); Action m_OnMeshGeneratedDelegate; XRMeshSubsystem m_Subsystem; static TrackableIdComparer s_TrackableIdComparer = new(); static List s_MeshInfos = new(); [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; /// /// A Prefab to be instantiated for each generated mesh. The Prefab must have at least a /// [MeshFilter](https://docs.unity3d.com/ScriptReference/MeshFilter.html) component on it. /// If it also has a [`MeshCollider`](https://docs.unity3d.com/ScriptReference/MeshCollider.html) /// component, the physics bounding volume data will be generated asynchronously. This does not block the /// main thread, but might take longer to process. /// public MeshFilter meshPrefab { get => m_MeshPrefab; set => m_MeshPrefab = value; } [SerializeField] [Tooltip("The density of the generated mesh [0..1]. 1 will be highly tessellated while 0 will be very low.\n\n" + "This feature may not be implemented on all platforms. See the platform-specific package documentation for your platform.")] [Range(0, 1)] float m_Density = 0.5f; /// /// The density of the generated mesh [0..1]. 1 will be densely tessellated, /// while 0 will have the lowest supported tessellation. /// public float density { get => m_Density; set { if (value is < 0f or > 1f) throw new ArgumentOutOfRangeException(nameof(value), value, "Mesh density must be between 0 and 1, inclusive."); m_Density = value; if (m_Subsystem != null) m_Subsystem.meshDensity = m_Density; } } [SerializeField] [Tooltip("If enabled, a normal is requested for each vertex.\n\n" + "This feature may not be implemented on all platforms. See the platform-specific package documentation for your platform.")] bool m_Normals = true; /// /// If `true`, requests a normal for each vertex in generated meshes. /// public bool normals { get => m_Normals; set => m_Normals = value; } [SerializeField] [Tooltip("If enabled, a tangent is requested for each vertex.\n\n" + "This feature may not be implemented on all platforms. See the platform-specific package documentation for your platform.")] bool m_Tangents; /// /// If `true`, requests a tangent for each vertex in generated meshes. /// public bool tangents { get => m_Tangents; set => m_Tangents = value; } [SerializeField] [Tooltip("If enabled, a UV texture coordinate is requested for each vertex.\n\n" + "This feature might not be implemented on all platforms. See the platform-specific package documentation for your platform.")] bool m_TextureCoordinates; /// /// If `true`, requests a texture coordinate for each vertex in generated meshes. /// public bool textureCoordinates { get => m_TextureCoordinates; set => m_TextureCoordinates = value; } [SerializeField] [Tooltip("If enabled, a color value is requested for each vertex.\n\n" + "This feature might not be implemented on all platforms. See the platform-specific package documentation for your platform.")] bool m_Colors; /// /// If `true`, requests a color value for each vertex in generated meshes. /// public bool colors { get => 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; /// /// The number of meshes to process concurrently. Meshes are processed on a background /// thread. Higher numbers will require additional CPU time. /// public int concurrentQueueSize { get => m_ConcurrentQueueSize; set => m_ConcurrentQueueSize = value; } /// /// Invoked whenever meshes have changed (been added, updated, or removed). /// public event Action meshesChanged; /// /// The [XRMeshSubsystem](https://docs.unity3d.com/ScriptReference/XR.XRMeshSubsystem.html) /// used by this component to generate meshes. /// public XRMeshSubsystem subsystem => m_Subsystem; /// /// Returns a collection of [MeshFilter](https://docs.unity3d.com/ScriptReference/MeshFilter.html)s /// that represents meshes generated by this component. /// public IList meshes => m_Meshes.Values; /// /// Destroys all generated meshes and ignores any pending meshes. /// public void DestroyAllMeshes() { m_Pending.Clear(); m_Generating.Clear(); foreach (var meshFilter in meshes) { if (meshFilter != null) Destroy(meshFilter.gameObject); } m_Meshes.Clear(); } // This is similar to GetComponentInParent but also considers inactive GameObjects, while GetComponentInParent // ignores GameObjects that are not activeInHierarchy. T GetComponentInParentIncludingInactive() where T : Component { var parent = transform.parent; while (parent) { var component = parent.GetComponent(); if (component) return component; parent = parent.parent; } return null; } internal XROrigin GetXROrigin() => GetComponentInParentIncludingInactive(); #if UNITY_EDITOR void Reset() { if (GetXROrigin() != null) transform.localScale = Vector3.one * 10f; } // Invoked by tests internal bool IsValid() => GetXROrigin() != null; void OnValidate() { if (!IsValid()) { UnityEditor.EditorUtility.DisplayDialog( "Hierarchy not allowed", $"An {nameof(ARMeshManager)} must be a child of an {nameof(XROrigin)}.", "Remove Component"); UnityEditor.EditorApplication.delayCall += () => { DestroyImmediate(this); }; } } #endif void SetBoundingVolume() { m_Subsystem.SetBoundingVolume(transform.localPosition, transform.localScale); transform.hasChanged = false; } void OnEnable() { if (GetXROrigin() == null) { enabled = false; throw new InvalidOperationException($"An {nameof(ARMeshManager)} must be a child of an {nameof(XROrigin)}."); } m_Subsystem ??= GetActiveSubsystemInstance(); if (m_Subsystem != null) { m_Subsystem.meshDensity = m_Density; SetBoundingVolume(); m_Subsystem.Start(); } else { enabled = false; } } static XRMeshSubsystem GetActiveSubsystemInstance() { XRMeshSubsystem activeSubsystem = null; // Query the currently active loader for the created subsystem, if one exists. if (XRGeneralSettings.Instance != null && XRGeneralSettings.Instance.Manager != null) { var loader = XRGeneralSettings.Instance.Manager.activeLoader; if (loader != null) { activeSubsystem = loader.GetLoadedSubsystem(); } } if (activeSubsystem == null) { Debug.LogWarning( $"No active {typeof(XRMeshSubsystem).FullName} is available. Please ensure that a valid loader configuration exists in the XR project settings and that meshing is supported."); } return activeSubsystem; } 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 && m_Subsystem.running) { if (transform.hasChanged) SetBoundingVolume(); UpdateMeshInfos(); if (m_MeshPrefab != null) Generate(); } // Invoke user callbacks try { if (m_Added.Count + m_Updated.Count + m_Removed.Count > 0) { // If normals were requested, compute the normals before invoking meshesChanged if (m_Normals) { foreach (var meshFilter in m_Added) { var mesh = (meshFilter.sharedMesh != null) ? meshFilter.sharedMesh : meshFilter.mesh; // Calculate normals if they weren't populated by the provider. if (mesh.normals.Length == 0) mesh.RecalculateNormals(); } foreach (var meshFilter in m_Updated) { var mesh = (meshFilter.sharedMesh != null) ? meshFilter.sharedMesh : meshFilter.mesh; // Calculate normals if they weren't populated by the provider. if (mesh.normals.Length == 0) mesh.RecalculateNormals(); } } meshesChanged?.Invoke(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(); var mesh = meshFilter.sharedMesh != null ? meshFilter.sharedMesh : meshFilter.mesh; m_Generating.Add(meshId, meshInfo); m_Subsystem.GenerateMeshAsync( meshId: meshInfo.MeshId, mesh: mesh, meshCollider: meshCollider, attributes: vertexAttributes, onMeshGenerationComplete: m_OnMeshGeneratedDelegate, options: MeshGenerationOptions.ConsumeTransform ); } } void OnMeshGenerated(MeshGenerationResult result) { if (!m_Generating.Remove(result.MeshId, out MeshInfo meshInfo)) return; if (result.Status != MeshGenerationStatus.Success) return; var meshTransform = GetOrUpdateMeshTransform(new MeshTransform( result.MeshId, result.Timestamp, result.Position, result.Rotation, result.Scale)); if (!m_Meshes.TryGetValue(GetTrackableId(result.MeshId), out MeshFilter meshFilter) || meshFilter == null) return; SetMeshTransform(meshFilter.transform, meshTransform); 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; } } MeshTransform GetOrUpdateMeshTransform(MeshTransform meshTransform) { if (m_Transforms.TryGetValue(meshTransform.MeshId, out var currentTransform) && currentTransform.Timestamp > meshTransform.Timestamp) return currentTransform; m_Transforms[currentTransform.MeshId] = meshTransform; return meshTransform; } static void SetMeshTransform(Transform transform, in MeshTransform meshTransform) { transform.localPosition = meshTransform.Position; transform.localRotation = meshTransform.Rotation; transform.localScale = meshTransform.Scale; } void UpdateMeshInfos() { s_MeshInfos.Clear(); 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); m_Transforms.Remove(meshInfo.MeshId); // Add to list of removed meshes var trackableId = GetTrackableId(meshInfo.MeshId); if (m_Meshes.Remove(trackableId, out MeshFilter meshFilter)) { if (meshFilter != null) { m_Removed.Add(meshFilter); } } break; } } } using var meshTransforms = m_Subsystem.GetUpdatedMeshTransforms(Allocator.Temp); foreach (var newMeshTransform in meshTransforms) { var meshTransform = GetOrUpdateMeshTransform(newMeshTransform); if (m_Meshes.TryGetValue(GetTrackableId(meshTransform.MeshId), out var filter) && filter != null) { SetMeshTransform(filter.transform, meshTransform); } } } void OnDisable() { if (m_Subsystem != null && m_Subsystem.running) m_Subsystem.Stop(); } void OnDestroy() => 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 origin = GetXROrigin(); meshFilter = origin == null ? Instantiate(m_MeshPrefab) : Instantiate(m_MeshPrefab, origin.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(); m_Updated = new List(); m_Removed = new List(); m_Pending = new MeshQueue(); m_Generating = new Dictionary(); m_Meshes = new SortedList(s_TrackableIdComparer); m_OnMeshGeneratedDelegate = OnMeshGenerated; } class TrackableIdComparer : IComparer { public int Compare(TrackableId trackableIdA, TrackableId trackableIdB) { return trackableIdA.subId1 == trackableIdB.subId1 ? trackableIdA.subId2.CompareTo(trackableIdB.subId2) : trackableIdA.subId1.CompareTo(trackableIdB.subId1); } } } }