Files
2026-02-27 17:44:21 +08:00

771 lines
27 KiB
C#

// ╔════════════════════════════════════════════════════════════════╗
// ║ Copyright © 2025 NWH Coding d.o.o. All rights reserved. ║
// ║ Licensed under Unity Asset Store Terms of Service: ║
// ║ https://unity.com/legal/as-terms ║
// ║ Use permitted only in compliance with the License. ║
// ║ Distributed "AS IS", without warranty of any kind. ║
// ╚════════════════════════════════════════════════════════════════╝
#region
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
#endregion
namespace NWH.Common.Utility
{
/// <summary>
/// Collection of geometric utility functions for 3D math operations.
/// Includes vector math, mesh calculations, triangle operations, and spatial queries.
/// </summary>
public static class GeomUtility
{
/// <summary>
/// Checks if two Vector3 values are approximately equal within a threshold.
/// Uses squared magnitude for performance.
/// </summary>
/// <param name="a">First vector.</param>
/// <param name="b">Second vector.</param>
/// <param name="threshold">Maximum squared distance to consider equal.</param>
/// <returns>True if vectors are within threshold distance.</returns>
public static bool NearEqual(this Vector3 a, Vector3 b, float threshold = 0.01f)
{
return Vector3.SqrMagnitude(a - b) < threshold;
}
/// <summary>
/// Checks if two Quaternion values are approximately equal.
/// </summary>
/// <param name="a">First quaternion.</param>
/// <param name="b">Second quaternion.</param>
/// <returns>True if angle between quaternions is less than 0.1 degrees.</returns>
public static bool Equal(this Quaternion a, Quaternion b)
{
return Mathf.Abs(Quaternion.Angle(a, b)) < 0.1f;
}
/// <summary>
/// Clamps the magnitude of a vector between minimum and maximum values.
/// </summary>
/// <param name="v">Vector to clamp.</param>
/// <param name="min">Minimum magnitude.</param>
/// <param name="max">Maximum magnitude.</param>
/// <returns>Vector with clamped magnitude.</returns>
public static Vector3 ClampMagnitude(this Vector3 v, float min, float max)
{
float mag = v.magnitude;
if (mag == 0)
{
return Vector3.zero;
}
return Mathf.Clamp(mag, min, max) / mag * v;
}
/// <summary>
/// Calculates a perpendicular vector to the given vector.
/// </summary>
/// <param name="v">Input vector.</param>
/// <returns>Perpendicular vector.</returns>
public static Vector3 Perpendicular(this Vector3 v)
{
return new Vector3(CopySign(v.z, v.x), CopySign(v.z, v.y), -CopySign(Mathf.Abs(v.x) + Mathf.Abs(v.y), v.z));
}
/// <summary>
/// Copies the sign from one float to the magnitude of another.
/// </summary>
/// <param name="mag">Magnitude value.</param>
/// <param name="sgn">Sign donor value.</param>
/// <returns>Magnitude with the sign of sgn.</returns>
public static float CopySign(float mag, float sgn)
{
ref uint magI = ref UnsafeUtility.As<float, uint>(ref mag);
ref uint sgnI = ref UnsafeUtility.As<float, uint>(ref sgn);
uint result = (magI & ~(1u << 31)) | (sgnI & (1u << 31));
return UnsafeUtility.As<uint, float>(ref result);
}
/// <summary>
/// Returns a vector with only the largest component preserved (rounded to 1 or -1), others set to 0.
/// </summary>
/// <param name="v">Input vector.</param>
/// <returns>Vector with dominant axis isolated.</returns>
public static Vector3 RoundedMax(this Vector3 v)
{
int maxIndex = -1;
float maxValue = -Mathf.Infinity;
for (int i = 0; i < 3; i++)
{
float value = Mathf.Abs(v[i]);
if (value > maxValue)
{
maxValue = value;
maxIndex = i;
}
}
for (int i = 0; i < 3; i++)
{
v[i] = i == maxIndex ? Mathf.Sign(v[i]) * 1f : 0f;
}
return v;
}
/// <summary>
/// Finds the nearest point on an infinite line to a given point.
/// </summary>
/// <param name="linePnt">Point on the line.</param>
/// <param name="lineDir">Direction of the line.</param>
/// <param name="pnt">Point to find nearest point from.</param>
/// <returns>Nearest point on the line.</returns>
public static Vector3 NearestPointOnLine(Vector3 linePnt, Vector3 lineDir, Vector3 pnt)
{
lineDir.Normalize(); //this needs to be a unit vector
Vector3 v = pnt - linePnt;
float d = Vector3.Dot(v, lineDir);
return linePnt + lineDir * d;
}
/// <summary>
/// Calculates the distance between a point and a line segment.
/// </summary>
/// <param name="pt">Point to measure from.</param>
/// <param name="p1">First endpoint of segment.</param>
/// <param name="p2">Second endpoint of segment.</param>
/// <returns>Distance to the segment.</returns>
public static float FindDistanceToSegment(Vector3 pt, Vector3 p1, Vector3 p2)
{
float dx = p2.x - p1.x;
float dy = p2.y - p1.y;
if (dx == 0 && dy == 0)
{
// It's a point not a line segment.
dx = pt.x - p1.x;
dy = pt.y - p1.y;
return Mathf.Sqrt(dx * dx + dy * dy);
}
// Calculate the t that minimizes the distance.
float t = ((pt.x - p1.x) * dx + (pt.y - p1.y) * dy) /
(dx * dx + dy * dy);
// See if this represents one of the segment's
// end points or a point in the middle.
if (t < 0)
{
dx = pt.x - p1.x;
dy = pt.y - p1.y;
}
else if (t > 1)
{
dx = pt.x - p2.x;
dy = pt.y - p2.y;
}
else
{
Vector3 closest = new(p1.x + t * dx, p1.y + t * dy);
dx = pt.x - closest.x;
dy = pt.y - closest.y;
}
return Mathf.Sqrt(dx * dx + dy * dy);
}
/// <summary>
/// Calculates squared distance between two points. Faster than regular distance.
/// </summary>
/// <param name="a">First point.</param>
/// <param name="b">Second point.</param>
/// <returns>Squared distance.</returns>
public static float SquareDistance(Vector3 a, Vector3 b)
{
float x = a.x - b.x;
float y = a.y - b.y;
float z = a.z - b.z;
return x * x + y * y + z * z;
}
/// <summary>
/// Finds the intersection point between a line and a plane.
/// </summary>
/// <param name="planePoint">Point on the plane.</param>
/// <param name="planeNormal">Normal vector of the plane.</param>
/// <param name="linePoint">Point on the line.</param>
/// <param name="lineDirection">Direction of the line.</param>
/// <returns>Intersection point, or Vector3.zero if parallel.</returns>
public static Vector3 LinePlaneIntersection(Vector3 planePoint, Vector3 planeNormal, Vector3 linePoint,
Vector3 lineDirection)
{
if (Vector3.Dot(planeNormal, lineDirection.normalized) == 0)
{
return Vector3.zero;
}
float t = (Vector3.Dot(planeNormal, planePoint) - Vector3.Dot(planeNormal, linePoint)) /
Vector3.Dot(planeNormal, lineDirection.normalized);
return linePoint + lineDirection.normalized * t;
}
/// <summary>
/// Finds a point along the chord line of a quad at the specified percentage.
/// </summary>
/// <param name="a">First corner.</param>
/// <param name="b">Second corner.</param>
/// <param name="c">Third corner.</param>
/// <param name="d">Fourth corner.</param>
/// <param name="chordPercent">Position along chord (0-1).</param>
/// <returns>Point on chord line.</returns>
public static Vector3 FindChordLine(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float chordPercent)
{
return QuadLerp(a, b, c, d, 0.5f, chordPercent);
}
/// <summary>
/// Finds a point along the span line of a quad at the specified percentage.
/// </summary>
/// <param name="a">First corner.</param>
/// <param name="b">Second corner.</param>
/// <param name="c">Third corner.</param>
/// <param name="d">Fourth corner.</param>
/// <param name="spanPercent">Position along span (0-1).</param>
/// <returns>Point on span line.</returns>
public static Vector3 FindSpanLine(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float spanPercent)
{
return QuadLerp(a, b, c, d, spanPercent, 0.5f);
}
/// <summary>
/// Calculates the area of a quadrilateral defined by four points.
/// </summary>
/// <param name="A">First corner.</param>
/// <param name="B">Second corner.</param>
/// <param name="C">Third corner.</param>
/// <param name="D">Fourth corner.</param>
/// <returns>Area of the quad.</returns>
public static float FindArea(Vector3 A, Vector3 B, Vector3 C, Vector3 D)
{
return TriArea(A, B, D) + TriArea(B, C, D);
}
/// <summary>
/// Finds the center point of a quad or triangle defined by 3 or 4 points.
/// </summary>
/// <param name="a">First corner.</param>
/// <param name="b">Second corner.</param>
/// <param name="c">Third corner.</param>
/// <param name="d">Fourth corner (can equal first corner for triangle).</param>
/// <returns>Center point.</returns>
public static Vector3 FindCenter(Vector3 a, Vector3 b, Vector3 c, Vector3 d)
{
if (a == d)
{
return (a + b + c) / 4f;
}
return (a + b + c + d) / 4f;
}
/// <summary>
/// Calculates the distance between two points projected along a normal vector.
/// </summary>
/// <param name="a">First point.</param>
/// <param name="b">Second point.</param>
/// <param name="normal">Normal vector to project along.</param>
/// <returns>Distance along normal.</returns>
public static float DistanceAlongNormal(Vector3 a, Vector3 b, Vector3 normal)
{
Vector3 dir = b - a;
return Vector3.Project(dir, normal).magnitude;
}
/// <summary>
/// Checks if a point lies inside a triangle.
/// </summary>
/// <param name="A">First triangle vertex.</param>
/// <param name="B">Second triangle vertex.</param>
/// <param name="C">Third triangle vertex.</param>
/// <param name="P">Point to test.</param>
/// <param name="dotThreshold">Tolerance for point-on-plane test.</param>
/// <returns>True if point is inside triangle.</returns>
public static bool PointInTriangle(Vector3 A, Vector3 B, Vector3 C, Vector3 P, float dotThreshold = 0.001f)
{
if (SameSide(P, A, B, C) && SameSide(P, B, A, C) && SameSide(P, C, A, B))
{
Vector3 vc1 = Vector3.Cross(B - A, C - A).normalized;
if (Mathf.Abs(Vector3.Dot(P - A, vc1)) <= dotThreshold)
{
return true;
}
}
return false;
}
private static bool SameSide(Vector3 p1, Vector3 p2, Vector3 A, Vector3 B)
{
Vector3 cp1 = Vector3.Cross(B - A, p1 - A).normalized;
Vector3 cp2 = Vector3.Cross(B - A, p2 - A).normalized;
if (Vector3.Dot(cp1, cp2) > 0)
{
return true;
}
return false;
}
/// <summary>
/// Checks if a 2D point is inside the screen rectangle.
/// </summary>
/// <param name="point">Point to check.</param>
/// <returns>True if point is inside screen bounds.</returns>
public static bool PointIsInsideRect(Vector2 point)
{
return new Rect(0, 0, Screen.width, Screen.height).Contains(point);
}
/// <summary>
/// Checks if two float values are nearly equal within an epsilon threshold.
/// </summary>
/// <param name="a">First value.</param>
/// <param name="b">Second value.</param>
/// <param name="epsilon">Maximum difference to consider equal.</param>
/// <returns>True if values are within epsilon.</returns>
public static bool NearlyEqual(this float a, float b, double epsilon)
{
return Mathf.Abs(a - b) < epsilon;
}
/// <summary>
/// Calculates the area of a triangle from three points.
/// </summary>
/// <param name="p1">First point.</param>
/// <param name="p2">Second point.</param>
/// <param name="p3">Third point.</param>
/// <returns>Area of the triangle.</returns>
public static float AreaFromThreePoints(Vector3 p1, Vector3 p2, Vector3 p3)
{
Vector3 u, v;
u.x = p2.x - p1.x;
u.y = p2.y - p1.y;
u.z = p2.z - p1.z;
v.x = p3.x - p1.x;
v.y = p3.y - p1.y;
v.z = p3.z - p1.z;
Vector3 crossUV = Vector3.Cross(u, v);
return Mathf.Sqrt(crossUV.x * crossUV.x + crossUV.y * crossUV.y + crossUV.z * crossUV.z) * 0.5f;
}
/// <summary>
/// Calculates the area of a quadrilateral from four points.
/// </summary>
/// <param name="p1">First point.</param>
/// <param name="p2">Second point.</param>
/// <param name="p3">Third point.</param>
/// <param name="p4">Fourth point.</param>
/// <returns>Area of the quadrilateral.</returns>
public static float AreaFromFourPoints(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4)
{
return AreaFromThreePoints(p1, p2, p4) + AreaFromThreePoints(p2, p3, p4);
}
/// <summary>
/// Calculates area of a single triangle from it's three points.
/// </summary>
public static float TriArea(Vector3 p1, Vector3 p2, Vector3 p3)
{
Vector3 u, v, crossUV;
u.x = p2.x - p1.x;
u.y = p2.y - p1.y;
u.z = p2.z - p1.z;
v.x = p3.x - p1.x;
v.y = p3.y - p1.y;
v.z = p3.z - p1.z;
crossUV = Vector3.Cross(u, v);
return Mathf.Sqrt(crossUV.x * crossUV.x + crossUV.y * crossUV.y + crossUV.z * crossUV.z) * 0.5f;
}
/// <summary>
/// Calculates area of a complete mesh.
/// </summary>
public static float MeshArea(Mesh mesh)
{
if (mesh.vertices.Length == 0)
{
return 0;
}
float area = 0;
Vector3[] verts = mesh.vertices;
int[] tris = mesh.triangles;
for (int i = 0; i < tris.Length; i += 3)
{
area += TriArea(verts[tris[i]], verts[tris[i + 1]], verts[tris[i + 2]]);
}
return area;
}
/// <summary>
/// Calculates area of a mesh as viewed from the direction vector.
/// </summary>
public static float ProjectedMeshArea(Mesh mesh, Vector3 direction)
{
float area = 0;
Vector3[] verts = mesh.vertices;
int[] tris = mesh.triangles;
Vector3[] normals = mesh.normals;
int count = 0;
for (int i = 0; i < tris.Length; i += 3)
{
area += TriArea(verts[tris[i]], verts[tris[i + 1]], verts[tris[i + 2]], direction);
count++;
}
return area;
}
/// <summary>
/// Calculates the area of a rectangle from four corner points.
/// </summary>
/// <param name="p1">First corner.</param>
/// <param name="p2">Second corner.</param>
/// <param name="p3">Third corner.</param>
/// <param name="p4">Fourth corner.</param>
/// <returns>Area of the rectangle.</returns>
public static float RectArea(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4)
{
return TriArea(p1, p2, p4) + TriArea(p2, p3, p4);
}
/// <summary>
/// Find mesh center by averaging. Returns local center.
/// </summary>
public static Vector3 FindMeshCenter(Mesh mesh)
{
if (mesh.vertices.Length == 0)
{
return Vector3.zero;
}
Vector3 sum = Vector3.zero;
int count = 0;
if (mesh != null)
{
foreach (Vector3 vert in mesh.vertices)
{
sum += vert;
count++;
}
}
return sum / count;
}
public static float TriArea(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 view)
{
Vector3 u, v, crossUV, normal;
float crossMagnitude;
u.x = p2.x - p1.x;
u.y = p2.y - p1.y;
u.z = p2.z - p1.z;
v.x = p3.x - p1.x;
v.y = p3.y - p1.y;
v.z = p3.z - p1.z;
crossUV = Vector3.Cross(u, v);
crossMagnitude = Mathf.Sqrt(crossUV.x * crossUV.x + crossUV.y * crossUV.y + crossUV.z * crossUV.z);
// Normal
if (crossMagnitude == 0)
{
normal.x = normal.y = normal.z = 0f;
}
else
{
normal.x = crossUV.x / crossMagnitude;
normal.y = crossUV.y / crossMagnitude;
normal.z = crossUV.z / crossMagnitude;
}
float angle = Vector3.Angle(normal, view);
float cos = Mathf.Cos(angle);
if (cos < 0)
{
return 0;
}
return Mathf.Sqrt(crossUV.x * crossUV.x + crossUV.y * crossUV.y + crossUV.z * crossUV.z) * 0.5f * cos;
}
/// <summary>
/// Calculates the signed volume contribution of a triangle relative to the origin.
/// </summary>
/// <param name="p1">First vertex.</param>
/// <param name="p2">Second vertex.</param>
/// <param name="p3">Third vertex.</param>
/// <returns>Signed volume.</returns>
public static float SignedVolumeOfTriangle(Vector3 p1, Vector3 p2, Vector3 p3)
{
float v321 = p3.x * p2.y * p1.z;
float v231 = p2.x * p3.y * p1.z;
float v312 = p3.x * p1.y * p2.z;
float v132 = p1.x * p3.y * p2.z;
float v213 = p2.x * p1.y * p3.z;
float v123 = p1.x * p2.y * p3.z;
return 1.0f / 6.0f * (-v321 + v231 + v312 - v132 - v213 + v123);
}
/// <summary>
/// Calculates the volume enclosed by a mesh.
/// </summary>
/// <param name="mesh">Mesh to calculate volume for.</param>
/// <returns>Volume of the mesh.</returns>
public static float VolumeOfMesh(Mesh mesh)
{
float volume = 0;
Vector3[] vertices = mesh.vertices;
int[] triangles = mesh.triangles;
for (int i = 0; i < mesh.triangles.Length; i += 3)
{
Vector3 p1 = vertices[triangles[i + 0]];
Vector3 p2 = vertices[triangles[i + 1]];
Vector3 p3 = vertices[triangles[i + 2]];
volume += SignedVolumeOfTriangle(p1, p2, p3);
}
return Mathf.Abs(volume);
}
/// <summary>
/// Transforms a point from local to world space without applying scale.
/// </summary>
/// <param name="transform">Transform to use.</param>
/// <param name="position">Local position.</param>
/// <returns>World position without scale.</returns>
public static Vector3 TransformPointUnscaled(this Transform transform, Vector3 position)
{
return Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one).MultiplyPoint3x4(position);
}
/// <summary>
/// Transforms a point from world to local space without applying scale.
/// </summary>
/// <param name="transform">Transform to use.</param>
/// <param name="position">World position.</param>
/// <returns>Local position without scale.</returns>
public static Vector3 InverseTransformPointUnscaled(this Transform transform, Vector3 position)
{
return Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one).inverse
.MultiplyPoint3x4(position);
}
/// <summary>
/// Changes the layer of a transform and all its children recursively.
/// </summary>
/// <param name="trans">Root transform.</param>
/// <param name="name">Layer name.</param>
public static void ChangeLayersRecursively(this Transform trans, string name)
{
trans.gameObject.layer = LayerMask.NameToLayer(name);
foreach (Transform child in trans)
{
child.ChangeLayersRecursively(name);
}
}
/// <summary>
/// Changes the color of a GameObject's material.
/// </summary>
/// <param name="gameObject">GameObject to modify.</param>
/// <param name="color">New color.</param>
public static void ChangeObjectColor(GameObject gameObject, Color color)
{
gameObject.GetComponent<MeshRenderer>().material.SetColor("_Color", color);
}
/// <summary>
/// Changes the alpha value of a GameObject's material color.
/// </summary>
/// <param name="gameObject">GameObject to modify.</param>
/// <param name="alpha">New alpha value (0-1).</param>
public static void ChangeObjectAlpha(GameObject gameObject, float alpha)
{
MeshRenderer mr = gameObject.GetComponent<MeshRenderer>();
Color currentColor = mr.material.GetColor("_Color");
currentColor.a = alpha;
mr.material.SetColor("_Color", currentColor);
}
/// <summary>
/// Returns a vector with absolute values of all components.
/// </summary>
/// <param name="v">Input vector.</param>
/// <returns>Vector with absolute values.</returns>
public static Vector3 Vector3Abs(Vector3 v)
{
return new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z));
}
/// <summary>
/// Rounds all components of a vector to nearest integer.
/// </summary>
/// <param name="v">Input vector.</param>
/// <returns>Rounded vector.</returns>
public static Vector3 Vector3RoundToInt(Vector3 v)
{
return new Vector3(Mathf.RoundToInt(v.x), Mathf.RoundToInt(v.y), Mathf.RoundToInt(v.z));
}
/// <summary>
/// Returns a vector with reciprocal values (1/x, 1/y, 1/z).
/// </summary>
/// <param name="v">Input vector.</param>
/// <returns>Vector with reciprocal values.</returns>
public static Vector3 Vector3OneOver(Vector3 v)
{
return new Vector3(1f / v.x, 1f / v.y, 1f / v.z);
}
/// <summary>
/// Rounds a value to the nearest multiple of step.
/// </summary>
/// <param name="value">Value to round.</param>
/// <param name="step">Step size.</param>
/// <returns>Rounded value.</returns>
public static float RoundToStep(float value, float step)
{
return Mathf.Round(value / step) * step;
}
/// <summary>
/// Rounds a value to the nearest multiple of step.
/// </summary>
/// <param name="value">Value to round.</param>
/// <param name="step">Step size.</param>
/// <returns>Rounded value.</returns>
public static float RoundToStep(int value, int step)
{
return Mathf.RoundToInt(Mathf.Round(value / step) * step);
}
/// <summary>
/// Rotates a point around a pivot by the specified angles.
/// </summary>
/// <param name="point">Point to rotate.</param>
/// <param name="pivot">Pivot point.</param>
/// <param name="angles">Euler angles for rotation.</param>
/// <returns>Rotated point.</returns>
public static Vector3 RotatePointAroundPivot(Vector3 point, Vector3 pivot, Vector3 angles)
{
return Quaternion.Euler(angles) * (point - pivot) + pivot;
}
/// <summary>
/// Performs bilinear interpolation on a quad defined by four points.
/// </summary>
/// <param name="a">First corner.</param>
/// <param name="b">Second corner.</param>
/// <param name="c">Third corner.</param>
/// <param name="d">Fourth corner.</param>
/// <param name="u">U parameter (0-1).</param>
/// <param name="v">V parameter (0-1).</param>
/// <returns>Interpolated point.</returns>
public static Vector3 QuadLerp(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float u, float v)
{
Vector3 abu = Vector3Lerp(a, b, u);
Vector3 dcu = Vector3Lerp(d, c, u);
return Vector3Lerp(abu, dcu, v);
}
/// <summary>
/// Linear interpolation between two vectors with value clamping.
/// </summary>
/// <param name="v1">Start vector.</param>
/// <param name="v2">End vector.</param>
/// <param name="value">Interpolation value (0-1).</param>
/// <returns>Interpolated vector.</returns>
public static Vector3 Vector3Lerp(Vector3 v1, Vector3 v2, float value)
{
if (value > 1.0f)
{
return v2;
}
if (value < 0.0f)
{
return v1;
}
return new Vector3(v1.x + (v2.x - v1.x) * value,
v1.y + (v2.y - v1.y) * value,
v1.z + (v2.z - v1.z) * value);
}
/// <summary>
/// Calculates the magnitude of a quaternion.
/// </summary>
/// <param name="q">Quaternion.</param>
/// <returns>Magnitude.</returns>
public static float QuaternionMagnitude(Quaternion q)
{
return Mathf.Sqrt(q.w * q.w + q.x * q.x + q.y * q.y + q.z * q.z);
}
}
}