toc

content

2D Hard Shadows

This is an in-depth explanation about how I implemented hard shadows for Glass Bubble. To be clear, I am using C# for the following code examples.

Most explanations I found online about how to implement 2D hard shadows usually worked by making a quad mesh for every light with the shadows cut out of them. Instead, I approached the problem in a different way, partly because my friend convinced me it would be easier and I wanted a different graphical effect involving light shafts or god ray-like shadows. So, I did the opposite of what I had seen so many times online: create a mesh for each shadow.

The implementation is limited as it doesn't work for concave objects. I haven't addressed this problem yet since I do not expect to encounter this use case in the game.

Here is a summary of what I'll go over:

  • Setup - How the code works overall and a few lines of setup code
  • Finding Extremes - Finding the extremes on the object we want to cast a shadow with
  • 2D Cross Product - An explanation of how the math works for finding the extremes
  • Creating the mesh - Creating the mesh using the extremes

I expect you to have basic knowledge about Unity and some programming experience in order to follow along as I won't be explaining everything, although I hope to provide a clear explanation of how I established this effect.

Setup

I specify which GameObjects I want to cast hard shadows with by adding the script HardShadows.cs to them. This script contains the logic for creating the necessary shadows. This script will also assume that the GameObject I put it on has a mesh component on it which defines the object's shape that it can access.

In this script, we have a list of shadows that the GameObject is currently casting. In the Update() loop, we delete all of those shadows before generating the new ones. Then, we grab a list of all of the lights in the scene that we want to create a shadow with.

void Update() {
	ClearShadows();

	GameObject[] lights = GameObject.FindGameObjectsWithTag("Light");
	foreach(GameObject go in lights) {
		Light light = go.GetComponent<Light>();
		CreateShadowFromLight(light);
	}
}

I've tagged each light that I want a shadow for with the tag "Light". I probably need a better name for this tag, but for now it will suffice. You could instead search for all GameObjects that use a Light component, but I only want to check certain lights, especially because I don't want all of the lights in a scene casting god rays everywhere (as cool as some may think that would be).

We use each light as a parameter to the CreateShadowFromLight(Light) function. This call, which we will go over in the next section, will create a new shadow for that light and save it in the list of shadows.

As an aside, the Update() code could be optimized to clear and update the shadows only when the GameObject actually does move.

Finding Extremes

Inside the function CreateShadowFromLight(Light), we grab the light's position and the vertices that make up the GameObject's mesh.

Vector3 lightPos = light.gameObject.transform.position;
Vector3[] vertices = GetComponent<MeshFilter>().mesh.vertices;

We want to calculate where the left-most and right-most points on the GameObject's mesh are, respective to the light. We will call these points the extreme points. On lines 1 and 2 we treat the first vertex in the array as the left and right extreme, but we will find better solutions when we enter the for loop on line 5.

Vector3 left = this.transform.TransformPoint(vertices[0]);
Vector3 right = this.transform.TransformPoint(vertices[0]);

// Calculate extremes
for(int i = 1; i < vertices.Length; i++) {
	Vector3 point = this.transform.TransformPoint(vertices*);

	// Left-most
	float crossProduct = (left.x-lightPos.x)*(point.y-lightPos.y) - (left.y-lightPos.y)*(point.x-lightPos.x);
	if(crossProduct > 0) {
		left = point;
	}

	// Right-most
	crossProduct = (right.x-lightPos.x)*(point.y-lightPos.y) - (right.y-lightPos.y)*(point.x-lightPos.x);
	if(crossProduct < 0) {
		right = point;
	}
}

On line 5, we start at index 1 instead of 0 because we are already using the first vertex in the array as the starting value.

On lines 10 and 16, we calculate whether or not the point we are looking at is a better left or right extreme then the one we currently have. The math behind calculating whether or not the point is to the left or to the right of another point respective to the light's perspective might as well be black magic to me, but I will attempt to explain it in the next section.

2D Cross Product

First of all, we don't need to bother checking whether or not the point can be seen by the camera or not. In other words, we don't need to check if the point is on a face that is not facing the light. This is because from the light's perspective, points that are on faces not facing the light will always be between the left and right extremes.

What we do need to be able to figure out is whether or not a point is to the right or to the left of another. To do this we'll do a 2D cross product. Yes, a 2D cross product. 2D cross products don't actually exist, but we can derive some interesting properties from the operation.

To get the equation that I used earlier, we start with the 3D cross product:

a*b = (a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x)

a and b are our two vectors. The result of the cross product is another vector.

However, since we are in 2D, we will assume that the z value for both vectors is 0. This results in the following equation:

a*b = (0, 0, a.x*b.y - a.y*b.x)

We can ignore the x and y components of the resulting vector and define the result of a 2D cross product to be just the value of the z component of that vector:

a*b = a.x*b.y - a.y*b.x

The result of this equation has a few useful properties such as figuring out the area of a parallelogram formed by the two vectors or whether or not rotating from vector a to vector b is in the clockwise or counter-clockwise direction. The latter property is the one we will use later.

I then use the vector from the light to the current extreme and the vector from the light to the point we want to check as the a and b vectors. To get these vectors, you subtract the light's position from the extreme's or point's position.

a = extreme-light
b = point-light

So now we have defined vector a as the extreme's position minus the light's position and vector b as the position of the point we want to check minus the light's position. If we look at the values for each of the components in each vector, we get the following values:

a.x = extreme.x - lightPos.x
a.y = extreme.y - lightPos.y
b.x = point.x - lightPos.x
b.y = point.y - lightPos.y

If we substitute those values in for the formula we got earlier, we get:

a*b = (extreme.x-lightPos.x)*(point.y-lightPos.y) - (extreme.y-lightPos.y)*(point.x-lightPos.x);

And that's how I got to that formula, except with extreme substituted with which extreme we want to check with.

Finally, we check the result of that equation. If the result is less than 0, the point is to the left of our extreme. If it is more than 0, the point is to the right of our extreme. In either case, the point we are checking is a better extreme then we had, so we save that as our new extreme value.

Creating the mesh

After we find the extreme points on the GameObject, we make a list of points that we want to use for our shadow mesh. The first two points are the left and right extreme (order is important). We also need to figure out where the distant point is. The distant point is the point we would get if we drew a line from the light to the extreme and continued into infinity.

// Create the mesh
List<Vector2> verts = new List<Vector2>(4);
verts.Add(left);
verts.Add(right);
verts.Add(right + (right-lightPos).normalized * 9999);
verts.Add(left + (left-lightPos).normalized * 9999);

The math for getting the distant points is a little bit simpler. The difference between the light's position and the right or left extreme will give us the vector from the light to that extreme. All we have to do after that is normalize it (in case the vector is extremely small) and multiply it by some sufficiently large number.

Then, we create the mesh.

MeshCreator.CreateMesh(CreateShadow(alpha).mesh, verts);

CreateShadow(float) is another method within the HardShadows.cs file that creates a GameObject to store the shadow mesh, assigns the material, and sets the color of the shadow. The only significant thing to note is that the GameObject's x and y position is 0. This is because the mesh's coordinates will already be in the correct location. This also means that the GameObject we created to hold the shadow can't be a child of any other GameObjects that have an altered position, rotation, or scale.

MeshCreator is a piece of utility code that I wrote to wrap a call to the Triangulator script from the Unity Wiki. You can find the code for MeshCreator.cs below:

using UnityEngine;
using System.Collections.Generic;

public class MeshCreator {

	public static Mesh CreateMesh(Mesh mesh, List<Vector2> points) {
		// Save verticies
		List<Vector3> verticies = new List<Vector3>();
		foreach(Vector3 vec in points) {
			verticies.Add(vec);
		}
		mesh.vertices = verticies.ToArray();

		// Save UVs
		Vector2[] uvs = new Vector2[points.Count];
		for(int i = 0; i < points.Count; i++) {
			uvs* = new Vector2(0, 0);
		}
		mesh.uv = uvs;

		// Save triangles
		Triangulator tri = new Triangulator(points.ToArray());
		mesh.triangles = tri.Triangulate();

		return mesh;
	}
}

And that is how I create shadows in Glass Bubble! I hope you found this post interesting or informative.

meta

tags: glass-bubble, unity

created: published: modified: migrated:

backlinks: Glass Bubble Week 2

commit: d31bdd26