Texture Variety & Connections

by Taylor Hadden | 23:51

Pixels Please

I’m quite enamored with pixel art on voxels. The odd marriage of the 2D and 3D concepts appeals to me – a side effect of a prior Minecraft addiction, perhaps.  The addiction evolves, and variation is one of the first fruits off of the vine.

Texture Variation

The previous texturing system supported different textures for the different faces of the texture, though this wasn’t taken advantage of in the textures used in Alpha 6. This functionality has been expanded, and a material can now have any number of variants for any of its sides.

unity_personal_64bit_-_game-unity_-_unity_projec_2016-11-16_18-19-11

These two tiles have the same number of variations. Variations are weighted, so their frequency can be freely adjusted.

All textures are packed into a collection of atlasses, so each texture is actually just a UV coordinate. These UV coordinate lists are grouped into a lookup table of materials and grouped further by a lookup table of block faces.

class BlockSet {
    Dictionary<ushort, MaterialDefinition> materialLookup;
}

class MaterialDefinition {
    ushort materialID;
    Dictionary<Sides, TextureLookupCluster> textureLookup;
}

struct TextureLookupCluster {
    readonly float[] weights;
    readonly Vector2[] uvs;
}

The texture lookup cluster is built from a list of the texture maps that apply to that particular face:

class TextureMap {
    readonly Sides flags;
    readonly Vector2 uv;
    readonly float weight;
}

struct TextureLookupCluster {
    public TextureLookupCluster(List<TextureMap> textures) {
        uvs = new Vector2[textures.Count];

        for (int i = 0; i < uvs.Length; i++) {
            uvs[i] = textures[i].uv;
        }
        if (uvs.Length > 1) {
            weights = new float[uvs.Length];

            float maxWeight = 0;
            for (int i = 0; i < uvs.Length; i++) {
                maxWeight += textures[i].weight;
            }

            // This makes the weights ascend while keeping relative value
            // vs a random input of 0..1
            weights[0] = textures[0].weight / maxWeight;
            for (int i = 1; i < uvs.Length; i++) {
                weights[i] = weights[i - 1] + textures[i].weight / maxWeight;
            }
        }
        else {
            weights = null;
        }
    }
}

During meshing, this bank of uv coordinates is queried like this:

class BlockSet {
    // The primary entry point
    public Vector2 MaterialUV(Voxel voxel, Sides face, float random) {
        MaterialDefinition map;
        if (materialMap.TryGetValue(voxel.MaterialID, out map)) {
            return map.MapForSide(face).UV(random);
        }
        return Vector2.zero;
    }
}

class MaterialDefinition {
    public TextureLookupCluster MapForSide(Sides face) {
        return textureLookup[face];
    }
}

struct TextureLookupCluster {
    public Vector2 UV(float random) {
         if (uvs.Length == 1) {
             return uvs[0];
         }
         else {
              for (int i = 0; i < uvs.Length; i++) {
                  if (weights[i] >= random) {
                      return uvs[i];
                  }
              }
              return uvs[0];
         }
    }
}

Variations are selected procedurally and will be consistent between individual meshing operations. This is achieved by creating a Random object seeded by a chunk’s id and always querying that Random instance in the same order every time.

// Metaphors are fun
class ChunkViewLoaf {
    public void BakeMesh() {
        /*
         Setup Happens
         */
        System.Random rng = new SystemRandom(chunk.ID.GetHashCode());
    
        for (int i = 0; i < chunk.Length; i++) {
             random = (float)rng.NextDouble();
         
             /*
              The rest of the meshing algorithm
              */
         }
         // Since spacial organization of a chunk's voxel array is consistent,
         // and the Random instance will always start with the same seed, this
         // setup will always return the same "random" number for a voxel in a
         // specific position.
    }
}

Texture Connections

Now we cook with butter. Connecting textures to one another allows for the illusion of voxels being parts of larger objects. The simplest form of this is blocks of stone — or jade in this case.

unity_personal_64bit_-_game-unity_-_unity_projec_2016-11-16_18-36-24

The illusion is quite effective. Materials designed to use connections use 6 of the 16 data bits in the voxel to store whether or not a voxel is connected on each of its 6 faces. When meshing a face of the voxel, the data from the perpendicular faces are reworked to 4 bits for the faces 4 sides. Finally, there are 16 possible combinations of those four bits, and at least one texture is needed in the lookup table for each of those possibilities.

unity_personal_64bit_-_game-unity_-_unity_projec_2016-11-16_18-41-24

The implementation of this feature is actually quite simple: just another layer in the material lookup tree. The complex part was updating all of the various tools in order to author textures with the connection flags and edit the flags of relevant voxels as they are edited. A number that should jump out at you is that I need to make and maintain 16 textures for every connected material. The thought of having to manage 16 separate textures for this feature – let alone any variations – led me to build a node-based texture editor that I will show off in a future post.

I’m looking forward to digging into the potential for expression these two features provide.

A New Voxel Engine

by Taylor Hadden | 1:36

He’s Dead, Jim

I am in fact not dead, and neither is this project. The past several months have been busy with external issues, virtual reality experiments, and finally some proper work on Freedom of Motion that I will share some of with you now.

Voxels – A History

A big change that took up the bulk of my time was the overhaul of the voxel engine. My previous implementation relied on a naively-approached octree structure. Octrees were originally used with the notion that they would save on memory; after all, it makes sense to represent a big area of identical data as a single object. While this system clearly functioned, it had a number of limitations and drawbacks.

Meshing performance was quite poor. FoM uses 16x16x16 meter chunks with a minimum voxel size of .5 meters. Using a dry load from the starting area of the map in Alpha 6, the fastest meshing time was around 4ms and the average time was 22ms. However, the slowest chunks were built in 938ms. While meshing was threaded and didn’t affect the frame rate, this kind of performance spread was a major red flag.

Memory usage was also poor to abysmal. A uniform chunk may have had the relatively light footprint of a single volume, but this structure did not scale well. A single octree object used three doubles for positioning, a float for size, a 16 bit unsigned integer for material type, a 16 bit uint for form type and two additional bytes for the planned but unused features of per-side connection and smoothness values, all adding up to a whopping 240 bits per unique volume of voxels. In a worst-case scenario with the octree split all the way down, a single chunk would take up over 1.2 megabytes of memory.

Clearly, something had to be done.

The New Hotness

Memory and storage in the new voxel system is back-to-basics. Chunks represent voxels in a simple flat array. A single voxel now uses 32 bits: a 16bit material ID and a 16bit form data field for storing additional information. More on that later. Chunks are now stored using a 64bit positional id as defined in VoxelFarm’s WorldOctree documentation. While the actual implementation of the Chunk also contains dirty flags and C# edit events, as well as any entity data, the voxel-specific memory usage now uses a consistent 131kB of memory.

Like the old engine, chunks are stored in 8x8x8 chunk region files on disk. When writing to disk, identical series’ of voxels are run-length-encoded; a chunk containing uniform voxel data takes up only 102bits. While the use of a 16bit uint for length encoding does mean that the potential worst-case-scenario has a 50 percent overhead compared to just the raw data output, I expect the real-world savings of this system to be substantial. Another significant addition to the engine is that loading regions into memory (as well as generating chunks) is threaded. This was a significant source of per-frame lag in Alpha 6 when first loading a level and should now be alleviated.

The improvement from the previous engine is clear:

Min Chunk Size Max Chunk Size
Original 240b 1.25mB
New 112b 131kB
% Reduction 53% 89%

Even the potential waste from extra run-length-encoding information in especially diverse chunks is well below the maximum of the old system.

Meshing performance was slightly more difficult to test. Every part of voxel engine has been completely rewritten, and given that none of the previous levels we built are going to actually be used in the final game, I decided that spending the not-insignificant amount of effort required to build a migration system for that old data was not a good use of time. Unfortunate, but we move forward. However, performance was tested in a new scene, and I think the numbers speak for themselves:

Min Time Max Time Average
Original 3.74ms 913.16ms 17.81ms
New 1.44ms 17.36ms 2.6ms
% Reduction 61.6% 98% 85%

I think losing a fiew exploratory levels is worth it for this level of performance gain.

Why?

There are a number of elements at play here. First of all, using less memory is going to make something faster, as the CPU simply has to chew through less data. This explains the worst-case scenario easily, but we are still seeing dramatic performance increases even in empty chunks. Surely, since we are now looping through a 32,768 length array even when a chunk has no information in it, the process should be slower? It’s not, and it’s because all of the data we need is right next to each other. Voxels are structs, not objects like octrees were. That means that not only do we not have to dive into the heap to get at an object (slow), we don’t have to dance all over memory as we move through the octrees.

The previous octree implementation also did not scale very well when it comes to large worlds, as a new parent would have to be created every time a new chunk outside of the old structure was required. In the new system, chunks are stored in a simple dictionary keyed by their 64bit ChunkID, making adding a new chunk or hopping to a specific one a very simple operation.

Further Improvement

There are still some hot-spots in the engine that must be addressed. For example, I am currently using Unity’s mesh collider for collision. Regenerating this when a chunk changes is quite slow (up to 30ms all by itself from current testing) and cannot be threaded. Additionally, while the overall framework for lower level-of-detail meshes at far distances is there, it currently only functions with procedurally generated chunks. Sub-sampling of real data needs to be implemented and there are issues with seams between levels of detail that need to be resolved before that feature is fully ready for prime time.

Next Time

No new alpha build yet. That will occur when I have implemented an actual gameplay loop (gasp!).

I’ve said it before, and I’ll say it again: I want to increase the pace of updates and posts. An eight-month gap says that I’m quite bad at following through on this; however, I have new features to share and new tools to show off, and I will do so over the next few weeks. Stay tuned.

Freedom of Motion Alpha 6

by Taylor Hadden | 19:30

An Exploration Update

For this alpha, we had two primary goals. We wanted to make a solid pass at improving the exploration experience, and we also wanted to implement a few graphics and lighting improvements.

The two big gameplay additions come in the form of glowing orbs. The larger and most active glowing orb has been named Ivan, and he follows you around providing light. The smaller orbs are called mementos, and act as keys for gates that unlock new areas. The mementos are supported by new behind-the-scenes infrastructure that lets world entities store references to each other. This is a core bit of tech that will continue to be exploited in the future.

Two other important pieces of technology introduced in this alpha are screen-space ambient occlusion and bloom. We are using Sonic Ether’s excellent implementations of both. These two additions have let us greatly expand the dynamic range of lighting while maintaining a readable image. It does get a little dark sometimes though; Ivan is not yet perfect.

We’re moving away from using static, stationary lights for a couple of reasons. Our levels are built dynamically at runtime, cutting us off from the baked lighting and global illumination solutions of Unity. As we get further into our work with physically shaded pixel art, we’ve found that dynamic lighting and the constant fluctuation of the lighting angle really helps sell the affect of normal and parallax maps. However, dynamic shadowed lights quickly become very expensive. Having Ivan (and the sun in outdoor areas) be the primary light source, cuts down on the number of lights we can expect to be rendering at any given time dramatically. At the same time, sometimes you want to illuminate a certain area “just so”, and the crystals that Ivan illuminates throughout the world gives us some authorial control.

The loss of global illumination is not something we take lightly; the image quality improvements it brings are very impressive. As we continue to delve into possible art styles, we will be reevaluating the potential impact of such systems and how they would feed into the kind of game world we are trying to build.

An incomplete changelist:

  • Added bloom and SSAO along with graphical options for some basic control over them.
  • The beginnings of a custom skybox shader that matches the sun’s direction
  • Added an illumination-providing glowing orb named Ivan
  • Added crystal sconces for Ivan to illuminate
  • Added the ability to direct Ivan by holding “E”.
  • Added the ability for entities to store type-safe individual, list, and set references to other entities.
  • Added Memento Groups
    • Added collectible mementos
    • Added memento gates that open when the requisite number of mementos have been collected.
    • Added crystalline memento trees that display a leaf node that glows when you have picked up a memento and provide a way of keeping track of what mementos you need.
  • Made it so that the position of the sun is dependent on the number of mementos you’ve collected.
  • Added a new tab to the entity tools allowing you to access world entities.
  • Added new visual and sound effects for spawning.
  • Added ragdolling and visual and sound effects for death.
  • Improved how the editor interface selects entities so that entities with triggers can choose whether or not their triggers can be used to select the entity.
  • Improved the world materials to include the use of the specular, normal, and parallax maps.
  • Fixed a bug that would cause the game to crash if you had autosave enabled, left a world, and sat on the main menu long enough for the autosave timer to fire.

Future Plans

For Alpha 7, we will be pursuing some performance and functionality improvements for the voxel engine. We hope to add level-of-detail support and greatly improve the render distance, and we will also be exploring the implementation of foliage and soils. As always, you can follow our progress on Trello.

Freedom of Motion Alpha 5

by Taylor Hadden | 0:26

An Editing Tools Update

Over the past month, we have reworked the entire editing framework for Freedom of Motion.

The meat of these changes aren’t all that particularly interesting, but the code base for the editing tools is now more robust. Some critical features are now supported, and new features will be straightforward to implement.

An incomplete changelist:

  • Editing mode defaults to a third-person camera which can pan, orbit, retarget, and zoom.
  • Blocks are defined by the new Block Set Editor
  • You can select what blocks you have available in your block bar with the block library
    • Clicking a block add it to the end of your bar
    • Hovering over it and pressing a number key will place the block in that number’s slot
  • You now place entities by selecting them from the entity library and clicking where they should be spawned.
  • The Cube Brush was given a border that appears through walls
  • Added the Paint Brush: Paint a material on a surface with left click, select a material with right click
  • Added the Free Brush:
    • Right click and drag to create a selection volume.
    • Left click and drag on the volume to move it.
    • Fill the volume with a material by clicking on the material or pressing the corresponding number key.
    • Scroll the mouse wheel over a face of the volume to push and pull the opposite face, resizing the volume.
    • Hold Shift to make Left and Right mouse actions act along the normal of the selected face.
    • Hold Shift to make the mouse wheel actions on the volume affect the face the cursor is over instead of the opposite face.
  • All editing keybindings can be rebound in the options menu
  • Removed all previously hard-coded keys, with the exception of Escape
  • Added the ability to rename a World
  • Entity editor now appears in world space
  • Added a minor noise texture as a detail map to the terrain

The downloadable release has the same levels as Alpha 5, and gameplay will be the same.

Future Plans

For the next release, we will shift our attention back to the game itself. The two points of focus will be on lighting and on enhancing the sense of exploration in the game. Once again, you can follow our progress on Trello.