this is a little landscape generation / watershed calculator for a game project i'm working on. it's roughly based off of a similar experiment by redblobgames/amit patel. there's additional documentation for other parts of the gamedev over on my gamedev tumblr, c. late 2024 and early 2025. and hopefully into the future, who knows.

in the context of the game, i want to generate mountainous, rocky desert landscapes akin to those of the aïr mountains in niger, or the area around todgha gorge in morocco. here's the mountain on google maps; here's the gorge. in the aïr mountains, there's a lot of dry wadis/arroyos bordered by vegetation, with riverbeds in sandy or dusty valleys, occasionally being channeled into canyons or fanning out into endorheic deltas in the middle of some mountain valley. in todgha gorge, there's a canyon river, where sometimes the canyon walls pull back to permit a green oasis/farming region within the gorge. in the aïr mountains, the rainy season leads to seasonal floods from rainwater flowing down the impermeable mountainsides, and a substantial amount of the water is trapped in gueltas, waterfall basins of impermeable rock carved in canyons.

what i'm looking for is a watershed that's the size of a mountain range, and endorheic and disconnected, ideally with a fair amount of internal complexity for drainage involving multiple mountain valleys, connected with canyons. outflows 'past the edge of the map' are fine, it's just assumed their drainage is ultimately some random basin in the desert, rather than any ocean. but having explicit delta fans or dwindling flows at the edges would be nice.

the geological formations i'm looking to generate for my purposes: dry riverbeds, delta fans in mountain basins, canyon channels, a substantial enough elevation change in the canyons to permit gueltas. the water flows here don't represent permanent springs; small flows would be rendered more like brush-filled gullies, with larger flows having enough flow to generate as swept-clean sandy riverbeds. certain flows may be picked as being permanent springs (and thus turned into points of interest like shrines) but that's not really a fact of the generated flow network itself. i'm not interested in lake formation, or ensuring local minima have outflows etc; this is intended to be a very dry region. i'm also not super concerned with huge maps; each flow tile here will likely represent somewhere between a 32x32 tile region and a, idk, 576x576 tile region in-game. so they're going to be quite large, and even a 'small' drainage network is going to be suitable for my purposes. this also means i'm expecting to be able to paper over small discontinuities in height on the local level -- it should be assumed any time there's a sloping tile in the flow map, that that's actually representing a gully with a lowered channel in the middle, rather than a perfectly planar slope.

the problem with procedural generation is that you can dump effectively infinite amounts of time and energy and thought into refining your output. i've been wanting to actually code, you know, the rest of the game, but now i have dipped my toes into the dark waters of procgen just to test it out. in practice, the village where the game is set might as well be along a simple T river junction. but i know video game players; the first thing people will do on seeing a riverbed is try to follow it upstream or downstream, and i'd like there to be coherent landscapes there, too, out to a pretty fair distance.

some notes on control options:

it's that last step that leads to the characteristic cliffs between watersheds redblobgames mentioned -- if height is entirely a function of distance from the watershed base, there's no reason to expect two random flow tips will have the exact same distance, and so you frequently see really abrupt cliff edges between watersheds. (there's the same effect between distant forks of the same watershed, but on a random cost field the effect is generally much less pronounced.) in real geology, you'd expect to see a mountain ridge with roughly-equivalent heights on either side of the ridge. finessing this by manually adjusting edges is kind of tricky, because it's easy for there to be cyclical interactions -- if there's a cliff between watersheds A and B and you raise B, this can produce a new cliff between B and C, and if you raise C now there's a cliff between C and A. fundamentally this is because 'a distance value of a breadth-first search across a random cost field' is not how actual geology is formed, and so those height values are always going to have some weird properties that don't accord to actual landscapes. you can finesse them, but then you're just going to be patching up the issues in a flawed algorithm. what counts as 'finessing' vs. 'a post-processing step to ensure coherency' is a subjective judgement up to the individual.

as i messed with the various parameters, though, i started to focus on internal watersheds: there's no reason why i couldn't have a bunch of disconnected internal basins; mountain ranges blocking drainage is how many of the real-world endorheic basins work. but what might be fun, and allow for a bit more structure, is if i generated watersheds and then linked them together by having one outflow into another. theoretically this is a fairly simple operation: find a point where two adjacent flow tiles are in different watersheds. first, realign one of the flows to flow into the other. then, recursively move 'downstream' along that flow, reversing the flow direction as you go, until you hit the former outflow/basin tile. this reverses a 'downstream' flow into an 'upstream' flow, and makes the former basin into a tributary watershed. doing this repeatedly could link up the entire system. more relevant to my purposes, it gives me a distinct place to put 'canyon' zones -- at the two joined flows that were formerly in different watersheds, maybe randomly extending a few tiles in either direction.

this makes the ridge discrepancy (when using flow distance as heights) even more pronounced, because it's introducing longer flows. in thinking about how this algorithm works & thinking over redblobgames' writeup, i am basically in agreement that a bfs-distance metric is fundamentally unsuited for this; what you'd want would be an algorithm that specifically generates maps with the desired properties, rather than a messy scattershot approach that may randomly appear suitable on certain outputs.

this also has some interesting properties about the ordering of the inner-outer drainage vs. inner-inner drainage. if inner-outer is run first, the overall effect is more 'mountainous', with separate watersheds splitting in a continental divide throughout the middle of the map. if inner watersheds are merged first, the overall effect is more like a 'basin', with most of the internal structures draining into each other before there's one big flow out of the central region. so that's a neat control point. it does make me think about introducing some kind of 'rain shadow' into the system at a later stage -- if i say that, say, the western side of the map gets 'more rain' (or e.g., acid rainstorms in the south), then i can adjust those flows to present more or different flora in regions downstream of that drainage. which is neat!

(another thought i had was to fully reverse-reflow the exterior watersheds entirely, using their outforw as a 'drainage' point, and then interpreting all the flows 'towards' it as a delta fan, to imitate the kind of desert delta-into-sand you see in big desert endorheic watersheds. this would require altering the data structure itself to handle downstream branches, but i don't see any reason why it wouldn't work.)

in looking for other sources of inspiration for this, i ended up reading Modeling Landscapes with Ridges and Rivers: bottom up approach, which (as a rough and inaccurate summary) has a brief simulation step where it first generates ridge heights, then generates river heights, and then zeroes out and recreates all other height values using noise/distance checks. that got me thinking about forcing the ridge height by creating some kind of pseudo-elevation field, which is then used to guide river hydrology. i also started thinking about more explicit handling of canyon routes -- the simple assumption of longer flows = higher terrain is not actually held out geologically. rives generally have extremely shallow slopes. so tweaking some of the elevation parameters to allow for extremely shallow slopes while also explicitly handling reversed watershed flows seemed like an approach with at least a little potential.

another thing i considered but ended up not implementing in time was a way to 'erode extreme cliffs' -- if i did end up in a situation where there were adjacent tiles with extremely large height differences, and the high one was a leaf flow node (a not uncommon situation), i could change the direction of the flow to switch it to the lower watershed, which which end up forcibly lowering its height and (ideally) reducing the overall height difference. there would be certain situations where this wouldn't help at all, but running that kind of smoothing/'erosion' pass multiple time would probably make the map look more ridge-like. but that seemed a little too involved to try, since flipping flows like that could end up changing the strahler value for the lower watershed, which would change heights in a more complex fashion

ultimately this whole thing is a big mess of ad-hoc processing steps with no principled justification. but unless i commit to realistic simulation of geological principles, that's always going to be a little bit the case. that being said, i have spent only a week on this and it does seem like what i have is capable of generating & identifying most the terrain features i wanted -- mostly locally smooth, constantly descending to coherent basins and outflows, capable of generating labeled canyons & complex chokepoints. i haven't added the reflow to simulate endorheic delta fans yet, but it would be pretty easy to add in the future, and also pretty irrelevant right now since they'd by definition only occur on the very edge of the map.

something that i didn't think of at all until i was putting this down was that the actua game does support caves, which means i absolutely could have introduced cave flows into this 'model'. following a path downstream (or upstream!) and running into an underground river entrance (with or without currently-flowing water) would be a neat game experience.

i'm not super satisfied with the overall algorithm, but i guess it has succeeded at giving me enough terrain to motivate me to draw roughly a billion more variations of sand, rocks, and rocky sand in order to robustly represent the generated landscape. we'll see how it goes! given the amount of other features i'd like to include, i'm definitely going to revisit this at some point in the future, so who knows what that'll look like.