080 - Terrain Editor Groundwork

August 16, 2020

The first three tools that I plan to add to the game editor are placing objects, sculpting terrain and painting terrain.

All of those tools require to editor to be able to collision test against a representation of the terrain in CPU memory (there is separate data in GPU memory which is used for rendering the terrain), so this week I worked on giving the editor access to terrain data.

The game run time already had a CPU model of the terrain, but I intentionally did not take the quick route of just exposing those data structures to the editor. I instead split them out into a general purpose crate.

One of the architectural pillars of the relationship between the editor and the game run time is that they should share as little information as possible.

This approach has forced me to split code out of the game into more general purpose workspace crates such as user-interface and terrain.

This means that years from now it will be easier to re-purpose Akigi's game engine for other projects should I want to expand its footprint, since more and more of the engine code is being moved into re-usable libraries.

Saving and Loading Terrain

This week I started working on the EditorTerrain data structure that will help power terrain editing.

/// A data structure representing all of the terrain in the game world.
///
/// An editor can modify this data structure and undo/redo these modifications.
///
/// When the desired look is reached the terrain can be saved. This will write the relevant
/// texture maps and data files to disk.
pub struct EditorTerrain {
    chunks: HashMap<TerrainChunkId, EditorTerrainChunk>,
    materials: HashSet<String>,
}

The terrain is saved and loaded across many different files. Each chunk of terrain has a file for its displacement map, one for its blend map and another with meta data about the chunk.

The idea behind splitting this data across many files is that:

  1. It's more scale-able. If you want a massive terrain you don't end up having a massive displacement map that you need to load into memory when editing terrain. Instead you can load and unload the data for a subset of the chunks making disk space, instead of memory, the cap on terrain size. The trade-off to this approach is that you need logic to keep track of which chunks are modified as well as a place on disk for these modified chunks so that when you save your edits terrain chunks that have been unloaded from memory still get saved to disk. But I can ignore that for now since my terrain is currently small enough to be able to keep all chunks in memory in the editor at all times. I also don't anticipate this being too difficult too implement. Maybe a day or two for a well-tested implementation.

  2. Easier collaboration. I'm the only person working on Akigi but I like to approach the code as if it were going to be maintained by many others. Splitting data across files drastically reduces the chances of merge conflicts when editing terrain.

Here's the function for loading terrain data from disk so far:

impl EditorTerrain {
    /// Load the EditorTerrain from a directory that follows the expected conventions.
    pub fn load_from_dir(terrain_dir: &dyn AsRef<Path>) -> Result<EditorTerrain, LoadTerrainError> {
        validate_terrain_dir(terrain_dir)?;

        let terrain_dir = terrain_dir.as_ref().to_path_buf();
        let chunk_definitions = chunk_definitions_dir(&terrain_dir);
        let materials = materials_dir(&terrain_dir);

        let mut terrain = EditorTerrain::new();

        for chunk in std::fs::read_dir(chunk_definitions)? {
            let chunk_path = chunk?.path();
            maybe_insert_terrain_chunk(&mut terrain, chunk_path)?;
        }

        for material in std::fs::read_dir(materials)? {
            let mat_dir = material?.path();

            let material_name = mat_dir.file_name().unwrap().to_str().unwrap();
            let material_name = material_name.to_string();
            terrain
                .materials
                .insert(material_name.to_string(), TerrainMaterial {});
        }

        Ok(terrain)
    }
}

Refactoring Terrain

The core terrain code that allows for ray collision testing against the terrain was written when I was first learning Rust and is both bad and untested.

It's one of the few remaining areas of the code base that was written within my first year or so of Rust and has yet to be fully refactored.

When I'm working on an older part of the code base I'll refactor anything that I'm working with and leave the parts that I am not currently touching for a future day.

This approach allows me to bring these older parts of the code base up to new standards (which really just means written with test-driven development) without spending too much time trying to clean the whole house and garage in one sitting.

I refactored a good bit of the terrain code a few months ago, and this time around it's time to finish the job.

I'm simplifying the data structures and functions that power the terrain's collision testing, with the nice side-benefit that they'll now work for arbitrary sized terrain at arbitrary levels of detail.

After that the terrain code will be up to today's standards within the code base, and any future work will be adding new functionality.

Other Notes / Progress

I'm very excited to see the editor tooling come together. I can't wait to throw a user interface in front of some of these core editor functions and start to see and feel the power of these tools.

Next Week

I'll start the week by finishing the terrain refactor.

After that I'm going to add the functions and interface for placing objects within the world.

Implementing that should take the better part of the week, but if there is time left I'll get started on either sculpting or painting terrain.


Cya next time!

- CFN