Hello and welcome back to another Blightmare dev blog! Today is another entry in the ongoing series about lighting. Specifically, this post is a followup to last week's introduction to shadowing. After last week, our test scene had some nice shadowed areas, but the shadowing was very harsh and today we're going to try to do something about that. Just so we're all on the same page, the test scene looks like this:
What we would like to do is make the edges of the shadowed regions less abrupt. One of the hardest problems for me when I approach an art task is to figure out precisely what changes are needed to achieve the description of what is wrong. In this example, the transition between shadow and not shadow is too "hard". Hard actually means there is no smoothing between shadow and not. If we could expand the border of the shadows to be a gradient instead of a single pixel, we would achieve a softer shadow. Identifying the problem that we want to solve is most of the battle, so we're in good shape now.
How can we blend the shadow edge better? If we think about how the shadow is actually being added to the scene, there's a step where we combine the shadow map with the light map. This is a fragment shader, so we have a good opportunity to perform our blend in a way that softens the shadow edges. Essentially what we want to do is average the color of an area around each pixel. This is usually called a blur. Let's give it a try.
What we're trying to do is this:
It turns out that we can do this in a very straightforward way in our fragment shader. The shader is run once for each pixel, and we know which pixel we're on based on the texture coordinate so all we need is the size of the texture to be able to average an area of texture space that is consistently sized. The size of the area can be input as a constant along with the number of steps we take to get to the full extent of the size. The step count can be thought of as a quality parameter. Lastly, we may want to pull values from a variety of directions around us, so that can also be a parameter. The code to do this can be written directly then:
half2 maxRadius = _SoftShadowRadius / float2(_ShadowMapWidth, _ShadowMapHeight); half4 shadowSample = SAMPLE_TEXTURE2D(_ShadowTex, sampler_ShadowTex, input.shadowUV); for (int dirStep = 0; dirStep < _SoftShadowDirections; ++dirStep) { half theta = (float(dirStep) / float(_SoftShadowDirections)) * 6.28318530718f; half2 dir = float2(cos(theta), sin(theta)); for (int radStep = 0; radStep < _SoftShadowQuality; ++radStep) { half2 radius = (float(radStep) / float(_SoftShadowQuality)) * maxRadius; float2 blurUV = input.shadowUV + dir * radius; shadowSample += SAMPLE_TEXTURE2D(_ShadowTex, sampler_ShadowTex, blurUV); } } shadowSample /= (float(_SoftShadowQuality) * float(_SoftShadowDirections) + 1); half shadowIntensity = 1 - (shadowSample.r * saturate(2 * (shadowSample.g - 0.5f * shadowSample.b))); color.rgb = (color.rgb * shadowIntensity) + (color.rgb * intensity * (1 - shadowIntensity));
All we have to do now is play around with the settings until we get something that looks pretty good, and we're in business. Applying this to our test scene gives us a result like this:
We're really starting to look good now. There's a couple more graphics features to add and then we are going to have to start thinking about performance. Exciting! If you're enjoying the content here, please add the game to your wishlist and head over to our Twitter to give us a follow. Thanks for reading!