Realtime Shadow Casting Arealights
Motivation
In the latest project, G.R.O.W, my group wanted to make a sci-fi game inspired by Alien Isolation.One of the main things that stand out in the game is the lighting, how the lighting controls the mood and the feeling of the entire game. I had already implemented some advanced lighting techniques into our engine. But it was only for common light types, pointlights and spotlights. These light types worked well for a Diablo clone but when we decided to go for a sci-fi theme we would need something called a arealights. Arealights simulate more accurately how light works in the real world, because when light emits from a light source it is emitted from a surface or an area.
Techniques
There are a few different ways of calculating arealights. You have the straightforward and the most accurate way of calculating light for an Arealight, path-tracing and precalculating the light offline. This implies rendering a single frame of the scene for every static light in the scene and inserting the data into lightmaps. And as the name implies, the technique uses rays to simulate a single light beam emitted from a source of light. It will give you the most accurate results of how light works in real life but will take a lot of time and compute power to calculate every light in the scene.
The big drawbacks of this solution are the following:
-
It only works for static lights and the time required to render all of these lights is way too slow for real-time performance.
-
It is a memory-intensive task because of the amount of rays it is simulating.
-
You need to recalculate the every light if you move a single object within the scene.
-
To actually see the result of the lighting in a level, you need to run the path-tracing which takes time. This makes it inconvenient for an iterative lighting process.
Then you have the ray-tracing method and it works in the same way as the path-tracer does but in real-time. The problem with this solution is that you require a GPU capable of ray tracing. There are also some different options for solving this problem that I won't mention. After extensive research, I found something called Linearly Transformed Cosines (LTC).
Linearly Transformed Cosines
I will not go into detail and explain the math behind this solution because I think the paper already covers this topic in detail, so I'll put a link at the bottom of this page to the source or press here. But I'll try to explain the implementation as simple as I can. Linearly Transformed Cosines (LTC) is a formula that allows us to approximate the affected parts of a spherical surface. LTCs can also be analytically integrated over arbitrary shapes which means we can compute offline a look-up table for the different outcomes of the material properties. It is integrated with the BRDF (Bidirectional reflectance distribution function) which is the current PBR pipeline we use in our engine. Therefore it was the clear choice to use as a way to render out Arealights.
Implementation
The engine uses a deferred renderer but the implementation also works for a forward renderer. I started with defining some standard parameters for what an arealight consists of. The main difference from the other light sources is that you have a size for the light. We need to send the right and up direction of the area light to the GPU, so we know what way the light is facing. Then the shader creates a simple rect that defines four points for the directional data we sent, where each point represents a corner of the area light.
-
Calculate the values needed for the LTC precomputed data. These values consist of the roughness of the object and the normal of the pixel we are operating on. We need the direction between the camera and the pixel we are working on (in world space).
-
Read the LTC pre-computed data and create a matrix based of the result from the LTC.
-
Use the LTC evaluation function, it will create four integrals moving between all four of our points on the object we calculation lighting for. This is how we can correctly shade an object consisting of an area instead of a single point.
-
Perform edge case checking if one of the edges of the rect goes through an object, then we need to discard those pixels.
-
Evaluate the diffuse, it uses the same function but we replace the precomputed matrix with an identity matrix.
-
To finish it all we add it all together and then multiply the result with the color and intensity of the arealight.
A single arealight rendering
We have an arealight that shades an object correctly and is run-time viable. The only thing missing now is for the light to cast shadows, this was one of the more uncomplicated tasks of this implementation. I had already implemented soft shadows to every light source in our game. I streamlined the entire shadow process so that every light utilized the same shadow functions and attributes. This meant I would get soft shadows on my arealights without much work. I implemented it and I could not wish for a better result, the shadow elevated our environments to another level. As you can see in the pictures, shadows make a big difference.
A single arealight rendering with soft shadows
Conclusion
I'm pleased with the visual fidelity the Arealights added to our game. It made the game moodier and contributed to the sci-fi look we went after. The shadows are vital to the feel as well as streamlining the shadowing process. It opened new possibilities to optimize the shadows to gain performance. Furthermore, I integrated the Arealight calculation to utilise Clustered shading, which optimizes away any unnecessary light evaluations. I would like to rework some of the shadow calculations to make more use of Clustered shading. I would also like to implement other shapes of these kinds of lights like capsule and disk lights.
Eric Heitz: https://eheitzresearch.wordpress.com/415-2/