Hello and welcome back to another installment of the Blightmare Development Blog! Today we will be beginning a series on 2D lighting that will show an in-depth look into how 2D lighting works, and how we are using it in Blightmare. In this first post, we will start with the basics of what 2D lighting is and how it works. I hope you're as excited as I am to go on this journey!
To start things off, I put together a simple little test scene that we can play around with. Please note that I am not an artist. The test scene looks like this:
In order to get a feel for what changes when we switch to using lighting, we should take a look at what is happening in the graphics pipeline to get the regular image. To do this, we can use the frame debugger. The frame debugger is a built-in Unity tool that records all the draw commands that are generated for a frame and displays them in a list that can be inspected. By clicking on a command, we can see the output in the game window. The overview of our test scene looks like this:
Let's go ahead and step through the steps to get to our final product
First we need to clear the screen to a default color:
Next, we start drawing our art. Note that in Blightmare, all art is composed of 2d rectangular images. This means that each of them has transparency sections to fill out the rectangle. This is why the next section is called Render Transparents, and it is in general a back-to-front drawing of all the layers that compose the scene. There are a number of performance considerations to be made before using this method, but we won't cover that now. Instead, lets see some draw calls.
The static background layer:
Next is our furthest parallax layer. Note that all of these clouds can be drawn in a single call because they share the same material:
Still on the furthest parallax, we have another large image. This cannot be drawn with the clouds even though it's on the same layer, because it uses a different material. Specifically, the clouds are animated and use a special spine shader while this image is static and uses a sprite shader:
Next we have another parallax layer. I've shown the result of 2 draw calls here because they are similar to the previous layer:
Watch these next 2 draw calls carefully. Notice that the first 3 rocks are entirely covered by the big wall of rocks. This is wasteful because you never see them in the final image, yet we're spending the time to actually draw them. It's easy to forget about things like this when there's many things in a scene, so it can be a good idea to periodically look for such problems. There are sophisticated tools to assist in this process, which would be extremely tedious to do by hand in a large game:
Next up is the terrain layer. The terrain is drawn in batches of similar terrain images. You can see this happen in the video:
After the terrain is our last decoration layer behind the game objects:
Now we're in the game object layer which has the lightbugs first:
Next we get the platforms, the player, and a plant. Note that the plant is actually on a layer in front of the player, but it shares the spine shader with the player, so it is grouped with the player. The platforms are sprites, so they aren't grouped with the spine assets - drawn before them in fact:
Finally we have some mushrooms that are static sprites and on the front layer, so they are drawn last:
That is what happens every frame to get us the final image of the game!
Alrighty, so now that we have some background on what it takes to get the whole image on screen, lets move on to considering what lighting is and why we would want it. Before we begin, I would like to say that lighting itself is an immense topic with a great body of research. I'm going to keep this description brief which means that I'll be leaving out many details, and often simplifying things to focus on concepts that are impactful for Blightmare. With that disclaimer out of the way, in simple terms lighting refers to the process by which a surface becomes something that we can see. You might think of this as the problem of determining what color each piece of something should be. In the normal 2D world, such as our frame from above, the lighting process is performed by the artist when they create the source art. The color choices, shading, and all the other details that go into creating a piece of art includes decisions about what the lighting should look like. What this means is that lighting decisions are made before the art is even imported into the game. That might sound like a great idea because it falls nicely into the category of work we can do before hand instead of trying to do it at runtime. However, there are some things that we want to do at runtime so that we can let parts of the game dynamically interact in ways that would be impossible to create ahead of time. Lighting usually falls into this category.
In our scene, we have some lightbugs. They have light in their name! It would be great if we could have the lightbugs actually brighten up the areas of the level that they were in. What does it mean to brighten up an area? The first step is to think about what brighten means. For our purposes, something is brightest when it's white and darkest when it's black, with a full gradient of steps in between. Using that definition, we can brighten something by changing its color to be more white. What does more white mean? The representation of a color used in Blightmare is a 32 bit integer. There's 3 8 bit color channels: red, blue, and green, and a final 8 bit channel for alpha or transparency. The color values range from 0 to 255 where 0 is none, and 255 is maximum. Therefore, to make a color more white, we can add something to the color channels. To recap: making something brighter is as simple as increasing the values in its color channels. Returning to our lightbug problem, the last part is to do this to an area. As we've seen now, the image is rendered in several layers. We can therefore create a new layer for our lighting which has some color value in the places that we want to light, and we can add the layers together to get a final image. Adding color in this way makes it difficult to modify images that are already at full color in useful ways. Another way to modify the color in a systematic way is by multiplication. If we multiply by a value between 0 and 1, then we will darken the image but still retain the same ratios between each color channel. If we multiply by a value more than 1, we will brighten the image again retaining the color ratios. Typical 2D lighting systems will provide a way to do both of these operations together to get the maximum flexibility.
Keeping our focus on the lightbugs, we can add small area of effect lights - called point lights - to them in Unity. By itself, this change won't have any effect because of a number of reasons. Specifically: the default render pipeline doesn't understand 2D lighting, and all the materials on our objects similarly are not aware of any 2D lighting attributes, so the scene will remain unchanged. I'm not going to detail the specifics of how to setup the 2D lighting capabilities of Unity, but I recommend using the official documentation if you're interested. Back to our scene - now with the render pipeline set to 2D, and all our materials changed over - with just the lights from the lightbugs, we can see the following result:
If we use the frame debugger again, we can see how this image is created. First we see the lights rendered to a temporary render target called _ShapeLightTexture0:
Next, we can see the static background layer combined with this light texture to create the spotlight effect:
As we go through each layer, we can see a similar process being repeated - first the lights are rendered, then the objects on the layer are combined with the light map:
This is kind of interesting, but we can't actually see the player or the level, so it's not all that great for the game like it is. The reason why we can't see most of the scene is because when we're using a lighting model, we have to provide light to every pixel that we want to be visible. One option to do this would be to create more point lights, or maybe a larger light. This becomes difficult to manage if we want to change the size of the level, or have the camera zoom in or out. Instead, it would be helpful if we could just provide a baseline amount of lighting to apply everywhere. This is called a global light. If we add a global light into the mix, then by default we will see everything almost as we did before - notice the brightness around the lightbugs:
What does the light map look like in this case?
This looks entirely white. But we know that we still get the yellow highlights when the final image is put together. How can that be possible? If you look closely at the frame debugger image above, you can see that the format of the light render texture is actually B10G11R11. This means there's 10 bits for the blue channel, and 11 bits each for green and red. This means that we can actually store values above 255 for those color channels. What that means then, is that the values actually stored internally are larger than the normal maximum which is why we see white on the preview - it's been clamped down to 255 for each channel - but when we do the combination to create the final image, the value gets normalized by division with 255 and then we multiply the result by the original color in the source art. This normalization allows the final image to get those yellow highlights even though the light texture looks all white by itself.
Having a global light at full intensity isn't all that interesting. If we change it around a little bit, we can create some interesting effects by changing not only the intensity, but the color itself. If we then take advantage of all the layers that we saw from before, we can create a final image that is quite different from the original:
Cool! This is already pretty different from where we started, and you can see how the lighting effects allow the game mechanics to not only stand out better, but also add some extra detail to the level that would not have been possible ahead of time. This is where we're going to stop for today because this is already getting to be super long! Fear not, there's more to come as we add additional features to our lighting effects.
If you're enjoying the blog and want to check out the game, please head over to our steam page and put Blightmare on your wishlist. We also have a Twitter which is where we post all our blog updates, so give us a follow to make sure you don't miss anything! Thanks for reading!