How do I perform interpolation between noise values sampled at a lower resolution?

I'm doing some experimentation with terrain generating using a method similar to that described in this blog post by Markus Persson, where I use 3D simplex noise as a "density" value. Like him, sampling every single position in a chunk was simply too much of a performance drain, so I sample at a lower resolution. This works perfectly fine if I just use the noise values sampled at a lower resolution as densities, but this obviously leads to very "chunky" terrain, like this:

The relevant code used to generate the noise values and the voxels that make up the above terrain looks like this:

// Initialize the width, height and depth of the noise values map, and initialize
// the noise value map itself.
int noiseValuesMapWidth = (this.ChunkWidth / this.HorizontalNoiseResolutionDivisor) + 2;
int noiseValuesMapHeight = (this.ChunkHeight / this.VerticalNoiseResolutionDivisor) + 2;
int noiseValuesMapDepth = (this.ChunkDepth / this.HorizontalNoiseResolutionDivisor) + 2;
float[,,] noiseValuesMap = new float[noiseValuesMapWidth, noiseValuesMapHeight, noiseValuesMapDepth];

// Fill the noise values map with noise values.
for(int x = 0; x < noiseValuesMapWidth; x++)
{
for(int y = 0; y < noiseValuesMapHeight; y++)
{
for(int z = 0; z < noiseValuesMapDepth; z++)
{
float xSeed = Mathf.Abs((x * this.HorizontalNoiseResolutionDivisor) + this.transform.position.x + seed);
float ySeed = Mathf.Abs((y * this.VerticalNoiseResolutionDivisor) + this.transform.position.y + seed);
float zSeed = Mathf.Abs((z * this.HorizontalNoiseResolutionDivisor) + this.transform.position.z + seed);

noiseValuesMap[x, y, z] = Noise.Generate(
xSeed * this.NoiseScaleX, ySeed * this.NoiseScaleY, zSeed * this.NoiseScaleZ
) - NoiseFalloff(
this.FalloffACoefficient,
this.FalloffBCoefficient,
this.FalloffCCoefficient,
(float)(y * this.VerticalNoiseResolutionDivisor) / (float)this.ChunkHeight
);
}
}
}

// Generate the chunk's terrain itself by iterating over every voxel position
// and generating simplex noise values at a lower resolution.
for(int x = 0; x < this.ChunkWidth; x++)
{
for(int y = 0; y < this.ChunkHeight; y++)
{
for(int z = 0; z < this.ChunkDepth; z++)
{
int noiseValuesMapX = (x / this.HorizontalNoiseResolutionDivisor) + 1;
int noiseValuesMapY = (y / this.VerticalNoiseResolutionDivisor) + 1;
int noiseValuesMapZ = (z / this.HorizontalNoiseResolutionDivisor) + 1;

if(noiseValuesMap[noiseValuesMapX, noiseValuesMapY, noiseValuesMapZ] >= 0.0f)
{
this.SetVoxel(new Stone(), x, y, z);
}
}
}
}

Obviously, the solution to the "chunky" terrain is to interpolate between each of the sampled noise values, but I have no idea how to perform that interpolation. How is it done?

• Is your goal to have a relatively smooth interpolation between the sampled points, or to maintain some of the multiscale noise? – Steven Stadnicki Jun 30 '16 at 0:41
• @StevenStadnicki Smooth interpolation between the sampled points. – Ethan Bierlein Jun 30 '16 at 1:04

Linear interpolation is the simplest way to interpolate. Here's the function for it:

private double linearInterpolate3d(double xm_ym_zm, double xp_ym_zm, double xm_yp_zm, double xp_yp_zm,
double xm_ym_zp, double xp_ym_zp, double xm_yp_zp, double xp_yp_zp,
double x, double y, double z) {
return (xm_ym_zm * (1 - x) * (1 - y) * (1 - z)) + (xp_ym_zm * x * (1 - y) * (1 - z)) + (xm_yp_zm * (1 - x) * y * (1 - z)) + (xp_yp_zm * x * y * (1 - z)) +
(xm_ym_zp * (1 - x) * (1 - y) * z) + (xp_ym_zp * x * (1 - y) * z) + (xm_yp_zp * (1 - x) * y * z) + (xp_yp_zp * x * y * z);
}

This code will take the nearest density values to your point and the position of the point within that cube, and return the interpolated value.

Here's how you might implement it in your code (it goes inside your last for loop):

int x2 = (x / this.HorizontalNoiseResolutionDivisor) + 1;
int y2 = (y / this.VerticalNoiseResolutionDivisor) + 1;
int z2 = (z / this.HorizontalNoiseResolutionDivisor) + 1;

double value = linearInterpolate3d(noiseValuesMap[x2][y2][z2], noiseValuesMap[x2 + 1][y2][z2],
noiseValuesMap[x2][y2 + 1][z2], noiseValuesMap[x2 + 1][y2 + 1][z2],
noiseValuesMap[x2][y2][z2 + 1], noiseValuesMap[x2 + 1][y2][z2 + 1],
noiseValuesMap[x2][y2 + 1][z2 + 1], noiseValuesMap[x2 + 1][y2 + 1][z2 + 1],
(x % HorizontalNoiseResolutionDivisor) / (double) HorizontalNoiseResolutionDivisor,
(y % VerticalNoiseResolutionDivisor) / (double) VerticalNoiseResolutionDivisor,
(z % HorizontalNoiseResolutionDivisor) / (double) HorizontalNoiseResolutionDivisor);

if(value >= 0.0) {
this.SetVoxel(new Stone(), x, y, z);
}

Here's more on how linear interpolation works: https://en.wikipedia.org/wiki/Bilinear_interpolation#Unit_Square

• I'm not sure what the offsets for the left/right and front/back axes are. Is it (x / this.HorizontalNoiseResolutionDivisor) for the left, and + 1 for the right, and (z / this.HorizontalNoiseResolutionDivisor) for the back and + 1 for the front? It's not immediately clear to me. – Ethan Bierlein Jul 4 '16 at 0:53
• Also, I'm getting really odd output when I try do run this with the conditions I specified above. It looks something like this: imgur.com/a/bjqVq – Ethan Bierlein Jul 4 '16 at 1:14
• @EthanBierlein Yeah, sorry, that wasn't at all clear. I refactored the function to make it clearer. That should solve most if not all of the problems. – FlyingPiMonster Jul 4 '16 at 1:26
• Still not working for some reason. The results still look like the ones in the picture I linked. – Ethan Bierlein Jul 4 '16 at 4:38
• @EthanBierlein I've updated my answer. The code I just posted should make it work. – FlyingPiMonster Jul 4 '16 at 14:02