//--------------------------------------------------------------------------//
// Copyright 2023-2025 Chocolate Dinosaur Ltd. All rights reserved. //
// For full documentation visit https://www.chocolatedinosaur.com //
//--------------------------------------------------------------------------//
using UnityEngine;
using UnityInternal = UnityEngine.Internal;
namespace ChocDino.UIFX
{
public enum FrustmIntersectResult
{
/// The object is completely outside of the planes.
Out,
/// The object is completely inside of the planes.The object is partially intersecting the planes.
Partial,
}
[UnityInternal.ExcludeFromDocs]
public static class MathUtils
{
public static FrustmIntersectResult GetFrustumIntersectsOBB(Plane[] planes, Vector3[] points)
{
Debug.Assert(planes != null);
Debug.Assert(planes.Length == 6);
Debug.Assert(points != null);
Debug.Assert(points.Length == 8);
FrustmIntersectResult result = FrustmIntersectResult.In;
for (int j = 0; j < 6; j++)
{
var plane = planes[j];
int inCount = 0;
int outCount = 0;
for (int i = 0; i < 8 && (inCount == 0 || outCount == 0); i++)
{
// NOTE: We could use !s_planes[j].GetSide(s_boundsPoints[i]); but this doesn't allow
// for points being ON the plane, which is a not uncommon scenario.
bool isBehindPlane = (Vector3.Dot(plane.normal, points[i]) + plane.distance) < 0f;
if (isBehindPlane)
{
outCount++;
}
else
{
inCount++;
}
}
if (inCount == 0)
{
result = FrustmIntersectResult.Out;
break;
}
else if (outCount != 0)
{
result = FrustmIntersectResult.Partial;
// NOTE: Don't break, keep looking, the object may still be outside other planes
}
}
return result;
}
[UnityInternal.ExcludeFromDocs]
public static float Snap(float v, float snap)
{
float isnap = 1f / snap;
return Mathf.FloorToInt(v * isnap) / isnap;
}
///
/// Adds padding to a number and then rounds up to the nearest multiple.
/// This is useful for textures to ensure they have constant minimum padding amount, but also have a width/height that is a multiple size.
/// This can allow a texture size that is frequently changing slightly (eg when filter sizes change) to not reallocate too frequently, and
/// can stabilise flickering caused when downsampling very small which can cause textures to oscilate between odd/even sizes causing the
/// texture sampling is jump around between frames and flicker.
/// Eg params [9,10,10] = 20, [10,10,10] = 20, [11,10,10] = 30
///
[UnityInternal.ExcludeFromDocs]
public static int PadAndRoundToNextMultiple(float v, int pad, int multiple)
{
multiple = Mathf.Max(1, multiple);
int result = Mathf.CeilToInt(((float)System.Math.Ceiling((v + pad) / multiple)) * multiple);
Debug.Assert(result >= v);
Debug.Assert((result % multiple) == 0);
return result;
}
[UnityInternal.ExcludeFromDocs]
// Based on https://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/
public static float GetDampLerpFactor(float lambda, float deltaTime)
{
return 1f - Mathf.Exp(-lambda * deltaTime);
}
[UnityInternal.ExcludeFromDocs]
public static float DampTowards(float a, float b, float lambda, float deltaTime)
{
return Mathf.Lerp(a, b, GetDampLerpFactor(lambda, deltaTime));
}
[UnityInternal.ExcludeFromDocs]
public static Vector2 DampTowards(Vector2 a, Vector2 b, float lambda, float deltaTime)
{
return Vector2.Lerp(a, b, GetDampLerpFactor(lambda, deltaTime));
}
[UnityInternal.ExcludeFromDocs]
public static Vector3 DampTowards(Vector3 a, Vector3 b, float lambda, float deltaTime)
{
return Vector3.Lerp(a, b, GetDampLerpFactor(lambda, deltaTime));
}
[UnityInternal.ExcludeFromDocs]
public static Vector4 DampTowards(Vector4 a, Vector4 b, float lambda, float deltaTime)
{
return Vector4.Lerp(a, b, GetDampLerpFactor(lambda, deltaTime));
}
[UnityInternal.ExcludeFromDocs]
public static Color DampTowards(Color a, Color b, float lambda, float deltaTime)
{
return Color.Lerp(a, b, GetDampLerpFactor(lambda, deltaTime));
}
[UnityInternal.ExcludeFromDocs]
public static Matrix4x4 DampTowards(Matrix4x4 a, Matrix4x4 b, float lambda, float deltaTime)
{
float t = GetDampLerpFactor(lambda, deltaTime);
return Matrix4x4.identity;
}
[UnityInternal.ExcludeFromDocs]
public static Matrix4x4 LerpUnclamped(Matrix4x4 a, Matrix4x4 b, float t, bool preserveScale)
{
Vector3 targetScale = Vector3.zero;
if (preserveScale)
{
targetScale = Vector3.LerpUnclamped(a.lossyScale, b.lossyScale, t);
}
Matrix4x4 result = new Matrix4x4();
result.SetColumn(0, Vector4.LerpUnclamped(a.GetColumn(0), b.GetColumn(0), t));
result.SetColumn(1, Vector4.LerpUnclamped(a.GetColumn(1), b.GetColumn(1), t));
result.SetColumn(2, Vector4.LerpUnclamped(a.GetColumn(2), b.GetColumn(2), t));
result.SetColumn(3, Vector4.LerpUnclamped(a.GetColumn(3), b.GetColumn(3), t));
if (preserveScale)
{
Vector3 scale = result.lossyScale;
result *= Matrix4x4.Scale(new Vector3(targetScale.x / scale.x, targetScale.y / scale.y, targetScale.z / scale.z));
}
return result;
}
[UnityInternal.ExcludeFromDocs]
public static void LerpUnclamped(ref Matrix4x4 result, Matrix4x4 b, float t, bool preserveScale)
{
Vector3 targetScale = Vector3.zero;
if (preserveScale)
{
targetScale = Vector3.LerpUnclamped(result.lossyScale, b.lossyScale, t);
}
result.SetColumn(0, Vector4.LerpUnclamped(result.GetColumn(0), b.GetColumn(0), t));
result.SetColumn(1, Vector4.LerpUnclamped(result.GetColumn(1), b.GetColumn(1), t));
result.SetColumn(2, Vector4.LerpUnclamped(result.GetColumn(2), b.GetColumn(2), t));
result.SetColumn(3, Vector4.LerpUnclamped(result.GetColumn(3), b.GetColumn(3), t));
if (preserveScale)
{
Vector3 scale = result.lossyScale;
result *= Matrix4x4.Scale(new Vector3(targetScale.x / scale.x, targetScale.y / scale.y, targetScale.z / scale.z));
}
}
///
/// Lerp between 3 values (a, b, c) using t with range [0..1]
///
[UnityInternal.ExcludeFromDocs]
public static float Lerp3(float a, float b, float c, float t)
{
// TODO: optimise this
t *= 2.0f;
float w1 = 1f - Mathf.Clamp01(t);
float w2 = 1f - Mathf.Abs(1f - t);
float w3 = Mathf.Clamp01(t-1f);
return a * w1 + b * w2 + c * w3;
}
[UnityInternal.ExcludeFromDocs]
public static bool HasMatrixChanged(Matrix4x4 a, Matrix4x4 b, bool ignoreTranslation)
{
// First check translation
if (!ignoreTranslation)
{
if (a.m03 != b.m03 || a.m13 != b.m13 || a.m23 != b.m23)
{
return true;
}
}
// Check the rest
if (a.m00 != b.m00 || a.m01 != b.m01 || a.m02 != b.m02 ||
a.m10 != b.m10 || a.m11 != b.m11 || a.m12 != b.m12 ||
a.m20 != b.m20 || a.m21 != b.m21 || a.m22 != b.m22 ||
a.m30 != b.m30 || a.m31 != b.m31 || a.m32 != b.m32 || a.m33 != b.m33)
{
return true;
}
return false;
}
[UnityInternal.ExcludeFromDocs]
public static void CreateRandomIndices(ref int[] array, int length)
{
// Only recreate if required to grow
if (array == null || array.Length < length)
{
array = new int[length];
}
// Populate
for (int i = 0; i < length; i++)
{
array[i] = i;
}
// Shuffle
for (int i = 0; i < length; i++)
{
int a = Random.Range(0, length);
int b = Random.Range(0, length);
if (a != b)
{
int t = array[a];
array[a] = array[b];
array[b] = t;
}
}
}
///
/// Given two rectangles in absolute coordinates, return the rectangle such that if src is remapped to range [0..1] relative to dst
/// If src and dst are the same, rect(0, 0, 1, 1) will be returned
/// If src is within dst, then rect values will be > 0 and < 1
/// If src is larger than dst, rect values will be < 0 and > 1
/// The returned rect could be used to offset and scale UV coordinates from one quad to another
///
public static Rect GetRelativeRect(Rect src, Rect dst)
{
Rect r = Rect.zero;
r.x = (src.x - dst.x) / dst.width;
r.y = (src.y - dst.y) / dst.height;
r.width = src.width / dst.width;
r.height = src.height / dst.height;
return r;
}
/// Move rect horizontally so that specific point along it's width matches the equivelent point along target's width. This is useful for snapping the ege of a rectangle to the edge of another.
private static Rect SnapRectToRectHoriz(Rect rect, Rect target, float sizeT)
{
float posA = Mathf.LerpUnclamped(target.xMin, target.xMax, sizeT);
float posB = Mathf.LerpUnclamped(rect.xMin, rect.xMax, sizeT);
rect.x += (posA - posB);
return rect;
}
/// Move rect vertically so that specific point along it's height matches the equivelent point along target's height. This is useful for snapping the ege of a rectangle to the edge of another.
private static Rect SnapRectToRectVert(Rect rect, Rect target, float sizeT)
{
float posA = Mathf.LerpUnclamped(target.yMin, target.yMax, sizeT);
float posB = Mathf.LerpUnclamped(rect.yMin, rect.yMax, sizeT);
rect.y += (posA - posB);
return rect;
}
///
/// Snap one rectangle to the edge of another (or fractional positions between)
/// widthT 0 is left, widthT 1 is right
/// heightT 0 is bottom, heightT 1 is top
///
public static Rect SnapRectToRectEdges(Rect rect, Rect target, bool applyWidth, bool applyHeight, float widthT, float heightT)
{
if (applyWidth)
{
rect = SnapRectToRectHoriz(rect, target, widthT);
}
if (applyHeight)
{
rect = SnapRectToRectVert(rect, target, heightT);
}
return rect;
}
/// Return rectangle of aspect ratio using the scaling mode
public static Rect ResizeRectToAspectRatio(Rect rect, ScaleMode scaleMode, float aspect)
{
float srcAspect = aspect;
float dstAspect = rect.width / rect.height;
float stretch = 1f;
// src is wider than dst
if (srcAspect > dstAspect)
{
stretch = dstAspect / srcAspect;
}
else
{
stretch = srcAspect / dstAspect;
}
Rect result = rect;
switch (scaleMode)
{
case ScaleMode.StretchToFill:
break;
case ScaleMode.ScaleAndCrop:
{
// src is wider than dst
if (srcAspect > dstAspect)
{
float newWidth = rect.width / stretch;
result = new Rect(rect.xMin - (newWidth - rect.width) * 0.5f, rect.yMin, newWidth, rect.height);
}
else
{
float newHeight = rect.height / stretch;
result = new Rect(rect.xMin, rect.yMin - (newHeight - rect.height) * 0.5f , rect.width, newHeight);
}
}
break;
case ScaleMode.ScaleToFit:
{
// src is wider than dst
if (srcAspect > dstAspect)
{
result = new Rect(rect.xMin, rect.yMin + rect.height * (1f - stretch) * 0.5f, rect.width, stretch * rect.height);
}
else
{
result = new Rect(rect.xMin + rect.width * (1f - stretch) * 0.5f, rect.yMin, stretch * rect.width, rect.height);
}
}
break;
}
return result;
}
}
}