So shadows…

What?! Shadows again? Yep, I’m sure you’re getting tired of hearing about it, I’m getting tired of thinking about them… But here is what I’ve done in the last few weeks.

  • Decoupled the filter radius from the SpotLight penumbra and added as an explicit softness property. In retrospect, this wasn’t the good idea I thought it was, it meant lights without penumbras could only have hard shadows without coming up with some other solution. Dumb.
  • Optimised sampling by adding an early out. I now do 4 samples first to check if the fragment is in shadow. Most fragments are either fully lit or fully in shadow, so those now only need 4 samples instead of 16. This meant I could actually increase the sample count for shadow edges (32, up from 16) while still seeing performance gains (theoretically, at least.)
  • While on the topic of sampling, I also addressed some muddy quality and banding issues. The first fix was randomly rotating the sample kernel, which helped but introduced noise. So I swapped to using a blue noise (ironic) texture to drive the rotation instead.
  • Fixed a pervasive SSAO issue where samples were flipping directions at face boundaries that was causing hard edges/obvious faceting. It was most obvious on curved objects and I had noticed it on the AT-ST model but shrugged it off. When I dropped a sphere into the scene I couldn’t ignore it.

Shader Time

After putting shadows to bed (for now…) I turned my attention to shaders.

Up until now I only had the humble Lambert shader (excluding utility shaders like SSAO, geometry pass etc.). Lambert is a good place to start because it’s simple to implement and understand. It is a lighting model that describes a completely matte surface where light is reflected equally in all directions.

But what about shiny things?

I’ve added a Blinn shader for that. Blinn adds a specular component, light that is reflected in one direction and creates the bright highlight on shiny objects.

While working on the Blinn shader, I realised I had hard-coded some “ambient” light into the Lambert shader to lighten things up a bit, and had forgotten all about it.

Ambient light is light with no definite source, light that’s just bouncing around the scene.

Adding ambient light to a shader is cheap, though not exactly accurate. But without it (in the absence of ray/path-tracing), shaders tend to drop into complete darkness and look very computer-graphicy. Of course, you shouldn’t hard-code values into your shader, I had done it temporarily and just forgot I did it.

As is with everything else so far, adding the Blinn shader though itself simple, came with a barrage of other fixes/additions/refactoring.

Here’s a few standouts

  • Added a very simple AmbientLight, which meant some refactoring of what the base Light class should be
  • To that end, added a ShadowCasterLight extention of Light that the SpotLight and DirectionalLight extend
  • Added a Shader DAG node for the SceneGraph, this Shader is now what is assigned to a Shape in the SceneGraph

Object Picking

Now that I have multiple property panels in the GUI to tweak when testing shader/light interactions, it quickly became necessary to be able to select objects in the scene and only show properties relevant to that object.

I read about object picking a while ago in Shi Yan’s WebGPU Unleashed series (https://shi-yan.github.io/webgpuunleashed/Control/object_picking.html), here Shi Yan describes two methods:

  • Raycasting: Shoot a ray from the camera through where the user clicked, iterate over the selectable objects, and check if the ray intersects their bounding box. The closest hit (if any) is your selected object.
  • Colour ID: Encode a unique RGBA colour for each selectable object and render the scene to a texture. Then sample the pixel where the user clicked and decode it — that’s your selected object.

There are pros and cons to both approaches. Raycasting typically uses bounding boxes for speed but they aren’t very precise and often overlap, making picking accuracy a bit iffy. A Colour ID pass, however, has pixel-perfect accuracy, but does have a one-frame latency and requires additional bind groups and render passes.

I ended up needing both, I use the raycasting approach for selecting locator shapes (largely because they aren’t part of the main render pass, but also because they’re not solid shapes) and Colour ID for mesh objects. Both of these systems run through a single SelectionManager that handles tracking selections and updating the GUI as necessary.

AT-ST Returns

  • h toggles the render options
  • ~ opens the terminal
  • Mouse look with left/right mouse buttons
  • w, a, s, d to move

Here there the AT-ST model is back (still no textures, I never got that far with it), but it has my new Blinn shader applied. The ground plane has the original Lambert.

Also just a reminder that you can enable ‘Locators’ in the ‘Render Options’ if you want to play with the light settings.

Next up is PBR (Physically Based Rendering) shaders!