I am trying to achieve smooth biomes blending for my procedural 3D terrain.
I've been doing some research on this creating biomes topic and I found some good sources that talk about creating biomes by the use of generated moisture and temperature maps.
I found this table, that I use as an example.
So far I've done something like this. I've defined regions that are the horizontal row (temperature)
[Serializable]
public struct Region
{
[SerializeField] string type;
[Range(0, 1)]
public float temperature;
public RegionBiome[] biomes;
}
[Serializable]
public class RegionBiome
{
[SerializeField] string type;
[Range(0, 1)]
public float moisture;
public Biome biome;
}
And biomes that are associated with the region
[CreateAssetMenu]
public class Biome : ScriptableObject
{
[SerializeField] new string name;
public Color color;
[Header("Noise")]
public float scale;
public Wave[] waves;
public float Sample(float x, float y)
{
return Noise.GetPerlinNoise(x, y, scale, 0, 0, waves);
}
}
I have set up the exact biomes table like in the picture.
Then to generate a heightmap I have firstly generated heat and moisture maps and looped through the entire map and sampled each coordinate to get the evaluation
void Generate()
{
float[,] temperatureMap = GenerateNoiseMap(size, 50f, 0f, 0f, new Wave[2] { new Wave(seed * -2, 1, 2), new Wave(seed * 2, .5f, 1) });
float[,] moistureMap = GenerateNoiseMap(size, 35f, 0f, 0f, new Wave[2] { new Wave(seed * -.5f, 1, 2), new Wave(seed * .5f, .5f, 1) });
float[,] heightMap = new float[size, size];
Color[] colorMap = new Color[size * size]; Color color;
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float heat = temperatureMap[x, y];
float moisture = moistureMap[x, y];
heightMap[x, y] = GetEvaluation(x, y, heat, moisture, out color);
colorMap[x + (y * size)] = color;
}
}
CreateOutput(colorMap, colorOutput, result1);
for (int i = 0; i < colorMap.Length; i++)
{
float value = heightMap[i % size, i / size];
colorMap[i] = new Color(value, value, value, 1);
}
CreateOutput(colorMap, noiseOutput, result2);
}
And this is the evaluation function
float GetEvaluation(float x, float y, float heat, float moisture, out Color color)
{
int regionIndex = -1;
for (int i = 0; i < regions.Length; i++)
{
if (i < regions.Length - 1)
{
if (heat >= regions[i].temperature && heat < regions[i + 1].temperature)
{
regionIndex = i;
break;
}
}
else
{
if (heat == 1)
{
regionIndex = i;
break;
}
regionIndex = i - 1;
}
}
int biomeIndex = -1;
for (int i = 0; i < regions[regionIndex].biomes.Length; i++)
{
if (i < regions[regionIndex].biomes.Length - 1)
{
if (moisture >= regions[regionIndex].biomes[i].moisture && moisture < regions[regionIndex].biomes[i + 1].moisture)
{
biomeIndex = i;
break;
}
}
else
{
if (moisture == 1)
{
biomeIndex = i;
break;
}
biomeIndex = i - 1;
}
}
color = regions[regionIndex].biomes[biomeIndex].biome.color;
float evaluation = regions[regionIndex].biomes[biomeIndex].biome.Sample(x, y);
float currHeat = regions[regionIndex].temperature;
float destHeat = heat == 1 ? currHeat : regions[regionIndex + 1].temperature;
float heatBlend = 0;
if (destHeat != currHeat)
{
heatBlend = Mathf.InverseLerp(currHeat, destHeat, heat);
evaluation = Mathf.Lerp(evaluation, regions[regionIndex + 1].biomes[biomeIndex].biome.Sample(x, y), heatBlend);
}
float currMoisture = regions[regionIndex].biomes[biomeIndex].moisture;
float destMoisture = moisture == 1 ? currMoisture : regions[regionIndex].biomes[biomeIndex + 1].moisture;
float moistureBlend = 0;
if (destMoisture != currMoisture)
{
moistureBlend = Mathf.InverseLerp(currMoisture, destMoisture, moisture);
evaluation = Mathf.Lerp(evaluation, regions[regionIndex].biomes[biomeIndex + 1].biome.Sample(x, y), moistureBlend);
}
if (heat < 1 && moisture < 1)
{
float cornerBlend = Mathf.Clamp01(heatBlend + moistureBlend);
evaluation = Mathf.Lerp(evaluation, regions[regionIndex + 1].biomes[biomeIndex + 1].biome.Sample(x, y), cornerBlend);
}
return evaluation;
}
And these are some utility functions that I used
private float[,] GenerateNoiseMap(int size, float scale, float offsetX, float offsetZ, Wave[] waves)
{
return Noise.GeneratePerlinNoiseMap(size, size, scale, offsetX, offsetZ, waves);
}
void CreateOutput(Color[] colorMap, RawImage image, Texture2D tex)
{
tex.SetPixels(colorMap); tex.Apply();
image.texture = tex;
}
And the noise class for generating Perlin noise maps
public static class Noise
{
public static float[,] GeneratePerlinNoiseMap(int mapDepth, int mapWidth, float scale, float offsetX, float offsetZ, Wave[] waves)
{
float[,] noiseMap = new float[mapDepth, mapWidth];
for (int zIndex = 0; zIndex < mapDepth; zIndex++)
{
for (int xIndex = 0; xIndex < mapWidth; xIndex++)
{
float sampleX = (xIndex + offsetX) / scale;
float sampleZ = (zIndex + offsetZ) / scale;
float noise = 0f;
float normalization = 0f;
foreach (Wave wave in waves)
{
noise += wave.amplitude * Mathf.PerlinNoise(sampleX * wave.frequency + wave.seed, sampleZ * wave.frequency + wave.seed);
normalization += wave.amplitude;
}
noise /= normalization;
noiseMap[zIndex, xIndex] = noise;
}
}
return noiseMap;
}
public static float GetPerlinNoise(float x, float y, float scale, float offsetX, float offsetZ, Wave[] waves)
{
float sampleX = (x + offsetX) / scale;
float sampleZ = (y + offsetZ) / scale;
float noise = 0f;
float normalization = 0f;
foreach (Wave wave in waves)
{
noise += wave.amplitude * Mathf.PerlinNoise(sampleX * wave.frequency + wave.seed, sampleZ * wave.frequency + wave.seed);
normalization += wave.amplitude;
}
noise /= normalization;
return noise;
}
}
The way I get the final height doesn't seem to be working correctly as the biomes noises are not interpolated correctly. Also, the color map doesn't seem right as well...
The color map
The height map
How do I blend the biomes? I'm definitely missing something here and can't wrap my head around it... What do I and how do I weigh the final evaluation value?
1 ?
in your code suggest it may be bugged. \$\endgroup\$