Alright, successfully generated my first chunk! Right now I’m handling everything with 3d Perlin noise (courtesy of Scrawk), and it’s close to the effect I’m looking for. I definitely need to figure out a means of eliminating floating islands since I’m going for realistic terrain. I also need to start figuring out how I’m going to make this infinite. I was thinking of some kind of 3D array where the first chunk is the exact center, but then I’d have to worry about constantly resizing it and shifting the contents back into the center. If anyone has seen a clever implementation of infinite terrain let me know, I’m really not sure how to proceed
Looks nice. Good work so far.
The floating island issue is something that is hard to fix. There are two ways.
The method used in the GPU Gems code is to ray trace out from each voxel to determine if it is attached to anything.
This is slow to do and does not work for every situation.
If you use a 2D noise function for the ground height you can then combine that with a 3D noise function for the caves. You would then only every apply the 3D noise if its below the ground height. That way the floating islands will only ever be below the ground level where they just look like caves.
For the infinite generation you would needed to define a area around the player that contains your chunks. As the player moves the chunks behind the player get removed and new chunks are generate in front of the player. This is a fairly standard method but I dont know if it has a name you could google to find out more info on it.
You will also need a caching system. Any chunks that are remove go into the cache. When you need to generate a new chunk you can first check to see if its in the cache. If it is just remove it from the cache and make it active again. This way you dont need to recreate it again which is quite costly performance wise.
The cache would have a predefined size depending on the memory of the system. When the cache is full remove any chunk that has not been used in a while and delete it.
A reply from Scrawk himself—awesome! Sidebar: Scrawk—thanks for your blog, and for this baggle of code in particular.
edit: This:
“If you use a 2D noise function for the ground height you can then combine that with a 3D noise function for the caves. You would then only every apply the 3D noise if its below the ground height. That way the floating islands will only ever be below the ground level where they just look like caves.”
seems like the most obvious solution to the OP author’s floating-island problem, too. Nice.

Thanks so much for the reply Scrawk, and for the fantastic code base. I think I may have found another viable solution - I generated the above from adding together 4 perlin noise functions. My code below:
for (int x = 0 ; x < w ; x++) {
for (int z = 0 ; z < l ; z++) {
for (int y = 0 ; y < h ; y++) {
// Start off by simply setting to the voxel's y coordinate (which in turn sets each vertex to its y position
float density = y + POS.y;
//orig_y retains the starting y value if ever needed
float orig_y = y + POS.y;
// adding 10 octaves of Perlin noise at once, with decreasing frequencies and increasing amplitudes
// higher frequency = looks more like 2D Noise (less chance of overhangs, etc)
density += perlin.FractalNoise3D(x,y,z,1,700.0f,200.0f)
+ perlin.FractalNoise3D(x,y,z,2,330.0f,6.0f)
+ perlin.FractalNoise3D(x,y,z,3,120.0f,18.0f)
+ perlin.FractalNoise3D(x,y,z,4,50.0f,40.0f);
VOXELS[x,y,z] = density;
}
}
}
While I haven’t been able to try multiple chunks together yet, I have tried different seeds and haven’t seen floating islands in any of them yet. I also plan on trying out the different terrace and overhang functions in the GPU Gems implementation. Time is decent: about 2.5 seconds to generate 1 64x64x64 chunk, 0.4 seconds for a 32x32x32 chunk. We’ll see what happens once I start calculating multiple chunks, hopefully the runtime will just be additive instead of multiplicative
[EDIT: comment on my code is wrong: 1 octave should have highest amplitude, then 2 octave to 4 octave should be increasing]
Wow, that looks really cool. The terrain has a really nice twist to it and that little bridge looks awesome.
Update time, here’s a list of things I’ve accomplished while moving into my new apartment:
- Any time you see an x,y or z in a Fractal Noise call, change it to world_x, world_y or world_z. In fact, add variables called world_x, world_y and world_z that are set to n + POS.n (where n is x,y or z)
- Modified GPU Gems hard_floor_y to work with Scrawk’s Marching Cubes algorithm. Basically, the difference is that Geiss’s implementation assumes vertices with values of 1 are inside the mesh, while Scrawk’s assumes they’re outside. As such, instead of saturating (aka Mathf.Clamp btw 0 and 1) you want to Clamp btw -1 and 0. Also made hard floor a variables visible in the Inspector
- Oh btw I can create more than one chunk at a time now. In fact, the image below is 7x3x7 chunks, and only took 18.4 seconds to generate. Awesome. Eventually I’ll have to figure out how to average normals, but that can wait
- Changed my overall density function. I now start with 2 octaves of 2D noise, then 3 octaves of 3D noise followed by 4 octaves of 3D. Each time I decrease frequency and amplitude.
As always, a photo:
public void createVoxels(PerlinNoise perlin ) {
int w = VOXELS.GetLength(0);
int h = VOXELS.GetLength(1);
int l = VOXELS.GetLength(2);
for (int x = 0 ; x < w ; x++) {
float world_x = x + POS.x;
for (int z = 0 ; z < l ; z++) {
float world_z = z + POS.z;
for (int y = 0 ; y < h ; y++) {
// Start off by simply setting to the voxel's y coordinate (which in turn sets each vertex to its y position
float density = y + POS.y;
//orig_y retains the starting y value if ever needed
float world_y = y + POS.y;
// adding 10 octaves of Perlin noise at once, with decreasing frequencies and amplitudes
// higher frequency = looks more like 2D Noise (less chance of overhangs, etc)
density += perlin.FractalNoise2D(world_x,world_z,2,100.0f,25.0f)
+ perlin.FractalNoise3D(world_x,world_y,world_z,3,78.0f,15.0f)
+ perlin.FractalNoise3D(world_x,world_y,world_z,4,32.0f,12.0f);
// Clamp density back toa range of [-1,1]
density = Mathf.Clamp(density , -1.0f , 1.0f);
// Modified hard floor formula from GPU Gems 3: whereas that marching cubes algorithm assumed a positive
// density represented a corner inside the mesh, this version uses negative values. As such, any y value
// below the hard floor should should be clamped to [-1,0] as opposed to [0,1]
density += Mathf.Clamp((world_y - HARD_FLOOR) * 3.0f , -1.0f , 0.0f)*40.0f;
// Clamp density again
density = Mathf.Clamp(density , -1.0f , 1.0f);
VOXELS[x,y,z] = density;
}
}
}
}
Fun fact: if you want a simple hard ceiling instead of a hard floor, replace
density += Mathf.Clamp((world_y - HARD_FLOOR) * 3.0f , -1.0f , 0.0f)*40.0f;
with
density += Mathf.Clamp((world_y - HARD_FLOOR) * 3.0f , 0.0f , 1.0f)*40.0f;
Alright, been banging my head against a wall for the past 24 hours trying to figure out GPU Gems’ functions for terraces and flat spots. If anyone has seen these functions and understands what exactly the code is doing please let me know. If it would help for me to post the code I can do that as well
Please post the code, yeah! Would be fun to all look at it.
Sorry about the delay, I’ve been dealing with my landlord and various contractors trying to set up the house my friends and I are moving into. Haven’t had much time to work on this as a result, but here’s the density.h file from the GPU Gems terrain program:
#include "sampleNoise.h" // contains NLQs, NMQs, NHQs, etc.
float3 rot(float3 coord, float4x4 mat)
{
return float3( dot(mat._11_12_13, coord), // 3x3 transform,
dot(mat._21_22_23, coord), // no translation
dot(mat._31_32_33, coord) );
}
float smooth_snap(float t, float m)
{
// input: t in [0..1]
// maps input to an output that goes from 0..1,
// but spends most of its time at 0 or 1, except for
// a quick, smooth jump from 0 to 1 around input values of 0.5.
// the slope of the jump is roughly determined by 'm'.
// note: 'm' shouldn't go over ~16 or so (precision breaks down).
//float t1 = pow(( t)*2, m)*0.5;
//float t2 = 1 - pow((1-t)*2, m)*0.5;
//return (t > 0.5) ? t2 : t1;
// optimized:
float c = (t > 0.5) ? 1 : 0;
float s = 1-c*2;
return c + s*pow((c+s*t)*2, m)*0.5;
}
float DENSITY(float3 ws)
{
//-----------------------------------------------
// This function determines the shape of the entire terrain.
//-----------------------------------------------
// Remember the original world-space coordinate,
// in case we want to use the un-prewarped coord.
// (extreme pre-warp can introduce small error or jitter to
// ws, which, when magnified, looks bad - so in those
// cases it's better to use ws_orig.)
float3 ws_orig = ws;
// start our density value at zero.
// think of the density value as the depth beneath the surface
// of the terrain; positive values are inside the terrain, and
// negative values are in open air.
float density = 0;
// sample an ultra-ultra-low-frequency (slowly-varying) float4
// noise value we can use to vary high-level terrain features
// over space.
float4 uulf_rand = saturate( NMQu(ws*0.000718, noiseVol0) * 2 - 0.5 );
float4 uulf_rand2 = NMQu(ws*0.000632, noiseVol1);
float4 uulf_rand3 = NMQu(ws*0.000695, noiseVol2);
//-----------------------------------------------
// PRE-WARP the world-space coordinate.
const float prewarp_str = 15; // recommended range: 5..25
float3 ulf_rand = 0;
#if 0 // medium-quality version; precision breaks down when pre-warp is strong.
ulf_rand = NMQs(ws*0.0041 , noiseVol2).xyz*0.64
+ NMQs(ws*0.0041*0.427, noiseVol3).xyz*0.32;
#endif
#if 1 // high-quality version
// CAREFUL: NHQu/s (high quality) RETURN A SINGLE FLOAT, not a float4!
ulf_rand.x = NHQs(ws*0.0041*0.971, packedNoiseVol2, 1)*0.64
+ NHQs(ws*0.0041*0.461, packedNoiseVol3, 1)*0.32;
ulf_rand.y = NHQs(ws*0.0041*0.997, packedNoiseVol1, 1)*0.64
+ NHQs(ws*0.0041*0.453, packedNoiseVol0, 1)*0.32;
ulf_rand.z = NHQs(ws*0.0041*1.032, packedNoiseVol3, 1)*0.64
+ NHQs(ws*0.0041*0.511, packedNoiseVol2, 1)*0.32;
#endif
ws += ulf_rand.xyz * prewarp_str * saturate(uulf_rand3.x*1.4 - 0.3);
//-----------------------------------------------
// compute 8 randomly-rotated versions of 'ws'.
// we probably won't use them all, but they're here for experimentation.
// (and if they're not used, the shader compiler will optimize them out.)
float3 c0 = rot(ws,octaveMat0);
float3 c1 = rot(ws,octaveMat1);
float3 c2 = rot(ws,octaveMat2);
float3 c3 = rot(ws,octaveMat3);
float3 c4 = rot(ws,octaveMat4);
float3 c5 = rot(ws,octaveMat5);
float3 c6 = rot(ws,octaveMat6);
float3 c7 = rot(ws,octaveMat7);
//-----------------------------------------------
// MAIN SHAPE: CHOOSE ONE
#if 1
// very general ground plane:
density = -ws.y * 1;
// to add a stricter ground plane further below:
density += saturate((-4 - ws_orig.y*0.3)*3.0)*40 * uulf_rand2.z;
#endif
#if 0
// small planet:
const float planet_str = 2;
const float planet_rad = 160;
float dist_from_surface = planet_rad - length(ws);
density = dist_from_surface * planet_str;
#endif
#if 0
// infinite network of caves: (small bias)
density = 12; // positive value -> more rock; negative value -> more open space
#endif
//----------------------------------------
// CRUSTY SHELF
// often creates smooth tops (~grass) and crumbly, eroded underneath parts.
#if 1
float shelf_thickness_y = 55;//2.5;
float shelf_pos_y = -1;//-2;
float shelf_strength = 10; // 1-4 is good
density = lerp(density, shelf_strength, 0.83*saturate( shelf_thickness_y - abs(ws.y - shelf_pos_y) ) * saturate(uulf_rand.y*1.5-0.5) );
#endif
// FLAT TERRACES
#if 0
{
const float terraces_can_warp = 0.5 * uulf_rand2.y;
const float terrace_freq_y = 0.35; //originally 0.13
const float terrace_str = 3*saturate(uulf_rand.z*2-1); // careful - high str here diminishes strength of noise, etc.
const float overhang_str = 1*saturate(uulf_rand.z*2-1); // careful - too much here and LODs interfere (most visible @ silhouettes because zbias can't fix those).
float fy = -lerp(ws_orig.y, ws.y, terraces_can_warp)*terrace_freq_y;
float orig_t = frac(fy);
float t = orig_t;
t = smooth_snap(t, 16); // faster than using 't = t*t*(3-2*t)' four times
fy = floor(fy) + t;
density += fy*terrace_str;
density += (t - orig_t) * overhang_str;
}
#endif
// SPHERICAL TERRACES (for planet mode)
#if 0
{
const float terraces_can_warp = 0.1; //TWEAK
const float terrace_freq_r = 0.2;
//const float terrace_str = 0; // careful - high str here diminishes strength of noise, etc.
const float overhang_str = 2; // careful - too much here and LODs interfere (most visible @ silhouettes because zbias can't fix those).
float r = length(ws);
float r_orig = length(ws_orig);
float fy = -lerp(r_orig, r, terraces_can_warp)*terrace_freq_r;
float orig_t = frac(fy);
float t = orig_t;
t = smooth_snap(t, 16); // faster than using 't = t*t*(3-2*t)' four times
fy = floor(fy) + t;
//density += fy*terrace_str;
density += (t - orig_t) * overhang_str;
}
#endif
// other random effects...
#if 1
// repeating ridges on [warped] Y coord:
density += NLQs(ws.xyz*float3(2,27,2)*0.0037, noiseVol0).x*2 * saturate(uulf_rand2.w*2-1);
#endif
#if 0
// to make it extremely mountainous & climby:
density += ulf_rand.x*80;
#endif
#ifdef EVAL_CHEAP //...used for fast long-range ambo queries
float HFM = 0;
#else
float HFM = 1;
#endif
// sample 9 octaves of noise, w/rotated ws coord for the last few.
// note: sometimes you'll want to use NHQs (high-quality noise)
// instead of NMQs for the lowest 3 frequencies or so; otherwise
// they can introduce UNWANTED high-frequency noise (jitter).
// BE SURE TO PASS IN 'PackedNoiseVolX' instead of 'NoiseVolX'
// WHEN USING NHQs()!!!
// note: higher frequencies (that don't matter for long-range
// ambo) should be modulated by HFM so the compiler optimizes
// them out when EVAL_CHEAP is #defined.
// note: if you want to randomly rotate various octaves,
// feed c0..c7 (instead of ws) into the noise functions.
// This is especially good to do with the lowest frequency,
// so that it doesn't repeat (across the ground plane) as often...
// and so that you can actually randomize the terrain!
// Note that the shader compiler will skip generating any rotated
// coords (c0..c7) that are never used.
density +=
( 0
//+ NLQs(ws*0.3200*0.934, noiseVol3).x*0.16*1.20 * HFM // skipped for long-range ambo
+ NLQs(ws*0.1600*1.021, noiseVol1).x*0.32*1.16 * HFM // skipped for long-range ambo
+ NLQs(ws*0.0800*0.985, noiseVol2).x*0.64*1.12 * HFM // skipped for long-range ambo
+ NLQs(ws*0.0400*1.051, noiseVol0).x*1.28*1.08 * HFM // skipped for long-range ambo
+ NLQs(ws*0.0200*1.020, noiseVol1).x*2.56*1.04
+ NLQs(ws*0.0100*0.968, noiseVol3).x*5
+ NMQs(ws*0.0050*0.994, noiseVol0).x*10*1.0 // MQ
+ NMQs(c6*0.0025*1.045, noiseVol2).x*20*0.9 // MQ
+ NHQs(c7*0.0012*0.972, packedNoiseVol3).x*40*0.8 // HQ and *rotated*!
);
// periodic flat spots:
#if 1
{
const float flat_spot_str = 1; // 0=off, 1=on
const float dist_between_spots = 130; //originally 330
const float spot_inner_rad = 44;
const float spot_outer_rad = 66;
float2 spot_xz = floor(ws.xz/dist_between_spots) + 0.5;
float dist = length(ws.xz - spot_xz*dist_between_spots);
float t = saturate( (spot_outer_rad - dist)/(spot_outer_rad - spot_inner_rad) );
t = (3 - 2*t)*t*t;
density = lerp(density, -ws.y*1, t*0.9*flat_spot_str);
}
#endif
// LOD DENSITY BIAS:
// this shrinks the lo- and medium-res chunks just a bit,
// so that the hi-res chunks always "enclose" them:
// (helps avoid LOD overdraw artifacts)
density -= wsChunkSize.x*0.009;
return density;
}

