Hello all! I hope all is well 🙂

Long time no see, but I have worked a lot on the engine in the meantime. I have implemented shadow mapping, deferred rendering, transparency and translucency and many other features. I’ll write about shadow mapping in this post, but the rest will be in the following ones.

I won’t dive into technical details but will cover what I have done and experienced since there are millions of technical talks/tutorials online.

Overall renderer and material changes

The renderer was performing only forward rendering operations beforehand. However it is needed to render multiple times in a frame from now on. For this purpose, I have created an enum class containing Forward, Shadow and PointLightShadow types, named RenderPassType and passed this type to void Renderer::Render(RenderPassType renderPassType) function. There are two different shadow type in this enum because point light and directional/spot light shadows are operated differently.

Due to this difference, I have made changes to the material class. Previously, there was only one shader(i.e. OpenGL program) in every material for only forward rendering of the final image. Since it is not needed to calculate any colors for shadow maps we need special shaders for them. I have created two more shaders for this purpose which are mapped to a RenderPassType type, so that in a void Renderer::Render(RenderPassType renderPassType) call, the corresponding shader can be bound through the material.

Shadows

The engine currently features point, spot and directional lights. I have implemented shadow mapping for all three types, and I have used OpenGL’s built-in Percentage Closer Filtering for making the shadows have smooth edges.

Additional operations for shadow mapping

I have changed auto shader creator ShaderBuilder class to do the necessary calculations for shadows dynamically. The following code snippet should give an idea of how it works:



in vec4 fragmentPositionLightSpace_SpotLight4;
uniform sampler2DShadow shadowMap_SpotLight4;

...

void main()
{
	vec4 texture5Color = texture(texture5, textureUV); 
	diffuseReflectance = vec4(texture5Color); 

	vec3 lightIntensity = sceneAmbient * ambientReflectance;

	vec3 lightSpaceScreenCoordinate_SpotLight4 = fragmentPositionLightSpace_SpotLight4.xyz / fragmentPositionLightSpace_SpotLight4.w;

	// Do not take out of screen space into account
	if(0.f <= lightSpaceScreenCoordinate_SpotLight4.x && lightSpaceScreenCoordinate_SpotLight4.x <= 1.f &&
	   0.f <= lightSpaceScreenCoordinate_SpotLight4.y && lightSpaceScreenCoordinate_SpotLight4.y <= 1.f)
	{
		float shadowValue_SpotLight4 = textureProj(shadowMap_SpotLight4, fragmentPositionLightSpace_SpotLight4);
			
		if(0.f < shadowValue_SpotLight4)
		{
			vec3 lightIntensity_SpotLight4 = CalculateSpotLightColor(SpotLight4Position, SpotLight4Direction, SpotLight4Intensity, SpotLight4CoverageAngle, SpotLight4FalloffAngle); 
			lightIntensity += shadowValue_SpotLight4 * lightIntensity_SpotLight4;
		}
	}
	else
	{
		lightIntensity +=  CalculateSpotLightColor(SpotLight4Position, SpotLight4Direction, SpotLight4Intensity, SpotLight4CoverageAngle, SpotLight4FalloffAngle);
	}
...
}

I also created a shadow manager for rendering shadow maps. In every frame its void ShadowManager::RenderShadowMaps() function is called and it traverses all the lights and call their shadow renderer function.


If a light is set to cast shadows, then a render camera, a frame buffer object, and a texture is created for it.

Directional and Spot Light Shadows

Directional and spot light shadows are quite simple to implement. They only require a framebuffer, 2D texture and a camera. The only difference between them is their camera, directional light requires an orthographic camera while spot light requires a perspective one.

Point Light Shadows

Point light shadows are a bit tricky though. The tricky part is to render the shadow map in every 6 directional axis. For this purpose, point lights’ shadow map textures are cube maps, and each side of the cube is needed to be rendered one by one. However, thanks to the geometry shader’s built-in gl_Layer, we can render the cubemap in one render pass.

The extra RenderPassType::PointLightShadow type is needed for a shader containing this geometry shader.

#version 440 core

layout (triangles) in;
layout (triangle_strip, max_vertices=18) out;

uniform mat4 viewMatrices[6];

out vec4 fragmentPositionWorldSpace;

void main()
{
    for(int face = 0; face < 6; ++face)
    {
        gl_Layer = face;
        for(int i = 0; i < 3; ++i)
        {
            fragmentPositionWorldSpace = gl_in[i].gl_Position;
            gl_Position = fragmentPositionWorldSpace * viewMatrices[face];
            EmitVertex();
        }
        EndPrimitive();
    }
}

What are still missing?

  • Shadow mapping for masked, transparent and translucent objects
  • Atlas mapping for shadow map textures
  • Cascaded shadow mapping for directional light which is fairly easy to implement, but mainly needed for first person games. Therefore, and because of not having a texture atlas for shadow maps, I decided to postpone it.

Conclusion

To conclude, I have implemented a dynamically working shadow mapping system into the engine. It can work with dynamic and static lights with fairly good quality and performance. It still have some task to work on, but they can be implemented as needed over the main algorithm. Overall, I am so happy for taking a step further with the engine!

External Links

  • https://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/
  • https://ogldev.org/www/tutorial42/tutorial42.html
  • https://learnopengl.com/Guest-Articles/2021/CSM
  • https://www.youtube.com/watch?v=C2Gn6oOxSu0 (A wonderful class from a wonderful wonderful professor 🙂 All of his playlists are also recomended! )