For a while I have wanted to create some kind of fantasy world map. My first experiments involved using Midjourney and Dall-E 3 to generate 2D cartography elements that I could then combine to create a world map in Photoshop. Most of these experiments were a failure due to the lack of consistent style, scale, and perspective of the generated images. Reattempting with Midjourney’s updated style references could yield much better results.
Mismatch in style and perspective
Mismatch in scale
After these failures I still had the urge to create a large fantasy map or overworld. I figured I’d give Gaea a try. I had the software for some time and never really used it for any significant project. Soon enough after starting up the software I was hooked on tuning every detail of the world. And I figured I would use generative AI to help me.
1: Concept Art with Midjourney
Since I did not have a specific end-goal in mind, I did not want to spend much time designing the world. I aimed to demonstrate that it’s possible to create a pleasant looking world map using Gaea that could be used and adjusted for other projects. So instead of using existing generators I figured I’d use Midjourney for the concept art. It didn’t take me long to get a concept that appealed to me. One crucial thing for the prompt was for the map to include different biomes. In my opinion, having distinct geographic regions is essential when designing any large world map as it’s so crucial for the geography and history of the world.
My initial prompt was as follows:
Pangea, islands and oceans map, top down, fantasy world, flat
I added ‘Pangea’ to the prompt to reduce the number of continents generated. And then used the following prompt to generate variations:
diverse fantasy world map
The ability to regenerate parts of the image is amazing to add biomes such as snowlands, deserts, and marshes. In the end it was primarily the overall shape that mattered in the concept art though.
2: Masking in Photoshop
Using the concept art as a base I moved to Photoshop to refine and detail the major geographical features such as oceans, mountains, continents, lakes, and shorelines. This step involved manual editing to create the masks that could then be directly imported into Gaea. In the end I came back quite a few times to these maps to adjust the location of the biomes so they made more sense, such as moving the desert farther from snowy mountains.. I also found that lakes required significant iteration to work well with the simulations in Gaea.
Black and White Ocean Mask
Grayscale Mountain Mask
3: Terrain Generation in Gaea
Designing the node graph in Gaea was by far the most fun and time consuming process of the entire workflow. I admit that I didn’t maintain a very tidy node structure. In my opinion structuring things neatly is only worth the effort once you’ve stopped iterating or when it becomes a problem. If I decide to come back to it later I’m sure it will be frustrating though.
Since it was my first time using Gaea it took a while to get used to the tool. There are definitely some idiosyncrasies to get used to. For example Gaea rounds height values often to whole percentages, but this is visual only - this definitely caused some confusion to me as I thought precision was limited. However, you can input values like 10.1, and although the display rounds it to 10, the actual value remains precise under the hood. Rescaling terrain is also painful, thankfully there is a constant node and blending options are powerful. Other than that it was surprisingly easy to become quickly productive with the tool. The nodes are powerful and mostly intuitive and the performance is acceptable, especially considering it’s all C# scripts and Image Magick under the hood. Maybe some day I’ll make my own tool that would run on the GPU and basically be instant!
What especially blew my mind was the SatMap node in Gaea. It’s actually simply a lookup texture based on the greyscale texturing that is generated based on height, curvature, AO and other parameters. But given the huge library and good quality of the texturing the results are really impressive. By blending different biomes together with different SatMap lookup textures it was easy to texture the different biomes, especially since I didn’t need to worry about close-ups. It made it also easy to texture the custom landmarks I added such as the volcano and mesa. With the initial terrain and texturing in place it was now ready for export into Unreal.
4: Integration into Unreal Engine
After struggling with the export settings for a while to get it to export 16bit PNG that Unreal can read I imported the height and color map into Unreal using the landscape.
After adding the sky atmosphere, clouds and tweaking the lighting the results started to look nice already. However two significant issues were immediately highlighted:
- Because of the planet curvature the heightmap would not bend with the earth
- Adjusting the heightmap to account for curvature would loose too much precision
- The ocean shader would stop at the end of the height map
Circumventing these issues using the heightmap landscape would be challenging. Since I didn’t need collision or Lumen a good candidate to replace the landscape seemed to be Nanite! It should easily have enough precision to handle the curved heightfield as one mesh. Additionally, we should benefit from the added performance of level-of-detail per cluster. But first I would need to generate the mesh from the heightfield. Gaea does not provide this option. Thankfully Houdini has great options for writing simple scripts that modify the landscape, as well as powerful export options.
5: Planet curvature in Houdini
Achieving the desired results required just a few nodes. Here is a quick overview:
- Load and scale the heightmap from Gaea to match the planet radius in Unreal
- Also load in the ocean mask output by Gaea
- Convert the heightfield to a polygon mesh
- Blast the polygon by the heightfield mask
- Getting the edges right proved tricky.
- The best I could do was promoting the point group to primitives using the ‘Minimum’ promotion method
- Next I filtered by the condition
@mask > 0.3
- Last step was curving the mesh by the planet radius
vector2 delta2D = set(@P.x / radius, @P.z/ radius);
float height = @P.y;
float length2D = length(delta2D);
float offsetXY = sin(length2D);
float offsetZ = cos(length2D);
vector2 normal2D = normalize(delta2D);
vector normal = set(normal2D.x * offsetXY, offsetZ, normal2D.y * offsetXY);
@P = normal * (radius + height);
@P.y -= radius;
This scripts adds a nice curve with minimal distortion. It does however change the bounds in the XY directions. Additionally to solve the problem of the ocean I cut out the ocean from the mesh. The ocean is then rendered as a separate mesh that covers half the globe. Just using the texture Gaea provided for the ocean in Unreal wouldn’t be enough though.
6: Ocean Material
Using Houdini it was easy to generate a mesh that followed the planet curve perfectly. This mesh would be used for the ocean material. I probably spent too much time on the ocean material. I tried to create a flowmap based on the heightfield. This worked well on the coastline but didn’t generate a nice macro-flow. Since I didn’t end up recording any animation, the effort probably wasn’t worth it. Other than that the material adjusts color based on distance to shore. It also has some scrolling normal effects that follow the flowmap. I am quite happy with the foam I added based on the normals and ocean height variation texture (which also scales the normal intensity to break up the water).
And to add some detail I used Midjourney to generate a tiling texture! It is not very noticeable but I believe this to be a very powerful workflow. Midjourney can be great for creating variation textures.
7: Vegetation Placement
With all this, the fantasy world looks good. But it’s missing detail. The satnav textures are nice but look flat. Gaea has an option to simulate vegetation growth which is baked into the height map. Instead I wanted to use actual tree meshes per biome to generate detail and make the world feel alive. The problem was that I couldn’t easily use the auto-grass that Unreal offers for the landscape to scatter the meshes based on the output density maps. Instead I wrote a simple C++ script that reads the texture and generates instances of trees and bushes. I tried PCG but it was impractical due to poor performance and high memory usage.
TArray<float> Points;
ReadTexture_PlatformData(Texture, Points);
TArray<float> Heights;
ReadTexture_PlatformData(HeightMap, Heights);
double Start = MapSize / 2.0;
double Spacing = MapSize / (double)Width;
for (int32 Y = 0; Y < TextureHeight; ++Y)
{
for (int32 X = 0; X < TextureWidth; ++X)
{
float Value = Points[Y * Width + X];
float Score = Value * Density - Random.GetFraction() * Noise;
if (Score > Threshold)
{
double Z = Heights[Y * Width + X];
FVector P(Start - Y * Spacing, -Start + X * Spacing, Z * HeightScale + ZOffset);
FVector2D Delta2D(P.X / WorldSize, P.Y / WorldSize);
double Length2D = Delta2D.Length();
double L = FMath::Sin(Length2D);
double H = FMath::Cos(Length2D);
FVector2D Normal2D = Delta2D.GetSafeNormal();
FVector Normal(Normal2D.X * L, Normal2D.Y * L, H);
P = Normal * WorldSize + Normal * P.Z;
P.Z -= WorldSize;
double ScaleMultiplier = Random.FRandRange(ScaleRange.X, ScaleRange.Y);
FQuat Rotation = FQuat(Normal, Random.FRandRange(0, UE_DOUBLE_TWO_PI));
FTransform InstanceTransform;
InstanceTransform.SetLocation(P);
InstanceTransform.SetScale3D(FVector(Scale * ScaleMultiplier));
InstanceTransform.SetRotation(Rotation);
Mesh->AddInstance(InstanceTransform);
}
}
}
10: Atmospheric Effects
With the trees, the ocean and the texture terrain the world was ready. The last step was to tweak the atmospheric setting to nicely fit the small planet size. As well I made sure to tweak the rendering settings for maximum quality. These are roughly the render settings I used:
- No Lumen because there is nothing to bounce offers
- Realtime sky capture placed at center of the planet
- Not exactly correct but generated good enough reflections and ambient lighting
- Heterogenous volumes for clouds
- I experimented with importing cloud VDBs before.
- Quality is great but it does not react to the skylight yet
- Convolution Bloom
11: Final Rendering
Original Concept
Final Result
In the end I am happy with what was a little over a week of work. For sure there is a lot of room for improvement. For example more details, higher quality meshes, roads and cities, better waves near the coast. With the power of generative AI getting better I am excited to try even more ambitious projects, for example to generate an entire history and population for the world.