using System; using System.Collections.Generic; using Unity.Collections; using Unity.Jobs; using Unity.XR.CoreUtils; using UnityEngine.SubsystemsImplementation; using UnityEngine.XR.ARSubsystems; namespace UnityEngine.XR.Simulation { /// /// Scanner that scans the simulation environment using raycasts and provides a list of points and their normals. /// class SimulationEnvironmentScanner : IDisposable { static SimulationEnvironmentScanner s_Instance; // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use. // Reference type collections must also be cleared after use static readonly List s_EnvironmentMeshes = new(); readonly List m_TrackingSubsystems = new(); EnvironmentScanParams m_EnvironmentScanParams; PhysicsScene m_PhysicsScene; Transform m_SimulationCameraTransform; Camera m_Camera; float m_CameraScale; Pose m_CameraPose; Pose m_PreviousCameraPose; int m_PointCount; float m_LastScanTime; bool m_Initialized; bool m_Running; GameObject m_EnvironmentRoot; bool m_MeshCollidersRequested; bool m_MeshCollidersCreated; NativeArray m_Normals; NativeArray m_Points; public float lastScanTime => m_LastScanTime; SimulationEnvironmentScanner() { m_EnvironmentScanParams = XRSimulationRuntimeSettings.Instance.environmentScanParams; m_Initialized = false; } public static SimulationEnvironmentScanner GetOrCreate() { // Written this way to make it easier to add a breakpoint when new instance is created // ReSharper disable once ConvertIfStatementToNullCoalescingExpression if (s_Instance == null) s_Instance = new SimulationEnvironmentScanner(); return s_Instance; } public void Initialize(SimulationCameraPoseProvider simulationCameraPoseProvider, PhysicsScene physicsScene, GameObject environmentRoot) { if (!physicsScene.IsValid()) throw new InvalidOperationException("The physics scene loaded for simulation is not valid."); m_PhysicsScene = physicsScene; m_EnvironmentRoot = environmentRoot; m_SimulationCameraTransform = simulationCameraPoseProvider.transform; m_Camera = simulationCameraPoseProvider.GetComponent(); m_PreviousCameraPose = m_SimulationCameraTransform.GetWorldPose(); m_Normals = new NativeArray(m_EnvironmentScanParams.raysPerCast, Allocator.Persistent); m_Points = new NativeArray(m_EnvironmentScanParams.raysPerCast, Allocator.Persistent); if (m_MeshCollidersRequested) { m_MeshCollidersRequested = false; CreateMeshColliders(); } m_Initialized = true; } public void Start() { if (!m_Initialized) throw new InvalidOperationException($"Attempting to start uninitialized {GetType().Name} Scanner"); m_Running = true; } public void Stop() { m_Running = false; s_EnvironmentMeshes.Clear(); } public void Dispose() { if (m_Normals.IsCreated) m_Normals.Dispose(); if (m_Points.IsCreated) m_Points.Dispose(); m_PointCount = 0; m_Initialized = false; s_EnvironmentMeshes.Clear(); m_MeshCollidersRequested = false; m_TrackingSubsystems.Clear(); s_Instance = null; } public void Update() { if (!m_Running || m_TrackingSubsystems.Count <= 0 || !ShouldRescan()) return; m_PreviousCameraPose = m_CameraPose; using (new ScopedProfiler("ScanSimulationEnvironment")) PerformEnvironmentScan(); } public NativeArray GetPoints(Allocator allocator) { var points = new NativeArray(m_PointCount, allocator); if (m_Initialized) NativeArray.Copy(m_Points, points, m_PointCount); return points; } public NativeArray GetNormals(Allocator allocator) { var normals = new NativeArray(m_PointCount, allocator); if (m_Initialized) NativeArray.Copy(m_Normals, normals, m_PointCount); return normals; } void PerformEnvironmentScan() { var raycastParams = new RaycastParams(m_SimulationCameraTransform, m_Camera, m_EnvironmentScanParams, m_CameraScale); #if UNITY_2022_1_OR_NEWER var raycastHits = PerformRaycastBatch(raycastParams); #else // UNITY_2022_1_OR_NEWER var raycastHits = PerformRaycast(raycastParams); #endif // UNITY_2022_1_OR_NEWER try { NativeArrayUtils.EnsureCapacity(ref m_Normals, m_EnvironmentScanParams.raysPerCast, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); NativeArrayUtils.EnsureCapacity(ref m_Points, m_EnvironmentScanParams.raysPerCast, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); m_PointCount = 0; for (var i = 0; i < m_EnvironmentScanParams.raysPerCast; i++) { var hit = raycastHits[i]; if (hit.collider == null || hit.distance < raycastParams.scaledMinimumDistance) continue; m_Normals[m_PointCount] = hit.normal; m_Points[m_PointCount] = hit.point; m_PointCount++; } } finally { if (raycastHits.IsCreated) raycastHits.Dispose(); m_LastScanTime = Time.timeSinceLevelLoad; } } #if UNITY_2022_1_OR_NEWER NativeArray PerformRaycastBatch(RaycastParams raycastParams) { var raycasts = new NativeArray(m_EnvironmentScanParams.raysPerCast, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var raycastHits = new NativeArray(m_EnvironmentScanParams.raysPerCast, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); try { var raycastGeneratorJob = new RaycastGeneratorJob { seed = (uint)Random.Range(1, uint.MaxValue), physicsScene = m_PhysicsScene, cameraPosition = raycastParams.cameraPosition, cameraRotation = raycastParams.cameraRotation, halfFov = raycastParams.halfFov, horizontalFov = raycastParams.horizontalFov, scaledMaximumHitDistance = raycastParams.scaledMaximumHitDistance, raysOut = raycasts }; var raycastGeneratorHandle = raycastGeneratorJob.Schedule(m_EnvironmentScanParams.raysPerCast, 32); raycastGeneratorHandle.Complete(); var raycastHandle = RaycastCommand.ScheduleBatch(raycasts, raycastHits, 32, raycastGeneratorHandle); raycastHandle.Complete(); } finally { raycasts.Dispose(); } return raycastHits; } #else // UNITY_2022_1_OR_NEWER NativeArray PerformRaycast(RaycastParams raycastParams) { var directions = new NativeArray(m_EnvironmentScanParams.raysPerCast, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); var raycastHits = new NativeArray(m_EnvironmentScanParams.raysPerCast, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); try { var directionGeneratorJob = new DirectionGeneratorJob { seed = (uint)Random.Range(1, uint.MaxValue), cameraRotation = raycastParams.cameraRotation, halfFov = raycastParams.halfFov, horizontalFov = raycastParams.horizontalFov, directions = directions }; var raycastGeneratorHandle = directionGeneratorJob.Schedule(m_EnvironmentScanParams.raysPerCast, 32); raycastGeneratorHandle.Complete(); for (var i = 0; i < m_EnvironmentScanParams.raysPerCast; i++) { m_PhysicsScene.Raycast(raycastParams.cameraPosition, directions[i], out var hit, raycastParams.scaledMaximumHitDistance); raycastHits[i] = hit; } } finally { directions.Dispose(); } return raycastHits; } #endif // UNITY_2022_1_OR_NEWER bool ShouldRescan() { if (Time.timeSinceLevelLoad - m_LastScanTime < m_EnvironmentScanParams.minimumRescanTime) return false; var cameraTransform = m_SimulationCameraTransform; m_CameraPose = cameraTransform.GetWorldPose(); m_CameraScale = cameraTransform.lossyScale.x; return Vector3.Distance(m_PreviousCameraPose.position, m_CameraPose.position) > m_EnvironmentScanParams.deltaCameraDistanceToRescan * m_CameraScale || Quaternion.Angle(m_PreviousCameraPose.rotation, m_CameraPose.rotation) > m_EnvironmentScanParams.deltaCameraAngleToRescan; } public void RegisterSubsystem(TSubsystem subsystem) where TSubsystem : SubsystemWithProvider, new() { if (m_TrackingSubsystems.Contains(subsystem)) return; m_TrackingSubsystems.Add(subsystem); EnsureMeshColliders(); } public void UnregisterSubsystem(TSubsystem subsystem) where TSubsystem : SubsystemWithProvider, new() { m_TrackingSubsystems.Remove(subsystem); } public void EnsureMeshColliders() { if (!m_Initialized) { m_MeshCollidersRequested = true; return; } if (!m_MeshCollidersCreated) CreateMeshColliders(); } void CreateMeshColliders() { // k_EnvironmentMeshes is cleared by GetComponentsInChildren m_EnvironmentRoot.GetComponentsInChildren(s_EnvironmentMeshes); foreach (var mesh in s_EnvironmentMeshes) { if (mesh.GetComponent()) continue; if (mesh.GetComponent() != null) continue; var meshObject = mesh.gameObject; meshObject.hideFlags = HideFlags.DontSave; meshObject.AddComponent(); } s_EnvironmentMeshes.Clear(); m_MeshCollidersCreated = true; } readonly struct RaycastParams { public Vector3 cameraPosition { get; } public Quaternion cameraRotation { get; } public float halfFov { get; } public float horizontalFov { get; } public float scaledMinimumDistance { get; } public float scaledMaximumHitDistance { get; } public RaycastParams(Transform transform, Camera camera, EnvironmentScanParams environmentScanParams, float cameraScale) { cameraPosition = transform.position; cameraRotation = transform.rotation; halfFov = camera.fieldOfView * 0.5f; horizontalFov = camera.GetHorizontalFieldOfView(); scaledMinimumDistance = environmentScanParams.minimumHitDistance * cameraScale; scaledMaximumHitDistance = environmentScanParams.maximumHitDistance * cameraScale; } } #if UNITY_2022_1_OR_NEWER struct RaycastGeneratorJob : IJobParallelFor { [Unity.Collections.ReadOnly] public uint seed; [Unity.Collections.ReadOnly] public PhysicsScene physicsScene; [Unity.Collections.ReadOnly] public Vector3 cameraPosition; [Unity.Collections.ReadOnly] public Quaternion cameraRotation; [Unity.Collections.ReadOnly] public float halfFov; [Unity.Collections.ReadOnly] public float horizontalFov; [Unity.Collections.ReadOnly] public float scaledMaximumHitDistance; [WriteOnly] public NativeArray raysOut; public void Execute(int index) { var random = Unity.Mathematics.Random.CreateFromIndex((uint)index + seed); var x = random.NextFloat(-halfFov, halfFov); var y = random.NextFloat(-horizontalFov, horizontalFov); var direction = cameraRotation * Quaternion.Euler(x, y, 0) * Vector3.forward; #if UNITY_2022_2_OR_NEWER raysOut[index] = new RaycastCommand(physicsScene, cameraPosition, direction, QueryParameters.Default, scaledMaximumHitDistance); #else raysOut[index] = new RaycastCommand(physicsScene, cameraPosition, direction, scaledMaximumHitDistance); #endif } } #else // UNITY_2022_1_OR_NEWER struct DirectionGeneratorJob : IJobParallelFor { [Unity.Collections.ReadOnly] public uint seed; [Unity.Collections.ReadOnly] public Quaternion cameraRotation; [Unity.Collections.ReadOnly] public float halfFov; [Unity.Collections.ReadOnly] public float horizontalFov; [WriteOnly] public NativeArray directions; public void Execute(int index) { var random = Unity.Mathematics.Random.CreateFromIndex((uint)index + seed); var x = random.NextFloat(-halfFov, halfFov); var y = random.NextFloat(-horizontalFov, horizontalFov); directions[index] = cameraRotation * Quaternion.Euler(x, y, 0) * Vector3.forward; } } #endif // UNITY_2022_1_OR_NEWER } }