Breaking glass effect PART IV

In this part of the Breaking glass effect series of posts I will present and explain how I implemented a simple mesh-breaking mechanism into a number of sub-meshes -the glass shards.


The glass breaking behaviour is realised by a C# script component called 'calcCollisionImpact', which is attached to the glass object (named 'GlassTerrain').


Public and private variables

The script exposes some public variables that the developer sets through the Unity Editor:


Public class calcCollisionImpact : MonoBehaviour {
    // Rule: The BreakingForceThreshold should always be greater than the ShutteringForceThreshold
    public float CrackingForceThreshold;
    public float BreakingForceThreshold;
    
    private bool cracked = false;
    private bool destroyed = false;
    private GameObject[] pieces;
    public Material shardMaterial;
    public Material BrokenGlassMaterial;
    public PhysicMaterial shardPhysicMaterial;
    public AudioClip GlassBreakingSound;

...

The 2 threshold values define the impact force limits that will result in cracking the glass (CrackingForceThresold) and breaking it in pieces (BreakingForceThreshold). Private variables (properties) cracked and destroyed flag the corresponding boolean states of the object. An array, pieces, holds the sub-meshes of the glass shards. Thus all such pieces can be easily controlled as one object, and be destroyed easily. The next public variables setup the glass object's components, and provide the rendering material, physic material and sound.


Glass object collision

The main function implemented is OnCollisionEnter of the glass object's simple box collider:


 // The gameobject that this script is attached to has collided with something:
 // Decide whether the object is just bruised or broken into pieces
 void OnCollisionEnter(Collision collision) {
  Debug.Log ("First collision point: " + collision.contacts[0].point.ToString());
  Debug.Log ("Impact force: " + collision.impactForceSum.ToString());
  MeshFilter filter = GetComponentInChildren(typeof(MeshFilter)) as MeshFilter;
  //Visualise the contact point
  Vector3 transformedPoint = new Vector3();
  transformedPoint = transform.worldToLocalMatrix.MultiplyPoint(collision.contacts[0].point);
  Debug.Log ("Collision point in local coords: " + transformedPoint.ToString() );
  // check if the force of the collision is greater than the shuttering threshold of the collided object
  if (collision.impactForceSum.magnitude > BreakingForceThreshold) {
   if (cracked)
    SimpleExplosion(collision.impactForceSum, transformedPoint);
   else {
    SimpleCrack(collision.impactForceSum, transformedPoint);
    SimpleExplosion(collision.impactForceSum, transformedPoint);
   }
  }
  else if (collision.impactForceSum.magnitude > CrackingForceThreshold && !cracked) {
   SimpleCrack(collision.impactForceSum, transformedPoint);
  }
 }

What goes on here, is first to get the point of impact, and translate it into local coordinates.
collision.contacts[0].point provides the first zero-based index point of collision with another object (a ball presumably, since it is the only other type of object that can fall on GlassTerrain), but expressed in world coordinates. In order to use the point to alter the mesh of GlassTerrain, we will have to translate it to local object coordinates. All object vertices that make up the object mesh, stored in the MeshFilter component, are local coordinates, and they are not to be mixed with the actual position, rotation and scale of the object in the scene.
transform.worldToLocalMatrix.MultiplyPoint does the trick of translating points from the world coordinate system to the local coordinate system. If you would like to do the same for directions/ vectors, then transform.worldToLocalMatrix.MultiplyVector would be your pick.

After this step, a condition decides whether we will go for a glass cracking effect or for a full glass breaking effect. In both cases the mesh is splitted into a number of sub-meshes, which are visible. The difference is that while in the former case the shards remain within their container (GlassTerrain), in the latter case the original GlassTerrain's renderer is disabled and a force is applied to each piece as they break up falling on the terrain floor behind GlassTerrain.

Simple crack

Here is the code for having a simple cracking effect, which is based on applying a prefab material having a cracking texture image on GlassTerrain. More explanations will follow soon.

 // Splits the gameobject in pieces, but keeps them in place, and
 // waits for SimpleExplosion to separate them
 void SimpleCrack(Vector3 impactForce, Vector3 impactPoint)
 {
  if (destroyed)
  {
   return;
  }
   
  //Change the material to the brokenglass material for this object:
  gameObject.GetComponent<MeshRenderer>().material = BrokenGlassMaterial;
  //construct all the individual destructible pieces from our mesh
  MeshFilter filter = GetComponentInChildren(typeof(MeshFilter)) as MeshFilter;
  pieces = new GameObject[filter.mesh.triangles.Length/3];
  //unit scale all children pieces when spawning them, then restore it later
  Vector3 oldScale = transform.localScale;
  transform.localScale = new Vector3(1,1,1);
  //impactPoint.Scale (new Vector3(1/oldScale.x,1/oldScale.y,1/oldScale.z));
  Debug.Log("Impact point after new scale: " + impactPoint.ToString());
  /*
  for (int v = 0; v < mesh.vertexCount; v++)
   Debug.Log ("Vertice[" + v + "]: " + mesh.vertices[v].ToString());
  for (int t = 0; t < mesh.triangles.Length; t++)
   Debug.Log ("Triangle[" + t + "]: " + mesh.triangles[t].ToString ());
  for (int n = 0; n < mesh.normals.Length; n++)
   Debug.Log ("Normal[" + n + "]: " + mesh.normals[n].ToString ());
  */ 
  for (int i = 0; i < filter.mesh.triangles.Length; i+=3)
  {
   // For each triangle (3 vertices), check that they are not on the same plane as with the impact vertex:  
   // and then make another 3 triangles for each 2 vertices and the impact vertex:
   GameObject piece = createShard(impactPoint, i, filter);
   pieces[i/3] = piece;

  }
  transform.localScale = oldScale;
  cracked = true;
 }

Simple explosion

Here is the code for the simple glass breaking effect, which will be explained soon in an update of this post.
 // SimpleCrack should have preceded before the execution on SimpleExplosion
 // SimpleExplosion breaks up the broken pieces created by SimpleCrack and 
 // applies to them an initial force, to let them "fly" towards the impact force direction
 void SimpleExplosion(Vector3 impactForce, Vector3 impactPoint) {
  Vector3 pieceForce = impactForce;
  // Deviate force direction a bit to each piece, by a random angle up to 20 degrees
  // in order to "simulate" breaking dynamics and collisions of the pieces when breaking up
  pieceForce = Quaternion.Euler (Random.Range(0, 20F), Random.Range(0, 20F), Random.Range(0, 20F)) * pieceForce;
  
  destroyed = true;
  // Deactivate the current GameObject since it's broken, only its children will behave physically (and be visible) from now on
  Object.Destroy (GetComponent<Rigidbody>());
  Object.Destroy (collider);
  this.renderer.enabled = false;
  // and then apply force to all its children (shards)
  for (int i=0; i < pieces.Length; i++) {
   pieces[i].GetComponent<MeshRenderer>().material = BrokenGlassMaterial;
   pieces[i].AddComponent<Rigidbody>();
   pieces[i].GetComponent<Rigidbody>().mass = 0.1f;
   pieces[i].GetComponent<Rigidbody>().drag = 0.1f;
   pieces[i].GetComponent<Rigidbody>().AddForce(pieceForce, ForceMode.Force);
  }
  this.gameObject.AddComponent<AudioSource>();
  this.gameObject.GetComponent<AudioSource>().PlayOneShot(GlassBreakingSound);
  Destroy(this.gameObject, 4f);
  //this.gameObject.BroadcastMessage("RespawnCube");
  startup k = (startup) GameObject.FindObjectOfType (typeof(startup));
  k.SendMessage("RespawnCube");
 }

Shard creation

Now, this is the main effect of this series of posts. If you would like to get a first-hand knowledge of how meshes are built and are manipulated by Unity, have a look in the official documentation here and here. If, however, you would like a more in-depth view and much better explanation, look this amazingly well explained example at Morten Nobel's blog. It's worth a Nobel, really!

Having said that, let me explain first a bit the main splitting logic, it is simple enough:

After determining the collision/impact point on the glass object (in local coordinates), then this is becoming part of the shard mesh vertices. If the original glass object had n vertices and m triangles, then m different meshes are created, each in a different game object, having 12 vertices and 4 triangles each. Let me clarify this further:

Each triangle of the original object occupies 3 different vertices in each of its corner. By adding the vertex of the collision point, we get a mesh, comprised of 4 vertices and 4 triangles. This can be seen in the selected mesh in the figure of this post above. Now each unique vertex is duplicated in each triangle, in order to have crisp and sharp edge while each traingle is shaded. Thus we get to 12 vertices from the initial 4.

By iterating for each triangle, we get to have one different mesh per triangle in the original glass object. So for the primitive cube that GlassTerrain uses as its mesh, we have 24 vertices (4 by 6) and 12 triangles (2 for each side). After the shard creation process, we get 12 different shards, each having 12 vertices and 4 triangles.

The function that implements this mechanism follows:

 // Creates a new GameObject, a (glass) shard, that has its own mesh and dynamics
 GameObject createShard(Vector3 impactPoint, int i, MeshFilter filter) {
  // a good reference is https://github.com/mortennobel/ProceduralMesh/blob/master/Tetrahedron.cs
  Mesh mesh = filter.mesh;

  GameObject shard = new GameObject("shard_" + i);
  shard.AddComponent<MeshFilter>();
  shard.AddComponent<MeshRenderer>();
  shard.GetComponent<MeshRenderer>().material = shardMaterial;
  shard.AddComponent<MeshCollider>();
  shard.GetComponent<MeshCollider>().material = shardPhysicMaterial;
  shard.GetComponent<MeshCollider>().convex = true;
     
  Mesh newmesh = (shard.GetComponentInChildren<MeshFilter>() as MeshFilter).mesh;
  if (newmesh == null){
   (shard.GetComponent<MeshFilter>() as MeshFilter).sharedMesh = new Mesh();
   newmesh = (shard.GetComponent<MeshFilter>() as MeshFilter).sharedMesh;
  } 
  newmesh.Clear ();
  
  newmesh.vertices = new Vector3[]
   {
   mesh.vertices[mesh.triangles[i+0]],
   mesh.vertices[mesh.triangles[i+1]],
   mesh.vertices[mesh.triangles[i+2]],
   impactPoint,
   mesh.vertices[mesh.triangles[i+1]],
   mesh.vertices[mesh.triangles[i+0]],
   impactPoint,
   mesh.vertices[mesh.triangles[i+0]],
   mesh.vertices[mesh.triangles[i+2]],
   impactPoint,
   mesh.vertices[mesh.triangles[i+2]],
   mesh.vertices[mesh.triangles[i+1]]
   };
  
  newmesh.triangles = new int[]
   {
   0, 1, 2,
   3, 4, 5,
   6, 7, 8,
   9, 10, 11
   };
  newmesh.RecalculateNormals();
  newmesh.RecalculateBounds();  
  Bounds bounds = newmesh.bounds;
  // planar UV coordinates
  newmesh.uv = new Vector2[]
   {
            new Vector2 (newmesh.vertices[0].x / bounds.size.x, newmesh.vertices[0].z / bounds.size.x),
   new Vector2 (newmesh.vertices[1].x / bounds.size.x, newmesh.vertices[1].z / bounds.size.x),
   new Vector2 (newmesh.vertices[2].x / bounds.size.x, newmesh.vertices[2].z / bounds.size.x),
   new Vector2 (newmesh.vertices[3].x / bounds.size.x, newmesh.vertices[3].z / bounds.size.x),
   new Vector2 (newmesh.vertices[4].x / bounds.size.x, newmesh.vertices[4].z / bounds.size.x),
   new Vector2 (newmesh.vertices[5].x / bounds.size.x, newmesh.vertices[5].z / bounds.size.x),
   new Vector2 (newmesh.vertices[6].x / bounds.size.x, newmesh.vertices[6].z / bounds.size.x),
   new Vector2 (newmesh.vertices[7].x / bounds.size.x, newmesh.vertices[7].z / bounds.size.x),
   new Vector2 (newmesh.vertices[8].x / bounds.size.x, newmesh.vertices[8].z / bounds.size.x),
   new Vector2 (newmesh.vertices[9].x / bounds.size.x, newmesh.vertices[9].z / bounds.size.x),
   new Vector2 (newmesh.vertices[10].x / bounds.size.x, newmesh.vertices[10].z / bounds.size.x),
   new Vector2 (newmesh.vertices[11].x / bounds.size.x, newmesh.vertices[11].z / bounds.size.x),
   };

  newmesh.Optimize();
  // now set the new mesh for the i-th shard gameobject's collider as well:
  (shard.GetComponent<Collider>() as MeshCollider).sharedMesh = newmesh;
  // and initialise its transform (position, rotation)
  shard.transform.parent = (this.gameObject.GetComponent<MeshFilter>() as MeshFilter).transform;
  shard.transform.localPosition = Vector3.zero;
  // set the tag to Respawn so as to destruct when it hits the shpereCollector
  shard.tag = "Respawn";
    
  return shard;
 }
As we should, we totally replace the arrays of vertices (newmesh.vertices), triangles (newmesh.triangles) and UVs (newmesh.uv) for each shard mesh. The first two arrays are necessary for defining the shard geometry, while the uv array sets the coordinate system in order each shard to be textured correctly.

Now, there are some pros and cons with this method. It is simple enough and quickly produces a set of glass shard sub-meshes. The downside is that if the glass object on which the process is applied is a complicate mesh with many vertices, the resulted number of sub meshes is too high. This is getting unreasonably complex and may easily create a bottleneck for either the physics engine or the rendering engine (or both). 

Keep on reading next... Some references to future improvements and what other people have done on this subject follows in the next and final part of this post series.

Comments