This implements nodes of a sparse voxel octtree (SVO), with optimizations that enable optional attributes to be stored frugally.
SVOs are fairly efficient at storing volumes where most of the voxels are identical for large chunks of landscape, but when we try to associate different types of data as a 'variant' or 'union' record in each node, the structure suffers from unnecessary bloat because variant records take the length of the largest variant; the unused fields still take memory.
Our code identifies a small number of 'parts' that may be included in a node. If we have three parts, then there are eight possible composite combinations of these parts (by binary combination: {fff, fft, ftf, ftt, tff, tft, ttf, fff} ), and each of these composites may have a different record length.
If we then store each combination (variant) in its own array or list, we avoid the wastage of unused records. The best improvements are found in scenarios that mix long records with short records.
Cons: it's possible that fragmentation can occur with the reallocation of memory when up-sizing a list. We have not addressed this potential problem, which may disappear with the virtual addressing that exists in most modern OSs, or be solved by adopting a simple hierarchical memory manager for the list or array.
Example
In the following, we present a simple example of the technique, which uses three attributes. Note that our actual code will include more structure, and some details are omitted:
- Basic (
TCNV_Part_Basic), which contains:- a pointer to the parent node,
- information about which attributes are set in this node (remember, just 1 byte!)
- information about which attributes are set in sub-nodes (also 1 byte).
- Material (
TCNV_Part_Material), which contains:- just a reference to an entry in the list of materials.
- Children (
TCNV_Part_Children), which contains:- 8 × octtree pointers to sub-nodes. Null values mean that there is no sub-detail in that sub-volume, and the volume is accurately represented by the current node.
- ...[and so on]...
Any data that is missing from the children may be obtained from parent nodes. We note which attributes are stored in the node using a set (a row of bits, of type TCubeFeatures where each bit denotes whether the attribute is present).
These are typed, fixed-length records, named with prefix "TCNV_Part_" to denote "Type: Cube Node Variant - Part"
TCNV_Part_Basic = record
Parent: PCubeNode; // 32 bits
FeaturesThis: TCubeFeatures; // 8 bits
FeaturesChildren: TCubeFeatures; // 8 bits
end;
TCNV_Part_Material = record // 16 bits
Material: TMaterialID;
end;
TCNV_Part_Children = record // 8 × 32 bits
ChildNodes: array[TUnitDirectionEnum] of PCubeNode;
end;
Code 1: 'Part' types.
These types are then combined into composite records:
TCNV = record // 48 bits
Basic: TCNV_Part_Basic;
end;
TCNV_M = record // 64 bits
Basic: TCNV_Part_Basic;
Material: TCNV_Part_Material;
end;
TCNV_C = record // 244 bits
Basic: TCNV_Part_Basic;
Children: TCNV_Part_Children;
end;
TCNV_MC = record // 304 bits
Basic: TCNV_Part_Basic;
Material: TCNV_Part_Material;
Children: TCNV_Part_Children;
end;
Code 2: Composite types, which combine the Part types.
...each of the Composite types can be stored in arrays where the elements are of fixed length. With sensible re-sizing of the allocated memory, we can minimize the amount of memory that would otherwise have been wasted by conventional variant records. In Code 2 (above), the largest record is 304 bits (38 bytes), and the smallest record is 48 bits (6 bytes), which gives a 'saving' of 256 bits (32 bytes, or 84%) for every 'leaf' node.
Because the data types and field offsets are different in each case for any given field (like 'Material' for example), we'll need to wrap these structures in methods that allow requests to be mapped into our non-standard variant structures.
It will be interesting to see whether this works in practice. If it does, then we'll be able to add many types of sparse data to existing matter in the world model, with very little penalty. This is how we'll inject lighting data into our world.
Further Optimizations
This is getting ahead of ourselves a little, but we should mention that the above technique still has lots of room for improvement. Further optimizations are possible by abstracting the subdivision and material packing, which we might need for small-scale detail, to bring massive efficiencies in specialized cases.
Material Packing
It's likely that fewer materials will be used within a player-sized cube, so we might be able to (for example) optionally treat sub-detail as an 8 × 8 × 8 array of bits, value
0 representing 'background' material, and
1 representing 'foreground' material. For any of you who know 8-bit computers, some computers (the ZX Spectrum, and the BBC's Teletext mode) allowed the creation of primitive graphics by allowing each 'text cell' to contain a 1-bit image, mapped into foreground and background colours.
Subdivision
At the expense of complicating all the supporting ray-casting routines, we can specify alternate ways of slicing up volume nodes into sub-volumes. It's well-known that octtrees can be very fussy about the placement of a plane surface: if a change of medium corresponds to the boundary of a large node then it is very efficient, but if that plane is just one detail voxel inside the neighbouring large node then the neighbouring node is sliced eight ways for each level of detail until the detail node is reached - very inefficient, especially if your plane happens to be a flat sea or landmass covering the whole map (it can make megabtyes of difference to the storage overhead on a 1024^3 world map).
Edited to provide more detailed examples. We'll revisit this entry if these methods prove to be successful.