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