Omniscient

Celeritas #003 - Basic Shadow Maps working

Published: Last updated:

Got basic shadow maps working in the C layer!

Shadow Depth Map Shadow Depth Map
Final image with shadows applied The final image rendered using the shadow depth map texture

It's very dark because I stripped out all the lights and lighting code not necessary to getting this part working.

From the cameras perspective Scene rendered but only showing points occluded from light's perspective

As an aside, it'd be nice to have a gallery widget for the above so they could sit horizontal rather than one at a time vertically.


The main loop roughly looks like this

// Main loop
while (!glfwWindowShouldClose(core->renderer.window)) {
  rend_frame_begin(core);

  glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
  glBindFramebuffer(GL_FRAMEBUFFER, depth_pass_framebuffer.id);
  glClear(GL_DEPTH_BUFFER_BIT);

  // light space transform setup
  f32 near_plane = 1.0, far_plane = 7.5;
  mat4 light_projection = mat4_orthographic(-10.0, 10.0, -10.0, 10.0, near_plane, far_plane);
  mat4 light_view = mat4_look_at(light_position, VEC3_ZERO, VEC3_Y);
  mat4 light_space_matrix = mat4_mult(light_view, light_projection);

  // 1. Render scene from light's perspective
  // upload the light space transform matrix to our shader
  set_shader(core, depth_shader);
  uniform_mat4f(depth_shader.program_id, "lightSpaceMatrix", &light_space_matrix);

  draw_model(core, cube, main, false);
  draw_model(core, cube, large, false);
  draw_model(core, cube, small, false);

  // 2. Render the scene as normal with the depth map available for shadow mapping
  set_backbuffer(); // set the active framebuffer back to 0 a.k.a. the final image
  set_shader(core, core->renderer.default_shader);

  // data setup
  mat4 view = mat4_look_at(vec3_create(5.0, 5.0, -4.0), VEC3_ZERO, VEC3_Y);
  mat4 projection =
      mat4_perspective(45.0 * 3.14 / 180.0, (f32)SCR_WIDTH / (f32)SCR_HEIGHT, 0.1, 100.0);
  mat4 view_proj = mat4_mult(view, projection);
  vec3 cam_pos = vec3_create(5.0, 5.0, -4.0);

  // upload uniforms
  uniform_i32(core->renderer.default_shader.program_id, "shadowMap", 2);
  uniform_mat4f(core->renderer.default_shader.program_id, "lightSpaceMatrix",
                &light_space_matrix);
  uniform_vec3f(core->renderer.default_shader.program_id, "lightPos", &light_position);
  uniform_vec3f(core->renderer.default_shader.program_id, "viewPos", &cam_pos);
  uniform_mat4f(core->renderer.default_shader.program_id, "viewProjection", &view_proj);

  glActiveTexture(GL_TEXTURE2);
  glBindTexture(GL_TEXTURE_2D, depth_tex.raw);

  draw_model(core, cube, main, true);
  draw_model(core, cube, large, true);
  draw_model(core, cube, small, true);

  // (Optional) 3. debug depth texture
  // set_shader(core, debug_quad);
  // glActiveTexture(GL_TEXTURE0);
  // glBindTexture(GL_TEXTURE_2D, depth_tex.raw);
  // model quad = core->underworld.models->data[plane.raw];
  // draw_mesh(&core->renderer, &quad.meshes.data[0], &TRANSFORM_DEFAULT, NULL);

  rend_frame_end(core);
}

The last (Optional) part can be uncommented to draw a fullscreen quad showing the contents of the shadow depth map texture shown in the first image in this post.

A relatively large change behind the scenes is removing a lot of the implicit uniform uploads inside draw_model(). It now more or less just uploads a model uniform for each mesh, binds textures if that final boolean is true indicating we want to bind textures from its material, and then execute the draw call. This has the consequence of meaning the calling code needs to be more explicit and handle uploading uniforms (although this can be abstracted on again if we want just a generic "draw object with material and camera") but I prefer it this way anyway, it makes it more flexible.