2026-01-03 18:19:39 -05:00
//--------------------------------------------------------------------------//
// Copyright 2023-2025 Chocolate Dinosaur Ltd. All rights reserved. //
// For full documentation visit https://www.chocolatedinosaur.com //
//--------------------------------------------------------------------------//
using System.Collections.Generic ;
using UnityEngine ;
using UnityEngine.UI ;
namespace ChocDino.UIFX
{
public struct RectAdjustOptions
{
public int padding ;
public int roundToNextMultiple ;
public bool clampToScreen ;
}
/// <summary>
/// Takes a collection of Mesh / VertexHelper and calculates the screen-space rectangle that would encapsulate all of them.
/// </summary>
public class ScreenRectFromMeshes
{
private bool _isFirstPoint ;
internal Bounds _screenBounds ;
private Rect _screenRect ;
private Rect _localRect ;
private RectInt _textureRect ;
private Camera _camera ;
private FilterRenderSpace _renderSpace ;
private static Vector3 [ ] s_boundsPoints = new Vector3 [ 8 ] ;
private static Plane [ ] s_planes = new Plane [ 6 ] ;
// Convert 8 points to 12 lines so we can clip with the camera frustum
// front: 3,7 0,4, 3,0, 7,4 (FTL,FTR) (FBL,FBR) (FTL,FBL) (FTR,FBR)
// back : 5,1, 2,6, 5,2 1,6 (BTL,BTR) (BBL,BBR) (BTL,BBL) (BTR,BBR)
// sides: 5,3 2,0 1,7 6,4 (BTL,FTL) (BBL,FBL) (BTR, FTR) (BBR,FBR)
private static int [ ] s_clipLinePairs = new int [ ] { 3 , 7 , 0 , 4 , 3 , 0 , 7 , 4 , 5 , 1 , 2 , 6 , 5 , 2 , 1 , 6 , 5 , 3 , 2 , 0 , 1 , 7 , 6 , 4 } ;
#if UNITY_EDITOR
static ScreenRectFromMeshes ( )
{
Debug . Assert ( s_boundsPoints . Length = = 8 ) ;
Debug . Assert ( s_planes . Length = = 6 ) ;
Debug . Assert ( s_clipLinePairs . Length = = 24 ) ;
}
#endif
public void Start ( Camera camera , FilterRenderSpace renderSpace )
{
_screenBounds = new Bounds ( ) ;
_screenRect = Rect . zero ;
_localRect = Rect . zero ;
_camera = camera ;
_renderSpace = renderSpace ;
_isFirstPoint = true ;
}
public void AddRect ( Transform xform , Rect rect )
{
Vector3 boundsMin = new Vector3 ( rect . xMin , rect . yMin , 0f ) ;
Vector3 boundsMax = new Vector3 ( rect . xMax , rect . yMax , 0f ) ;
AddBounds ( xform , boundsMin , boundsMax ) ;
}
public void AddMeshBounds ( Transform xform , Mesh mesh )
{
if ( mesh )
{
if ( mesh . vertexCount > 0 )
{
AddBounds ( xform , mesh . bounds . min , mesh . bounds . max ) ;
}
}
}
public void AddVertexBounds ( Transform xform , Vector3 [ ] verts , int vertexCount )
{
if ( verts ! = null & & vertexCount > 0 )
{
Vector3 boundsMin = new Vector3 ( float . MaxValue , float . MaxValue , float . MaxValue ) ;
Vector3 boundsMax = new Vector3 ( float . MinValue , float . MinValue , float . MinValue ) ;
for ( int i = 0 ; i < vertexCount ; i + + )
{
boundsMin = Vector3 . Min ( boundsMin , verts [ i ] ) ;
boundsMax = Vector3 . Max ( boundsMax , verts [ i ] ) ;
}
AddBounds ( xform , boundsMin , boundsMax ) ;
}
}
public void AddVertexBounds ( Transform xform , VertexHelper verts )
{
if ( verts ! = null )
{
// TODO: is there a more efficient way to get the bounds of VertexHelper?
int vertexCount = verts . currentVertCount ;
if ( vertexCount > 0 )
{
List < UIVertex > v = new List < UIVertex > ( vertexCount ) ;
verts . GetUIVertexStream ( v ) ;
Vector3 boundsMin = new Vector3 ( float . MaxValue , float . MaxValue , float . MaxValue ) ;
Vector3 boundsMax = new Vector3 ( float . MinValue , float . MinValue , float . MinValue ) ;
for ( int i = 0 ; i < vertexCount ; i + + )
{
boundsMin = Vector3 . Min ( boundsMin , v [ i ] . position ) ;
boundsMax = Vector3 . Max ( boundsMax , v [ i ] . position ) ;
}
AddBounds ( xform , boundsMin , boundsMax ) ;
}
}
}
public void AddTriangleBounds ( Transform xform , Vector3 [ ] verts , int [ ] triangles , Color32 [ ] colors = null )
{
if ( verts ! = null & & triangles ! = null )
{
if ( colors ! = null & & colors . Length = = 0 )
{
colors = null ;
}
if ( verts . Length > 0 & & triangles . Length > = 3 & & ( triangles . Length % 3 ) = = 0 )
{
Vector3 boundsMin = new Vector3 ( float . MaxValue , float . MaxValue , float . MaxValue ) ;
Vector3 boundsMax = new Vector3 ( float . MinValue , float . MinValue , float . MinValue ) ;
bool hasVisibleTriangles = false ;
2026-01-12 03:22:16 -05:00
int triangleCount = triangles . Length ;
for ( int i = 0 ; i < triangleCount ; i + = 3 )
2026-01-03 18:19:39 -05:00
{
2026-01-12 03:22:16 -05:00
int ia = triangles [ i + 0 ] ;
int ib = triangles [ i + 1 ] ;
int ic = triangles [ i + 2 ] ;
if ( ia ! = ib & & ib ! = ic & & ic ! = ia )
{
if ( colors = = null | | ( colors [ ia ] . a > 0 & & colors [ ib ] . a > 0 & & colors [ ic ] . a > 0 ) )
{
Vector3 va = verts [ ia ] ;
Vector3 vb = verts [ ib ] ;
Vector3 vc = verts [ ic ] ;
if ( va ! = vb & & vb ! = vc & & vc ! = va )
{
hasVisibleTriangles = true ;
boundsMin = Vector3 . Min ( boundsMin , va ) ;
boundsMax = Vector3 . Max ( boundsMax , va ) ;
boundsMin = Vector3 . Min ( boundsMin , vb ) ;
boundsMax = Vector3 . Max ( boundsMax , vb ) ;
boundsMin = Vector3 . Min ( boundsMin , vc ) ;
boundsMax = Vector3 . Max ( boundsMax , vc ) ;
}
}
}
}
if ( hasVisibleTriangles )
{
AddBounds ( xform , boundsMin , boundsMax ) ;
}
}
}
}
public void AddTriangleBounds ( Transform xform , List < Vector3 > verts , List < int > triangles , List < Color32 > colors = null )
{
if ( verts ! = null & & triangles ! = null )
{
if ( colors ! = null & & colors . Count = = 0 )
{
colors = null ;
}
if ( verts . Count > 0 & & triangles . Count > = 3 & & ( triangles . Count % 3 ) = = 0 )
{
Vector3 boundsMin = new Vector3 ( float . MaxValue , float . MaxValue , float . MaxValue ) ;
Vector3 boundsMax = new Vector3 ( float . MinValue , float . MinValue , float . MinValue ) ;
bool hasVisibleTriangles = false ;
int triangleCount = triangles . Count ;
for ( int i = 0 ; i < triangleCount ; i + = 3 )
{
int ia = triangles [ i + 0 ] ;
int ib = triangles [ i + 1 ] ;
int ic = triangles [ i + 2 ] ;
2026-01-03 18:19:39 -05:00
if ( ia ! = ib & & ib ! = ic & & ic ! = ia )
{
if ( colors = = null | | ( colors [ ia ] . a > 0 & & colors [ ib ] . a > 0 & & colors [ ic ] . a > 0 ) )
{
Vector3 va = verts [ ia ] ;
Vector3 vb = verts [ ib ] ;
Vector3 vc = verts [ ic ] ;
if ( va ! = vb & & vb ! = vc & & vc ! = va )
{
hasVisibleTriangles = true ;
boundsMin = Vector3 . Min ( boundsMin , va ) ;
boundsMax = Vector3 . Max ( boundsMax , va ) ;
boundsMin = Vector3 . Min ( boundsMin , vb ) ;
boundsMax = Vector3 . Max ( boundsMax , vb ) ;
boundsMin = Vector3 . Min ( boundsMin , vc ) ;
boundsMax = Vector3 . Max ( boundsMax , vc ) ;
}
}
}
}
if ( hasVisibleTriangles )
{
AddBounds ( xform , boundsMin , boundsMax ) ;
}
}
}
}
private void AddBounds ( Transform xform , Vector3 boundsMin , Vector3 boundsMax )
{
_localRect = Rect . MinMaxRect ( boundsMin . x , boundsMin . y , boundsMax . x , boundsMax . y ) ;
// Points of the bounding box
s_boundsPoints [ 0 ] = boundsMin ; // FBL
s_boundsPoints [ 1 ] = boundsMax ; // BTR
s_boundsPoints [ 2 ] = new Vector3 ( boundsMin . x , boundsMin . y , boundsMax . z ) ; // BBL
s_boundsPoints [ 3 ] = new Vector3 ( boundsMin . x , boundsMax . y , boundsMin . z ) ; // FTL
s_boundsPoints [ 4 ] = new Vector3 ( boundsMax . x , boundsMin . y , boundsMin . z ) ; // FBR
s_boundsPoints [ 5 ] = new Vector3 ( boundsMin . x , boundsMax . y , boundsMax . z ) ; // BTL
s_boundsPoints [ 6 ] = new Vector3 ( boundsMax . x , boundsMin . y , boundsMax . z ) ; // BBR
s_boundsPoints [ 7 ] = new Vector3 ( boundsMax . x , boundsMax . y , boundsMin . z ) ; // FTR
// Convert the local AABB points to world space points (OBB)
if ( xform )
{
for ( int i = 0 ; i < s_boundsPoints . Length ; i + + )
{
s_boundsPoints [ i ] = xform . localToWorldMatrix . MultiplyPoint ( s_boundsPoints [ i ] ) ;
}
}
bool needsClipping = false ;
if ( _renderSpace = = FilterRenderSpace . Screen & & _camera )
{
// Test the AABB corners against the frustum planes to determine whether
// A: all points are inside all planes - visible, no clipping
// B: all points are outline one plane - invisible, cull
// C: some points are inside, some are outside - needs clipping
GeometryUtility . CalculateFrustumPlanes ( _camera , s_planes ) ;
// Note: [0] = Left, [1] = Right, [2] = Down, [3] = Up, [4] = Near, [5] = Far
Debug . Assert ( s_planes . Length = = 6 ) ;
var frustumResult = MathUtils . GetFrustumIntersectsOBB ( s_planes , s_boundsPoints ) ;
if ( frustumResult = = FrustmIntersectResult . Out )
{
//return;
}
// Note: Disable clipping as not all cases are handled
needsClipping = ( frustumResult = = FrustmIntersectResult . Partial ) ;
}
if ( needsClipping )
{
// Note: This clipping isn't perfect, mostly when rotation is applied..
for ( int j = 0 ; j < 6 ; j + + )
{
var plane = s_planes [ j ] ;
for ( int i = 0 ; i < 12 ; i + + )
{
2026-01-12 03:22:16 -05:00
Vector3 a = s_boundsPoints [ s_clipLinePairs [ i * 2 + 0 ] ] ;
Vector3 b = s_boundsPoints [ s_clipLinePairs [ i * 2 + 1 ] ] ;
2026-01-03 18:19:39 -05:00
// In some cases where zMin and zMax are the same, the shape is 2D, so some points will be duplicated
// There is no point testing these pairs for clipping.
if ( a = = b ) continue ;
Debug . Assert ( a ! = b ) ;
2026-01-12 03:22:16 -05:00
2026-01-03 18:19:39 -05:00
float distance = 0f ;
if ( ! plane . GetSide ( a ) )
{
2026-01-12 03:22:16 -05:00
Vector3 dir = ( b - a ) . normalized ;
2026-01-03 18:19:39 -05:00
if ( plane . Raycast ( new Ray ( a , dir ) , out distance ) )
{
// Only if intersection point is within the a-b line segment
2026-01-12 03:22:16 -05:00
if ( distance < ( a - b ) . magnitude )
2026-01-03 18:19:39 -05:00
{
// Move to the intersection point
a + = dir * distance ;
}
}
}
if ( ! plane . GetSide ( b ) )
{
2026-01-12 03:22:16 -05:00
Vector3 dir = ( a - b ) . normalized ;
2026-01-03 18:19:39 -05:00
if ( plane . Raycast ( new Ray ( b , dir ) , out distance ) )
{
// Only if intersection point is within the a-b line segment
2026-01-12 03:22:16 -05:00
if ( distance < ( a - b ) . magnitude )
2026-01-03 18:19:39 -05:00
{
// Move to the intersection point
b + = dir * distance ;
}
}
}
2026-01-12 03:22:16 -05:00
s_boundsPoints [ s_clipLinePairs [ i * 2 + 0 ] ] = a ;
s_boundsPoints [ s_clipLinePairs [ i * 2 + 1 ] ] = b ;
2026-01-03 18:19:39 -05:00
}
}
}
{
for ( int i = 0 ; i < s_boundsPoints . Length ; i + + )
{
// Convert to screen space
if ( _camera )
{
s_boundsPoints [ i ] = _camera . WorldToScreenPoint ( s_boundsPoints [ i ] ) ;
}
if ( ! _isFirstPoint )
{
// Grow with the other points
_screenBounds . Encapsulate ( s_boundsPoints [ i ] ) ;
}
else
{
// Initialise with the first point
_screenBounds . center = s_boundsPoints [ i ] ;
_isFirstPoint = false ;
}
}
}
}
public void End ( )
{
Vector3 minScreen = _screenBounds . min ;
Vector3 maxScreen = _screenBounds . max ;
_screenRect = Rect . MinMaxRect ( minScreen . x , minScreen . y , maxScreen . x , maxScreen . y ) ;
}
public void Adjust ( Vector2Int leftDown , Vector2Int rightUp )
{
//Debug.Assert(leftDown.x >= 0 && leftDown.y >= 0);
//Debug.Assert(rightUp.x >= 0 && rightUp.y >= 0);
_screenRect . min - = leftDown ;
_screenRect . max + = rightUp ;
}
internal void SetRect ( Rect rect )
{
_screenRect = rect ;
}
public Rect GetRect ( )
{
return _screenRect ;
}
public Rect GetLocalRect ( )
{
return _localRect ;
}
public RectInt GetTextureRect ( )
{
return _textureRect ;
}
public void OptimiseRects ( RectAdjustOptions options )
{
// Crop to screen size optimisation
if ( options . clampToScreen )
{
Rect rect = _screenRect ;
// Problem, since it's cropped, you can't see it in the Scene view which is annoying..
// so we disable this in the editor, but have it enabled in builds.
#if UNITY_EDITOR
float padding = 16f ;
rect . xMin = Mathf . Max ( - padding , rect . xMin ) ;
rect . yMin = Mathf . Max ( - padding , rect . yMin ) ;
rect . xMax = Mathf . Max ( rect . xMax , rect . xMin ) ;
rect . yMax = Mathf . Max ( rect . yMax , rect . yMin ) ;
Vector2 screenResolution = Filters . GetMonitorResolution ( ) ;
rect . xMax = Mathf . Min ( screenResolution . x + padding , rect . xMax ) ;
rect . yMax = Mathf . Min ( screenResolution . y + padding , rect . yMax ) ;
#endif
_innerRect = _screenRect = rect ;
}
else
{
_innerRect = _screenRect ;
// To closest multiple
// This fixes temporal flickering in BlurFilter when animating BlurSize using downsampling, by preventing rapidly switching between odd/even texture sizes
// (For some reason 8 was the minimum value needed to fix flickering)
if ( options . padding > 0 | | options . roundToNextMultiple > 1 )
{
int targetWidth = MathUtils . PadAndRoundToNextMultiple ( _screenRect . width , options . padding , options . roundToNextMultiple ) ;
int targetHeight = MathUtils . PadAndRoundToNextMultiple ( _screenRect . height , options . padding , options . roundToNextMultiple ) ;
float dx = ( targetWidth - _screenRect . width ) ;
float dy = ( targetHeight - _screenRect . height ) ;
// Make sure dx and dy are even to prevent rounding errors
// NOTE: This logic has been removed as I can't understand it's purpose,
// and it doesn't have correct results for values < 1.
//dx -= Mathf.CeilToInt(dx)%2;
//dy -= Mathf.CeilToInt(dy)%2;
_screenRect . x - = dx / 2f ;
_screenRect . y - = dy / 2f ;
_screenRect . width = targetWidth ;
_screenRect . height = targetHeight ;
}
// To closest pow2
#if false
if ( false )
{
int targetWidth = Mathf . NextPowerOfTwo ( Mathf . CeilToInt ( _screenRect . width ) ) ;
int targetHeight = Mathf . NextPowerOfTwo ( Mathf . CeilToInt ( _screenRect . height ) ) ;
float dx = ( targetWidth - _screenRect . width ) ;
float dy = ( targetHeight - _screenRect . height ) ;
// Make sure dx and dy are even to prevent rounding errors
// NOTE: This logic has been removed as I can't understand it's purpose,
// and it doesn't have correct results for values < 1.
//dx -= Mathf.CeilToInt(dx)%2;
//dy -= Mathf.CeilToInt(dy)%2;
_screenRect . x - = dx / 2f ;
_screenRect . y - = dy / 2f ;
_screenRect . width = targetWidth ;
_screenRect . height = targetHeight ;
}
#endif
}
// Check that the above logic cannot produce a rectangle smaller than the initial inner rectangle.
Debug . Assert ( _screenRect . xMin < = _innerRect . xMin ) ;
Debug . Assert ( _screenRect . yMin < = _innerRect . yMin ) ;
Debug . Assert ( _screenRect . xMax > = _innerRect . xMax ) ;
Debug . Assert ( _screenRect . yMax > = _innerRect . yMax ) ;
_textureRect = new RectInt ( Mathf . FloorToInt ( _screenRect . xMin ) , Mathf . FloorToInt ( _screenRect . yMin ) , Mathf . CeilToInt ( _screenRect . xMax ) - Mathf . FloorToInt ( _screenRect . xMin ) , Mathf . CeilToInt ( _screenRect . yMax ) - Mathf . FloorToInt ( _screenRect . yMin ) ) ;
}
private Rect _innerRect ;
public void BuildScreenQuad ( Camera camera , Transform xform , Color32 color , VertexHelper vh )
{
// 4 corners of quad
Rect screenRect = _innerRect ;
Vector3 v0 = new Vector2 ( screenRect . xMin , screenRect . yMax ) ;
Vector3 v1 = new Vector2 ( screenRect . xMax , screenRect . yMax ) ;
Vector3 v2 = new Vector2 ( screenRect . xMax , screenRect . yMin ) ;
Vector3 v3 = new Vector2 ( screenRect . xMin , screenRect . yMin ) ;
// Set depth to maxmimum (deepest) value
// This useful in screen-space rendering mode to allows tilted geometry
// to not clip with the camera near plane.
v3 . z = v2 . z = v1 . z = v0 . z = _screenBounds . max . z ;
// Convert screen to world space
if ( camera )
{
v0 = camera . ScreenToWorldPoint ( v0 ) ;
v1 = camera . ScreenToWorldPoint ( v1 ) ;
v2 = camera . ScreenToWorldPoint ( v2 ) ;
v3 = camera . ScreenToWorldPoint ( v3 ) ;
}
// Convert world to local space
if ( xform )
{
Matrix4x4 worldToLocal = xform . worldToLocalMatrix ;
v0 = worldToLocal . MultiplyPoint ( v0 ) ;
v1 = worldToLocal . MultiplyPoint ( v1 ) ;
v2 = worldToLocal . MultiplyPoint ( v2 ) ;
v3 = worldToLocal . MultiplyPoint ( v3 ) ;
}
// Zero Z
//v3.z = v2.z = v1.z = v0.z = 0.0f;
// Texture range
Rect textureRect = new Rect ( Mathf . FloorToInt ( _screenRect . xMin ) , Mathf . FloorToInt ( _screenRect . yMin ) , Mathf . CeilToInt ( _screenRect . xMax ) - Mathf . FloorToInt ( _screenRect . xMin ) , Mathf . CeilToInt ( _screenRect . yMax ) - Mathf . FloorToInt ( _screenRect . yMin ) ) ;
float tx1 = ( _innerRect . xMin - textureRect . xMin ) / textureRect . width ;
float ty1 = ( _innerRect . yMin - textureRect . yMin ) / textureRect . height ;
float tx2 = ( _innerRect . xMax - textureRect . xMin ) / textureRect . width ;
float ty2 = ( _innerRect . yMax - textureRect . yMin ) / textureRect . height ;
// Build geometry
vh . Clear ( ) ;
vh . AddVert ( v0 , color , new Vector4 ( tx1 , ty2 , 0f , 0f ) ) ;
vh . AddVert ( v1 , color , new Vector4 ( tx2 , ty2 , 0f , 0f ) ) ;
vh . AddVert ( v2 , color , new Vector4 ( tx2 , ty1 , 0f , 0f ) ) ;
vh . AddVert ( v3 , color , new Vector4 ( tx1 , ty1 , 0f , 0f ) ) ;
vh . AddTriangle ( 0 , 1 , 2 ) ;
vh . AddTriangle ( 0 , 2 , 3 ) ;
}
}
}