How to load stacking chunks on the fly?

I’m currently working on an infinite world, mostly inspired by minecraft.
A Chunk consists of 16x16x16 blocks. A block(cube) is 1x1x1.

This runs very smoothly with a ViewRange of 12 Chunks (12x16) on my computer. Fine.
When I change the Chunk height to 256 this becomes - obviously - incredible laggy.

So what I basically want to do is stacking chunks. That means my world could be [∞,16,∞] Chunks large.

The question is now how to generate chunks on the fly?
At the moment I generate not existing chunks circular around my position (near to far). Since I don’t stack chunks yet, this is not very complex.

As important side note here: I also want to have biomes, with different min/max height. So in Biome Flatlands the highest layer with blocks would be 8 (8x16) - in Biome Mountains the highest layer with blocks would be 14 (14x16). Just as example.

What I could do would be loading 1 Chunk above and below me for example.
But here the problem would be, that transitions between different bioms could be larger than one chunk on y.

Transitions between Biomes

My current chunk loading in action

Chunk Loading Example

For the completeness here my current chunk loading “algorithm”

private IEnumerator UpdateChunks(){
    for (int i = 1; i < VIEW_RANGE; i += ChunkWidth) {
        float vr = i;
        for (float x = transform.position.x - vr; x < transform.position.x + vr; x += ChunkWidth) {
            for (float z = transform.position.z - vr; z < transform.position.z + vr; z += ChunkWidth) {

                _pos.Set(x, 0, z); // no y, yet
                _pos.x = Mathf.Floor(_pos.x/ChunkWidth)*ChunkWidth;
                _pos.z = Mathf.Floor(_pos.z/ChunkWidth)*ChunkWidth;

                Chunk chunk = Chunk.FindChunk(_pos);
                
                // If Chunk is already created, continue
                if (chunk != null)
                    continue;

                // Create a new Chunk..
                chunk = (Chunk) Instantiate(ChunkFab, _pos, Quaternion.identity);
            }
        }
        // Skip to next frame
        yield return 0;
    }
}

Update

Okay here is a picture of a - what I call it - ‘layered chunk’. Taken from Minecraft.

What you see here is actually one chunk - in sense of game mechanics. But in sense of logic / programming these are 16 chunks. 1x16x1 chunks.
So the uppermost layer is a noise-generated field (or whatever, a surface - to keep it simple) All underlying layers are ‘filled’ (every block is a material, not air).

Since the image is very large, I’ve decided to just link to the image.
(http://hydra-media.cursecdn.com/minecraft.gamepedia.com/e/ec/Chunk.png)

Update2:

Well while still thinking about this I came to the following idea, I’ll try now:

Load every chunk on x/z viewrange. When the chunk is empty / transpernt (full of air) then I’ll go as long as the chunk is empty go one layer down.

I think this should be a way to go. Maybe I’ll always load the chunk below the loaded chunk, to avoid falling into a void.

You don’t need any fancy logic how to decide which chunk you should load next, Just create a “sorted job queue”. So all you do is adding just the position (of logical representation of your chunks) that are within the current view range into a list. You then sort the list by the distance of the chunk from the player. The nearest ones are generated first.

When moving around you would check in intervals if all chunks in the view range of the player are already loaded. if not, add them to the loading list.

Your loading coroutine just picks a chunk from the list (if there is any), removes that chunk from the loading list and loads the chunk.

You need of course another list to hold the currently loaded chunks. This is also important when you want to remove chunks too far away. Keep in mind to use a larger unloading radius and a smaller loading radius to prevent unnecessary loading / unloading when the player moves in a small area.