holopy3/Assets/ShatterableGlass/Scripts/ShatterableGlass.cs

517 lines
18 KiB
C#
Raw Permalink Normal View History

2020-12-10 14:25:12 +00:00
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ShatterableGlass : MonoBehaviour
{
// !!!
// Before trying to read or modify code it is strongly recommended to read Readme.pdf.
// It contains detailed explanations with images and formulas.
// !!!
// Sector per side.
public int Sectors = 3;
// Figures per sector.
public int DetailsPerSector = 4;
// Sectors with area smaller than Area * SimplifyThreshold will be trimmed to simple triangle.
public float SimplifyThreshold = 0.05f;
// Generate Glass sides?
public bool GlassSides = true;
// Material of that sides.
public Material GlassSidesMaterial;
public float GlassThickness = 0.025f;
// Draw net, but not break?
public bool ShatterButNotBreak = false;
// A bit realistic effect, if glass not breakble?
public bool SlightlyRotateGibs = true;
// Destroy gibs?
public bool DestroyGibs = true;
// After time, seconds.
public float AfterSeconds = 10f;
// Move Gibs on a separate layer?
public bool GibsOnSeparateLayer = false;
// Index of that layer.
public int GibsLayer = 0;
// Maximum force applied to glass gibs.
public float Force = 100f;
// Should glass fragments have same parent?
public bool AdoptFragments = false;
// Abstract bounds of the glass.
Vector2[] Bounds = new Vector2[4];
// Area of the glass. Calculated at Start().
float Area = 1f;
// Original glass material. Same will be applied to glass gibs.
Material GlassMaterial;
// AudioSource of break sound.
AudioSource SoundEmitter;
// Using this for initialization
void Start()
{
// Dimmentions of the glass. Please refer to Figure 2.1. in Readme.pdf
float u = Mathf.Abs(transform.lossyScale.x / 2f);
float v = Mathf.Abs(transform.lossyScale.y / 2f);
// Calculate area.
Area = u * v;
// Corners of the glass.
Bounds[0] = new Vector2(u, v);
Bounds[1] = new Vector2(-u, v);
Bounds[2] = new Vector2(-u, -v);
Bounds[3] = new Vector2(u, -v);
SoundEmitter = GetComponent<AudioSource>();
// Check if Renderer and MeshFilter present.
if (GetComponent<Renderer>() == null || GetComponent<MeshFilter>() == null)
{
Debug.LogError(gameObject.name + ": No Renderer and/or MeshFilter components!");
Destroy(gameObject);
return;
}
// Original glass's material will be applied to glass gibs.
GlassMaterial = GetComponent<Renderer>().material;
// Throw an error, if second material required, but not set.
if (GlassSides && GlassSidesMaterial == null)
{
Debug.LogError(gameObject.name + ": GlassSide material must be assigned! Glass will be destroyed.");
Destroy(gameObject);
}
}
// Function for SendMessage usage.
// HitPoint is point in local glass coordinates. Typically {0, 0} (center). Force applied towards local z axis.
public void Shatter2D(Vector2 HitPoint)
{
Shatter(HitPoint, transform.forward);
}
// Converts Global 3D point to local 2D point and calls Shatter(). Please refer to Figure 2.2.
public void Shatter3D(ShatterableGlassInfo Inf)
{
// Check if any of parents have non-{1, 1, 1} scale.
// If any of parents have wrong scale conversion and shattering will be incorrect.
Transform Parent = gameObject.transform.parent;
bool Sucsess = true;
while (Parent != null)
{
if (Parent.localScale.x != 1f || Parent.localScale.y != 1f || Parent.localScale.y != 1f)
Sucsess = false;
Parent = Parent.parent;
}
// Using lossyScale may cause problems, throw warning.
if (!Sucsess)
Debug.LogWarning(gameObject.name + ": scale of all parents in hierarchy recommended to be {1, 1, 1}. Glass may shatter weirdly.");
// There we use triangle to determine 2D point. Please refer to figure 2.2.
// Local bottom left and bottom right points.
Vector3 A = transform.TransformPoint(new Vector3(-0.5f, -0.5f));
Vector3 B = transform.TransformPoint(new Vector3(0.5f, -0.5f));
// Sides of triangle.
float b = Vector3.Distance(Inf.HitPoint, A);
float c = Vector3.Distance(B, A);
float a = Vector3.Distance(Inf.HitPoint, B);
// Half perimeter.
float p = (a + b + c) / 2f;
// Area.
float S = Mathf.Sqrt(p * (p - a) * (p - b) * (p - c));
// Height of the triangle.
float h = 2 / c * S;
// Calculate u(x) using Pythagorean theorem.
float u = Mathf.Sqrt(b * b - h * h);
h -= Mathf.Abs(transform.lossyScale.y / 2f);
u -= Mathf.Abs(transform.lossyScale.x / 2f);
// Finaly, call Shatter().
Shatter(new Vector2(u * Mathf.Sign(transform.lossyScale.x), h * Mathf.Sign(transform.lossyScale.y)), Inf.HitDirrection);
}
// Main function. HitPoint is local glass's coordinates.
public void Shatter(Vector2 HitPoint, Vector3 ForceDirrection)
{
// 4 BaseLines for 4 courners. Plus Sectors per side (4 sides).
int BaseLinesCount = 4 + (Sectors - 1) * 4;
BaseLine[] BaseLines = new BaseLine[BaseLinesCount];
// For each side:
for (int j = 0; j < 4; j++)
{
// BaseLine from HitPoint to corner.
BaseLines[j * Sectors] = new BaseLine(HitPoint, Bounds[j], DetailsPerSector);
// Calculate Ratio. For example for FiguresPerSector == 4 Ratio must increase by 0.25;
float Margin = 1f / Sectors;
float Ratio = Margin;
for (int i = 1; i < Sectors; i++)
{
// Rest BaseLines per side.
BaseLines[j * Sectors + i] = new BaseLine(HitPoint, Vector2.Lerp(Bounds[j], Bounds[(j + 1) % 4], Ratio), DetailsPerSector);
Ratio += Margin;
}
}
// Figures.
List<Figure> Figures = new List<Figure>();
// 2 BaseLines constructs sector. usually FiguresPerSector Figures generated per sector.
// But if area of sector is relatively small it is replaced by single triangle.
// Please refer to figure 2.3.
// For Each BaseLine:
for (int i = 0; i < BaseLinesCount; i++)
{
//Index of next BaseLine. In last itteration used first BaseLine.
int k = (i + 1) % BaseLinesCount;
// Calculate sector area.
// Sides of triangle.
float a = Vector2.Distance(HitPoint, BaseLines[i].Points[DetailsPerSector]);
float b = Vector2.Distance(HitPoint, BaseLines[k].Points[DetailsPerSector]);
float c = Vector2.Distance(BaseLines[i].Points[DetailsPerSector], BaseLines[k].Points[DetailsPerSector]);
// Half perimeter.
float p = (a + b + c) * 0.5f;
// Area by Heron's formula.
float S = Mathf.Sqrt(p * (p - a) * (p - b) * (p - c));
// If Area is smaller than Sqare * Threshold.
if (S < Area * SimplifyThreshold)
Figures.Add(new Figure(new Vector2[] { BaseLines[i].Points[DetailsPerSector], BaseLines[k].Points[DetailsPerSector], HitPoint }, DetailsPerSector / 2));
else
{
//First triangle Generation.
Figures.Add(new Figure(new Vector2[] { BaseLines[i].Points[1], BaseLines[k].Points[1], HitPoint }, 1));
// Rest trapeze generation.
for (int j = 1; j < DetailsPerSector; j++)
{
// 4 points of trapeze.
Vector2[] Points = new Vector2[4];
Points[0] = BaseLines[i].Points[j];
Points[1] = BaseLines[(i + 1) % BaseLinesCount].Points[j];
Points[2] = BaseLines[i].Points[j + 1];
Points[3] = BaseLines[(i + 1) % BaseLinesCount].Points[j + 1];
Figures.Add(new Figure(Points, i + 1));
}
}
}
// Generate Mesh for each Figure.
foreach (Figure Fig in Figures)
{
GlassMaterial = GetComponent<Renderer>().material;
GameObject Obj = new GameObject("GlassGib");
// Apply original glass's transform.
Obj.transform.rotation = transform.rotation;
if (gameObject.name == "PySide1")
{
Obj.transform.position = transform.position + new Vector3(20, 10, 10);
}
else if (gameObject.name == "PySide2")
{
Obj.transform.position = transform.position + new Vector3(10, 10, -20);
}
else if (gameObject.name == "PySide3")
{
Obj.transform.position = transform.position + new Vector3(-10, 10, 20);
}
else if (gameObject.name == "PySide4")
{
Obj.transform.position = transform.position + new Vector3(-20, 10, -10);
}
if (AdoptFragments)
Obj.transform.parent = transform.parent;
MeshFilter Filter = Obj.AddComponent<MeshFilter>();
// Create gib's renderer and assign material(s).
MeshRenderer Rnd = Obj.AddComponent<MeshRenderer>();
if (GlassSides)
// First Material is original glass's material, secnd is GlassSideMaterial.
Rnd.materials = new Material[2] { GlassMaterial, GlassSidesMaterial };
else
Rnd.material = GlassMaterial;
Mesh Model = Fig.GenerateMesh(GlassSides, GlassThickness / 2f, new Vector2(transform.lossyScale.x, transform.lossyScale.y));
Filter.sharedMesh = Model;
// if gib must be Rigidbody:
if (!ShatterButNotBreak)
{
Fig.GenerateCollider(GlassThickness, Obj);
Rigidbody Rig = Obj.AddComponent<Rigidbody>();
// Apply Force. Closer to HitPoint - greater force.
Rig.AddForce(ForceDirrection * Random.Range(Force, Force * 1.5f) / Fig.ForceScale);
if (GibsOnSeparateLayer)
Obj.layer = GibsLayer;
if (DestroyGibs)
{
float AfterSecondsMargin = AfterSeconds * 0.1f;
Destroy(Obj, Random.Range(AfterSeconds - AfterSecondsMargin, AfterSeconds + AfterSecondsMargin));
}
}
else if (SlightlyRotateGibs)
// Slightly rotate gib.
// Rotation around glass's center.
Obj.transform.Rotate(new Vector3(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f)));
}
// Play sound, if AudioSource component is attached.
if (SoundEmitter)
SoundEmitter.Play();
// Remove useless components
Destroy(GetComponent<Renderer>());
Destroy(GetComponent<MeshFilter>());
Destroy(GetComponent<ShatterableGlass>());
if (ShatterButNotBreak)
// Stop access by Gun.
gameObject.tag = "Untagged";
else
{
//Stop colliding
Destroy(GetComponent<MeshCollider>());
// Destroy original glass after sound stops or now, if AudioSource not set.
if (SoundEmitter)
{
if (SoundEmitter.clip)
Destroy(gameObject, SoundEmitter.clip.length);
else
Debug.Log(gameObject.name + ": AudioSource component is present, but SoundClip is not set.");
}
else
Destroy(gameObject);
}
}
// Figure class. Figure consists of 3(triangle) or 4(trapeze) points.
// Multiple Figures created betwin 2 BaseLines.
class Figure
{
public Vector2[] Points;
// Closer to HitPoint lower should be this value. Applied force will be divided by this value.
public int ForceScale;
// Constructor.
public Figure(Vector2[] Points, int ForceScale)
{
this.Points = Points;
this.ForceScale = ForceScale;
}
// Generates BoxCollider for GameObject Obj.
// Method made for triangles but can be applied to trapeze since it consists of 2 triangles.
// Please refer to Figure 2.7.
public void GenerateCollider(float GlassThickness, GameObject Obj)
{
BoxCollider Col = Obj.AddComponent<BoxCollider>();
// Sides of triangle. Please refer to figure XX.
float a = Vector2.Distance(Points[2], Points[0]);
float b = Vector2.Distance(Points[2], Points[1]);
float c = Vector2.Distance(Points[1], Points[0]);
// Perimeter
float p = a + b + c;
// Incircle coordinates.
float ox = (a * Points[0].x + b * Points[1].x + c * Points[2].x) / p;
float oy = (a * Points[0].y + b * Points[1].y + c * Points[2].y) / p;
// Radiuse of that incircle.
p /= 2f;
float r = Mathf.Sqrt(((p - a) * (p - b) * (p - c)) / p);
// Insqare of that circle.
r *= Mathf.Sqrt(2);
// Apply calculations.
Col.center = new Vector3(ox, oy, 0f);
Col.size = new Vector3(r, r, GlassThickness);
}
// Generates Mesh from Poins[].
// Mesh UV mapped to original glass.
// Please refer to Figure 2.6.
public Mesh GenerateMesh(bool GenerateGlassSides, float GlassHalfThickness, Vector2 UVScale)
{
Mesh Model = new Mesh();
Model.name = "GlassGib";
// If sides needs to be generated, must will consist of 2 submesh. Each submesh rendered with corresponding material.
if (GenerateGlassSides)
Model.subMeshCount = 2;
bool IsTriangle = Points.Length == 3;
// Size of Vertices[] depends on figure: 3 vertices for triangle or 4 for trapeze.
// Plus 6 or 8 for sides.
// Please refer to Figure 2.5.
Vector3[] Vertices = new Vector3[IsTriangle ? GenerateGlassSides ? 9 : 3 : GenerateGlassSides ? 12 : 4];
// Size of Map[] MUST be the same although we assign only 3 or 4 vertices.
Vector2[] Map = new Vector2[Vertices.Length];
for (int i = 0; i < Points.Length; i++)
{
Vertices[i] = Points[i];
// As UV lies within {0, 1}, Point must be downscaled.
Map[i] = new Vector2(Points[i].x / UVScale.x, Points[i].y / UVScale.y) + new Vector2(0.5f, 0.5f);
}
// Triangles represented as triplet of vertex indices.
int[] MainTriangles;
if (IsTriangle)
MainTriangles = new int[3] { 2, 1, 0 }; // Indexes are always the same for each specific case.
else
MainTriangles = new int[6] { 0, 1, 2, 3, 2, 1 };
if (GenerateGlassSides)
{
// Triangles for sides. 2 triangle per edge.
int[] TrianglesSide;
if (IsTriangle)
{
for (int i = 0; i < 3; i++)
GlassSideVertex(Points[i], ref Vertices[i * 2 + 3], ref Vertices[i * 2 + 4], GlassHalfThickness);
TrianglesSide = new int[18] { 3, 4, 5, 4, 6, 5, 3, 4, 7, 7, 8, 4, 5, 6, 8, 8, 7, 5 };
}
else
{
for (int i = 0; i < 4; i++)
GlassSideVertex(Points[i], ref Vertices[i * 2 + 4], ref Vertices[i * 2 + 5], GlassHalfThickness);
TrianglesSide = new int[24] { 7, 5, 4, 6, 7, 4, 11, 7, 6, 10, 11, 6, 10, 11, 9, 9, 8, 10, 8, 9, 5, 8, 4, 5 };
}
// Apply Vertices and triangles.
Model.vertices = Vertices;
Model.SetTriangles(MainTriangles, 0);
Model.SetTriangles(TrianglesSide, 1);
}
else
{
// Apply Vertices and triangles.
Model.vertices = Vertices;
Model.triangles = MainTriangles;
}
// Apply UV map.
Model.uv = Map;
return Model;
}
// Offsets point Ref by +-GlassHalfThickness at z axis.
void GlassSideVertex(Vector2 Ref, ref Vector3 A, ref Vector3 B, float GlassHalfThickness)
{
A = new Vector3(Ref.x, Ref.y, GlassHalfThickness);
B = new Vector3(Ref.x, Ref.y, -GlassHalfThickness);
}
}
// Baseline class. Basically divides line between HotPoint and End by Count.
// Please refer to Figure 2.3.
class BaseLine
{
public Vector2[] Points;
public BaseLine(Vector2 HitPoint, Vector2 End, int Count)
{
Points = new Vector2[Count + 1];
Points[0] = HitPoint;
Points[Count] = End;
float Margin = 1f / Count;
float Ratio = Margin;
// Roll calculation. All calculations in radians. Please refer to figure 2.4.
// Angle of line at first quadrant.
float Angle = Mathf.Atan2(Mathf.Max(HitPoint.y, End.y) - Mathf.Min(HitPoint.y, End.y), Mathf.Max(End.x, HitPoint.x) - Mathf.Min(HitPoint.x, End.x));
// 45deg. Maximum angle.
float Pi4 = Mathf.PI / 4f;
// 90 deg.
float Pi2 = Mathf.PI / 2f;
// Invert angle relatively to 45 deg.
if (Angle > Pi4)
{
Angle = Pi2 - Angle;
}
// Inverse interpolation. 0 deg - 0, 45 deg - 1;
float Roll = Angle / Pi4;
for (int i = 0; i < Count - 1; i++)
{
// Interpolate between HitPoint and End by Ratio. Ratio depends on Roll.
Points[i + 1] = Vector2.Lerp(HitPoint, End, Ratio * Mathf.Lerp(1f, Mathf.Sqrt(2) / 2f, Roll));
Ratio += Margin;
}
}
}
}
// Class to be use as SendMessage argument.
public class ShatterableGlassInfo
{
public Vector3 HitPoint;
// Force will be multiplied by HitDirrection vector.
public Vector3 HitDirrection;
public ShatterableGlassInfo(Vector3 HitPoint, Vector3 HitDirrection)
{
this.HitPoint = HitPoint;
this.HitDirrection = HitDirrection;
}
}