Indie pig.
Indie Pig Indie games and experiences.

Thursday 14th, August 2014

3D Shadows in a 2D game, or, OpenGL for the Complete Beginner

For years now I've always felt dumb around my colleagues, who seem to always be talking about shaders, OpenGl, and other graphics related things I knew nothing about. It always seemed a little overwhelming to get into, and I didn't know where to start.

A few weeks ago I decided to just get over it, and just set a modest goal for myself to reach. My goal was to put shadows in my game, The Beast of Arin. I decided shadows were worth the investment because every imagination I had of the game involved a certain mood, and that mood would be enormously helped with the use of lighting and shadows. Defending your camp in the light of the fire, trecking across the desert as the sun sets, that sort of thing.

Shadows! Check it out:

Sorry, this video requires Adobe Flash.

 

So having just gone through learning this stuff from nothing, I figure it is a good time to help others just starting out. I find that it's often easy to find very advanced information, but difficult to find articles that don't rely on previous knowledge.

Per a recommendation from a colleague, I purchased "The Red Book" on Amazon. This is a good book, it is definitely a good one to get through, but it's not really a great place for idiots like me.

A common starting place, http://www.opengl-tutorial.org/ has a lot of good opengl tutorials, but in my opinion there's an even better place to start, especially for Java developers: https://github.com/mattdesl/lwjgl-basics/wiki/Shaders.

Of course, there are pre-requisites to learn before any of that, algebra, trigonometry, matrices, programming basics, and machine architecture will help a lot. In short, graphics programming isn't easy, so no matter what, you'll need a lot of patience.

When I started on my goal to create shadows in my 2d game, I came across this popular tutorial: https://github.com/mattdesl/lwjgl-basics/wiki/2D-Pixel-Perfect-Shadows. This is a great tutorial, and can teach you a lot about frame buffers, blurring, and how to do some cool graphics things in LibGDX. However, for my purposes, I wanted shadows that could end instead of going off endlessly.

After trying to avoid it, I learned that it's easiest to make 3D shadows by, well... making 3D objects. Now before you flip the table and hire a 3D modeler, know that you don't need to make good 3D models, we're only using them for occlusion, so they can be pretty basic and still make ok looking shadows [Pig's Opinion]. So for my dudes I just used LibGDX's runtime 3D Model Builder, and made a cylinder, a few spheres, and a box for the sword.

When you have the occlusion shapes, the idea is to make a shadow map by rendering the scene from the perspective of the light to a frame buffer, recording the distance between the occlusion shape and the light. When you have the shadow map, you render the scene with both the camera's MVP matrix and the light's MVP matrix, comparing the pixel's distance to the light with what's in the shadow map. This tutorial covers it pretty well: http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/, so read that.

There are a few things I'd like to add, however, that might make your results a little better, especially if you're using LibGDX.

The first thing I'd like to add is that workflow is very important. What I mean by that is that especially when you are learning something, it's important to be able to try things rapidly and experiment. The best way to do this for OpenGL I've found, is to use the program GLIntercept. GLIntercept will actually let you modify your shaders at runtime, so you don't need to waste time recompiling and restarting your program just to find out that you forgot to put "1.0" instead of "1". To get GLIntercept working for Java:

  1. Install GLIntercept
  2. Go to the install location, copy the gliConfig_AuthorStd.ini and OpenGL32.dll files to your java/bin directory. (Wherever the executable is that runs your program.)
  3. Rename gliConfig_AuthorStd.ini to gliConfig.ini
  4. Run your game, and press ctrl+shift+s within your game to open the shader editor.

Being able to iterate quickly on a shader allows me to make mistakes much more quickly.

Following the opengl-tutorial.org's instructions, I was eventually able to get some shadows, but when the angle was low and the shadows were long, they were very aliased:

Long shadows with jagged edges.

Notice how jagged the sword's shadow looks.

The problem is that if the shadow is 10x the size of the object, it's still stored as the object's size and therefore no amount of blurring will fix the aliasing. Fortunately, it doesn't need to be. When you construct your projection matrix for the light's perspective, you can stretch it, based on the light's angle. Here's what it should look like:

Long shadows with smooth edges.

The way we achieve this, is to take the bounding corners of your scene from the perspective of the camera, then transform them by the view of the light, and use those bounds for your light's projection.

Projection Matrix for Directional Lights

 
	// Homogeneous bounding box coordinates.
	private final Vector3[] coords = new Vector3[] {
			new Vector3(-1, -1, -1),
			new Vector3( 1, -1, -1),
			new Vector3( 1,  1, -1),
			new Vector3(-1,  1, -1),
			new Vector3(-1, -1,  1),
			new Vector3( 1, -1,  1),
			new Vector3( 1,  1,  1),
			new Vector3(-1,  1,  1)
	};
 
	private final Float[] bounds = new Float[6];
 
	private final Vector3 tmp = new Vector3();
	private final Matrix4 tmpMat = new Matrix4();
	private final Matrix4 boundsConversion = new Matrix4();
 
	public void updateCamera(Camera2d camera) {
		OrthographicCamera cam = camera.camera;
 
		boundsConversion.idt();
		boundsConversion.setToLookAt(direction, lightCamera.up); // Rotate the world coordinate to light's direction.
		boundsConversion.mul(tmpMat.set(cam.projection).inv()); // Convert homogeneous coordinate to world coordinate.
 
		for (int i = 0; i < coords.length; i++) {
			tmp.set(coords[i]);
			tmp.mul(boundsConversion);
			if (i == 0) {
				bounds[0] = tmp.x;
				bounds[1] = tmp.x;
				bounds[2] = tmp.y;
				bounds[3] = tmp.y;
				bounds[4] = tmp.z;
				bounds[5] = tmp.z;
			} else {
				if (tmp.x < bounds[0]) bounds[0] = tmp.x;
				if (tmp.x > bounds[1]) bounds[1] = tmp.x;
				if (tmp.y < bounds[2]) bounds[2] = tmp.y;
				if (tmp.y > bounds[3]) bounds[3] = tmp.y;
				if (tmp.z < bounds[4]) bounds[4] = tmp.z;
				if (tmp.z > bounds[5]) bounds[5] = tmp.z;
			}
		}
 
		lightCamera.viewportWidth = Math.abs(bounds[1] - bounds[0]);
		lightCamera.viewportHeight = Math.abs(bounds[3] - bounds[2]);
		float d = Math.abs(bounds[5] - bounds[4]);
		lightCamera.near = -d;
		lightCamera.far = d;
 
		lightCamera.position.set(bounds[0] + bounds[1],
				bounds[2] + bounds[3],
				bounds[4] + bounds[5]).scl(0.5f);
		lightCamera.position.add(cam.position.x, cam.position.y, 0);
		lightCamera.direction.set(direction);
		lightCamera.update();
	}

 

This manipulation will make your shadow map's viewport as small as possible, while still fitting in everything the camera can see. So if the camera's viewportHeight is 800px, and the light's direction is (0, 0.1, 1), then the shadows will be 10x the objects' height, and we can set our light's viewportHeight to 80.

Another thing of note specifically for mobile: mobile does not support depth textures, so you will need to pack your shadow depth as a color. http://stackoverflow.com/questions/9882716/packing-float-into-vec4-how-does-this-code-work

I hope this helps somebody! I welcome questions, but be warned, I'm really no expert with this stuff, I'm just sharing what I know. Good luck and have determination to learn.

-Nick

 

Add your comment