We have just released Map Editor Update!
We are opening the map editor for everyone, so you can create your own levels and then play them with your friends in multiplayer mode. We realized that creating first map is not straight-forward process, so we implemented a map generator. This article uncovers how the generator was implemented, including code snippets. You can find the full changelog at the end of the article.
Terrain Representation
Let’s uncover first how the terrain is represented, so we can explain the generator later.
Terrain is grid of small quad polygons, shaped and shaded by several bitmaps – distance between adjacent pixels correspond to one meter in the game. In addition to bitmaps, terrain makes use of several JSON config files linking other resources (like textures).
- Height map shapes the terrain. Lighter the pixels, higher the location.
- Terrain map chooses between two terrain textures specified in terrain JSON config. Black selects the first texture, white the second one, any shade of gray mixes them together.
- Color map affects terrain color. It can be used for darkening some terrain regions.
- Doodads map affects placement of „clutter“ models – usually grass or rocks. Doodads bitmap allows using all three channels to place models from three groups of models described in doodads JSON configs.
Terrain Generation
We know the representation of the terrain – the generator just exploits it by creating required bitmaps and feeding them to the game.
Height map is computed from Perlin noise. The noise is computed several times in varying density (noise octaves) and composed into summed map, which builds local features but also global shape of the terrain, preventing visible repetition.
// generate height for each terrain vertex:
constexpr int valCount = 4;
float v[valCount];
float x = (i + seed) * perlinDensity;
float z = (j + seed) * perlinDensity;
v[0] = (glm::perlin(glm::vec2(x, z)) - 0.5f);
v[1] = 0.5f * (glm::perlin(glm::vec2(2 * x, 2 * z)) - 0.5f);
v[2] = 0.5f * 0.5f * (glm::perlin(glm::vec2(4 * x, 4 * z)) - 0.5f);
v[3] = 0.5f * 0.5f * 0.5f * (glm::perlin(glm::vec2(8 * x, 8 * z)) - 0.5f);
float mx = (i + seed) * modulatorPerlinDensity;
float mz = (j + seed) * modulatorPerlinDensity;
float modulation = glm::perlin(glm::vec2(1.f * mx / map_size, 1.f * mz / map_size));
for (int i = 0; i < valCount; i++)
{
val += v[i];
}
val *= modulation;
val = 0.5f + map_elevation_scale * val;
val = glm::clamp(val, 0.f, 1.f);
heightMapFloat.SetValue(i, j, val);
Texture map, again, makes use of Perlin noise, this time without octaves. We apply thresholding to keep only black and white and then blurring the map slightly to hide any hard seams.
// generate color for each terrain vertex:
int perlinator = 20;
int index = (int)(j + (i * map_size)) * 3;
int height = h_map[(int)(j + (i * map_size))];
float x = (i + seed) * perlinDensity;
float y = (j + seed) * perlinDensity;
int wPerlin = height + (int)(perlinator + 2 * perlinator * glm::perlin(glm::vec2(x, y)));
int shadeHeight = MapRange(height + wPerlin, minHeight, maxHeight, 100, 220);
uint8_t r = (uint8_t) shadeHeight;
uint8_t g = (uint8_t) shadeHeight;
uint8_t b = (uint8_t) shadeHeight;
colorMapBytes.SetValue(i, j, r, g, b);
Color map is derived from height map. Higher elevation gets lighter color.
// generate texture weight for each terrain vertex:
int height = h_map[(int)(j + (i * map_size))]; // normalized height in range 0-255
float x = (i + seed) * perlinDensity;
float y = (j + seed) * perlinDensity;
int wPerlin = height + (int)(perlinator + 2 * perlinator * glm::perlin(glm::vec2(x, y)));
uint8_t w;
if (wPerlin >= midH + smoothing)
w = 255;
else if (wPerlin <= midH - smoothing)
w = 0;
else
w = 255 * (wPerlin - midH + smoothing) / (smoothing * 2);
textureMapBytes.SetValue(i, j, w);
Doodads map, you guessed it, uses the Perlin noise. We compute a gradient map and use it to mask out all areas on steep slopes.
// generate gradient value for each pixel in gradient map
const float x_prev = heightMap.GetValueI({ix - 1, iz});
const float x_next = heightMap.GetValueI({ix + 1, iz});
const float z_prev = heightMap.GetValueI({ix, iz - 1});
const float z_next = heightMap.GetValueI({ix, iz + 1});
const glm::vec2 gradient{(x_next - x_prev) * gradValNorm, (z_next - z_prev) * gradValNorm};
gradientMap->SetValue(index, gradient);
// generate doodads probability for each terrain vertex:
int perlinator = 255;
int perlinator = 255;
glm::vec2 gradient = 255.f * gradientMapFloat->GetValueI(i, j);
float flatness = glm::clamp(1.f - glm::sqrt(gradient.x * gradient.x - gradient.y * gradient.y), 0.0f, 1.f);
float x = (i + seed) * perlinDensity;
float y = (j + seed) * perlinDensity;
float wPerlin = flatness * perlinator * glm::perlin(glm::vec2(x, y));
uint8_t w;
if (wPerlin >= 0.95f)
w = 255;
else if (wPerlin <= 0.7f)
w = 0;
else
w = (uint8_t)(255 * (wPerlin - 0.7f) / (0.25f));
Environment Generation
Generator picks a random skybox texture and stores it in the skybox JSON config. Also, we sample approximate skybox color to set the lighting correctly.
Next step is to generate environment JSON config which contains lighting and fog setup. We set the fog color to the approximate sky color and randomize fog density. Ambient lighting matches the fog color, but directional light (sun) gets a compementary color.
Waypoint Generation
Several „spawn“ and „bonus“ waypoints are placed in circle around map center with connections between them. Only basic waypoints are generated to allow immediate testing of the map after its generation – we expect that creators replace generated waypoints after placing first objects in scene.
Color Lookup Table Generation
Last step is to generate color lookup table which is used in post-processing phase of renderer. With this bitmap, we are remapping color of every pixel rendered to a new one. This allows arbitrary color grading, (de)saturation, creating negative image or even crazier effects.
uint8_t* pLut = lut_map.data(); // lut_map contains pixels of the LUT bitmap
for (int g = 0; g < 16; ++g)
{
for (int b = 0; b < 16; ++b)
{
for (int r = 0; r < 16; ++r, pLut += 3)
{
float rf = r / 15.f;
float gf = g / 15.f;
float bf = b / 15.f;
// values can sometimes overflow which we want to happen since it creates nice glitches :)
pLut[0] = (uint8_t)((rf * lin.x + rf * rf * quad.x + rf * rf * rf * cubic.x) * 255);
pLut[1] = (uint8_t)((gf * lin.y + gf * gf * quad.y + gf * gf * gf * cubic.y) * 255);
pLut[2] = (uint8_t)((bf * lin.z + bf * bf * quad.z + bf * bf * bf * cubic.z) * 255);
}
}
}
Full Changelog
- map editor
- now open to everyone
- maps stored in AppData directory
- map generator
- terrain
- doodads
- skybox randomization
- lighting & fog
- color look-up tables
- basic waypoints for bots
- custom maps in multiplayer mode
- play any custom map hosted by server
- map is automatically downloaded to all connected players
See you in the arena!
Best,
Filip, Michal, Vojta
11thdreamgame.bluepulsar.cz
Follow us on twitter or instagram and ask us anything on our discord server. We are also happy to invite you to join our new mailing list, so you stop missing all those 11th updates!