2025-09-16 15:39:49 +02:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using UnityEditor ;
using UnityEngine ;
namespace Editor.Utilities
{
public class SpriteColliderGenerator : EditorWindow
{
private Vector2 scrollPosition ;
[Tooltip("List of GameObjects with SpriteRenderers to generate colliders for")]
private List < GameObject > selectedObjects = new List < GameObject > ( ) ;
[Tooltip("Controls how much to simplify the collider shape (lower values create more complex colliders)")]
private float simplificationTolerance = 0.05f ;
[Tooltip("When enabled, removes any existing PolygonCollider2D components before adding new ones")]
private bool replaceExistingColliders = true ;
[Tooltip("When enabled, applies colliders to all child objects with SpriteRenderers")]
private bool applyToChildren = false ;
[Tooltip("When enabled, allows scaling the collider outward or inward from the sprite center")]
private bool offsetFromCenter = false ;
[Tooltip("Distance to offset the collider from the sprite outline (positive values expand, negative values contract)")]
private float offsetDistance = 0f ;
[Tooltip("When enabled, creates trigger colliders instead of solid colliders")]
private bool generateTriggerColliders = false ;
[Tooltip("Threshold for transparency detection (pixels with alpha below this value are considered transparent)")]
private int alphaCutoff = 128 ; // Used when generating colliders (0-255)
[Tooltip("Controls the level of detail for the generated collider (affects vertex count)")]
private int detailLevel = 2 ; // 1 = low, 2 = medium, 3 = high
[Tooltip("When enabled, shows a preview of the colliders in the scene view before generating them")]
private bool previewColliders = true ;
[Tooltip("Color used for previewing colliders in the scene view")]
private Color previewColor = new Color ( 0.2f , 1f , 0.3f , 0.5f ) ;
2025-09-21 07:32:56 +00:00
[Tooltip("Layer to assign to GameObjects when colliders are generated")]
private int targetLayer = 0 ;
2025-09-16 15:39:49 +02:00
private List < Mesh > previewMeshes = new List < Mesh > ( ) ;
[MenuItem("Tools/Sprite Collider Generator")]
public static void ShowWindow ( )
{
GetWindow < SpriteColliderGenerator > ( "Sprite Collider Generator" ) ;
}
2025-09-21 07:32:56 +00:00
private void OnEnable ( )
{
// Subscribe to scene change events to clear invalid object references
UnityEditor . SceneManagement . EditorSceneManager . sceneOpened + = OnSceneOpened ;
UnityEditor . SceneManagement . EditorSceneManager . sceneClosed + = OnSceneClosed ;
// Also subscribe to playmode changes as they can invalidate references
EditorApplication . playModeStateChanged + = OnPlayModeStateChanged ;
// Subscribe to prefab stage changes (Unity 2018.3+)
UnityEditor . SceneManagement . PrefabStage . prefabStageOpened + = OnPrefabStageOpened ;
UnityEditor . SceneManagement . PrefabStage . prefabStageClosing + = OnPrefabStageClosing ;
}
2025-09-16 15:39:49 +02:00
private void OnDisable ( )
{
2025-09-21 07:32:56 +00:00
// Unsubscribe from events
UnityEditor . SceneManagement . EditorSceneManager . sceneOpened - = OnSceneOpened ;
UnityEditor . SceneManagement . EditorSceneManager . sceneClosed - = OnSceneClosed ;
EditorApplication . playModeStateChanged - = OnPlayModeStateChanged ;
UnityEditor . SceneManagement . PrefabStage . prefabStageOpened - = OnPrefabStageOpened ;
UnityEditor . SceneManagement . PrefabStage . prefabStageClosing - = OnPrefabStageClosing ;
2025-09-16 15:39:49 +02:00
// Clean up any preview meshes when window is closed
foreach ( var mesh in previewMeshes )
{
if ( mesh ! = null )
{
DestroyImmediate ( mesh ) ;
}
}
previewMeshes . Clear ( ) ;
}
2025-09-21 07:32:56 +00:00
private void OnSceneOpened ( UnityEngine . SceneManagement . Scene scene , UnityEditor . SceneManagement . OpenSceneMode mode )
{
// Clear selected objects when a scene is opened
ClearInvalidReferences ( ) ;
}
private void OnSceneClosed ( UnityEngine . SceneManagement . Scene scene )
{
// Clear selected objects when a scene is closed
ClearInvalidReferences ( ) ;
}
private void OnPlayModeStateChanged ( PlayModeStateChange state )
{
// Clear references when entering/exiting play mode as they become invalid
if ( state = = PlayModeStateChange . ExitingEditMode | | state = = PlayModeStateChange . ExitingPlayMode )
{
ClearInvalidReferences ( ) ;
}
}
private void OnPrefabStageOpened ( UnityEditor . SceneManagement . PrefabStage stage )
{
// Clear selected objects when entering a prefab stage
ClearInvalidReferences ( ) ;
}
2025-09-16 15:39:49 +02:00
2025-09-21 07:32:56 +00:00
private void OnPrefabStageClosing ( UnityEditor . SceneManagement . PrefabStage stage )
{
// Clear selected objects when exiting a prefab stage
ClearInvalidReferences ( ) ;
}
/// <summary>
/// Clears invalid GameObject references from the selected objects list
/// </summary>
private void ClearInvalidReferences ( )
{
if ( selectedObjects . Count > 0 )
{
selectedObjects . Clear ( ) ;
ClearPreviews ( ) ; // Also clear any preview meshes
Repaint ( ) ; // Refresh the window UI
}
}
2025-09-16 15:39:49 +02:00
private void OnGUI ( )
{
EditorGUILayout . BeginVertical ( ) ;
EditorGUILayout . LabelField ( "Sprite Collider Generator" , EditorStyles . boldLabel ) ;
EditorGUILayout . HelpBox ( "Select GameObjects with SpriteRenderers and generate accurate PolygonCollider2D components based on the sprite outlines." , MessageType . Info ) ;
EditorGUILayout . Space ( ) ;
// Object selection section
EditorGUILayout . LabelField ( "Selected Objects" , EditorStyles . boldLabel ) ;
EditorGUILayout . BeginHorizontal ( ) ;
if ( GUILayout . Button ( new GUIContent ( "Add Selected GameObjects" , "Add GameObjects currently selected in the scene or project to the list for processing." ) ) )
{
AddSelectedGameObjects ( ) ;
}
EditorGUILayout . EndHorizontal ( ) ;
scrollPosition = EditorGUILayout . BeginScrollView ( scrollPosition , GUILayout . Height ( 150 ) ) ;
for ( int i = 0 ; i < selectedObjects . Count ; i + + )
{
EditorGUILayout . BeginHorizontal ( ) ;
selectedObjects [ i ] = ( GameObject ) EditorGUILayout . ObjectField ( selectedObjects [ i ] , typeof ( GameObject ) , true ) ;
if ( GUILayout . Button ( "X" , GUILayout . Width ( 20 ) ) )
{
selectedObjects . RemoveAt ( i ) ;
i - - ;
}
EditorGUILayout . EndHorizontal ( ) ;
}
EditorGUILayout . EndScrollView ( ) ;
if ( GUILayout . Button ( new GUIContent ( "Clear All" , "Remove all objects from the selection list." ) ) )
{
selectedObjects . Clear ( ) ;
}
EditorGUILayout . Space ( ) ;
// Collider generation options
EditorGUILayout . LabelField ( "Generation Options" , EditorStyles . boldLabel ) ;
// Detail level for collider generation (affects vertex count)
string [ ] detailOptions = new string [ ] { "Low" , "Medium" , "High" } ;
EditorGUILayout . BeginHorizontal ( ) ;
EditorGUILayout . LabelField ( new GUIContent ( "Detail Level:" , "Controls the level of detail for the generated collider (affects vertex count)." ) , GUILayout . Width ( 180 ) ) ;
detailLevel = EditorGUILayout . Popup ( detailLevel - 1 , detailOptions ) + 1 ;
EditorGUILayout . EndHorizontal ( ) ;
// Simplification tolerance (how much to simplify the collider)
EditorGUILayout . BeginHorizontal ( ) ;
EditorGUILayout . LabelField ( new GUIContent ( "Simplification Tolerance:" , "Controls how much to simplify the collider shape (lower values create more complex colliders)." ) , GUILayout . Width ( 180 ) ) ;
simplificationTolerance = EditorGUILayout . Slider ( simplificationTolerance , 0.01f , 0.2f ) ;
EditorGUILayout . EndHorizontal ( ) ;
// Alpha cutoff for transparency
EditorGUILayout . BeginHorizontal ( ) ;
EditorGUILayout . LabelField ( new GUIContent ( "Alpha Cutoff (0-255):" , "Threshold for transparency detection (pixels with alpha below this value are considered transparent)." ) , GUILayout . Width ( 180 ) ) ;
alphaCutoff = EditorGUILayout . IntSlider ( alphaCutoff , 0 , 255 ) ;
EditorGUILayout . EndHorizontal ( ) ;
EditorGUILayout . Space ( ) ;
// Additional options
replaceExistingColliders = EditorGUILayout . Toggle (
new GUIContent ( "Replace Existing Colliders" , "When enabled, removes any existing PolygonCollider2D components before adding new ones." ) ,
replaceExistingColliders ) ;
applyToChildren = EditorGUILayout . Toggle (
new GUIContent ( "Apply To Children" , "When enabled, applies colliders to all child objects with SpriteRenderers." ) ,
applyToChildren ) ;
generateTriggerColliders = EditorGUILayout . Toggle (
new GUIContent ( "Generate Trigger Colliders" , "When enabled, creates trigger colliders instead of solid colliders." ) ,
generateTriggerColliders ) ;
2025-09-21 07:32:56 +00:00
// Layer selection
EditorGUILayout . BeginHorizontal ( ) ;
EditorGUILayout . LabelField ( new GUIContent ( "Target Layer:" , "Layer to assign to GameObjects when colliders are generated. Leave as 'Nothing' to keep current layer." ) , GUILayout . Width ( 180 ) ) ;
targetLayer = EditorGUILayout . LayerField ( targetLayer ) ;
EditorGUILayout . EndHorizontal ( ) ;
2025-09-16 15:39:49 +02:00
// Offset option
offsetFromCenter = EditorGUILayout . Toggle (
new GUIContent ( "Offset From Center" , "When enabled, allows scaling the collider outward or inward from the sprite center." ) ,
offsetFromCenter ) ;
if ( offsetFromCenter )
{
EditorGUI . indentLevel + + ;
offsetDistance = EditorGUILayout . FloatField (
new GUIContent ( "Offset Distance" , "Distance to offset the collider from the sprite outline (positive values expand, negative values contract)." ) ,
offsetDistance ) ;
EditorGUI . indentLevel - - ;
}
// Preview option
previewColliders = EditorGUILayout . Toggle (
new GUIContent ( "Preview Colliders" , "When enabled, shows a preview of the colliders in the scene view before generating them." ) ,
previewColliders ) ;
if ( previewColliders )
{
EditorGUI . indentLevel + + ;
previewColor = EditorGUILayout . ColorField (
new GUIContent ( "Preview Color" , "Color used for previewing colliders in the scene view." ) ,
previewColor ) ;
// Create a horizontal layout for the preview buttons
EditorGUILayout . BeginHorizontal ( ) ;
if ( GUILayout . Button ( new GUIContent ( "Update Preview" , "Refresh the preview display in the scene view." ) ) )
{
GenerateColliderPreviews ( ) ;
}
if ( GUILayout . Button ( new GUIContent ( "Clear Preview" , "Remove all preview colliders from the scene view." ) ) )
{
ClearPreviews ( ) ;
}
EditorGUILayout . EndHorizontal ( ) ;
EditorGUI . indentLevel - - ;
}
EditorGUILayout . Space ( ) ;
// Generate colliders button
GUI . enabled = selectedObjects . Count > 0 ;
if ( GUILayout . Button ( new GUIContent ( "Generate Colliders" , "Create polygon colliders for all selected sprites based on current settings." ) ) )
{
GenerateColliders ( ) ;
}
GUI . enabled = true ;
EditorGUILayout . EndVertical ( ) ;
// Force the scene view to repaint if we're showing previews
if ( previewColliders & & Event . current . type = = EventType . Repaint )
{
SceneView . RepaintAll ( ) ;
}
}
private void AddSelectedGameObjects ( )
{
foreach ( GameObject obj in Selection . gameObjects )
{
if ( ! selectedObjects . Contains ( obj ) )
{
// Only add if it has a SpriteRenderer or any of its children do
if ( obj . GetComponent < SpriteRenderer > ( ) ! = null | |
( applyToChildren & & obj . GetComponentInChildren < SpriteRenderer > ( ) ! = null ) )
{
selectedObjects . Add ( obj ) ;
}
}
}
}
private void GenerateColliderPreviews ( )
{
// Clean up existing preview meshes
foreach ( var mesh in previewMeshes )
{
if ( mesh ! = null )
{
DestroyImmediate ( mesh ) ;
}
}
previewMeshes . Clear ( ) ;
if ( ! previewColliders | | selectedObjects . Count = = 0 )
return ;
foreach ( var obj in selectedObjects )
{
if ( obj = = null ) continue ;
var spriteRenderers = applyToChildren ?
obj . GetComponentsInChildren < SpriteRenderer > ( ) :
new SpriteRenderer [ ] { obj . GetComponent < SpriteRenderer > ( ) } ;
foreach ( var renderer in spriteRenderers )
{
if ( renderer = = null | | renderer . sprite = = null )
continue ;
Sprite sprite = renderer . sprite ;
List < Vector2 [ ] > paths = GetSpritePaths ( sprite , simplificationTolerance ) ;
if ( paths . Count = = 0 )
continue ;
foreach ( var path in paths )
{
// Create a preview mesh from the path
Mesh previewMesh = CreateMeshFromPath ( path , renderer . transform ) ;
if ( previewMesh ! = null )
previewMeshes . Add ( previewMesh ) ;
}
}
}
}
/// <summary>
/// Clears all preview meshes from the scene view
/// </summary>
private void ClearPreviews ( )
{
foreach ( var mesh in previewMeshes )
{
if ( mesh ! = null )
{
DestroyImmediate ( mesh ) ;
}
}
previewMeshes . Clear ( ) ;
// Force a repaint of the scene view
SceneView . RepaintAll ( ) ;
}
private Mesh CreateMeshFromPath ( Vector2 [ ] path , Transform transform )
{
if ( path . Length < 3 )
return null ;
Mesh mesh = new Mesh ( ) ;
// Convert the path to 3D vertices and apply the sprite's transform
Vector3 [ ] vertices = new Vector3 [ path . Length ] ;
for ( int i = 0 ; i < path . Length ; i + + )
{
// Convert the local position to world space using the transform
vertices [ i ] = transform . TransformPoint ( new Vector3 ( path [ i ] . x , path [ i ] . y , 0 ) ) ;
}
// Triangulate the polygon
Triangulator triangulator = new Triangulator ( path ) ;
int [ ] triangles = triangulator . Triangulate ( ) ;
mesh . vertices = vertices ;
mesh . triangles = triangles ;
mesh . RecalculateNormals ( ) ;
return mesh ;
}
private void GenerateColliders ( )
{
int successCount = 0 ;
List < string > errors = new List < string > ( ) ;
Undo . RecordObjects ( selectedObjects . ToArray ( ) , "Generate Sprite Colliders" ) ;
foreach ( var obj in selectedObjects )
{
if ( obj = = null ) continue ;
try
{
var spriteRenderers = applyToChildren ?
obj . GetComponentsInChildren < SpriteRenderer > ( ) :
new SpriteRenderer [ ] { obj . GetComponent < SpriteRenderer > ( ) } ;
foreach ( var renderer in spriteRenderers )
{
if ( renderer = = null | | renderer . sprite = = null )
continue ;
// Check if we're working with a prefab
bool isPrefab = PrefabUtility . IsPartOfPrefabAsset ( renderer . gameObject ) ;
GameObject targetObject = renderer . gameObject ;
if ( isPrefab )
{
// If it's a prefab, we need special handling
string prefabPath = AssetDatabase . GetAssetPath ( targetObject ) ;
targetObject = PrefabUtility . LoadPrefabContents ( prefabPath ) ;
SpriteRenderer prefabRenderer = targetObject . GetComponent < SpriteRenderer > ( ) ;
if ( prefabRenderer = = null | | prefabRenderer . sprite = = null )
{
PrefabUtility . UnloadPrefabContents ( targetObject ) ;
continue ;
}
if ( GenerateColliderForRenderer ( prefabRenderer ) )
{
// Save the changes to the prefab
PrefabUtility . SaveAsPrefabAsset ( targetObject , prefabPath ) ;
successCount + + ;
}
PrefabUtility . UnloadPrefabContents ( targetObject ) ;
}
else
{
// For scene objects, just generate the collider directly
if ( GenerateColliderForRenderer ( renderer ) )
{
successCount + + ;
}
}
}
}
catch ( Exception e )
{
errors . Add ( $"{obj.name}: {e.Message}" ) ;
}
}
// Clean up any preview meshes as we've now generated real colliders
ClearPreviews ( ) ;
if ( successCount > 0 )
{
Debug . Log ( $"Successfully generated colliders for {successCount} sprite(s)." ) ;
}
if ( errors . Count > 0 )
{
Debug . LogError ( $"Errors occurred while generating colliders:\n{string.Join(" \ n ", errors)}" ) ;
}
}
private bool GenerateColliderForRenderer ( SpriteRenderer renderer )
{
if ( renderer = = null | | renderer . sprite = = null )
return false ;
Sprite sprite = renderer . sprite ;
GameObject targetObject = renderer . gameObject ;
// Remove existing colliders if specified
if ( replaceExistingColliders )
{
PolygonCollider2D [ ] existingColliders = targetObject . GetComponents < PolygonCollider2D > ( ) ;
foreach ( var collider in existingColliders )
{
Undo . DestroyObjectImmediate ( collider ) ;
}
}
// Create a new polygon collider
PolygonCollider2D polygonCollider = Undo . AddComponent < PolygonCollider2D > ( targetObject ) ;
if ( polygonCollider = = null )
return false ;
// Set as trigger if specified
polygonCollider . isTrigger = generateTriggerColliders ;
// Get paths from the sprite
List < Vector2 [ ] > paths = GetSpritePaths ( sprite , simplificationTolerance ) ;
if ( paths . Count = = 0 )
return false ;
// Apply offset if needed
if ( offsetFromCenter & & offsetDistance ! = 0 )
{
for ( int i = 0 ; i < paths . Count ; i + + )
{
Vector2 [ ] offsetPath = new Vector2 [ paths [ i ] . Length ] ;
for ( int j = 0 ; j < paths [ i ] . Length ; j + + )
{
// Calculate direction from center (0,0) to the point
Vector2 dir = paths [ i ] [ j ] . normalized ;
// Apply offset in that direction
offsetPath [ j ] = paths [ i ] [ j ] + dir * offsetDistance ;
}
paths [ i ] = offsetPath ;
}
}
// Set the paths on the collider
polygonCollider . pathCount = paths . Count ;
for ( int i = 0 ; i < paths . Count ; i + + )
{
polygonCollider . SetPath ( i , paths [ i ] ) ;
}
2025-09-21 07:32:56 +00:00
// Set the layer on the GameObject if a specific layer is selected
SetTargetLayer ( targetObject ) ;
2025-09-16 15:39:49 +02:00
return true ;
}
2025-09-21 07:32:56 +00:00
/// <summary>
/// Sets the target layer on the GameObject if a layer is selected
/// </summary>
/// <param name="targetObject">The GameObject to set the layer on</param>
private void SetTargetLayer ( GameObject targetObject )
{
if ( targetLayer ! = 0 )
{
Undo . RecordObject ( targetObject , "Set GameObject Layer" ) ;
targetObject . layer = targetLayer ;
}
}
2025-09-16 15:39:49 +02:00
private List < Vector2 [ ] > GetSpritePaths ( Sprite sprite , float tolerance )
{
List < Vector2 [ ] > result = new List < Vector2 [ ] > ( ) ;
if ( sprite = = null )
return result ;
// Get the raw physics shape data from the sprite
int physicsShapeCount = sprite . GetPhysicsShapeCount ( ) ;
if ( physicsShapeCount = = 0 )
{
// Use the sprite's bounds if no physics shape is defined
Vector2 [ ] boundingBoxPath = new Vector2 [ 4 ] ;
Bounds bounds = sprite . bounds ;
boundingBoxPath [ 0 ] = new Vector2 ( bounds . min . x , bounds . min . y ) ;
boundingBoxPath [ 1 ] = new Vector2 ( bounds . min . x , bounds . max . y ) ;
boundingBoxPath [ 2 ] = new Vector2 ( bounds . max . x , bounds . max . y ) ;
boundingBoxPath [ 3 ] = new Vector2 ( bounds . max . x , bounds . min . y ) ;
result . Add ( boundingBoxPath ) ;
return result ;
}
// Adjust the detail level based on the setting
float actualTolerance = tolerance ;
switch ( detailLevel )
{
case 1 : // Low
actualTolerance = tolerance * 2.0f ;
break ;
case 2 : // Medium - default
actualTolerance = tolerance ;
break ;
case 3 : // High
actualTolerance = tolerance * 0.5f ;
break ;
}
// Get all physics shapes from the sprite
for ( int i = 0 ; i < physicsShapeCount ; i + + )
{
List < Vector2 > path = new List < Vector2 > ( ) ;
sprite . GetPhysicsShape ( i , path ) ;
// Apply simplification if needed
if ( actualTolerance > 0.01f )
{
path = SimplifyPath ( path , actualTolerance ) ;
}
if ( path . Count > = 3 ) // Need at least 3 points for a valid polygon
{
result . Add ( path . ToArray ( ) ) ;
}
}
return result ;
}
private List < Vector2 > SimplifyPath ( List < Vector2 > points , float tolerance )
{
if ( points . Count < = 3 )
return points ;
// Implementation of Ramer-Douglas-Peucker algorithm for simplifying a polygon
List < Vector2 > result = new List < Vector2 > ( ) ;
List < int > markers = new List < int > ( new int [ points . Count ] ) ;
markers [ 0 ] = 1 ;
markers [ points . Count - 1 ] = 1 ;
SimplifyDouglasPeucker ( points , tolerance , markers , 0 , points . Count - 1 ) ;
for ( int i = 0 ; i < points . Count ; i + + )
{
if ( markers [ i ] = = 1 )
{
result . Add ( points [ i ] ) ;
}
}
return result ;
}
private void SimplifyDouglasPeucker ( List < Vector2 > points , float tolerance , List < int > markers , int start , int end )
{
if ( end < = start + 1 )
return ;
float maxDistance = 0 ;
int maxIndex = start ;
Vector2 startPoint = points [ start ] ;
Vector2 endPoint = points [ end ] ;
// Find the point furthest from the line segment
for ( int i = start + 1 ; i < end ; i + + )
{
float distance = PerpendicularDistance ( points [ i ] , startPoint , endPoint ) ;
if ( distance > maxDistance )
{
maxDistance = distance ;
maxIndex = i ;
}
}
// If the furthest point is beyond tolerance, mark it for keeping and recurse
if ( maxDistance > tolerance )
{
markers [ maxIndex ] = 1 ;
SimplifyDouglasPeucker ( points , tolerance , markers , start , maxIndex ) ;
SimplifyDouglasPeucker ( points , tolerance , markers , maxIndex , end ) ;
}
}
private float PerpendicularDistance ( Vector2 point , Vector2 lineStart , Vector2 lineEnd )
{
if ( lineStart = = lineEnd )
return Vector2 . Distance ( point , lineStart ) ;
float dx = lineEnd . x - lineStart . x ;
float dy = lineEnd . y - lineStart . y ;
// Normalize
float norm = Mathf . Sqrt ( dx * dx + dy * dy ) ;
if ( norm < float . Epsilon )
return Vector2 . Distance ( point , lineStart ) ;
dx / = norm ;
dy / = norm ;
// Calculate perpendicular distance
float px = point . x - lineStart . x ;
float py = point . y - lineStart . y ;
float projectionLength = px * dx + py * dy ;
Vector2 projection = new Vector2 (
lineStart . x + projectionLength * dx ,
lineStart . y + projectionLength * dy ) ;
return Vector2 . Distance ( point , projection ) ;
}
// Scene view event handling for previewing colliders
[InitializeOnLoadMethod]
static void Initialize ( )
{
SceneView . duringSceneGui + = OnSceneGUI ;
}
static void OnSceneGUI ( SceneView sceneView )
{
// Find all open collider generator windows
var windows = Resources . FindObjectsOfTypeAll < SpriteColliderGenerator > ( ) ;
foreach ( var window in windows )
{
window . DrawColliderPreviews ( sceneView ) ;
}
}
void DrawColliderPreviews ( SceneView sceneView )
{
if ( ! previewColliders | | previewMeshes . Count = = 0 )
return ;
// Draw all preview meshes with the selected color
Material previewMaterial = new Material ( Shader . Find ( "Hidden/Internal-Colored" ) ) ;
previewMaterial . SetPass ( 0 ) ;
previewMaterial . SetColor ( "_Color" , previewColor ) ;
if ( Event . current . type = = EventType . Repaint )
{
GL . PushMatrix ( ) ;
GL . MultMatrix ( Matrix4x4 . identity ) ;
// Enable blending for transparency
GL . Begin ( GL . TRIANGLES ) ;
GL . Color ( previewColor ) ;
foreach ( var mesh in previewMeshes )
{
if ( mesh ! = null )
{
// Draw each triangle in the mesh
for ( int i = 0 ; i < mesh . triangles . Length ; i + = 3 )
{
Vector3 v0 = mesh . vertices [ mesh . triangles [ i ] ] ;
Vector3 v1 = mesh . vertices [ mesh . triangles [ i + 1 ] ] ;
Vector3 v2 = mesh . vertices [ mesh . triangles [ i + 2 ] ] ;
GL . Vertex ( v0 ) ;
GL . Vertex ( v1 ) ;
GL . Vertex ( v2 ) ;
}
}
}
GL . End ( ) ;
GL . PopMatrix ( ) ;
// Also draw the outline
GL . PushMatrix ( ) ;
GL . MultMatrix ( Matrix4x4 . identity ) ;
GL . Begin ( GL . LINES ) ;
// Set a more visible outline color
Color outlineColor = new Color ( previewColor . r , previewColor . g , previewColor . b , 1f ) ;
GL . Color ( outlineColor ) ;
foreach ( var mesh in previewMeshes )
{
if ( mesh ! = null )
{
// Create a dictionary to track which edges we've drawn
HashSet < string > drawnEdges = new HashSet < string > ( ) ;
// Draw edges of each triangle
for ( int i = 0 ; i < mesh . triangles . Length ; i + = 3 )
{
DrawEdgeIfNotDrawn ( mesh . vertices [ mesh . triangles [ i ] ] , mesh . vertices [ mesh . triangles [ i + 1 ] ] , drawnEdges ) ;
DrawEdgeIfNotDrawn ( mesh . vertices [ mesh . triangles [ i + 1 ] ] , mesh . vertices [ mesh . triangles [ i + 2 ] ] , drawnEdges ) ;
DrawEdgeIfNotDrawn ( mesh . vertices [ mesh . triangles [ i + 2 ] ] , mesh . vertices [ mesh . triangles [ i ] ] , drawnEdges ) ;
}
}
}
GL . End ( ) ;
GL . PopMatrix ( ) ;
}
}
private void DrawEdgeIfNotDrawn ( Vector3 v1 , Vector3 v2 , HashSet < string > drawnEdges )
{
// Create a unique key for this edge (order vertices to ensure uniqueness)
string edgeKey ;
if ( v1 . x < v2 . x | | ( v1 . x = = v2 . x & & v1 . y < v2 . y ) )
edgeKey = $"{v1.x},{v1.y},{v1.z}_{v2.x},{v2.y},{v2.z}" ;
else
edgeKey = $"{v2.x},{v2.y},{v2.z}_{v1.x},{v1.y},{v1.z}" ;
// Only draw if we haven't drawn this edge yet
if ( ! drawnEdges . Contains ( edgeKey ) )
{
GL . Vertex ( v1 ) ;
GL . Vertex ( v2 ) ;
drawnEdges . Add ( edgeKey ) ;
}
}
}
// Helper class for triangulating polygons
public class Triangulator
{
private List < Vector2 > m_points ;
public Triangulator ( Vector2 [ ] points )
{
m_points = new List < Vector2 > ( points ) ;
}
public int [ ] Triangulate ( )
{
List < int > indices = new List < int > ( ) ;
int n = m_points . Count ;
if ( n < 3 )
return indices . ToArray ( ) ;
int [ ] V = new int [ n ] ;
if ( Area ( ) > 0 )
{
for ( int v = 0 ; v < n ; v + + )
V [ v ] = v ;
}
else
{
for ( int v = 0 ; v < n ; v + + )
V [ v ] = ( n - 1 ) - v ;
}
int nv = n ;
int count = 2 * nv ;
for ( int v = nv - 1 ; nv > 2 ; )
{
if ( ( count - - ) < = 0 )
return indices . ToArray ( ) ;
int u = v ;
if ( nv < = u )
u = 0 ;
v = u + 1 ;
if ( nv < = v )
v = 0 ;
int w = v + 1 ;
if ( nv < = w )
w = 0 ;
if ( Snip ( u , v , w , nv , V ) )
{
int a , b , c , s , t ;
a = V [ u ] ;
b = V [ v ] ;
c = V [ w ] ;
indices . Add ( a ) ;
indices . Add ( b ) ;
indices . Add ( c ) ;
for ( s = v , t = v + 1 ; t < nv ; s + + , t + + )
V [ s ] = V [ t ] ;
nv - - ;
count = 2 * nv ;
}
}
indices . Reverse ( ) ;
return indices . ToArray ( ) ;
}
private float Area ( )
{
int n = m_points . Count ;
float A = 0.0f ;
for ( int p = n - 1 , q = 0 ; q < n ; p = q + + )
{
Vector2 pval = m_points [ p ] ;
Vector2 qval = m_points [ q ] ;
A + = pval . x * qval . y - qval . x * pval . y ;
}
return ( A * 0.5f ) ;
}
private bool Snip ( int u , int v , int w , int n , int [ ] V )
{
int p ;
Vector2 A = m_points [ V [ u ] ] ;
Vector2 B = m_points [ V [ v ] ] ;
Vector2 C = m_points [ V [ w ] ] ;
if ( Mathf . Epsilon > ( ( ( B . x - A . x ) * ( C . y - A . y ) ) - ( ( B . y - A . y ) * ( C . x - A . x ) ) ) )
return false ;
for ( p = 0 ; p < n ; p + + )
{
if ( ( p = = u ) | | ( p = = v ) | | ( p = = w ) )
continue ;
Vector2 P = m_points [ V [ p ] ] ;
if ( InsideTriangle ( A , B , C , P ) )
return false ;
}
return true ;
}
private bool InsideTriangle ( Vector2 A , Vector2 B , Vector2 C , Vector2 P )
{
float ax , ay , bx , by , cx , cy , apx , apy , bpx , bpy , cpx , cpy ;
float cCROSSap , bCROSScp , aCROSSbp ;
ax = C . x - B . x ; ay = C . y - B . y ;
bx = A . x - C . x ; by = A . y - C . y ;
cx = B . x - A . x ; cy = B . y - A . y ;
apx = P . x - A . x ; apy = P . y - A . y ;
bpx = P . x - B . x ; bpy = P . y - B . y ;
cpx = P . x - C . x ; cpy = P . y - C . y ;
aCROSSbp = ax * bpy - ay * bpx ;
cCROSSap = cx * apy - cy * apx ;
bCROSScp = bx * cpy - by * cpx ;
return ( ( aCROSSbp > = 0.0f ) & & ( bCROSScp > = 0.0f ) & & ( cCROSSap > = 0.0f ) ) ;
}
}
}