using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; namespace Framework.Geometry.Volumes.Fitting { /// Scans meshes to obtain samples for fitting primitives to them public static class MeshScanner { /// How far apart the rays used for scanning the mesh will be private const float RaySpacing = 0.125f; #region class UnityInternal /// Exposes internals methods from the Unity editor API private static class UnityInternal { /// Initializes the static members of the class static UnityInternal() { Type handleUtilityType = typeof(HandleUtility); intersectRayMeshMethodInfo = handleUtilityType.GetMethod( "IntersectRayMesh", BindingFlags.Static | BindingFlags.NonPublic ); if(intersectRayMeshMethodInfo == null) { Debug.LogError("HandleUtility class does not have a private IntersectRayMesh() method"); } } /// Intersects a ray against a mesh based on the mesh's raw vertices /// Ray that will be tested against the mesh /// Mesh against which the ray will be tested /// /// Matrix defining the position, orientation and scale of the mesh /// /// Receives informations about the contact point, if any /// True if the ray intersects the mesh, false otherwise public static bool IntersectRayMesh( Ray ray, Mesh mesh, Matrix4x4 matrix, out RaycastHit hit ) { var arguments = new object[] { ray, mesh, matrix, null }; object result = intersectRayMeshMethodInfo.Invoke(null, arguments); hit = (RaycastHit) arguments[3]; return (bool) result; } /// /// Reflection information for the HandleUtility.IntersectRayMesh() method /// private static MethodInfo intersectRayMeshMethodInfo; } #endregion // class UnityInternal /// Fetches a list of the visible mesh filters on the game object /// Game object that will be checked for visible mesh filters /// A list of visible mesh filters /// /// Mesh filters are components that reference the geometry used to render /// a 3D object to the screen. /// public static ArraySegment GetVisibleMeshFilters(GameObject gameObject) { // Get all mesh filters on the game object itself and its children MeshFilter[] meshFilters = gameObject.GetComponentsInChildren(); if((meshFilters == null) || (meshFilters.Length == 0)) { //Debug.LogWarning("Mesh fit found no mesh filters, box collider unchanged."); return new ArraySegment(); } // Eliminate those that are disabled, invisible or only used for shadow rendering int lastIndex = meshFilters.Length - 1; for(int index = 0; index <= lastIndex;) { MeshRenderer renderer = meshFilters[index].GetComponent(); bool isRendered = (renderer != null); if(isRendered) { isRendered &= (renderer.enabled) && (renderer.isVisible) && (renderer.shadowCastingMode != ShadowCastingMode.ShadowsOnly); } if(isRendered) { ++index; } else { meshFilters[index] = meshFilters[lastIndex]; --lastIndex; } } // Build an array segment with the rendered mesh filters. There may be no mesh // filters left in which case we return a valid array segment of 0 elements. int meshFilterCount = lastIndex + 1; return new ArraySegment(meshFilters, 0, meshFilterCount); } /// Fetches a list of the visible mesh filters on multiple game objects /// Game objects that will be checked for visible mesh filters /// A list of visible mesh filters /// /// Mesh filters are components that reference the geometry used to render /// a 3D object to the screen. /// public static ArraySegment GetVisibleMeshFilters(IList gameObjects) { var meshFilters = new List(); int gameObjectCount = gameObjects.Count; for(int gameObjectIndex = 0; gameObjectIndex < gameObjectCount; ++gameObjectIndex) { ArraySegment gameObjectMeshFilters = GetVisibleMeshFilters( gameObjects[gameObjectIndex] ); { int gameObjectMeshFilterCount = gameObjectMeshFilters.Count; for(int index = 0; index < gameObjectMeshFilterCount; ++index) { meshFilters.Add(gameObjectMeshFilters.Array[gameObjectMeshFilters.Offset + index]); } } } return new ArraySegment(meshFilters.ToArray(), 0, meshFilters.Count); } /// Determines the boundaries of the specified mesh /// Mesh filter whose boundaries will be determined /// Whether the bounds are calculated in world space /// The bounds of the mesh filter /// /// The bounds are calculated in the mesh's local space. /// public static Bounds GetBounds(MeshFilter meshFilter, bool worldSpace = true) { Vector3[] vertices = meshFilter.sharedMesh.vertices; int vertexCount = vertices.Length; if(vertexCount == 0) { return new Bounds(); } Vector3 min = vertices[0]; if(worldSpace) { min = meshFilter.transform.TransformPoint(min); } Vector3 max = min; for(int vertexIndex = 1; vertexIndex < vertexCount; ++vertexIndex) { Vector3 vertex = vertices[vertexIndex]; if(worldSpace) { vertex = meshFilter.transform.TransformPoint(vertex); } if(vertex.x < min.x) { min.x = vertex.x; } if(vertex.x > max.x) { max.x = vertex.x; } if(vertex.y < min.y) { min.y = vertex.y; } if(vertex.y > max.y) { max.y = vertex.y; } if(vertex.z < min.z) { min.z = vertex.z; } if(vertex.z > max.z) { max.z = vertex.z; } } return new Bounds((min + max) / 2.0f, max - min); } /// Determines the boundaries of multiple meshes /// Meshe filters whose boundaries will be determined /// The bounds of the mesh filters public static Bounds GetBounds(ArraySegment meshFilters) { int meshCount = meshFilters.Count; if(meshCount == 0) { return new Bounds(); } Bounds bounds = GetBounds(meshFilters.Array[meshFilters.Offset]); for(int index = 1; index < meshFilters.Count; ++index) { bounds.Encapsulate(GetBounds(meshFilters.Array[meshFilters.Offset + index])); } return bounds; } /// Scans a set of meshes via ray casts /// Meshes that will be scanned /// Direction towards which rays will be cast /// Area in which scanning will take place /// All contact points between the meshes and the rays public static IList ScanMeshes( ArraySegment meshFilters, Vector3 direction, Bounds limitVolume = new Bounds() ) { int meshCount = meshFilters.Count; if(meshCount == 0) { return new List(capacity: 0); } // Remember the meshes and their transforms. Getting these informations may // be costly (ie. the mesh vertices may have to be copied into a managed array). var meshes = new Mesh[meshCount]; var meshTransforms = new Matrix4x4[meshCount]; var meshBounds = new Bounds[meshCount]; for(int index = 0; index < meshCount; ++index) { MeshFilter meshFilter = meshFilters.Array[meshFilters.Offset + index]; meshes[index] = meshFilter.sharedMesh; meshTransforms[index] = meshFilter.transform.localToWorldMatrix; meshBounds[index] = GetBounds(meshFilter); } // The scan volume is the boundary volume of all meshes. This will be used to // have a source from which to cast rays. Since we want to simulate a scan of // all meshes as a cohesive unit, we need some source far enough away that it // will hit meshes that occlude other meshes. Bounds scanVolume = meshBounds[0]; for(int index = 1; index < meshCount; ++index) { scanVolume.Encapsulate(meshBounds[index]); } scanVolume.extents += Vector3.one; // Now scan the volume of each mesh from the distance of the maximum scan volume // so rays hitting other meshes before the scanned one will be blocked, too. // This provides better quality data for distance fields, fitting algorithms, etc. var contacts = new List(); { var ray = new Ray(Vector3.zero, direction); RaycastHit earliestContact, contact; // Scan the area in front of (seen from 'direction') each mesh for(int meshIndex = 0; meshIndex < meshCount; ++meshIndex) { limitByOptionalBounds(ref meshBounds[meshIndex], limitVolume); if(boundsAreEmpty(meshBounds[meshIndex])) { continue; // Limit volume has cut this mesh completely off. } Vector3[] rayOrigins = generateRayOrigins(meshBounds[meshIndex], scanVolume, direction); int rayCount = rayOrigins.Length; /* Debug.Log( "Scanning " + meshes[meshIndex].name + " around " + meshBounds[meshIndex] + " with " + rayCount + " rays." ); */ // Check each ray for(int rayIndex = 0; rayIndex < rayCount; ++rayIndex) { ray.origin = rayOrigins[rayIndex]; // But check the ray against *all* meshes to include occluders! // This could be a four liner, but then C# static analysis won't see // that earliestContact is guaranteed to be initialized if (wasHit == true) bool wasHit = UnityInternal.IntersectRayMesh( ray, meshes[0], meshTransforms[0], out earliestContact ); for(int index = 1; index < meshCount; ++index) { bool wasHitAgain =UnityInternal.IntersectRayMesh( ray, meshes[index], meshTransforms[index], out contact ); if(wasHitAgain) { if(!wasHit || contact.distance < earliestContact.distance) { earliestContact = contact; wasHit = true; } } } // If this ray made contact with something, add it to the result list if(wasHit) { contacts.Add(earliestContact.point); } } } } return contacts; } /// Generates origin coordinates for rays into a volume /// Volume for which origin coordinates will be generated /// Volume from outside which rays should be cast /// Direction into which the rays will extend /// The origin coordinates from which to cast rays into the volume private static Vector3[] generateRayOrigins( Bounds meshVolume, Bounds scanVolume, Vector3 direction ) { Vector3[] origins; { float absX = Math.Abs(direction.x); float absY = Math.Abs(direction.y); float absZ = Math.Abs(direction.z); if((absX >= absY) && (absX >= absZ)) { float x = (direction.x >= 0) ? scanVolume.min.x : scanVolume.max.x; int rayCountY = Mathf.CeilToInt(meshVolume.size.y / RaySpacing); int rayCountZ = Mathf.CeilToInt(meshVolume.size.z / RaySpacing); origins = new Vector3[rayCountY * rayCountZ]; int rayIndex = 0; for(int y = 0; y < rayCountY; ++y) { for(int z = 0; z < rayCountZ; ++z) { origins[rayIndex] = new Vector3( x, y * RaySpacing + meshVolume.min.y, z * RaySpacing + meshVolume.min.z ); ++rayIndex; } } } else if((absY >= absX) && (absY >= absZ)) { float y = (direction.y >= 0) ? scanVolume.min.y : scanVolume.max.y; int rayCountX = Mathf.CeilToInt(meshVolume.size.x / RaySpacing); int rayCountZ = Mathf.CeilToInt(meshVolume.size.z / RaySpacing); origins = new Vector3[rayCountX * rayCountZ]; int rayIndex = 0; for(int x = 0; x < rayCountX; ++x) { for(int z = 0; z < rayCountZ; ++z) { origins[rayIndex] = new Vector3( x * RaySpacing + meshVolume.min.x, y, z * RaySpacing + meshVolume.min.z ); ++rayIndex; } } } else { float z = (direction.z >= 0) ? scanVolume.min.z : scanVolume.max.z; int rayCountX = Mathf.CeilToInt(meshVolume.size.x / RaySpacing); int rayCountY = Mathf.CeilToInt(meshVolume.size.y / RaySpacing); origins = new Vector3[rayCountX * rayCountY]; int rayIndex = 0; for(int x = 0; x < rayCountX; ++x) { for(int y = 0; y < rayCountY; ++y) { origins[rayIndex] = new Vector3( x * RaySpacing + meshVolume.min.x, y * RaySpacing + meshVolume.min.y, z ); ++rayIndex; } } } } return origins; } /// /// Limits a boundary volume to be fully contained by another, optional volume /// /// Boundary volume that will be limited /// /// Limits that will be placed on the volume. /// /// > /// The volume will be limited on any dimension of the /// volume that has a size of more than 0. So passing limits with a size of 0, 10, 0 would /// restrict the input volume to 10 units on Y, but not touch the X and Z borders. /// private static void limitByOptionalBounds(ref Bounds bounds, Bounds optionalLimits) { Vector3 boundsMin = bounds.min; Vector3 boundsMax = bounds.max; { Vector3 optionalLimitsMin = optionalLimits.min; Vector3 optionalLimitsMax = optionalLimits.max; if(optionalLimitsMax.x > optionalLimitsMin.x) { boundsMin.x = Math.Max(boundsMin.x, optionalLimitsMin.x); boundsMax.x = Math.Min(boundsMax.x, optionalLimitsMax.x); } if(optionalLimitsMax.y > optionalLimitsMin.y) { boundsMin.y = Math.Max(boundsMin.y, optionalLimitsMin.y); boundsMax.y = Math.Min(boundsMax.y, optionalLimitsMax.y); } if(optionalLimitsMax.z > optionalLimitsMin.z) { boundsMin.z = Math.Max(boundsMin.z, optionalLimitsMin.z); boundsMax.z = Math.Min(boundsMax.z, optionalLimitsMax.z); } } bounds.center = (boundsMin + boundsMax) / 2.0f; bounds.size = (boundsMax - boundsMin); } /// Checks whether a bounds objects holds nothing /// Bounds that will be checked /// True of the bounds hold nothing private static bool boundsAreEmpty(Bounds bounds) { Vector3 extents = bounds.extents; return (extents.x <= 0.0f) || (extents.y <= 0.0f) || (extents.z <= 0.0f); } } } // namespace Framework.Geometry.Volumes.Fitting