Skip to content

11 October 2025 - Terrain System and Lighting

<<< Previous Post

You're on the latest post!


  Some interesting technical stuff in today's post.

New Lighting, and Thermal Energy

  Long-time readers will be familiar with lighting in the game, and how many changes it has gone through. It has just gone through another change that I am very happy with and very excited to tell you about!

  The Conservatory now uses a hybrid baked lighting system. One of the things I realized I wanted was the ability for things like light detectors, or querying light levels for monster spawning. Typically the way to do this would be something like Minecraft, where the lighting is part of world generation. This is baked lighting; it is computed as-needed, and the light levels are stored in the world. This is in contrast to real-time lighting, which computes each light every frame for dynamic lighting and reflections.

Dynamic Lights

  The thing is, I wanted dynamic lights as well (flashlights, holdable torches, that sort of stuff). I am now proud to announce that not only is this possible, but lights being held by the player or emitted by an entity can actually affect the light level and be detected! This was significantly harder to do than it might sound, especially because of the nature of how dynamic lights are purely on the GPU. Typically, the CPU and GPU are separate. The CPU tells the GPU to do some work, and the GPU does it and either puts it on your screen, or gives it back to the CPU. You can think of it kind of like a company outsourcing its work. The two don't work together directly, so what goes on in one does not concern the other, only the result of that operation, where applicable.

  This problem was solved using some complex trickery involving virtual rendering. Basically, I make a fake camera and some geometry, as well as a special detector material, and read back the pixels illuminated by the lights nearby in that fake material. This never appears on your screen, of course.

Baked Lights

  The previous technique is great, but you have to remember: Every light, especially lights that can cast shadows, have a performance cost per pixel. This means that having thousands of lights will lag your game into oblivion if they are real-time lights. This is fixed with baked lighting, like what Minecraft does. Rather than compute the lights every frame, they are computed once and stored per voxel.

  Unlike Minecraft, my technique does not use flood fill. Flood fill is where the light "spreads out" until it hits a wall or runs out of energy. It works, but there's one problem, and you'll know this if you have ever tried making a dynamic light in Minecraft without Optifine or Iris or whatever you use: It's slow. I opted for a different technique. My technique has two major benefits.

  1. My technique can be run in parallel. This means many lights can be computed simultaneously, and it can even be done on your GPU for improved performance!
  2. My technique allows for colored shadows.

  In particular, colored shadows are a huge deal for me. If you look at any game, there's one thing I promise you won't ever see on a dynamic light unless you are using RTX, and that's colored shadows. There's a reason for this: It's almost impossible to reliably compute in real time. It's just too much work for your GPU to do, and current technologies designed to make shadows fast to render simply can't support translucent shadow casters.

  Below is an image showing off an example of this baked lighting. Press on the images for a description of what you are seeing.

The setup being rendered, showing a 4x4 block square. There is a light source in the top left corner, a translucent cyan block to the right of the light, and an opaque black block near the bottom-center. The rendered result representing the computed lighting values for each voxel in the 4x4 square. The top right part of the image is cyan, a result of the clear cyan block casting a cyan shadow. Likewise, a dark black shadow is cast by the opaque block.

Thermal Energy

  One of the last things to detail is thermal lighting. In my game, temperature is a fundamental part of the mechanical balance of the game; different species have different ranges of temperature that they consider acceptable, and some planets aren't accessible until you make technology for your space suit to adapt its temperature. Moreover, the Novan species sees in the infrared spectrum. This means that a way to change the temperatures (heating, air conditioning) and visualize thermal energy is extremely important to me.

  Thankfully, I achieved this using the systems I just designed above, inspired by the real world: Heat is light. One thing those two images above didn't show was an associated temperature for that light source, with each block having a thermal conductivity alongside its transparency (which is transparency for heat energy). That's right, I didn't just improve the performance and behavior of lights, but I also introduced this new mechanic that leverages it in a fantastic one-two punch.

  The only important detail is that this does not simulate atmosphere; heat can't spread in the air, so if you have a portable star just sitting in the open on a planet, it won't gradually heat up the whole thing. This is an unfortunate limitation but one that is required lest you be forced to run the game at an impressive 0.000001 FPS.

Bugfixes and Performance Improvements

Fixing an Obscure Crash

  One of the issues plaguing the game for a long time was a random crash that would sometimes happen when generating a world. I isolated the cause of this crash: I misunderstood the rules and limitations for telling the game engine about physics and rendering changes.

  For context, world generation in my game is multithreaded. This means your modern CPU, which has multiple cores, can use those multiple cores to work on generating many parts of the world at once. This really improves the amount of time it takes to load a world; think of it as having a team of workers instead of just one. This is great and all, until I have to tell the game engine "hey, the physics and the appearance of this part of the world needs to be changed!"

  That specific action, informing the engine about changes, is not thread-safe. The analogy I have used in the past is that this is a bit like multiple people trying to write at the same spot on a sheet of paper. You don't get clean writing, you get a disaster of scribbles and the pencils clacking into each other into an incomprehensible mess. The same is true for your computer, and this results in memory corruption. This corruption is usually invisible, and by the time you notice it, it's too late: The program has crashed because of it.

  The solution to this problem was to break it up into two steps. Multiple threads can do the work on generating the world at once, but as soon as they are done, they collect all their work together into one pile. Later on, one thread goes over that pile and sends that neatly packaged data off to the game engine in an orderly and predictable fashion.

Reducing Memory Requirements and Improving Task Scheduling

  Another major problem I had was excessive memory usage for high render distances. The game could use upwards of 12GB of RAM if your render distance was set to 24 chunks. This is unacceptable, but thankfully it was an easy fix, especially because it worked in tandem with the crash fix up above. It's much simpler than you might think: Now, I dispose of the data for the world after sending it to the engine. No need to keep it around once I'm done with it, right? The actual voxels are still kept, of course, but information like the geometry and physics objects are left to the engine to work with as it needs to.

  The other problem that I ran into was issues with task scheduling. This ties back into multithreaded code; you can imagine the Task Scheduler like the manager of a worksite, telling people what jobs they need to complete in the moment, and coordinating who does what, and when they do it. My old task scheduler was the default that comes with the .NET Runtime (the thing that actually runs the code of the game). It's pretty good, but not the right choice here. I had to make a new scheduler which makes more efficient use of individual workers by limiting the amount of threads that can be doing work on a task at once.

New Mechanics: Voxel Damage

  Voxel Damage itself isn't new, but this technique is. Voxels in the game now have two damage values:

  1. Soft Damage, which is based on time and is intrinsic to the voxel (that is, I don't need to tell the world about the voxel healing, it's a property of time itself)
  2. Hard Damage, which is a health value in the voxel that does not heal over time.

  Most tools and player interactions will deal in Soft Damage. Hard Damage is instead intended for things like map creators to give the appearance of damaged or broken materials in the world, either as a set piece or as a mechanic for dungeons.

Closing Thoughts

  This blog post took a really long time to get out (it was originally planned for 22 September!) but I decided I wanted to work on more than just a generator crash fix in that time. Originally I was going to work on gameplay, but the lack of good lighting really irked me and made it hard to really get a feel for the game, so I got side tracked and worked on light and heat before releasing this post.

  Also, you'll have to forgive me if the writing in this one seems a bit abrupt or oddly paced. I've been really worn out from working, and so my patience is a bit bleh. There's actually a lot more to do with the new lighting system, for example directional transparency (created by having a carved/shaped voxel) isn't handled yet, nor is the actual part of the process where I apply that lighting data to the world. Those are the last two major problems to solve, then it will be done.