Proto Chunks and Visibility Rays
Home -->
Programming Projects -->
Fractal Block Engine -->
Coordinate Systems
The Problem
We do not want to render a chunk that is not visible.
Even though we are using view frustum culling,
we do not want to render chunks that are
completely occluded
by blocks in front of them.
Moreover, we do not want to expand a block into a chunk
in the first place if that block is not visible.
The Solution: Proto Chunks and Visibility Rays
To solve these problems, every second we shoot thousands of rays
from the camera into the world.
Call these visibility rays.
What these rays hit determine when we should render
a chunk and when we should expand a block into a chunk.
We shoot more rays closer to the direction of where
the player is looking.
What is a Proto Chunk?
For a given level of detail, a block is expanded into a chunk
when it is a certain distance from the player.
However when it is first expanded, it is a
proto chunk.
This is an empty shell.
It has not yet been loaded (procedurally generated)
and behaves as if it is still just a block.
Once a proto chunk is hit by a visibility ray,
a request is made to load the chunk (procedurally generate it
and update it with any changes that have previously been saved).
When Do We Render Chunks?
When do we render a chunk (including proto chunks)?
First, to render a chunk, it must intersect the view frustum.
We do a little math to perform this test every frame.
We even do this for proto chunks,
although perhaps that is not worth it.
Assume that a chunk has passed the view frustum test.
- If it is a proto chunk, we now render it
(as a single cube).
- If it is not a proto chunk (if it is a fully loaded chunk),
we only render it if it has been hit by a
visibility ray within the last 10 seconds.
Rendering all 6 sides of a Proto Chunk?
When we do render a proto chunk P, we probably do not need to render
all 6 sides of the cube.
This is because some of the sides of P may be adjacent to either
- solid blocks or
- proto chunks that were previously solid blocks.
Not rendering certain sides of a proto chunk might seem like
only a small optimization.
However keep in mind that each of these
proto chunk cubes takes up a large amount of the screen,
so drawing these would draw many pixels.
Code for shooting visibility rays
Here is code for shooting a visibility ray into the world.
We start at a point, and then repeatedly move the point
by 0.9 (a block is 1.0 wide).
So if a chunk is visible, it is possible to hit it with
a visibility ray.
If a chunk is NOT visible, then it is very unlikely
to be hit by a visibility ray (but it is theoretically possible).
We are trading speed for accuracy.
Once the point is no longer inside a chunk on the current
level, we consider the point on the next coarsest level.
That is, we have to change coordinates systems from a level L
to a level L-1.
The FastBlockHolder is a class that holds all the blocks of a chunk.
void ShootVisRay(
ChunkGetter * chunk_getter,
CoordConverter * coord_converter,
int start_level,
const Vector & start_pos,
const Vector & dir)
{
int cur_level = start_level;
Vector cur_pos = start_pos;
Vector add = dir;
Normalize(add);
add *= 0.9f;
//We cache the last chunk that we considered.
//If our vcp has not changed, we reuse that chunk's
//fast block holder.
bool have_cached = false;
ViewerCentricPos cached_chunk_vcp;
FastBlockHolder * cached_chunk_fbh = 0;
while(true) {
//Only shooting a ray through the 5 finest levels.
if( start_level - cur_level >= 5 ) return;
//The reader can figure out how to
//write fast versions of these next two functions.
//We might want to inline them.
ViewerCentricPos cur_vcp = VectorToVCP(cur_pos);
LocalBlockPos cur_lbp = VectorToLBP(cur_pos);
//Fast track if we are in same chunk as last time.
if( have_cached &&
cached_chunk_vcp == cur_vcp )
{
bool solid = cached_chunk_fbh->IsSolid(cur_lbp);
if( solid ) return;
cur_pos += add;
continue;
}
//At this point, we do not have any cached information
//about the current chunk.
Chunk * chunk = chunk_getter->GetChunk(cur_level, cur_vcp);
if( !chunk ) {
//The chunk does not exist.
//Going one level closer to the root
//(going to a coarser level).
have_cached = false;
if( cur_level-1 < 0 ) return;
//Converting the vector cur_pos
//for level cur_level to a vector
//for level cur_level-1.
Vector new_pos;
coord_converter->ConvertPos(
cur_pos,
cur_level,
cur_level-1,
new_pos);
cur_pos = new_pos;
cur_level = cur_level-1;
} else {
//The first time within a particular chunk.
have_cached = true;
cached_chunk_vcp = cur_vcp;
//Telling the chunk that is has been hit
//by a visibility ray.
chunk->MarkVisRay();
//If the chunk is a proto chunk, the
//solid function will return true.
//That is important.
if( chunk->IsSolid(cur_lbp) ) return;
//At this point, the chunk is not a proto chunk,
//and so the fbh will be non-null.
cached_chunk_fbh = chunk->GetFastBlockHolder();
cur_pos += add;
}
}
}
Different types of solid
There are actually several different notions of what it means
for a block to be solid:
- Physically solid
- Visibly solid
- VisRay solid
A block is physically solid if the player cannot move through it.
A block is visibly solid if it appears solid for rendering.
A block is visray solid if and only if it is both
physically and visibly solid.
The code above should really be asking if the blocks
are visray solid.
Cocoon Rendering a Chunk
Given a chunk, we say that we
"cocoon render" the chunk if we
just render the block that occupies the same position
as the chunk instead of rendering anything inside the chunk
of its children.
In the case that the block type of the chunk is visibly solid,
we should say that we "solid cocoon render" the chunk
as a single block.
In the case that the block type is NOT visibly solid,
then there is nothing to do if we wanted to
"empty cocoon render" the chunk.
The concept of (solid) cocoon rendering a chunk is
related to the concept of proto chunks, because when we
render a proto chunk, we can only cocoon render it.
However, if a chunk is an actual chunk (and not a proto chunk),
there is a situation when we might want to cocoon render it.
Namely, the situation when the chunk has not been hit by
a visibly ray in 10 seconds or so.
In terms of (solid) cocoon rendering a chunk,
there are at least two important rules we want to follow.
Rule 1: If we (solid) cocoon render a chunk,
then we should not render any descendent chunks
of that chunk at all.
Otherwise, we might get an ugly "Z fighting"
rendering glitch.
Note that we are assuming the solid block being
rendered has no transparency
(otherwise that causes a problem).
Rule 2: We should not (solid) cocoon render
any chunk whose VCP is (0,0,0).
If we did do this, then because of Rule 1
and since the player is in every chunk with vcp (0,0,0),
then parts of the world would not be rendered when they
should be.
One might wonder why we have proto chunks at all.
A big reason is because we clear the depth buffer
after rending each level.
"Fertile" proto chunks are rendered along with
other chunks on their same level
before we clear the depth buffer.
"Non-fertile" proto chunks on level L
are instead rendered with the chunks
on level L-1 before we clear the depth buffer.
Without the existence of proto chunks, parts of the world
might be rendered in the wrong order.
An engine that does not clear the depth buffer at all
would be much simpler, and the reader can
figure out how to organize things in that situation.
Instead of having a proto chunk, we could simply
render the corresponding block as part of the parent chunk.
Once a block is subdivided into a chunk, it is
automatically populated.
The IsVisiblySolidMod1 Function
The following is an important function for
figuring out what to render: IsVisiblySolidMod1.
This is a member function of the chunk class.
It takes a local block position as an argument
and returns a bool.
If the local block position is for a child chunk
(including a proto chunk),
the function automatically returns true.
Otherwise, the function returns true iff
the block type of the block at that position
is "visible solid".
This function is used to figure out what is the
block surface to render when we render a chunk.
"Mod1" stands for the first modified version
of the IsVisiblySolid function.
The IsVisiblySolid function
just returns if a block is visibly solid
(ignoring whether or not it is a child chunk).