Glass.cs
using Sandbox;
using Sandbox.Diagnostics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;

public sealed class Glass : Component, Component.ExecuteInEditor, Component.IDamageable
{
	private sealed class Shard : IValid
	{
		public SceneObject SceneObject { get; private set; }
		public PhysicsBody PhysicsBody { get; private set; }
		public PhysicsShape PhysicsShape { get; private set; }
		public Vector2[] Points { get; private set; }
		public float Area { get; private set; }
		public bool IsLoose { get; set; }
		public TimeSince TimeCreated { get; private set; }

		public bool IsValid => SceneObject.IsValid() && PhysicsBody.IsValid();

		public Shard( SceneObject sceneObject, PhysicsShape shape, Vector2[] points )
		{
			SceneObject = sceneObject;
			PhysicsBody = shape.Body;
			PhysicsShape = shape;
			Points = points;
			TimeCreated = 0;
			Area = CalculateArea();
		}

		public void Destroy()
		{
			SceneObject?.Delete();
			PhysicsBody?.Remove();
			PhysicsBody = null;
		}

		public bool IsPointInside( Vector2 point )
		{
			if ( Points == null || Points.Length < 3 )
				return false;

			int positive = 0;
			int negative = 0;

			for ( int i = 0; i < Points.Length; i++ )
			{
				var v1 = Points[i];
				var v2 = Points[i < Points.Length - 1 ? i + 1 : 0];

				float cross = (point.x - v1.x) * (v2.y - v1.y) - (point.y - v1.y) * (v2.x - v1.x);

				if ( cross > 0 )
				{
					positive++;
				}
				else if ( cross < 0 )
				{
					negative++;
				}

				if ( positive > 0 && negative > 0 )
				{
					return false;
				}
			}

			return true;
		}

		private float CalculateArea()
		{
			var area = 0.0f;

			if ( Points is not null && Points.Length >= 3 )
			{
				var v1 = Points[0];

				for ( var i = 1; i < Points.Length - 1; i++ )
				{
					var v2 = Points[i];
					var v3 = Points[i + 1];
					var x1 = v2.x - v1.x;
					var y1 = v2.y - v1.y;
					var x2 = v3.x - v1.x;
					var y2 = v3.y - v1.y;

					area += MathF.Abs( x1 * y2 - x2 * y1 );
				}

				area = MathF.Abs( area * 0.5f );
			}

			return area;
		}
	}

	private readonly Dictionary<PhysicsShape, Shard> Shards = new();
	private readonly List<PhysicsShape> ShardsToRemove = new();

	[Property, MakeDirty] public Material Material { get; set; }
	[Property, MakeDirty] public Surface Surface { get; set; }
	[Property, MakeDirty] public float Thickness { get; set; } = 1;
	[Property, MakeDirty] public Vector3 TextureAxisU { get; set; } = Vector3.Forward;
	[Property, MakeDirty] public Vector3 TextureAxisV { get; set; } = Vector3.Right;
	[Property, MakeDirty] public Vector2 TextureScale { get; set; } = 1;
	[Property, MakeDirty] public Vector2 TextureOffset { get; set; } = 0;
	[Property, MakeDirty] public Vector2 TextureSize { get; set; } = 512;
	[Property] public List<Vector2> Points { get; set; }
	[Property] public float ShardLifeTime { get; set; } = 1.0f;

	[StructLayout( LayoutKind.Sequential )]
	private struct Vertex
	{
		public Vector3 Position;
		public Vector3 Normal;
		public Vector2 TexCoord0;
		public Vector2 TexCoord1;
		public Vector3 Color;
		public Vector4 Tangent;

		public static readonly VertexAttribute[] Layout =
		{
			new( VertexAttributeType.Position, VertexAttributeFormat.Float32, 3 ),
			new( VertexAttributeType.Normal, VertexAttributeFormat.Float32, 3 ),
			new( VertexAttributeType.TexCoord, VertexAttributeFormat.Float32, 2, 0 ),
			new( VertexAttributeType.TexCoord, VertexAttributeFormat.Float32, 2, 1 ),
			new( VertexAttributeType.Color, VertexAttributeFormat.Float32, 3, 0 ),
			new( VertexAttributeType.Tangent, VertexAttributeFormat.Float32, 4 ),
		};
	}

	protected override void DrawGizmos()
	{
		foreach ( var point in Points )
		{
			Gizmo.Draw.LineSphere( new Vector3( point.x, point.y, 0 ), 4 );
		}

		Gizmo.Draw.Color = Color.Red;

		foreach ( var shard in Shards.Values )
		{
			if ( !shard.IsValid() )
				continue;

			var body = shard.PhysicsBody;
			if ( !body.IsValid() )
				continue;

			Gizmo.Draw.LineSphere( WorldTransform.ToLocal( body.Transform ).Position, 1 );

			foreach ( var point in shard.Points )
			{
				var p = WorldTransform.PointToLocal( body.Transform.PointToWorld( new Vector3( point.x, point.y, 0 ) ) );
				Gizmo.Draw.LineSphere( p, 1 );
			}
		}
	}

	protected override void OnEnabled()
	{
		base.OnEnabled();

		CreatePrimaryShard();
	}

	protected override void OnDisabled()
	{
		base.OnDisabled();

		DestroyShards();
	}

	protected override void OnDirty()
	{
		base.OnDirty();

		DestroyShards();
		CreatePrimaryShard();
	}

	protected override void OnValidate()
	{
		base.OnValidate();

		Surface ??= Surface.FindByName( "glass" );
		Material ??= Material.Load( "materials/glass.vmat" );
	}

	private void CreatePrimaryShard()
	{
		var points = IsPathClockwise( Points ) ? Points.Reverse<Vector2>().ToList() : Points.ToList();
		CreateShard( WorldTransform, points );
	}

	private void DestroyShards()
	{
		foreach ( var shard in Shards.Values )
		{
			shard.Destroy();
		}

		Shards.Clear();
	}

	protected override void OnPreRender()
	{
		foreach ( var kv in Shards )
		{
			var shard = kv.Value;
			if ( !shard.IsValid() )
				continue;

			var body = shard.PhysicsBody;
			if ( !body.IsValid() )
				continue;

			if ( !shard.IsLoose )
			{
				body.Transform = WorldTransform;
			}

			shard.SceneObject.Transform = body.Transform;

			if ( shard.IsLoose && shard.TimeCreated > ShardLifeTime )
			{
				ShardsToRemove.Add( kv.Key );
				shard.Destroy();
			}
		}

		foreach ( var shard in ShardsToRemove )
		{
			Shards.Remove( shard );
		}

		ShardsToRemove.Clear();
	}

	private Shard CreateShard( Transform transform, List<Vector2> points )
	{
		if ( CalculatePathArea( points ) < 4.0f )
			return null;

		var hull = new List<Vector3>();
		float halfThickness = Thickness * 0.5f;

		foreach ( var point in points )
		{
			hull.Add( new Vector3( point.x, point.y, halfThickness ) );
			hull.Add( new Vector3( point.x, point.y, -halfThickness ) );
		}

		var body = new PhysicsBody( Scene.PhysicsWorld );
		var shape = body.AddHullShape( Vector3.Zero, Rotation.Identity, hull );
		shape.Tags.SetFrom( GameObject.Tags );
		shape.Surface = Surface;

		body.Component = this;
		body.EnableCollisionSounds = false;
		body.OnIntersectionStart += OnPhysicsTouchStart;
		body.BodyType = PhysicsBodyType.Keyframed;
		body.Transform = transform;

		var model = CreateModel( points );
		var sceneObject = new SceneObject( Scene.SceneWorld, model, transform );
		sceneObject.SetComponentSource( this );
		sceneObject.Tags.SetFrom( GameObject.Tags );
		sceneObject.Batchable = false;

		var shard = new Shard( sceneObject, shape, points.ToArray() );
		Shards.Add( shape, shard );

		return shard;
	}

	private void DestroyShard( Shard shard )
	{
		if ( shard is null )
			return;

		Shards.Remove( shard.PhysicsShape );
		shard.Destroy();
	}

	public void OnDamage( in DamageInfo damage )
	{
		var shape = damage.Shape;
		if ( !shape.IsValid() )
			return;

		if ( !Shards.TryGetValue( shape, out var shard ) )
			return;

		if ( !shard.IsValid() )
			return;

		var transform = shard.PhysicsBody.Transform;
		var position = transform.PointToLocal( damage.Position );
		ShatterLocalSpace( shard, position, 0 );
	}

	private void ShatterLocalSpace( Shard shard, Vector2 position, Vector3 impulse )
	{
		if ( !shard.IsValid() )
			return;

		if ( !shard.IsPointInside( position ) )
			return;

		var points = shard.Points.ToList();
		var transform = shard.PhysicsBody.Transform;
		var isLoose = shard.IsLoose;
		var area = shard.Area;

		DestroyShard( shard );

		if ( area < 4.0f )
			return;

		var shards = GenerateShatterShards( position, points, transform );
		foreach ( var newShard in shards )
		{
			if ( !newShard.IsValid() )
				continue;

			if ( newShard.Points == null || newShard.Points.Length == 0 )
				continue;

			if ( isLoose || !IsPathOnEdge( newShard.Points, Points, 0.1f ) )
			{
				var body = newShard.PhysicsBody;
				if ( body.IsValid() )
				{
					body.BodyType = PhysicsBodyType.Dynamic;
					body.ApplyImpulseAt( body.Transform.PointToWorld( position ), impulse );
				}

				newShard.IsLoose = true;
			}
		}
	}

	private void OnPhysicsTouchStart( PhysicsIntersection c )
	{

	}

	private static float DistanceToEdge( Vector2 point, Vector2 start, Vector2 end )
	{
		var delta = end - start;
		var length = delta.Length;
		var direction = delta / length;
		var closestPoint = start + Vector3.Dot( point - start, direction ).Clamp( 0, length ) * direction;
		return (point - closestPoint).Length;
	}

	private static bool IsPathOnEdge( IList<Vector2> path1, IList<Vector2> path2, float threshold )
	{
		foreach ( var point in path1 )
		{
			for ( int i = 0; i < path2.Count; i++ )
			{
				float dist = DistanceToEdge( point, path2[i], path2[(i + 1) % (path2.Count)] );

				if ( dist <= threshold )
				{
					return true;
				}
			}
		}

		return false;
	}

	private static float CalculatePathArea( IList<Vector2> points )
	{
		float area = 0;

		int vertexCount = points.Count;
		if ( vertexCount < 3 )
		{
			return 0;
		}
		else
		{
			var v1 = points[0];

			for ( int i = 1; i < vertexCount - 1; i++ )
			{
				var v2 = points[i];
				var v3 = points[i + 1];

				float x1 = v2.x - v1.x;
				float y1 = v2.y - v1.y;
				float x2 = v3.x - v1.x;
				float y2 = v3.y - v1.y;

				area += MathF.Abs( x1 * y2 - x2 * y1 );
			}

			area = MathF.Abs( area * 0.5f );
		}

		return area;
	}

	private struct ShatterSpoke
	{
		public Vector2 OuterPos;
		public Vector2 IntersectionPos;
		public int IntersectsEdgeIndex;
		public float Length;
	};

	private struct ShatterEdgeSegment
	{
		public ShatterEdgeSegment( Vector2 start, Vector2 end )
		{
			Start = start;
			End = end;
		}

		public Vector2 Start;
		public Vector2 End;
	};

	private List<Shard> GenerateShatterShards( Vector2 stressPosition, IList<Vector2> points, Transform transform )
	{
		var shards = new List<Shard>();
		var shatterType = ShatterTypes[1];

		var minX = points.Min( x => x.x );
		var minY = points.Min( x => x.y );
		var maxX = points.Max( x => x.x );
		var maxY = points.Max( x => x.y );

		var min = new Vector2( minX, minY );
		var max = new Vector2( maxX, maxY );

		float spokeLength = (max - min).LengthSquared;
		int numSpokes = Math.Max( 3, Game.Random.Int( shatterType.SpokesMin, shatterType.SpokesMax ) );
		var spokes = new List<ShatterSpoke>();

		float segmentRange = (MathF.PI * 2.0f) / numSpokes;
		float limitedRangeDeviation = Math.Min( segmentRange, (MathF.PI * 2.0f) * (1.0f / 3.0f) );

		for ( int i = 0; i < numSpokes; i++ )
		{
			float spokeRadians = (i * segmentRange) + (Game.Random.Float( limitedRangeDeviation * -0.5f, limitedRangeDeviation * 0.5f ) * 0.9f);

			var spoke = new ShatterSpoke
			{
				OuterPos = new Vector2( stressPosition.x + spokeLength * MathF.Cos( spokeRadians ), stressPosition.y + spokeLength * MathF.Sin( spokeRadians ) ),
				IntersectionPos = Vector2.Zero,
				IntersectsEdgeIndex = -1,
				Length = -1
			};

			spokes.Insert( 0, spoke );
		}

		var edgeSegments = new List<ShatterEdgeSegment>();

		for ( int i = 0; i < points.Count; i++ )
		{
			var v1 = points[i];
			var v2 = points[i < points.Count - 1 ? i + 1 : 0];

			edgeSegments.Add( new ShatterEdgeSegment( v1, v2 ) );
		}

		for ( int spokeIndex = 0; spokeIndex < spokes.Count; spokeIndex++ )
		{
			for ( int edgeIndex = 0; edgeIndex < edgeSegments.Count; edgeIndex++ )
			{
				if ( LineIntersect( edgeSegments[edgeIndex].Start, edgeSegments[edgeIndex].End, spokes[spokeIndex].OuterPos, stressPosition, out var point ) )
				{
					var spoke = spokes[spokeIndex];
					spoke.IntersectionPos = point;
					spoke.IntersectsEdgeIndex = edgeIndex;
					spoke.Length = Vector2.DistanceBetween( stressPosition, spoke.IntersectionPos );

					spokes[spokeIndex] = spoke;

					break;
				}
			}
		}

		var centerHoleVertices = new List<Vector2>();

		for ( int spokeIndex = 0; spokeIndex < spokes.Count; spokeIndex++ )
		{
			int nextSpokeIndex = spokeIndex < spokes.Count - 1 ? spokeIndex + 1 : 0;
			int currentEdgeIndex = spokes[spokeIndex].IntersectsEdgeIndex;
			int nextEdgeIndex = spokes[nextSpokeIndex].IntersectsEdgeIndex;

			if ( nextSpokeIndex < 0 || currentEdgeIndex < 0 || nextEdgeIndex < 0 )
				continue;

			if ( spokes[spokeIndex].Length < 0.5f && spokes[nextSpokeIndex].Length < 0.5f )
				continue;

			var subShard = new List<Vector2>
			{
				stressPosition,
				spokes[spokeIndex].IntersectionPos
			};

			if ( currentEdgeIndex == nextEdgeIndex )
			{
				subShard.Add( spokes[nextSpokeIndex].IntersectionPos );
			}
			else
			{
				for ( int i = 0; i < 32 && currentEdgeIndex != nextEdgeIndex; i++ )
				{
					subShard.Add( edgeSegments[currentEdgeIndex].End );

					currentEdgeIndex = currentEdgeIndex < edgeSegments.Count - 1 ? currentEdgeIndex + 1 : 0;
				}

				subShard.Add( spokes[nextSpokeIndex].IntersectionPos );
			}

			Assert.True( subShard.Count >= 3 );

			var tipPoint1 = Vector2.Lerp( subShard[0], subShard[1], Game.Random.Float( shatterType.TipScaleMin, shatterType.TipScaleMax ) );
			var tipPoint2 = Vector2.Lerp( subShard[0], subShard[^1], Game.Random.Float( shatterType.TipScaleMin, shatterType.TipScaleMax ) );

			centerHoleVertices.Add( Vector2.Lerp( tipPoint1, tipPoint2, 0.5f ) );

			if ( shatterType.TipSpawnChance > 0 && Game.Random.Float( 0, shatterType.TipSpawnChance ) < 1.0f )
			{
				var tipShard = new List<Vector2>
				{
					subShard[0],
					tipPoint1,
					tipPoint2
				};
				ScaleVerts( tipShard, shatterType.TipScale );
				shards.Add( CreateShard( transform, tipShard ) );
			}

			if ( shatterType.SecondTipSpawnChance > 0 && Game.Random.Float( 0, shatterType.SecondTipSpawnChance ) < 1.0f )
			{
				var secondTipPoint1 = Vector2.Lerp( tipPoint1, subShard[1], Game.Random.Float( 0.2f, 0.5f ) );
				var secondTopPoint2 = Vector2.Lerp( tipPoint2, subShard[^1], Game.Random.Float( 0.2f, 0.5f ) );

				var tipShard = new List<Vector2>
				{
					tipPoint1,
					secondTipPoint1,
					secondTopPoint2,
					tipPoint2
				};
				ScaleVerts( tipShard, shatterType.SecondShardScale );
				shards.Add( CreateShard( transform, tipShard ) );

				tipPoint1 = secondTipPoint1;
				tipPoint2 = secondTopPoint2;
			}

			subShard.RemoveAt( 0 );
			subShard.Insert( 0, tipPoint1 );
			subShard.Add( tipPoint2 );

			if ( (tipPoint1 - tipPoint2).LengthSquared > 9.0f )
			{
				var vecBetweenCorners = Vector2.Lerp( Vector2.Lerp( tipPoint1, tipPoint2, Game.Random.Float( 0.4f, 0.6f ) ), stressPosition, Game.Random.Float( 0.1f, 0.3f ) );
				subShard.Add( vecBetweenCorners );
			}

			ScaleVerts( subShard, shatterType.ShardScale );
			shards.Add( CreateShard( transform, subShard ) );
		}

		if ( shatterType.HasCenterChunk && centerHoleVertices.Count > 2 )
		{
			var pShardCenter = new List<Vector2>();

			foreach ( var vertex in centerHoleVertices )
			{
				pShardCenter.Add( vertex );
			}

			ScaleVerts( pShardCenter, shatterType.CenterChunkScale );
			shards.Add( CreateShard( transform, pShardCenter ) );
		}

		return shards;
	}

	private static void ScaleVerts( List<Vector2> points, float scale )
	{
		if ( scale <= 0.0f )
			return;

		var average = Vector2.Zero;
		var pointCount = points.Count;

		if ( pointCount > 0 )
		{
			foreach ( var point in points )
			{
				average += point;
			}

			average /= pointCount;
		}

		for ( int i = 0; i < points.Count; ++i )
		{
			points[i] = Vector2.Lerp( average, points[i], scale );
		}
	}

	public Model CreateModel( List<Vector2> points )
	{
		var renderData = new RenderData();
		renderData.Init( points.Count );

		var average = Vector2.Zero;
		var pointCount = points.Count;

		if ( pointCount > 0 )
		{
			foreach ( var point in points )
			{
				average += point;
			}

			average /= pointCount;
		}

		float halfThickness = Thickness * 0.5f;

		renderData.Vertices.Add( new Vector3( average.x, average.y, halfThickness ) );

		for ( var i = 0; i < renderData.FaceVertexCount - 1; i++ )
		{
			renderData.Vertices.Add( new Vector3( points[i].x, points[i].y, halfThickness ) );
		}

		renderData.Vertices.Add( new Vector3( average.x, average.y, -halfThickness ) );

		for ( var i = 0; i < renderData.FaceVertexCount - 1; i++ )
		{
			renderData.Vertices.Add( new Vector3( points[i].x, points[i].y, -halfThickness ) );
		}

		var modelBuilder = new ModelBuilder();
		modelBuilder.AddCollisionHull( renderData.Vertices.ToArray() );

		for ( var i = 0; i < renderData.EdgeQuadCount; i++ )
		{
			var next = (i < renderData.EdgeQuadCount - 1) ? i + 1 : 0;
			renderData.Vertices.Add( new Vector3( points[i].x, points[i].y, -halfThickness ) );
			renderData.Vertices.Add( new Vector3( points[next].x, points[next].y, -halfThickness ) );
			renderData.Vertices.Add( new Vector3( points[next].x, points[next].y, halfThickness ) );
			renderData.Vertices.Add( new Vector3( points[i].x, points[i].y, halfThickness ) );
		}

		renderData.EdgeVerticesStart = renderData.TotalShardVertices - renderData.EdgeVertexCount;

		Assert.AreEqual( renderData.Vertices.Count, renderData.TotalShardVertices );

		return modelBuilder.AddMesh( CreateMesh( renderData ) )
			.Create();
	}

	private static readonly Vector2[] EdgeUVs = new[]
	{
		new Vector2( 0.0f, 0.0f ),
		new Vector2( 0.0f, 0.01f ),
		new Vector2( 0.01f, 0.01f ),
		new Vector2( 0.01f, 0.0f )
	};

	private struct RenderData
	{
		public List<Vector3> Vertices;

		public int TotalShardVertices;
		public int TotalSharedIndices;
		public int EdgeVerticesStart;
		public int FaceVertexCount;
		public int FaceTriangleCount;
		public int FaceIndexCount;
		public int EdgeQuadCount;
		public int EdgeVertexCount;
		public int EdgeTriangleCount;
		public int EdgeIndexCount;

		public void Init( int numPanelVerts )
		{
			FaceVertexCount = numPanelVerts + 1;
			FaceTriangleCount = FaceVertexCount - 1;
			FaceIndexCount = FaceTriangleCount * 3;

			EdgeQuadCount = FaceVertexCount - 1;
			EdgeVertexCount = EdgeQuadCount * 4;
			EdgeTriangleCount = EdgeQuadCount * 2;
			EdgeIndexCount = EdgeTriangleCount * 3;

			TotalShardVertices = FaceVertexCount + FaceVertexCount + EdgeVertexCount;
			TotalSharedIndices = FaceIndexCount + FaceIndexCount + EdgeIndexCount;

			Vertices = new List<Vector3>( (FaceVertexCount * 2) + EdgeVertexCount );
		}
	};

	private Mesh CreateMesh( RenderData renderData )
	{
		var vertices = new Vertex[renderData.TotalShardVertices];
		var indices = new int[renderData.TotalSharedIndices];
		var bounds = new BBox();

		for ( var i = 0; i < renderData.TotalShardVertices; i++ )
		{
			vertices[i].Position = renderData.Vertices[i];
			bounds = bounds.AddPoint( vertices[i].Position );

			var vertexPos = new Vector3( renderData.Vertices[i].x, renderData.Vertices[i].y, 0 );
			var u = Vector3.Dot( TextureAxisU, vertexPos ) / TextureScale.x;
			var v = Vector3.Dot( TextureAxisV, vertexPos ) / TextureScale.y;

			u += TextureOffset.x;
			v += TextureOffset.y;

			u /= TextureSize.x;
			v /= TextureSize.y;

			var uv = new Vector2( u, v );

			vertices[i].TexCoord0 = uv;
			vertices[i].TexCoord1 = vertexPos;

			if ( i < renderData.EdgeVerticesStart )
			{
				vertices[i].Color = Vector3.Zero;
			}
			else
			{
				vertices[i].TexCoord0 += EdgeUVs[i % 4];
				vertices[i].TexCoord1 += EdgeUVs[i % 4];
				vertices[i].Color[0] = 1;
				vertices[i].Color[1] = 0;
				vertices[i].Color[2] = 0;
			}
		}

		ComputeTriangleNormalAndTangent( out var normalSideA, out var tangentSideA,
			vertices[1].Position, vertices[0].Position, vertices[2].Position,
			vertices[1].TexCoord1, vertices[0].TexCoord1, vertices[2].TexCoord1 );

		ComputeTriangleNormalAndTangent( out var normalSideB, out var tangentSideB,
			vertices[renderData.FaceVertexCount].Position, vertices[renderData.FaceVertexCount + 1].Position, vertices[renderData.FaceVertexCount + 2].Position,
			vertices[renderData.FaceVertexCount].TexCoord1, vertices[renderData.FaceVertexCount + 1].TexCoord1, vertices[renderData.FaceVertexCount + 2].TexCoord1 );

		for ( var i = 0; i < renderData.FaceTriangleCount; i++ )
		{
			var index = i * 3;
			var offset0 = i + 1;
			var offset1 = (i + 2 < renderData.FaceVertexCount) ? i + 2 : 1;
			var offset2 = 0;

			indices[index] = offset1;
			indices[index + 1] = offset0;
			indices[index + 2] = offset2;

			vertices[offset0].Normal = normalSideA;
			vertices[offset1].Normal = normalSideA;
			vertices[offset2].Normal = normalSideA;

			vertices[offset0].Tangent = tangentSideA;
			vertices[offset1].Tangent = tangentSideA;
			vertices[offset2].Tangent = tangentSideA;

			index += renderData.FaceIndexCount;
			offset0 += renderData.FaceVertexCount;
			offset1 += renderData.FaceVertexCount;
			offset2 = renderData.FaceVertexCount;

			indices[index] = offset0;
			indices[index + 1] = offset1;
			indices[index + 2] = offset2;

			vertices[offset0].Normal = normalSideB;
			vertices[offset1].Normal = normalSideB;
			vertices[offset2].Normal = normalSideB;

			vertices[offset0].Tangent = tangentSideB;
			vertices[offset1].Tangent = tangentSideB;
			vertices[offset2].Tangent = tangentSideB;
		}

		var edgeIndexOffset = renderData.TotalSharedIndices - renderData.EdgeIndexCount;
		for ( var i = 0; i < renderData.EdgeQuadCount; i++ )
		{
			var index = edgeIndexOffset + (i * 6);
			var vertexOffset = renderData.EdgeVerticesStart + (i * 4);

			indices[index] = vertexOffset + 2;
			indices[index + 1] = vertexOffset + 1;
			indices[index + 2] = vertexOffset;

			indices[index + 3] = vertexOffset + 3;
			indices[index + 4] = vertexOffset + 2;
			indices[index + 5] = vertexOffset;

			ComputeTriangleNormalAndTangent( out var faceNormal, out var faceTangent,
				vertices[vertexOffset + 2].Position, vertices[vertexOffset + 1].Position, vertices[vertexOffset].Position,
				vertices[vertexOffset + 2].TexCoord1, vertices[vertexOffset + 1].TexCoord1, vertices[vertexOffset].TexCoord1 );

			vertices[vertexOffset].Normal = faceNormal;
			vertices[vertexOffset + 1].Normal = faceNormal;
			vertices[vertexOffset + 2].Normal = faceNormal;
			vertices[vertexOffset + 3].Normal = faceNormal;

			vertices[vertexOffset].Tangent = faceTangent;
			vertices[vertexOffset + 1].Tangent = faceTangent;
			vertices[vertexOffset + 2].Tangent = faceTangent;
			vertices[vertexOffset + 3].Tangent = faceTangent;
		}

		var mesh = new Mesh( Material ?? Material.Load( "materials/glass.vmat" ) );
		mesh.CreateVertexBuffer<Vertex>( vertices.Length, Vertex.Layout, vertices );
		mesh.CreateIndexBuffer( indices.Length, indices );
		mesh.Bounds = bounds;

		return mesh;
	}

	private struct ShatterType
	{
		public int SpokesMin;
		public int SpokesMax;
		public float TipScaleMin;
		public float TipScaleMax;
		public float TipSpawnChance;
		public float TipScale;
		public float ShardScale;
		public float SecondTipSpawnChance;
		public float SecondShardScale;
		public bool HasCenterChunk;
		public float CenterChunkScale;
		public int ShardLimit;

		public ShatterType(
			int spokesMin,
			int spokesMax,
			float tipScaleMin,
			float tipScaleMax,
			float tipSpawnChance,
			float tipScale,
			float shardScale,
			float secondTipSpawnChance,
			float secondShardScale,
			bool hasCenterChunk,
			float centerChunkScale,
			int shardLimit )
		{
			SpokesMin = spokesMin;
			SpokesMax = spokesMax;
			TipScaleMin = tipScaleMin;
			TipScaleMax = tipScaleMax;
			TipSpawnChance = tipSpawnChance;
			TipScale = tipScale;
			ShardScale = shardScale;
			SecondTipSpawnChance = secondTipSpawnChance;
			SecondShardScale = secondShardScale;
			HasCenterChunk = hasCenterChunk;
			CenterChunkScale = centerChunkScale;
			ShardLimit = shardLimit;
		}
	};

	private static readonly ShatterType[] ShatterTypes = new[]
	{
		new ShatterType( 5,    10,     0.2f,   0.5f,   1.0f,   0.95f,  1.0f,   8.0f,    0.98f,     false,  0.0f,   4   ),
		new ShatterType( 8,    14,     0.1f,   0.3f,   3.0f,   0.95f,  1.0f,   16.0f,   0.98f,     false,  0.0f,   4   ),
		new ShatterType( 8,    10,     0.4f,   0.6f,   0.0f,   0.95f,  0.95f,  1.2f,    0.98f,     false,  0.0f,   2   ),
		new ShatterType( 20,   20,     0.7f,   0.99f,  3.0f,   0.95f,  1.0f,   16.0f,   0.98f,     false,  0.9f,   10  ),
	};

	private static Vector4 ComputeTangentForFace( Vector3 faceS, Vector3 faceT, Vector3 normal )
	{
		var leftHanded = Vector3.Dot( Vector3.Cross( faceS, faceT ), normal ) < 0.0f;
		var tangent = Vector4.Zero;

		if ( !leftHanded )
		{
			faceT = Vector3.Cross( normal, faceS );
			faceS = Vector3.Cross( faceT, normal );
			faceS = faceS.Normal;

			tangent.x = faceS[0];
			tangent.y = faceS[1];
			tangent.z = faceS[2];
			tangent.w = 1.0f;
		}
		else
		{
			faceT = Vector3.Cross( faceS, normal );
			faceS = Vector3.Cross( normal, faceT );
			faceS = faceS.Normal;

			tangent.x = faceS[0];
			tangent.y = faceS[1];
			tangent.z = faceS[2];
			tangent.w = -1.0f;
		}

		return tangent;
	}

	private static Vector3 ComputeTriangleNormal( Vector3 v1, Vector3 v2, Vector3 v3 )
	{
		var e1 = v2 - v1;
		var e2 = v3 - v1;

		return (Vector3.Cross( e1, e2 ).Normal + (Vector3.Random * 0.1f)).Normal;
	}

	private static void ComputeTriangleTangentSpace( Vector3 p0, Vector3 p1, Vector3 p2, Vector2 t0, Vector2 t1, Vector2 t2, out Vector3 s, out Vector3 t )
	{
		const float epsilon = 1e-12f;

		s = Vector3.Zero;
		t = Vector3.Zero;

		var edge0 = new Vector3( p1.x - p0.x, t1.x - t0.x, t1.y - t0.y );
		var edge1 = new Vector3( p2.x - p0.x, t2.x - t0.x, t2.y - t0.y );

		var cross = Vector3.Cross( edge0, edge1 );

		if ( MathF.Abs( cross.x ) > epsilon )
		{
			s.x += -cross.y / cross.x;
			t.x += -cross.z / cross.x;
		}

		edge0 = new Vector3( p1.y - p0.y, t1.x - t0.x, t1.y - t0.y );
		edge1 = new Vector3( p2.y - p0.y, t2.x - t0.x, t2.y - t0.y );

		cross = Vector3.Cross( edge0, edge1 );

		if ( MathF.Abs( cross.x ) > epsilon )
		{
			s.y += -cross.y / cross.x;
			t.y += -cross.z / cross.x;
		}

		edge0 = new Vector3( p1.z - p0.z, t1.x - t0.x, t1.y - t0.y );
		edge1 = new Vector3( p2.z - p0.z, t2.x - t0.x, t2.y - t0.y );

		cross = Vector3.Cross( edge0, edge1 );

		if ( MathF.Abs( cross.x ) > epsilon )
		{
			s.z += -cross.y / cross.x;
			t.z += -cross.z / cross.x;
		}

		s = s.Normal;
		t = t.Normal;
	}

	private static void ComputeTriangleNormalAndTangent( out Vector3 outNormal, out Vector4 outTangent, Vector3 v0, Vector3 v1, Vector3 v2, Vector2 uv0, Vector2 uv1, Vector2 uv2 )
	{
		outNormal = ComputeTriangleNormal( v0, v1, v2 );
		ComputeTriangleTangentSpace( v0, v1, v2, uv0, uv1, uv2, out var faceS, out var faceT );
		outTangent = ComputeTangentForFace( faceS, faceT, outNormal );
	}

	private static bool LineIntersect( Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, out Vector2 intersection )
	{
		intersection = Vector2.Zero;

		float xD1, yD1, xD2, yD2, xD3, yD3;
		float dot, deg, length1, length2;
		float segmentLength1, segmentLength2;
		float ua, div;

		xD1 = p2.x - p1.x;
		xD2 = p4.x - p3.x;
		yD1 = p2.y - p1.y;
		yD2 = p4.y - p3.y;
		xD3 = p1.x - p3.x;
		yD3 = p1.y - p3.y;

		length1 = MathF.Sqrt( xD1 * xD1 + yD1 * yD1 );
		length2 = MathF.Sqrt( xD2 * xD2 + yD2 * yD2 );

		dot = (xD1 * xD2 + yD1 * yD2);
		deg = dot / (length1 * length2);

		if ( Math.Abs( deg ) == 1.0f )
		{
			return false;
		}

		div = yD2 * xD1 - xD2 * yD1;
		ua = (xD2 * yD3 - yD2 * xD3) / div;
		intersection.x = p1.x + ua * xD1;
		intersection.y = p1.y + ua * yD1;

		xD1 = intersection.x - p1.x;
		xD2 = intersection.x - p2.x;
		yD1 = intersection.y - p1.y;
		yD2 = intersection.y - p2.y;
		segmentLength1 = MathF.Sqrt( xD1 * xD1 + yD1 * yD1 ) + MathF.Sqrt( xD2 * xD2 + yD2 * yD2 );

		xD1 = intersection.x - p3.x;
		xD2 = intersection.x - p4.x;
		yD1 = intersection.y - p3.y;
		yD2 = intersection.y - p4.y;
		segmentLength2 = MathF.Sqrt( xD1 * xD1 + yD1 * yD1 ) + MathF.Sqrt( xD2 * xD2 + yD2 * yD2 );

		if ( MathF.Abs( length1 - segmentLength1 ) > 0.01f || MathF.Abs( length2 - segmentLength2 ) > 0.01f )
		{
			return false;
		}

		return true;
	}

	private static bool IsPathClockwise( IList<Vector2> points )
	{
		float area = 0;
		for ( int i = 0; i < points.Count; i++ )
		{
			int j = (i + 1) % points.Count;
			area += (points[j].x - points[i].x) * (points[j].y + points[i].y);
		}

		return area < 0;
	}
}