A Monthly Recap
Another month in the books, and another month closer to release! We now have shadows!
The grind continues!
I've been working nonstop day and night in order to get Antiutopia ready for playtesting, and its subsequent demo released for Steam Fest.
With the new physics I talked about in my last post I decided to upgrade and improve the first map, and additionally improved quite a bit of the rendering side
in order to handle a larger map with multiple Z-levels, as well as transparant billboards which I use for various details around the map.
I also expanded upon the lore inside the environment, adding some subtle details and posters that hint at the backstory. I've always thought that environments
should tell a story, and thats what I attempted to replicate on the level. You'll just have to play it to find out the backstory! :)
One major improvement was the addition of a shadow map on top of the current renderer. While I've had lights since the beginning- I really only just used a single
directional light that acted as the sun.
This is how it looks previous to shadows being implemented. The gun model shown in this image is not owned by me- it was a Sketchfab model I used to test out the performance of loading in
a very complex model. It's since been removed and I'm back to my actual self-made gun models.
After finally getting the initial set up completed, I ran into quite a few issues getting it to look just right.
One of the more difficult issues encountered during development occurred after transitioning to hardware depth comparisons.
I initially thought the issue to be the depth bias, however this proved to be incorrect. I also added out-of-bounds rejections, as I thought this could be the cause.
Eventually though, I traced the bug to a mismatch in the light-space coordinate calculations, which was used during the shadow lookup.
The next course of action, aside from overlooking my math, was to attempt to soften the shadows. I implemented a simple 3-tap hardware PCF (Percentage-Closer Filtering)
to account for this, and later improved it further by stepping it up to a 5-tap hardware PCF.
PCF compares the current fragment and the depth stored in the shadow map,
then repeats that comparison at multiple nearby sample locations.
The average of these comparisons results in the final shadow visibility.
Basically, if we get these results;
Sample Result
Center 1.0
Left 1.0
Right 1.0
Up 0.0
Down 0.0
Result Average = (1 + 1 + 1 + 0 + 0) / 5 = 0.6
This means the fragment is 60% visible to the light and 40% shadowed.
Instead of a hard snap from 1 to 0, the fragment shader receives the 0.6, creating the appearance of a soft shadow edge.So with that fixed, I then implemented frustum culling. Rather than drawing every mesh every frame, the renderer first determines whether an object's bounding sphere intersects the current viewing volume. Thus, only visible objects are inserted into the render list.
I built out the frustum using six clipping planes. Using the camera's view-projection matrix, the renderer extracts the six clipping planes:
Ax + By + Cz + D = 0
where:
(A,B,C) is the plane normal.
D is the plane distance.
From here, I wrote my GLTF loader to generate bounding spheres for the level. During GLTF loading, a bounding sphere is generated for each mesh. The renderer then computes the signed distance from the sphere's center to each of the planes.
A mesh is visible only if it survives all six plane tests:
for each plane
{
if (sphere completely outside plane)
reject sphere
}
sphere intersects the frustum
After implementing frustum culling for rendering, I additionally did the same for shadow rendering.
These improvements increased performance significantly, while also adding a major boost in the overall look of the game!
Here's what Antiutopia looks like today!That's about all for now. Until next time!
- Nick
- 8/6/2026 ← Back to Devlog