Virtual Chunks

Home --> Programming Projects --> Fractal Block Engine --> Procedural World Generation --> Virtual Chunks

What are Virtual Chunks?

Suppose we want to generate a chunk that has some spheres of blocks intersecting it. If each sphere has radius < 16, we can find the centers of these sphere in each of the 3x3x3 surrounding chunks. Recall that a chunk is 16 blocks wide.

Here is a picture of the chunk (green) to the left of the given chunk we are procedurally generating (blue) with a sphere in it (red). The red sphere intersects the blue chunk.


We call the green chunk the virtual chunk.

So we find if the virtual chunk contains the center of a red sphere, we find the center of such a sphere in the coordinate system of the virtual chunk, and then we convert that to the coordinate system of the chunk being generated (the blue chunk). We can then iterate over all blocks in the blue chunk and see if the center of each block is inside the red sphere. We can change block types of each block accordingly.

Larger Virtual Chunks

What if we want to do the same as in the example above, but we want the sphere to have a radius between 16 and 32? Then it is not enough to look at the 3x3x3 surrounding chunks, we need to look at the 5x5x5 surrounding chunks instead. However 5^3 = 125 is kind of a large number.

What we can do instead is look at 2x2x2 regions of chunks. Temporarily call such a region a "big chunk". Then we find the big chunk that contains the blue chunk and then look at the 3x3x3 surrounding big chunks. See the following picture:


The BinPath Class

See here for a description of what a binary path (BinPath) is. See here for the BlockPath class. Here are parts of the class for BinPath:
class BinPath {
public:
    //A LocalBlockPos has members x,y,z.
    //Each one is a signed char,
    //but we will only used the values 0 and 1
    //for these members.
    vector path;

    BinPath();

    void SetFromBlockPath(const BlockPath & block_path);

    string ToString() const;
};

//This can be faster.
void BlockPath::SetFromBlockPath(const BlockPath & block_path) {
    for(int i = 0; i < bp.path.size(); ++i) {
        LocalBlockPos lbp = block_path.path[i];
        LocalBlockPos lbp1(
            (lbp.x/8),
            (lbp.y/8),
            (lbp.z/8));
        LocalBlockPos lbp2(
            (lbp.x/4) % 2,
            (lbp.y/4) % 2,
            (lbp.z/4) % 2);
        LocalBlockPos lbp3(
            (lbp.x/2) % 2,
            (lbp.y/2) % 2,
            (lbp.z/2) % 2);
        LocalBlockPos lbp4(
            lbp.x % 2,
            lbp.y % 2,
            lbp.z % 2);
        path.push_back(lbp1);
        path.push_back(lbp2);
        path.push_back(lbp3);
        path.push_back(lbp4);
    }
}

Getting the Seed for a Virtual Chunk

Given the BlockPath of the chunk to be procedurally generated, we can convert that path to a BinPath, chop off the last few elements of the path to get a larger chunk, then move to an adjacent chunk to the the BinPath of the virtual chunk. We can then get the seed from that path:
int GetVirtualChunkSeedXYZ(
    int world_seed,
    int salt,
    const BlockPath & block_path,
    int chop,
    int dx, int dy, int dz)
{
    BinPath bin_path;
    bin_path.SetFromBlockPath(block_path);
    
    for(int i = 0; i < chop; ++i) {
        if( bin_path.size() == 0 ) break; 
        bin_path.path.pop_back();
    }
    
    BinPath nearby_bin_path;
    bool ComputeNearbyPath(
        bin_path, dx, dy, dz, nearby_bin_path);
    
    int seed = GenerateSeedFromPath(bin_path);
    
    return seed;
}
Here ComputeNearbyPath is like the function defined here except it works with a BinPath as input and returns a BinPath as output (instead of BlockPath's).

Here GenerateSeedFromPath is like the function defined here but with a BinPath instead of a BlockPath.

If we are ok using tail seeds (see here) then we can use the following much faster code:
int GetVirtualChunkSeedXYZ(
    int world_seed,
    int salt,
    const BlockPath & block_path,
    int chop,
    int dx, int dy, int dz)
{
    BinPath bin_path;
    bin_path.SetFromBlockPath(block_path);
    
    for(int i = 0; i < chop; ++i) {
        if( bin_path.size() == 0 ) break; 
        bin_path.path.pop_back();
    }
    
    int seed = GetAdjTailSeedXYZ(
        world_seed, salt, bin_path, dx, dy, dz);
    
    return seed;
}
If we want the variant GetVirtualChunkSeedXY of GetVirtualChunkSeedXYZ that only uses x and y components to generate the seed, then we simply use that in the function instead of GetAdjTailSeedXYZ.

Virtual Chunk Seed + Virtual Chunk Block Type

To generate the red circle in the examples above, we use the seed for the virtual chunk. However in addition to the seed we can use one more thing: the block type of the virtual chunk. We can use this provided the virtual chunk and the chunk being generated are on the same level, because the block types of the 5x5x5 region of chunks surrounding the chunk that is being generated (the blue chunk) is given to the blue chunk.

Someone making their own engine may want to pass the blocks types of the 5x5x5 (or perhaps just 3x3x3) region of chunks surrounding not just the chunk being generated BUT THE FIRST FEW ANCESTORS OF THE CHUNK BEING GENERATED.

Converting Between the Coordinate Systems

Let C be the chunk being procedurally generated and let V be the virtual chunk. It is important that we can convert to and from the coordinate systems of these two chunks.

We need the following functions which gives the min and max positions of the chunk being generated from the coordinate system of the virtual chunk. We call these the "reverse" min (rmin) and "reverse" max (rmax). Once we know the rmin and rmax values, we can find the min and max positions of the virtual chunk within the coordinate system of the chunk being procedurally generated.

For example, if the virtual chunk is the same as the chunk being generated, then rmin = (0,0,0) and rmax = (1,1,1) will be returned. More generally, if the chunk being generated is inside the virtual chunk, then rmin and rmax will always be between (0,0,0) and (1,1,1).
void GetOffsetsFromChop(
    const BinPath & chopped_bin,
    const BinPath & bin,
    Vector & rmin,
    Vector & rmax)
{
    rmin = Vector(0.0f, 0.0f, 0.0f); //Will add to this.
    float cur_width = 1.0f;

    for(int i = chopped_bin.path.size(); i < bin.path.size(); ++i) {
        cur_width *= 0.5f;
        rmin.x += cur_width * (float)bin.path[i].x;
        rmin.y += cur_width * (float)bin.path[i].y;
        rmin.z += cur_width * (float)bin.path[i].z;
    }
    rmax = rmin + Vector(cur_width, cur_width, cur_width);
}

void GetOffsetsFromChopNearby(
    const BinPath & chopped_bin,
    const BinPath & bin,
    int dx, int dy, int dz,
    Vector & rmin,
    Vector & rmax)
{
    GetOffsetsFromChop(
        chopped_bin, bin,
        rmin, rmax);
    Vector add(-dx, -dy, -dz)
    rmin += add;
    rmax += add;
}
So here is a function to get all relevant data about the virtual chunk:
//min/max are the coords of the vchunk (virtual chunk)
//in the coord system of the chunk being generated.
//rmin/rmax (reverse min and max)
//are the coords of the chunk being generated
//in the coord system of the vchunk.
void GetVChunkData(
    const NodeGenInput & input, //Input to chunk generation.
    int chop,
    int dx, int dy, int dz,
    Vector & min,
    Vector & max,
    Vector & rmin,
    Vector & rmax,
    int & seed,
    string & bt)
{
    //Converting the block path to a binary path.
    BinPath bin;
    bin.SetFromBlockPath(input.full_path);
   
    //Setting the chopped_bin path to be
    //an initial segment of the bin path:
    //we remove the last chop members.
    BinPath chopped_bin = bin;
    for(int i = 0; i < chop; ++i) {
        if( bin_path.size() == 0 ) break; 
        bin_path.path.pop_back();
    }

    //Getting rmin/rmax values.
    GetOffsetsFromChopNearby(
        chopped_bin, bin,
        dx, dy, dz,
        rmin, rmax);

    //Getting min/max values.
    float width = rmax.x - rmin.x;
    float iw = 1.0f / width; //Inverse width.
    min.x = (0.0 - rmin.x) * iw;
    max.x = (1.0 - rmin.x) * iw;
    min.y = (0.0 - rmin.y) * iw;
    max.y = (1.0 - rmin.y) * iw;
    min.z = (0.0 - rmin.z) * iw;
    max.z = (1.0 - rmin.z) * iw;

    //Getting the seed.
    seed = GetNearbyTailSeed(chopped_bin, dx, dy, dz);

    //Need to get the block type from input somehow.
    //Fill in code here!!!  (Get it from the
    //procedural world generation input).
    bt = ""; //Hack!!!
}
For the block type bt, we want to set it to be the block type of the finest real chunk which contains the virtual chunks. These block types are possibly available for the input class.

The "Block Type" of a Virtual Chunk

A virtual chunk might not be an actual chunk. However sometimes we want to get the block type of the finest actual chunk which contains a given virtual chunk. This is done by accessing the adjacent block types given as input to procedural world generation (see here). That is, we access the "block types of chunks adjacent to the parent chunk".