Stick and Ball Caves
Home -->
Programming Projects -->
Fractal Block Engine -->
Procedural World Generation -->
Stick And Ball Caves
The Idea
Please read this page about virtual
chunks.
For simplicity, we will assume on this page that
each virtual chunk is the same size as the chunk
being procedurally generated (the blue chunk
in the picture).
Consider a model of a molecule what has balls connected
together by sticks.
This is what we mean by
stick and ball caves.
This is the primary cave generation technique in
Fractal Block World.
For each of the 3x3x3 surrounding virtual chunks
(surrounding the blue chunk being generated),
we place balls.
Each ball has a randomly generated radius.
Then we iterate over all pairs of balls in the
3x3x3 region of virtual chunks.
If two balls are less than the
"max edge distance" apart, we join them with a
stick.
Intersection Tests
Once we create all the balls and the sticks
for the surrounding 3x3x3 region of chunks,
we make a short list of all balls and sticks
that intersect the blue chunk
(the chunk being procedurally generated).
Note that it would be a mistake to make a list
of all balls that intersect the blue chunk but then
only consider sticks that connect those balls.
There may be two balls that do not intersect the
blue chunk, but the stick joining them
DOES intersect the blue chunk.
First, we need a function that determines if a sphere
intersects an axis aligned box.
Here is Lua code for such a function:
--This is Lua code.
--Got from Ben Voigt's answer on stackoverlow.
--See: https://stackoverflow.com/questions/4578967/cube-sphere-intersection-test
--c1, c2 are the corners of the box.
--s is the center of the sphere.
--r is the radius of the sphere.
function box_intersects_sphere(c1, c2, s, r)
local counter = r * r
if s.x < c1.x then counter = counter - p.squared(s.x - c1.x)
elseif s.x > c2.x then counter = counter - p.squared(s.x - c2.x) end
if s.y < c1.y then counter = counter - p.squared(s.y - c1.y)
elseif s.y > c2.y then counter = counter - p.squared(s.y - c2.y) end
if s.z < c1.z then counter = counter - p.squared(s.z - c1.z)
elseif s.z > c2.z then counter = counter - p.squared(s.z - c2.z) end
return counter > 0
end
Determining if a stick intersects the blue chunk
is more annoying.
The key is the following function
which determines if a line segment
intersects an axis aligned box.
Here is Lua code for such a function:
--This is Lua code.
--box_min, box_max are the corners of the box.
--seg_start and seg_end are the endpoints of the line segment.
--There is a less tedious way to write this code.
function seg_intersects_box(
seg_start, seg_end,
box_min, box_max)
--
if( seg_start.x > box_max.x and
seg_end.x < box_max.x )
then
local param =
(seg_start.x - box_max.x) /
(seg_start.x - seg_end.x)
local p_y = seg_start.y * (1.0 - param) + seg_end.y * param
local p_z = seg_start.z * (1.0 - param) + seg_end.z * param
if( p_y >= box_min.y and
p_z >= box_min.z and
p_y <= box_max.y and
p_z <= box_max.z )
then
return true
end
end
if( seg_start.y > box_max.y and
seg_end.y < box_max.y )
then
local param =
(seg_start.y - box_max.y) /
(seg_start.y - seg_end.y)
local p_x = seg_start.x * (1.0 - param) + seg_end.x * param
local p_z = seg_start.z * (1.0 - param) + seg_end.z * param
if( p_x >= box_min.x and
p_z >= box_min.z and
p_x <= box_max.x and
p_z <= box_max.z )
then
return true
end
end
if( seg_start.z > box_max.z and
seg_end.z < box_max.z )
then
local param =
(seg_start.z - box_max.z) /
(seg_start.z - seg_end.z)
local p_x = seg_start.x * (1.0 - param) + seg_end.x * param
local p_y = seg_start.y * (1.0 - param) + seg_end.y * param
if( p_x >= box_min.x and
p_y >= box_min.y and
p_x <= box_max.x and
p_y <= box_max.y )
then
return true
end
end
if( seg_start.x < box_min.x and
seg_end.x > box_min.x )
then
local param =
(box_min.x - seg_start.x) /
(seg_end.x - seg_start.x)
local p_y = seg_start.y * (1.0 - param) + seg_end.y * param
local p_z = seg_start.z * (1.0 - param) + seg_end.z * param
if( p_y >= box_min.y and
p_z >= box_min.z and
p_y <= box_max.y and
p_z <= box_max.z )
then
return true
end
end
if( seg_start.y < box_min.y and
seg_end.y > box_min.y )
then
local param =
(box_min.y - seg_start.y) /
(seg_end.y - seg_start.y)
local p_x = seg_start.x * (1.0 - param) + seg_end.x * param
local p_z = seg_start.z * (1.0 - param) + seg_end.z * param
if( p_x >= box_min.x and
p_z >= box_min.z and
p_x <= box_max.x and
p_z <= box_max.z )
then
return true
end
end
if( seg_start.z < box_min.z and
seg_end.z > box_min.z )
then
local param =
(box_min.z - seg_start.z) /
(seg_end.z - seg_start.z)
local p_x = seg_start.x * (1.0 - param) + seg_end.x * param
local p_y = seg_start.y * (1.0 - param) + seg_end.y * param
if( p_x >= box_min.x and
p_y >= box_min.y and
p_x <= box_max.x and
p_y <= box_max.y )
then
return true
end
end
if( seg_start.x >= box_min.x and
seg_start.y >= box_min.y and
seg_start.z >= box_min.z and
seg_start.x <= box_max.x and
seg_start.y <= box_max.y and
seg_start.z <= box_max.z )
then
--seg_start is inside box.
return true
end
return false
end
Then to determine if a cylinder
(surrounding a line segment) intersects a box,
we simply extrude the box by the radius
of the cylinder and then intersect the line
segment with the extruded box.
This does not make 100% complete sense,
but it is close enough.
--This is Lua code.
--box_min and box_max are the corners of the box.
--seg_start and seg_ends are the endpoints of the line segment.
--radius is the radius of the cylinder surrounding the line segment.
function box_intersects_cylinder(
box_min, box_max,
seg_start, seg_end, radius)
--
local ext_box_min = vec(
box_min.x-radius,
box_min.y-radius,
box_min.z-radius)
local ext_box_min = vec(
box_max.x+radius,
box_max.y+radius,
box_max.z+radius)
return seg_intersects_box(
seg_start, seg_end, ext_box_min, ext_box_max)
end
Iterating Over all Block Positions
Once we have our short list of all balls and sticks
that intersect the blue chunk (the chunk being generated),
we then must iterate over every block position
in the blue chunk and see if the block position is close enough
to either a ball or a stick.
This is relatively slow because there are 16x16x16 block positions in a chunk.
Determining if a block position is close enough to a sphere is easy:
we compute the square of the distance from the center of the block
position to the center of the ball.
We then compare that with the square of the radius of the ball.
Doing it this way, we do not have to compute any square roots.
Determining if a block position is close enough to a stick
is more annoying.
We use the following (Lua) code:
--This is Lua code.
--p is the point.
--v1,v2 are the endpoints of the line segment.
--r is the radius of the cylinder that surrounding the line segment.
function vec_close_to_cylinder(p, v1, v2, r)
local a = vec_sub(v2,v1) --v2 minus v1
local b = vec_sub(p,v1) --p minus v1
local u = proj_of_b_on_a(a,b) --Standard vector projection.
local dot = dot(u,a)
if dot < 0.0 then return false end
if dot > dot(a,a) then return false end
local dist_sq = length_sq(vec_sub(u,b))
return dist_sq < r*r
end
This code can be optimized.
For example, when we compute the projection
of b onto a, we must divide by a dot a.
We could cache a, a dot a, and 1 divided by a dot a
and use these values for all 16x16x16 block positions.
Maximum Radius of a Ball
For a technical reason, the maximum radius of a sphere
is 16 blocks (this is because a chunk is 16 blocks wide).
If we had a sphere with a larger radius, it might spill
over into a chunk that is 2 chunks away, which would require
us to consider the 5x5x5 virtual chunks
surrounding the chunk being generated (which we do not want to do).
Maximum Stick Length
Just like the case with balls,
the maximum stick length is 16 blocks
(because a chunk is 16 blocks wide).
Varying the Maximum Stick Length per Chunk
If you want, you could have each chunk specify its own
maximum stick length (which is at most 16).
With the simple implementation of this, there could be balls
in adjacent chunks, and a stick that goes from out of one
but not the other.
See this picture:
We think it is probably best to just not worry about this.
However one fix would be to iterate over all chunks intersecting
a stick and take the maximum of the max stick length of all
those chunks.
This seems like a lot of work.
Note that taking the max of the max stick lengths
over all 3x3x3 chunks would also yield similar bad artifacts.
Modifying Stick and Ball Data by a Chunk's Block Type
We can make it so the stick and balls given by a chunk depend on that
chunk's block type.
This is safe because the procedural world generation system
has access to the block types of all chunks in the 3x3x3 region
surrounding the chunk being generated
(see here).
Here is a picture showing adjacent chunks that
have different block types but which are using a
continuously changing stick and ball system.
Actually in this picture we are using virtual chunks that are
each 2x2x2 normal chunks.
This allows us to have sticks with length at most
2 * 16 = 32.
Level of Detail (LOD)
Here is a picture showing stick and balls with
two levels of detail.
Since we are close enough to a huge stick
and at the same time we are small,
the stick looks like a huge cylinder.
Stick and Ball Caves in Fractal Block World
The following are some pictures of stick and ball caves in
fractal block world.
Here we are subtracting stick and balls
from a solid area to create caves.
This is why we call them "stick and ball caves".
Here is a similar picture.
We are in the middle of a carved out ball,
and from this position we can see the center of
several other carved out balls.
We can fly between the carved out balls
by going through the caved out sticks.
Perlin Noise vs Stick and Balls?
Is it better to make caves using Perlin noise or with Sticks and Balls?
We do not have an ultimate answer here,
but one could easily incorporate both methods into a game.
They are not mutually exclusive.
Also, the techniques give different results.
Note that one must be careful when using the stick and ball method
to not make it very slow.
Part of the problem is there can be several balls/sticks intersecting
the chunk being generated.
On the other hand, the Perlin noise method is easier to make fast.