<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xml:base="https://frozenfractal.com/"><channel><title>Frozen Fractal : Development blog</title><link>https://frozenfractal.com/blog/</link><description>Frozen Fractal is the alter ego of Thomas ten Cate, independent and freelance game developer.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Tue, 24 Dec 2024 20:26:11 +0100</lastBuildDate><atom:link href="https://frozenfractal.com/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>Around The World, Part 30: Making better waves</title><link>https://frozenfractal.com/blog/2026/3/27/around-the-world-30-making-better-waves/</link><pubDate>Fri, 27 Mar 2026 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2026/3/27/around-the-world-30-making-better-waves/</guid><description>&lt;p&gt;I &lt;a href="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/"&gt;previously blogged&lt;/a&gt; about how I&amp;rsquo;m modelling and rendering water waves in the game, but that was in the previous incarnation on a sphere. I&amp;rsquo;ve finally ported that over to the current version, and made some considerable improvements along the way. People told me it looks good, and I tend to agree. Let&amp;rsquo;s dive in!&lt;/p&gt;
&lt;h2 id="reflections"&gt;Reflections&lt;/h2&gt;
&lt;p&gt;Even if a water surface is perfectly still, you can still tell that it&amp;rsquo;s water. This is because of how light interacts with it: part of the light passes through (transparency), and part of the light is reflected. Let&amp;rsquo;s tackle reflection first.&lt;/p&gt;
&lt;p&gt;Since the water surface is mostly reflecting stuff that is also on screen (distant coasts), screen-space reflections seem like the way to go. Godot significantly improved its SSR algorithm in version 4.6, so we now get this without writing any shader code:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2026/3/27/around-the-world-30-making-better-waves/reflection_builtin.jpg" alt="Test scene with reflection" &gt;
&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s nice; it already looks pretty watery. However, real water is also transparent. I want some transparency in the game too, so the player can see and interact with nearby underwater objects like fish.&lt;/p&gt;
&lt;p&gt;And here we hit a problem: Godot&amp;rsquo;s built-in material supports screen-space reflections, but not at the same time as transparency. So I&amp;rsquo;ll have to do the work myself and write a shader.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s just a fairly standard raymarch that inspects the depth buffer and returns the local colour when it hits something. However, because it&amp;rsquo;s meant for horizontal water surfaces, I can pull a few tricks: when hitting the side of the screen, rather than fading out like a normal SSR shader would because there&amp;rsquo;s no data, the raymarch just reverses direction. This means we still have reflections near the edges of the screen, even if they&amp;rsquo;re not quite correct.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2026/3/27/around-the-world-30-making-better-waves/reflection_custom.jpg" alt="Custom reflections" &gt;
&lt;/p&gt;
&lt;p&gt;You can see that my implementation is not as great as Godot&amp;rsquo;s built-in one, most notably because it doesn&amp;rsquo;t use &lt;a href="https://sugulee.wordpress.com/2021/01/19/screen-space-reflections-implementation-and-optimization-part-2-hi-z-tracing-method/"&gt;Hi-Z&lt;/a&gt;. But in practice, the water surface won&amp;rsquo;t usually be this smooth, and the artifacts are no longer noticeable.&lt;/p&gt;
&lt;p&gt;It took me &lt;em&gt;forever&lt;/em&gt; to figure out how to get the reflected colour to combine properly with albedo and shadows, but the answer turned out to be very simple: Godot shaders have a dedicated output variable just for this, called &lt;code&gt;RADIANCE&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Though it may be hard to see in the above screenshot, there&amp;rsquo;s also a bit of transparency going on already.&lt;/p&gt;
&lt;h2 id="ripples"&gt;Ripples&lt;/h2&gt;
&lt;p&gt;Before we start moving the water surface physically, let&amp;rsquo;s add some of that nice glittering that real water has. This is caused by small ripples (capillary waves) randomly disturbing the smooth surface.&lt;/p&gt;
&lt;p&gt;In my implementation, they won&amp;rsquo;t be travelling a particular direction; it&amp;rsquo;s just a bit of seamless Perlin noise used as a normal map:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2026/3/27/around-the-world-30-making-better-waves/normal_map.png" alt="Normal map noise configuration" &gt;
&lt;/p&gt;
&lt;p&gt;The trick to make this look good in motion is to have multiple layers of this noise (I use 4), each with a random offset, and add them together. The layers are faded in and out over half a second, and when a layer&amp;rsquo;s opacity reaches zero, its offset is assigned a new random value. By setting the phases of the layers 90 degrees apart (360° / 4), we make sure that the total amplitude of the sum of the layers remains constant.&lt;/p&gt;
&lt;p&gt;To fit in with my pixelated-textures art style, I&amp;rsquo;m using nearest-neighbour sampling on the noise texture. The result speaks for itself:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="ripples.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;See? I told you the reflection artifacts wouldn&amp;rsquo;t be a problem!&lt;/p&gt;
&lt;h2 id="a-wave"&gt;A wave&lt;/h2&gt;
&lt;p&gt;Now let&amp;rsquo;s add some bigger motion to the water surface. Actual water waves follow a trochoidal motion, where each point on the surface describes a circle:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2026/3/27/around-the-world-30-making-better-waves/Trochoidal_wave.svg.png" alt="Trochoidal wave" &gt;
&lt;br&gt;
&lt;em&gt;Source: &lt;a href="https://en.wikipedia.org/wiki/File:Trochoidal_wave.svg"&gt;Wikipedia&lt;/a&gt; by Kraaiennest, CC-BY-SA 4.0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In the previous version of the game, I decided &lt;em&gt;not&lt;/em&gt; to do this, but to only move the vertices of the water mesh up and down. I thought I had a good reason for this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It looks good, but it has a significant drawback: the water surface is no longer a simple height field. In other words, given a coordinate in the world, it’s very hard to compute what the water height at that point is. And we’ll need that computation later, when we want the ship to float on the surface and move in response to it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is nonsense! If the ship is floating on the water, and the water is moving sideways, the ship should move sideways along with it!&lt;/p&gt;
&lt;p&gt;So let&amp;rsquo;s implement proper trochoidal motion, for a single sine wave at first (that is, a sine for the vertical motion and a cosine for the horizontal motion).&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="single_wave.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;The &amp;ldquo;texture&amp;rdquo; that the water surface gets from the normal map really helps sell the horizontal motion.&lt;/p&gt;
&lt;h2 id="waves-and-wind"&gt;Waves and wind&lt;/h2&gt;
&lt;p&gt;The game will have a dynamic weather system, so the waves will have to react to the wind. On a stormy day, we&amp;rsquo;ll have big waves, and on a calm day the waves will be small.&lt;/p&gt;
&lt;p&gt;And they will never be simple sine waves; there&amp;rsquo;s always more going on. The real ocean surface is, essentially, a sum of sine waves, each with its own amplitude and wavelength, travelling at its own speed in its own direction.&lt;/p&gt;
&lt;p&gt;It turns out there&amp;rsquo;s a deep rabbit hole of theory and measurements about this, but eventually I found the &lt;a href="https://www.wikiwaves.org/index.php/Ocean-Wave_Spectra#Pierson-Moskowitz_Spectrum"&gt;Pierson-Moskowitz spectrum&lt;/a&gt; to be perfect for my needs. It allows me to calculate the wave frequencies that occur at a given wind speed; from that I can compute their propagation speeds; and from those two follow their wavelengths. For the amplitude, I&amp;rsquo;m using &lt;a href="https://en.wikipedia.org/wiki/Wind_wave_model#The_formulae_of_Bretschneider,_Wilson,_and_Young_&amp;amp;_Verhagen"&gt;another model&lt;/a&gt;, which gives me the &lt;em&gt;significant wave height&lt;/em&gt; at a particular wind speed. The significant wave height is defined as &amp;ldquo;four times the standard deviation of the surface elevation&amp;rdquo;, and with some statistical gymnastics I&amp;rsquo;m able to make the sum of an array of sines match that standard deviation.&lt;/p&gt;
&lt;p&gt;I thought I&amp;rsquo;d need to resort to manual inputs to get some artistic control over all this, but no, the outcome of the model is pretty convincing already.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what that looks like at a moderate gale of 15 m/s, which is around 29 knots or 7 Bft:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="multiple_waves.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;h2 id="changing-wind-speed"&gt;Changing wind speed&lt;/h2&gt;
&lt;p&gt;During gameplay, the wind speed can change at any time, though the change will always be gradual. How do we deal with this? We cannot simply update the wavelength of our existing sine waves, because the water surface would appear to contract or expand horizontally.&lt;/p&gt;
&lt;p&gt;In the previous version, I solved this by not changing the sines at all, but rather fade them in and out over time. Whenever a sine would expire and a new one would take its place, it would check the wind speed and set up its parameters to match. It worked, but it was a bit difficult to experiment with, because every change would take 30-60 seconds to propagate. Fade the waves any faster, and the changes would be too visible.&lt;/p&gt;
&lt;p&gt;So in the new version, I came up with another algorithm. It&amp;rsquo;s still based on fading sine waves in and out, but now, the set of sines is completely fixed. Each of them has a fixed wavelength and a direction, which we can plot in polar coordinates:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2026/3/27/around-the-world-30-making-better-waves/wave_inspector.png" alt="Wave inspector" &gt;
&lt;/p&gt;
&lt;p&gt;Each dot represents a single sine; the farther from the center the dot is, the larger its wavelength. The blue dots indicate active sines, that is, those that align with the current wind speed. The size of the dot is relative to that sine&amp;rsquo;s amplitude. So in the above image, the wind is blowing west to east, activating the waves that travel to the right. Because the wind speed is 15 m/s and I&amp;rsquo;ve accounted for wind speeds up to 35 m/s (hurricane speeds, 12 Bft), the biggest waves are barely active at all.&lt;/p&gt;
&lt;p&gt;This lets me change the wind speed quickly in the Godot editor or the game&amp;rsquo;s debug panel, and the water surface is updated instantly to match. While tuning the various parameters, this saved me a lot of time.&lt;/p&gt;
&lt;h2 id="breaking-waves"&gt;Breaking waves&lt;/h2&gt;
&lt;p&gt;This is all fine for deep water, but in shallow water, waves behave differently. As they approach the coast, they slow down and grow taller, until they break and topple into a flatter shape.&lt;/p&gt;
&lt;p&gt;This is difficult to model accurately, but I&amp;rsquo;ve made a rough approximation of the process:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="land_interaction.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;Essentially, what I&amp;rsquo;m doing is flattening the circular motion into an ellipse, making it taller and narrower the shallower the water is relative to the wavelength. This effect needs to remain somewhat subtle, otherwise the sum-of-sines tends to go haywire at higher wind speeds, causing the water surface to turn inside out and intersect itself.&lt;/p&gt;
&lt;h2 id="foam"&gt;Foam&lt;/h2&gt;
&lt;p&gt;At higher wind speeds, the wind causes foam to form on the water surface. Let&amp;rsquo;s add that! I happened to be in the Dutch coast town of Scheveningen during strong winds, so I used the opportunity to take some reference photos from the famous pier:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2026/3/27/around-the-world-30-making-better-waves/sea_foam_photo.jpg" alt="Photo of sea foam" &gt;
&lt;/p&gt;
&lt;p&gt;The foam is far from uniform; it tends to form &amp;ldquo;tendrils&amp;rdquo; that slowly fade away. To mimic that, cellular noise works best:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2026/3/27/around-the-world-30-making-better-waves/foam_noise.png" alt="Foam noise texture setup" &gt;
&lt;/p&gt;
&lt;p&gt;Another observation is that there&amp;rsquo;s no such thing as &amp;ldquo;a little foam&amp;rdquo;; it&amp;rsquo;s either there (white) or not there (transparent). We can do that by thresholding the noise texture. Of course we&amp;rsquo;ll add a &lt;em&gt;little&lt;/em&gt; bit of smoothing to the threshold to make the transition between foam and not-foam less jarring.&lt;/p&gt;
&lt;p&gt;And where should we put the foam? It tends to form at the crest of waves, and then fades out gradually until the next wave comes along. However, since we&amp;rsquo;re doing a sum of many sines, what exactly is the &amp;ldquo;crest&amp;rdquo;? I compute that by summing up all the phase vectors of the waves, multiplied by their respective amplitudes. This gives me a phase vector that more or less follows the biggest sines, but doesn&amp;rsquo;t discount the small ones entirely either. And the angle of that vector tells me where in its cycle that combined wave currently is: at the crest, falling, at the trough, or rising. With some simple arithmetic we can make foam density increases quickly as a wave crest approaches, then make it gradually fall off towards the trough.&lt;/p&gt;
&lt;p&gt;And in the end, how does it look?&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="foam.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;I think that looks perfectly adequate for a one-person indie game. On to the next thing!&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 29: One year in</title><link>https://frozenfractal.com/blog/2026/2/14/around-the-world-29-one-year-in/</link><pubDate>Sat, 14 Feb 2026 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2026/2/14/around-the-world-29-one-year-in/</guid><description>&lt;p&gt;Exactly one year ago this Valentine&amp;rsquo;s Day, I started development on Around The World. Let&amp;rsquo;s look back on the past, but more importantly, look forward at what&amp;rsquo;s still ahead.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Wait,&amp;rdquo; you ask, &amp;ldquo;haven&amp;rsquo;t you been at this for far longer than one year?&amp;rdquo; Yes, that&amp;rsquo;s true. The &lt;a href="https://frozenfractal.com/blog/2023/11/2/around-the-world-1-continents/"&gt;first post in this series&lt;/a&gt; was published in November 2023, over two years ago. But the code repository of &lt;em&gt;that&lt;/em&gt; project was called &lt;code&gt;aroundtheworld4&lt;/code&gt;, so clearly there is even more untold history here. Let&amp;rsquo;s dive in!&lt;/p&gt;
&lt;h2 id="iteration-1-tectonics-mostly"&gt;Iteration 1: tectonics, mostly&lt;/h2&gt;
&lt;p&gt;The first repository, simply called &lt;code&gt;aroundtheworld&lt;/code&gt;, was started in 2019. The idea of the game was already on my mind, but mostly it was an excuse to play around with procedural world generation. I never got around to even start on gameplay, but the tectonic plate simulation made for some pretty satisfying videos:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="aroundtheworld1_tectonics.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;The bright parts are continental crust, the dark parts are oceanic crust, and the colours are determined based on the plate. Note that a tectonic plate isn&amp;rsquo;t either oceanic or continental; it can contain both types of crust attached to each other. Each tectonic plate is modelled as a set of discs of radius &lt;em&gt;r&lt;/em&gt;, and the rule is that two discs may never overlap.&lt;/p&gt;
&lt;p&gt;A birds-eye view of the algorithm:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generate discs on a sphere using Poisson disc sampling. For &lt;em&gt;r&lt;/em&gt; = 0.01 radians, this gives about 43000 points.&lt;/li&gt;
&lt;li&gt;Generate 12 random tectonic plates, each with a type (continental or oceanic), a height above/below sea level, a centroid on the sphere, and a velocity (axis + angle).&lt;/li&gt;
&lt;li&gt;Assign each disc to the nearest plate center. To make plate edges less straight, add a random offset to each disc before looking up the nearest plate centroid. The random offset comes from a couple octaves of 3D simplex noise.&lt;/li&gt;
&lt;li&gt;For each time step:
&lt;ul&gt;
&lt;li&gt;Move all discs according to the velocity of the plate they belong to.&lt;/li&gt;
&lt;li&gt;Detect collisions between discs. If two discs overlap, then one of the two survives. Which one depends on crust type and height/depth: oceanic crust subducts underneath continental crust and disappears; deeper oceanic crust subducts under less deep; and for continental/continental collisions we flip a coin.&lt;/li&gt;
&lt;li&gt;Colliding discs also apply a force to the plate, causing colliding plates to slow down relative to each other.&lt;/li&gt;
&lt;li&gt;Adjust the height/depth of the surviving disc: subducting oceanic crust pushes up continental crust to form coastal mountains; colliding continental crust also forms mountain ranges.&lt;/li&gt;
&lt;li&gt;To create new crust near divergent boundaries, we just run another round of Poisson disc sampling, spawning new discs near existing ones. The crust type and height/depth here also depends on the existing point, to create ocean ridges and rift valleys.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Although it was fun to work on and to look at, I eventually realized that this approach was not suitable for a game: it was too slow, and gave me too little control over the output. So within the same repository, I started on a new approach that was mostly just layers of noise. It got as far as a basic height map:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="aroundtheworld1_noise.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;The approach you see here survives to this day:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generate random jagged Voronoi regions called &amp;ldquo;subplates&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;Accumulate these into actual plates.&lt;/li&gt;
&lt;li&gt;Divide each plate into a continental and oceanic part (either of which may be empty).&lt;/li&gt;
&lt;li&gt;Determine plate velocities based on adjacent plates (e.g. oceanic crust is pulled under continental crust).&lt;/li&gt;
&lt;li&gt;Generate features like mountain ranges and rift valleys along plate boundaries.&lt;/li&gt;
&lt;li&gt;Add simplex noise to make the rest of the terrain interesting as well.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As you can see at the end of the video, I took some effort to match the distribution of heights on the actual Earth. Not really necessary for the game, but a good check that my work made some kind of sense.&lt;/p&gt;
&lt;p&gt;The video doesn&amp;rsquo;t show the flight_view or map_view tabs because they seem to be broken in the latest version, but here&amp;rsquo;s an old screenshot of the uplift map:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2026/2/14/around-the-world-29-one-year-in/aroundtheworld1_uplift_map.png" alt="An elliptical map of the generated world" &gt;
&lt;/p&gt;
&lt;p&gt;The map shows the boundaries of tectonic plates, their velocities, and the resulting uplift (mountain creation) along plate boundaries.&lt;/p&gt;
&lt;p&gt;This project started in 2019 using Godot Engine version 3.1, and eventually got upgraded all the way to Godot 4.0. It used Rust and gdnative (later gdextension) for the high-performance bits. I abandoned it in January 2023.&lt;/p&gt;
&lt;h2 id="iteration-2-an-actual-game"&gt;Iteration 2: an actual game&lt;/h2&gt;
&lt;p&gt;In September 2020, a fortunate thing happened. It was the time of the &lt;a href="https://alakajam.com/10th-alakajam/"&gt;10th Alakajam&lt;/a&gt;, a small game jam community similar in format to Ludum Dare, that I&amp;rsquo;ve been involved with from the beginning. To celebrate the 10th instalment, there wasn&amp;rsquo;t just one theme, but three: &lt;em&gt;maps&lt;/em&gt;, &lt;em&gt;ships&lt;/em&gt; and &lt;em&gt;chaos&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Guess who had been spending years thinking about a game involving at least &lt;em&gt;maps&lt;/em&gt; and &lt;em&gt;ships&lt;/em&gt;? To cover the &lt;em&gt;chaos&lt;/em&gt; theme, I was hoping that procedural generation would help me out there. So I created a new repository, &lt;code&gt;aroundtheworld2&lt;/code&gt;, and started work using Godot 3.2.&lt;/p&gt;
&lt;p&gt;Making this game in a weekend was wildly ambitious, because my original plans were for a project that would take at least months to make. But I distilled it down to the essentials: you control a ship in a procedurally generated world, in which you have to find your way using maps. You can buy maps with gold, which is obtained by trading. You can also buy upgrades to sailing speed and view range. To add time pressure, you can only carry provisions for 10 in-game days of sailing; if you run out, you die. If you manage a complete circumnavigation and came back to your home port, you win. Somehow I pulled it off: it was a complete game!&lt;/p&gt;
&lt;p&gt;The game came in 2nd place overall (out of 24 entries), and the feedback from fellow jammers was &lt;a href="https://alakajam.com/10th-alakajam/1014/around-the-world/"&gt;very positive&lt;/a&gt;, with several people suggesting I develop it further:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As a base for a longer solo project this seems very solid.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;I know I will keep returning to play this game, and would love to see if it could be expanded post-jam&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;I see no reason why this could not be a paid-for game.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;I would really like to see the post-jam version of this when it is released.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;I was so happy to see that a full release is finally in the works, and I&amp;rsquo;ll be buying it for sure when you finish.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You can &lt;a href="https://frozenfractal.com/games/around-the-world-akj/"&gt;play this version online&lt;/a&gt; if you like. By sheer coincidence, random seed 0 gives you a good starting situation.&lt;/p&gt;
&lt;h2 id="iteration-3-bevy-briefly"&gt;Iteration 3: Bevy, briefly&lt;/h2&gt;
&lt;p&gt;In April 2022, I started &lt;code&gt;aroundtheworld3&lt;/code&gt;, still using Rust but this time using the Bevy engine. I think the reason was that I was dissatisfied with the quality of the Godot-Rust integration at the time, and I thought that a code-first approach would be a better fit for this project. I also may have been infatuated with the ECS (Entity-Component-System) architecture that Bevy uses.&lt;/p&gt;
&lt;p&gt;Bevy was still fairly immature at that point, so it took a long time to figure out how to do anything. I liked Bevy and still do, but it just wasn&amp;rsquo;t ready yet to build this project on. I may also have been missing the ease of development that an editor like Godot brings, especially for building GUIs.&lt;/p&gt;
&lt;p&gt;This incarnation was abandoned in the same month that it started, with only 622 lines of code written. All it did was show a sphere mapped with some simplex noise:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="aroundtheworld3.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;h2 id="iteration-4-godot-and-c"&gt;Iteration 4: Godot and C#&lt;/h2&gt;
&lt;p&gt;In April 2023, I seem to have decided to go with mature and trusted technology again. That meant the Godot engine. However, the Rust bindings still weren&amp;rsquo;t as great as they are today, and I knew that GDScript wouldn&amp;rsquo;t have the performance to do what I needed, so I opted for C# instead and started &lt;code&gt;aroundtheworld4&lt;/code&gt;. Next to GDScript, this is the only other language that Godot has official support for, so I assumed it would be stable and solid. And it was!&lt;/p&gt;
&lt;p&gt;The blog posts from &lt;a href="https://frozenfractal.com/blog/2023/11/2/around-the-world-1-continents/"&gt;part 1&lt;/a&gt; all the way through &lt;a href="https://frozenfractal.com/blog/2025/2/4/around-the-world-21-visibility/"&gt;part 21&lt;/a&gt; were produced with this version, so I don&amp;rsquo;t need to say much about it here. Suffice it to say that I got some basic gameplay and trading going, in a 3D spherical world with a top-down camera.&lt;/p&gt;
&lt;p&gt;However, I found that C# did not grow on me like I hoped it would. This is very subjective, and if I were working with it in a salaried job I wouldn&amp;rsquo;t even complain about it. However, if I&amp;rsquo;m going to sustain development on this game for months or years, it&amp;rsquo;s important that I keep up my motivation — and C# just tended to drain it. Because C# is a strongly typed language so &amp;ldquo;I could always refactor it easily later&amp;rdquo;, I racked up a fair bit of technical debt. Furthermore, working with a spherical world meant fighting the engine every step of the way. Moreover, the Godot-Rust bindings had improved a lot in the meantime.&lt;/p&gt;
&lt;p&gt;All this eventually led to the decision to start over. I described my reasons more fully in &lt;a href="https://frozenfractal.com/blog/2025/5/12/around-the-world-22-dropping-the-ball/"&gt;part 22&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="iteration-5-back-to-rust"&gt;Iteration 5: back to Rust&lt;/h2&gt;
&lt;p&gt;That brings us to the project I am working on today, &lt;code&gt;aroundtheworld5&lt;/code&gt;, started on 14 February 2025. This time, I decided to tackle the technically hard parts first, so I would have a solid foundation upon which to build the game.&lt;/p&gt;
&lt;p&gt;This approach seems to be paying off: I now have a robust multithreaded implementation of LayerProcGen, some speedy noise functions, a procedural world generator whose output I can control to a sufficient degree, a floating origin to keep precision issues at bay, and on-the-fly chunk generation so we don&amp;rsquo;t have to generate the entire world in detail. Many of the procedural algorithms were ported over from the C# version, with some improvements along the way. Although there is always room for refactoring and cleanup, I still feel reasonably good about the codebase.&lt;/p&gt;
&lt;p&gt;I could not have done any of this nearly so well without the experience of the previous implementations!&lt;/p&gt;
&lt;h2 id="where-to-next"&gt;Where to next?&lt;/h2&gt;
&lt;p&gt;I also have more of a project plan than before, with the first three milestones fully mapped out. Milestone 1, &amp;ldquo;solid technical foundation&amp;rdquo; was achieved in November 2025. This allows me to focus now on Milestone 2, &amp;ldquo;core gameplay&amp;rdquo;. The aim is to implement all features from the Alakajam version, so we have a complete game loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Camera controls — done.&lt;/li&gt;
&lt;li&gt;Ship controls — in progress. Still not decided whether it should be point-and-click or WASD.&lt;/li&gt;
&lt;li&gt;Buying and viewing maps — mostly done. The UI could use some improvement.&lt;/li&gt;
&lt;li&gt;Trading — not yet started.&lt;/li&gt;
&lt;li&gt;Provisions — not yet started.&lt;/li&gt;
&lt;li&gt;Some basic upgrades — not yet started.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Considering that the remaining items are things I implemented before in a single weekend, completion of this milestone shouldn&amp;rsquo;t take too much work.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also cherry-picking some fun items from Milestone 3, &amp;ldquo;looking good&amp;rdquo;, to keep my motivation up. Good-looking visuals are also important to generate some early buzz about the game. Nobody gets excited about grey boxes! Here&amp;rsquo;s a teaser, showing some recent progress on waves and water rendering:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2026/2/14/around-the-world-29-one-year-in/aroundtheworld5_water.jpg" alt="Screenshot of the game, showing a water surface with waves and pixelated foam" &gt;
&lt;/p&gt;
&lt;h2 id="the-long-term"&gt;The long term&lt;/h2&gt;
&lt;p&gt;This one-year anniversary is also a good moment to reveal my plans for the full game, so I&amp;rsquo;ve set up a &lt;a href="https://frozenfractal.com/games/around-the-world/"&gt;dedicated page&lt;/a&gt; for it. That text can eventually be used for a dedicated website, and onto the Steam page and other storefronts (I will definitely sell it on itch.io, and hopefully GOG as well if they&amp;rsquo;ll have me.). Go check it out!&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 28: Scaling up</title><link>https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/</link><pubDate>Fri, 12 Dec 2025 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/</guid><description>&lt;p&gt;Even though the in-game world is &lt;a href="https://frozenfractal.com/blog/2024/6/18/around-the-world-16-a-matter-of-scale/"&gt;fairly small&lt;/a&gt; compared to Earth, I want it to &lt;em&gt;feel&lt;/em&gt; big. Part of that is being able to see things that are far away.&lt;/p&gt;
&lt;p&gt;For context: by now, I have settled on a third-person “chase camera” rather than a top-down view. It feels so much more immersive, and makes the gameplay (in particular, matching up maps to your surroundings) more interesting as well.&lt;/p&gt;
&lt;p&gt;Remember that my world &lt;a href="https://frozenfractal.com/blog/2025/5/12/around-the-world-22-dropping-the-ball/"&gt;is now a flat plane&lt;/a&gt; that wraps around, rather than an actual sphere. However, it would be great if we could still make it &lt;em&gt;look&lt;/em&gt; like a sphere, so that distant lands would gradually appear over the horizon. Mountains would be visible from a greater distance, so that the terrain actually matters beyond just the shape of the coastline.&lt;/p&gt;
&lt;p&gt;Until now, I&amp;rsquo;ve been using a view distance of 5 km. That&amp;rsquo;s still a lot compared to other open-world games! But a landmass at 5 km does not look very far away, because it&amp;rsquo;s so big. For gameplay purposes, it would be good if view distance could be increased to about 25 km. Otherwise, the part of the world that the player can see is too small compared to the maps, and they won&amp;rsquo;t have anything to orient themselves by.&lt;/p&gt;
&lt;p&gt;How does it work out if we just look at the geometry? My planar planet is 512 km wide, so let&amp;rsquo;s pretent that&amp;rsquo;s the circumference of a spherical planet. Its radius would then be 83 km. Terrain generation is scaled down relative to the real world, so that the tallest mountain is about 2500 m high. Assuming a ship of up to 25 m tall, the mountain&amp;rsquo;s peak will be visible over the horizon from 22 km away. That&amp;rsquo;s pretty close to the 25 km I figured out above, so I decided to sail with these numbers for now: let&amp;rsquo;s fake a planet radius of 83 km.&lt;/p&gt;
&lt;p&gt;We now have some problems to solve.&lt;/p&gt;
&lt;h2 id="world-wrapping"&gt;World wrapping&lt;/h2&gt;
&lt;p&gt;To actually be able to sail &lt;em&gt;around&lt;/em&gt; the world, you need to be able to sail off the west edge and reappear at the east edge, and vice versa. That&amp;rsquo;s easy enough to implement with some modular arithmetic applied to the ship&amp;rsquo;s coordinates. However, any nearby objects and terrain must also exhibit the same wrapping: if you are near the west edge of the world, you should be able to see across it, so anything near the east edge would have to be moved near you as well. This, too, can be implemented with some modular arithmetic.&lt;/p&gt;
&lt;p&gt;However, it would have to be applied to every object in the game individually. That&amp;rsquo;s tedious at best, and bad for performance at worst, so I came up with the solution of parenting every object to a 2×2 km chunk. Only the chunks are moving around; their children just come along for the ride. The node hierarchy looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/chunk_nodes.png" alt="Screenshot of the Godot editor, showing the chunk node hierarchy" &gt;
&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example of how a nearby NPC ship would become visible, even though it&amp;rsquo;s all the way on the other side of the map in world coordinates:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/wrapping.svg" alt="Diagram showing a chunk being moved from the east edge of the world to just beyond the west edge" &gt;
&lt;/p&gt;
&lt;p&gt;The chunks themselves take care to move their children to adjacent chunks whenever they cross a chunk boundary, so the code of individual objects doesn&amp;rsquo;t need to care about the chunking at all.&lt;/p&gt;
&lt;h2 id="floating-origin"&gt;Floating origin&lt;/h2&gt;
&lt;p&gt;As &lt;a href="https://frozenfractal.com/blog/2024/4/11/around-the-world-14-floating-the-origin/"&gt;before&lt;/a&gt; with the spherical world, we&amp;rsquo;re going to run into floating-point precision issues. Godot by default uses 32-bit floats for object and vertex positions, which have only 23 bits of precision; at 512 km from the origin, the distance between two successive floating-point numbers is about 6 cm. The &lt;a href="https://docs.godotengine.org/en/stable/tutorials/physics/large_world_coordinates.html"&gt;official recommendation&lt;/a&gt; in the Godot docs is not to go beyond 8 km for a 3D third-person game, and not beyond 64 km for &lt;em&gt;any&lt;/em&gt; 3D game.&lt;/p&gt;
&lt;p&gt;I started out hoping that this wouldn&amp;rsquo;t be an issue, but it turns out to be real: far away from the world origin, the ship started jittering weirdly, and the sails started jittering relative to the ship. A solution was needed.&lt;/p&gt;
&lt;p&gt;Building Godot with 64-bits precision is possible, but adds a lot of overhead: now &lt;em&gt;every&lt;/em&gt; vector and matrix in the game uses twice the memory, including the large procedurally generated terrain meshes. Would that be a problem? Not necessarily, but it turns out there&amp;rsquo;s now an easier solution.&lt;/p&gt;
&lt;p&gt;Since we&amp;rsquo;re moving chunks around anyway, we can implement a floating origin system there as well. Just before rendering a frame, the chunk containing the player&amp;rsquo;s ship is moved to the global origin (0, 0), and all other chunks are positioned relative to it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/floating_origin.svg" alt="Same diagram as above, but with the origin moved to the chunk where the player is" &gt;
&lt;/p&gt;
&lt;p&gt;This ensures that every object we care about remains close to the global origin, where floating-point precision is sufficient. As long as none of the code assumes that &lt;em&gt;global&lt;/em&gt; coordinates (relative to Godot&amp;rsquo;s global origin) are the same as &lt;em&gt;world&lt;/em&gt; coordinates (relative to the north-west corner of the world map), this works great: the jitter is gone.&lt;/p&gt;
&lt;h2 id="planet-curvature"&gt;Planet curvature&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s take a look at how we could implement a fake planet curvature. In particular, I want distant objects to gradually sink below the horizon.&lt;/p&gt;
&lt;p&gt;This could be done in a vertex shader: simply compute the distance to the camera (measured on the horizontal plane), calculate how far downwards the vertex should move, and move it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/project_vertex.svg" alt="Projecting a point downwards onto the sphere" &gt;
&lt;/p&gt;
&lt;p&gt;The drawback is that this shader would need to be applied to &lt;em&gt;every&lt;/em&gt; object in the world, meaning I would have to use custom shaders everywhere and couldn&amp;rsquo;t benefit from Godot&amp;rsquo;s built-in materials anymore. Tedious! Another drawback is that the visible meshes would no longer align with their physics shapes, which becomes relevant if we want to do object picking (e.g. clicking on the peak of a distant mountain to set a course towards it).&lt;/p&gt;
&lt;p&gt;But I had an idea I wanted to try. Now that we have chunks, maybe we could pull the chunks themselves downwards, and have all their children come along for the ride?&lt;/p&gt;
&lt;p&gt;My first attempt was to introduce some skew. In 3D games, the position and orientation of an object is usually represented as a matrix, and mine is no different. Godot uses 3×4 matrices: a 3×3 part to represent rotation and scale, and a 3x1 part for translation. But rotation has 3 degrees of freedom, and scale has another 3, so that means that we have 3 more degrees of freedom we can play with to make the chunk align (somewhat) with the sphere. Because chunks are in the &lt;code&gt;y = 0&lt;/code&gt; plane and are neither rotated nor scaled, the matrix looks like this, where &lt;code&gt;tx, 0, tz&lt;/code&gt; is the translation vector (the chunk&amp;rsquo;s position):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 1 0 0 tx
0 1 0 0
0 0 1 tz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we want to move points within the chunk only on the vertical axis (&lt;code&gt;y&lt;/code&gt;), depending on their position in the &lt;code&gt;xz&lt;/code&gt; plane, we have two components &lt;code&gt;rx, rz&lt;/code&gt; we can play with, as well as &lt;code&gt;ty&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 1 0 0 tx
rx 1 rz ty
0 0 1 tz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My idea is then as follows: modify these variables &lt;code&gt;rx, rz, ty&lt;/code&gt; in such a way that the chunk&amp;rsquo;s corners end up on the sphere. Because we have 4 corners and only 3 variables, this is impossible; instead, we do it only for the 3 corners that are closest to the camera, and let the 4th end up where it will. It&amp;rsquo;s hard to draw in 3D, but in 2D you can see the idea better:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/skew_chunks.svg" alt="Chunks being skewed downwards onto the sphere" &gt;
&lt;/p&gt;
&lt;p&gt;Every object inside these chunks will end up skewed as well, but because the effect is slight, this shouldn&amp;rsquo;t be visible.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how it looks when I modify the curvature in real time:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="skew_curvature.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;At lower curvatures, it&amp;rsquo;s quite effective!&lt;/p&gt;
&lt;p&gt;However, there&amp;rsquo;s a problem with this approach that might not be obvious at first: physics. The built-in Godot Physics engine is well known to be bad at anything except pure translation and rotation; it doesn&amp;rsquo;t even support scaling. The newly integrated Jolt engine is better in this regard, but it draws the line at skewing, and starts emitting a lot of errors if a physics shape like a cylinder has any skew introduced to it.&lt;/p&gt;
&lt;p&gt;So, let&amp;rsquo;s try something so stupid that it couldn&amp;rsquo;t possibly work: do not skew chunks at all, but just move them on the vertical axis, like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/move_chunks.svg" alt="Chunks being moved downwards onto the sphere" &gt;
&lt;/p&gt;
&lt;p&gt;Amazingly, at small curvatures such as I&amp;rsquo;m using, this works &lt;em&gt;just fine&lt;/em&gt; and the visual difference compared to skewing is not even noticeable. You might think it would introduce visible cracks between terrain chunks, but no: because farther chunks are moved farther downwards, the cracks are always hidden behind nearby terrain. It&amp;rsquo;s a keeper!&lt;/p&gt;
&lt;h2 id="terrain-lods"&gt;Terrain LODs&lt;/h2&gt;
&lt;p&gt;Now that we have the ability to make stuff gradually appear over the horizon, we need some stuff that actually &lt;em&gt;will&lt;/em&gt; gradually appear over the horizon. With a ship of 25 m tall, even though the horizon itself is (surprisingly) only 2 km away, we can see tall mountains at a distance of 25 km. So a draw distance of 5 km is not nearly enough anymore.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s easy enough to increase this value in the configuration, but this increased the amount of terrain by a factor of 5² = 25, and tanked the frame rate. Some quick math shows that, at the terrain resolution of 8 m that I&amp;rsquo;m using, the 50×50 km patch of terrain around the player consists of 39 million triangles, and this is too much for my GPU.&lt;/p&gt;
&lt;p&gt;Fortunately, we don&amp;rsquo;t need that many triangles, because on distant terrain they&amp;rsquo;re smaller than a pixel. It&amp;rsquo;s time to introduce a LOD (level-of-detail) scheme to our terrain: render nearby terrain at full detail, but reduce the number of triangles on terrain chunks farther away.&lt;/p&gt;
&lt;p&gt;A classic problem with this are seams, or cracks, between adjacent terrain meshes of a different LOD. It&amp;rsquo;s often just a few pixels, but it&amp;rsquo;s still quite visible especially in motion. Here, I tweaked some values to make the problem more visible:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/crack.jpg" alt="Screenshot of a crack in the terrain" &gt;
&lt;/p&gt;
&lt;p&gt;There are various solutions. The most elegant is to introduce special bits of mesh to cover the cracks, which are only made visible when the two adjacent terrain chunks are currently displayed at different LODs. Due to all the different edge cases (pun wholeheartedly intended), it&amp;rsquo;s surprisingly fiddly to get this right.&lt;/p&gt;
&lt;p&gt;Fortunately there&amp;rsquo;s a simpler method, which is to simply add a ‘skirt’ of quads that hangs down from the edge of each chunk:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/skirt.jpg" alt="Screenshot showing a skirt at the edge of a terrain chunk" &gt;
&lt;/p&gt;
&lt;p&gt;The normals on the skirt are copied from the adjacent vertices, so they receive the same lighting, and it won&amp;rsquo;t look as if there&amp;rsquo;s an actual small cliff there. And with that, the crack is gone:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/no_crack.jpg" alt="Screenshot of the same terrain, now without a crack" &gt;
&lt;/p&gt;
&lt;p&gt;This should also cover up any cracks caused by moving chunks vertically, though I haven&amp;rsquo;t spotted any so far.&lt;/p&gt;
&lt;h2 id="impostors"&gt;Impostors&lt;/h2&gt;
&lt;p&gt;The trees I added &lt;a href="https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/"&gt;in the previous post&lt;/a&gt; make a huge difference to the believability and sense of scale of the terrain. It would be sad if they couldn&amp;rsquo;t be rendered up to a large distance.&lt;/p&gt;
&lt;p&gt;However, as it currently stands, being near a rainforest will drop the framerate to about 40 fps; the tree meshes take 15 ms to render. Since I want this game to run well on potato hardware, it should run at around 150-200 fps on my mid-range Radeon RX 7600 card, meaning a frame budget of only 5-7 ms.&lt;/p&gt;
&lt;p&gt;The standard trick is to not render full meshes at a large distance, but rather pre-render the mesh into a texture from various points of view, and then just render a single quad with the texture applied: an &lt;em&gt;impostor&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Godot doesn&amp;rsquo;t have support for impostors built in, but adding tooling inside the editor is really easy, so I built this quick and dirty impostor baking scene:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="impostor_baker.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;It iterates through all the tree meshes at 8 different angles, and renders each to a suitably sized viewport through an orthogonal camera (to simulate an infinite distance). Each orientation is rendered twice: once to capture colour and alpha, once to capture normals. The textures have pretty low resolution, but that&amp;rsquo;s fine, because they&amp;rsquo;re never viewed up close anyway:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/tonka_bean_albedo.png" alt="Albedo texture of a tree impostor" &gt;
&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/12/12/around-the-world-28-scaling-up/tonka_bean_normal.png" alt="Normal texture of a tree impostor" &gt;
&lt;/p&gt;
&lt;p&gt;These are then applied directly to the impostor quad, depending on the viewing angle:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="impostors.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;This increased the frame rate from 40 fps to about 130 fps. There&amp;rsquo;s some more low-hanging fruit that will improve the frame rate further, so this is fast enough for the time being.&lt;/p&gt;
&lt;h2 id="all-together-now"&gt;All together now&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s see all of the above in action while the player sails towards a remote island: the distant terrain, planet curvature, and tree impostors. I&amp;rsquo;ve edited the video for brevity, because it ran over 3 minutes; one of the biggest game design challenges will be to make sure the player has something to do during these long voyages.&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="approaching_island.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;The transition from impostors to real geometry is still fairly noticeable, probably because the impostors cast a smaller shadow. But it&amp;rsquo;s plenty good enough for now.&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 27: Planting trees</title><link>https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/</link><pubDate>Fri, 28 Nov 2025 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/</guid><description>&lt;p&gt;In the &lt;a href="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/"&gt;previous post&lt;/a&gt;, I determined what kind of vegetation should grow where in my procedurally generated world. Now it&amp;rsquo;s time to actually plant those plants!&lt;/p&gt;
&lt;p&gt;As I mentioned last week, I figured out a list of tree species that belong to each &amp;ldquo;plant functional type&amp;rdquo; in the BIOME1 system. I made sure to get a set of distinctive-looking trees, so now it was time to fire up Blender, dust off my modelling skills (such as they are) and create some low-poly tree models and an assortment of other plants:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/arboretum.jpg" alt="A render of 14 low-poly trees, standing on a dark brown circle. On the ground next to each tree is its English and scientific name. The trees are ordered in seven rows of two, and each row labelled with the biome in which those trees occur." &gt;
&lt;/p&gt;
&lt;p&gt;Most of the game takes place at sea, so you won&amp;rsquo;t often see these models up close. By keeping the polygon count very low, I&amp;rsquo;m hoping I can render a large enough number of trees without having to resort to impostors. The tallest tree in the back (tonka bean) has only 44 triangles. The simplest plants are just distorted octahedra, with only 8 triangles.&lt;/p&gt;
&lt;p&gt;The grasses are generated with Blender&amp;rsquo;s geometry nodes and are actually way too detailed, with up to 500 triangles each, but I&amp;rsquo;m not sure I&amp;rsquo;ll be keeping them anyway. If I do, a handful of intersecting textured planes would be a better implementation.&lt;/p&gt;
&lt;h2 id="inputs"&gt;Inputs&lt;/h2&gt;
&lt;p&gt;Recall that we have a fairly coarse map of biomes, and that each biome corresponds to a set of plant functional types, each of which contains some plant species. So that indirectly gives us an occurrence map for each species, containing 1.0 where the plant can occur and 0.0 where it can&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;However, that map only has a resolution of 1×1 km. We don&amp;rsquo;t want our forest boundaries to be big straight-edged squares, so we&amp;rsquo;ll have to add some detail to this. In the previous post, I used domain warping to distort the boundaries, because I didn&amp;rsquo;t want to blend between biome terrain colours. Let&amp;rsquo;s apply the same trick here, using the same domain warp, so that the plants nicely follow the biome boundaries.&lt;/p&gt;
&lt;p&gt;On top of that, I want some artistic control over how often each species appears. For example, in tropical rainforest, most of the visible trees are part of the canopy, but the canopy is occasionally pierced by even taller, so-called &amp;ldquo;emergent&amp;rdquo; trees, like the tonka bean we saw above. These should be rarer than the other species, so I&amp;rsquo;ll give each species a base &amp;ldquo;occurrence rate&amp;rdquo;, to be evaluated relative to the other ones in its biome.&lt;/p&gt;
&lt;p&gt;And on top of &lt;em&gt;that&lt;/em&gt;, not every square meter of land should be covered by trees, even in biomes where they can grow. In nature, factors like soil quality and grazing animals keep areas of land open. This differs by biome: tropical rainforest should have near 100% coverage, but colder or dryer biomes will have less. I&amp;rsquo;ll mimic that using a single layer of simplex noise, and give each biome a threshold value between 0 and 1. Plants can only grow where the value of the noise is below the threshold.&lt;/p&gt;
&lt;p&gt;In the end, this gives me two functions, which can be evaluated at any point in the world:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Coverage amount: what is the probability of a plant growing here?&lt;/li&gt;
&lt;li&gt;Relative species frequency: if there is a plant here, how likely is it to be of a particular species?&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="placement"&gt;Placement&lt;/h2&gt;
&lt;p&gt;First off, we don&amp;rsquo;t want plants to overlap. Maybe in a dense forest, the trees will intersect a little bit, but never by too much. So I&amp;rsquo;ll assign each species a radius, and declare that the discs defined by these radii must never overlap. This also gives some artistic control; for example, by setting a large radius, we could create a &amp;ldquo;loner&amp;rdquo; tree species that doesn&amp;rsquo;t grow near others.&lt;/p&gt;
&lt;p&gt;However, remember that the terrain is generated in chunks (of 1×1 kilometer, like the biome map, but this is a coincidence). When placing plants in one chunk, we cannot refer to trees in the neighbouring chunks, because those might not have been generated yet. If we force generation of neighbouring chunks, we run into a chicken-and-egg problem, because they&amp;rsquo;ll require &lt;em&gt;their&lt;/em&gt; neighbours, and so on. And yet, we have to prevent trees from overlapping.&lt;/p&gt;
&lt;p&gt;A simple approach is &lt;em&gt;rejection sampling&lt;/em&gt;: pick a uniformly random point inside the chunk, choose a plant species for it, and if there is room for that plant, spawn it there. But then, how would we prevent overlaps with plants from other chunks? We could avoid placing plants near chunk edges, keeping their entire disc inside their own chunk, but then we&amp;rsquo;d get weird straight paths along chunk edges where no plants grow.&lt;/p&gt;
&lt;h2 id="grid-placement"&gt;Grid placement&lt;/h2&gt;
&lt;p&gt;A more suitable approach would be to place plants in a grid (ideally a hex grid, but squares are a bit simpler to work with). Each grid cell contains the center of at most one plant, whose species and position within the cell are computed deterministically from the hash of the cell&amp;rsquo;s global coordinates. Here sketched on single chunk containing a 3×3 grid for two species:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;species &amp;ldquo;green&amp;rdquo; has a small radius and a relative probability of 1&lt;/li&gt;
&lt;li&gt;species &amp;ldquo;blue&amp;rdquo; has a large radius and a relative probability of 0.5&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/grid_placement.svg" alt="A 3×3 grid of squares, each containing a point with a circle drawn around it" &gt;
&lt;/p&gt;
&lt;p&gt;Of course, plants will end up overlapping, so we&amp;rsquo;ll have to prune them. To do that, my first thought was to hash the coordinates of their cells, and keep only the plant with the largest hash. We can then &amp;ldquo;predict&amp;rdquo; where plants will spawn in the neighbouring chunks, and deal with overlaps that way. With some fictional two-digit hashes, it could look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/cell_hashing.svg" alt="The same grid as above, but now each cell contains a random-looking two-digit number and some plants have become very transparent" &gt;
&lt;/p&gt;
&lt;p&gt;However, this has an ordering dependency: suppose plant A overlaps with B, and B overlaps with C. The hashes are ordered as A &amp;gt; B &amp;gt; C. If we handle the overlap A-B first, then B is pruned and C can continue to exist. But if we handle the overlap B-C first, then C is pruned. I didn&amp;rsquo;t notice this problem until drawing the above image! For instance, the plant with hash 02 could only continue to exist because 43 and 46 were pruned first, since they in turn were dominated by 93 and 88 respectively.&lt;/p&gt;
&lt;p&gt;We could impose some fixed ordering for handling overlaps, such as left-to-right, top-to-bottom, but it&amp;rsquo;s not clear how that would work across chunk boundaries. There might be an entire chain of overlaps running across a chunk, meaning information could &amp;ldquo;travel&amp;rdquo; across many chunks, most of which we haven&amp;rsquo;t generated yet. This would make placement depend, at least a little bit, on chunk creation order – something I&amp;rsquo;d rather avoid.&lt;/p&gt;
&lt;p&gt;On top of that, there is another fundamental problem with this approach: it creates a bias towards smaller plants. Imagine we use a grid of 1×1 meter squares, a shrub has a radius of 1 meter, and a tree has a radius of 10 meters. A potential tree will then overlap with many shrubs, and the probability that it&amp;rsquo;ll &amp;ldquo;win&amp;rdquo; over all of them is near zero. We could try adjusting the relative probabilities to compensate, but I&amp;rsquo;m not sure how that should work when more than two species are in play.&lt;/p&gt;
&lt;p&gt;Rather, since we already applied the relative spawn probabilities of each species, from now on each candidate should have an &lt;em&gt;equal&lt;/em&gt; probability of spawning. And… I have no idea how to achieve that.&lt;/p&gt;
&lt;h2 id="rejection-sampling"&gt;Rejection sampling&lt;/h2&gt;
&lt;p&gt;So maybe I should use rejection sampling after all? Pick a random point inside the chunk, pick a species for it, and if there are no overlaps, spawn a plant of that species there. But this runs into the exact same problem! Even if the tree and the shrub are configured with equal probabilities, the tree has a larger radius, and therefore a smaller probability of actually fitting in between the already spawned plants.&lt;/p&gt;
&lt;p&gt;Maybe we should spawn larger plants first? But this won&amp;rsquo;t work either: if two species have equal probability and nearly equal radius, the slightly larger one will dominate.&lt;/p&gt;
&lt;p&gt;Maybe we should adjust the spawn probability by radius, or by surface area, to make larger plants more likely to spawn? This should fix the balancing issue – and in fact it should even work with the grid-based approach – but now a large tree with a small probability will create a great many candidates, most of which will be rejected. With rejection sampling, this would kill performance, and with the grid placement, it would occupy most grid cells with plants that will never spawn, and thus not achieve maximum density.&lt;/p&gt;
&lt;p&gt;Maybe we could select a plant species &lt;em&gt;first&lt;/em&gt;, according to its relative probability, and find a suitable place for it second? Then we could keep searching until it fits somewhere. However, what do we do if we can&amp;rsquo;t fit it in anymore? To keep the relative frequencies of all plants, we&amp;rsquo;d have to abort the loop, otherwise we&amp;rsquo;ll just keep spawning only smaller and smaller plants to fill the gaps, upsetting the balance. But if we do abort the loop, it might mean we haven&amp;rsquo;t achieved maximum density: a single failed attempt to fit in a large tree would mean that the entire chunk would not be as densely covered as it could be. Another issue is that we can&amp;rsquo;t select a plant species without knowing the biome, and the biome depends on the location within the chunk.&lt;/p&gt;
&lt;h2 id="iterative-methods"&gt;Iterative methods&lt;/h2&gt;
&lt;p&gt;Maybe we could iteratively improve our plant placement to converge to the desired balance, while also keeping density. Let&amp;rsquo;s call this &amp;ldquo;acceptance sampling&amp;rdquo;: pick a point, pick a species based on that point&amp;rsquo;s biome, unconditionally place that plant there, then prune everything it overlaps with. Repeat until satisfied.&lt;/p&gt;
&lt;p&gt;However, this has the same problem of imbalance: though large plants now have the right probability of &lt;em&gt;spawning&lt;/em&gt;, they instead have a disproportionately large probability of being pruned. We could increase their spawn probability to compensate, but then they&amp;rsquo;d often spawn only to be pruned shortly afterwards, leaving a gap in coverage. And that&amp;rsquo;s not even considering how this would work across chunk boundaries.&lt;/p&gt;
&lt;h2 id="turning-down-the-difficulty"&gt;Turning down the difficulty&lt;/h2&gt;
&lt;p&gt;This is a much harder problem than I thought at first. I don&amp;rsquo;t think it&amp;rsquo;s fundamentally impossible to solve; if you have any ideas, let me know! But I have to avoid wasting even more time on it, so for now, I&amp;rsquo;m adjusting my requirements: &lt;em&gt;overlapping plants are okay&lt;/em&gt; and I&amp;rsquo;m not going to keep that from happening.&lt;/p&gt;
&lt;p&gt;To ensure somewhat even coverage, I&amp;rsquo;ll still use the grid approach. Now the grid spacing becomes all-important, since it directly determines how many plants will be placed and how much overlap there will be. I&amp;rsquo;ll have to find some compromise so that large trees don&amp;rsquo;t overlap too much, while the distance between small plants doesn&amp;rsquo;t get too large either.&lt;/p&gt;
&lt;p&gt;This nicely avoids any problems at chunk boundaries as well, since we don&amp;rsquo;t need to account for overlaps with plants from neighbouring chunks.&lt;/p&gt;
&lt;p&gt;With all that, I&amp;rsquo;m getting decent results. Here are some patchy coniferous forests interspaced with shrublands:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/trees_1.jpg" alt="Screenshot of a lowland coast with patches of coniferous trees" &gt;
&lt;/p&gt;
&lt;p&gt;And a tropical rainforest:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/trees_2.jpg" alt="Screenshot of coast with a dense rainforest canopy" &gt;
&lt;/p&gt;
&lt;h2 id="remaining-issues"&gt;Remaining issues&lt;/h2&gt;
&lt;p&gt;There are a few more issues to resolve. First, it looks weird if plants grow on sheer cliff faces:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/plants_on_cliffs.jpg" alt="Screenshot showing some cliffs with trees growing on the cliff faces" &gt;
&lt;/p&gt;
&lt;p&gt;To fix this, I just computed the gradient of the local terrain, and reject the plant if it tries to spawn on a location that&amp;rsquo;s too steep for that species. This is configurable per species, so that smaller shrubs can still spawn on steep slopes, where big trees couldn&amp;rsquo;t grow. This helps:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/plants_on_cliffs_gone.jpg" alt="Screenshot of the same cliffs, now devoid of trees" &gt;
&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s another issue that needs to be solved:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/trees_in_port.jpg" alt="Screenshot of a forested coast, with white houses intersecting the trees" &gt;
&lt;/p&gt;
&lt;p&gt;The white houses represent a port town, and of course it shouldn&amp;rsquo;t be overgrown like that. We could prevent plants spawning wherever buildings have already spawned, but we can do better: typically, humans will cut down trees for firewood, so there should be some clearing around the port itself.&lt;/p&gt;
&lt;p&gt;Thus, my solution is to assign each port an inner and outer radius. Within the inner radius, no plants can spawn at all; the probability is 0. Between the inner and the outer radius, the plant spawn probability smoothly increases towards 1. This is multiplied with the base spawn probability for plants, which is already a noisy function, so we shouldn&amp;rsquo;t get a hard-edged perfectly circular clearing around the port.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s see how that looks:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/trees_in_port_gone.jpg" alt="Screenshot of the same coast, but now the trees have drawn back around the houses" &gt;
&lt;/p&gt;
&lt;p&gt;Much better!&lt;/p&gt;
&lt;h2 id="performance"&gt;Performance&lt;/h2&gt;
&lt;p&gt;At the start, I wrote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;By keeping the polygon count very low, I&amp;rsquo;m hoping I can render a large enough number of trees without having to resort to impostors.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;How is that working out? Not great, unfortunately. On this densely forested archipelago, the trees bring the framerate down from 132 fps to 75 fps:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/11/28/around-the-world-27-planting-trees/dense_forest.jpg" alt="Steep islands covered densely with rainforest trees" &gt;
&lt;/p&gt;
&lt;p&gt;It gets worse on flat continents, which have even more trees and also more overdraw, even though most of the trees are hidden behind other trees. The framerate goes down to 45 fps on those.&lt;/p&gt;
&lt;p&gt;These numbers would be fine if I were testing on a low-end machine and wasn&amp;rsquo;t planning to add more stuff, but at this stage of development I should be aiming for about 150-200 fps to keep this game playable on potato hardware as well. So it&amp;rsquo;s clear that I will need to implement impostors after all. But that&amp;rsquo;s for some other day!&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 26: Biomes</title><link>https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/</link><pubDate>Fri, 26 Sep 2025 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/</guid><description>&lt;p&gt;I &lt;em&gt;should&lt;/em&gt; be working on gameplay, but I got tired of looking at drab grey terrain, so I decided to beautify it first by adding some vegetation. In a way, this is in line with my plan to add a solid technical foundation for the game before stacking too much gameplay on top, because I&amp;rsquo;m not sure that the hardware will be able to render all those trees in a huge open world.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s take a look at our starting point:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/grey_terrain.jpg" alt="A rendered mountainous island seen from a bird&amp;rsquo;s eye perspective. Its surface is a drab dark grey." &gt;
&lt;/p&gt;
&lt;p&gt;(That mountain village is placed a bit unrealistically, but I&amp;rsquo;ll tackle that later.)&lt;/p&gt;
&lt;p&gt;You probably don&amp;rsquo;t remember from &lt;a href="https://frozenfractal.com/blog/2023/12/19/around-the-world-8-seasons/"&gt;a post almost two years ago&lt;/a&gt;, but I already implemented generation of temperature, wind and precipitation patterns, and inferred the local climate from those. That was still in C# on a spherical world though, so I dutifully started porting it all to Rust on a flat world. In the end, I got my algorithms to produce a nice climate map using the Köppen-Geiger scheme, like before:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/koppen.png" alt="A map of the actual Earth, with different colours indicating different different climates." &gt;
&lt;/p&gt;
&lt;p&gt;This map uses a common colour scheme, same as &lt;a href="https://commons.wikimedia.org/wiki/File:Koppen-Geiger_Map_v2_World_1991%E2%80%932020.svg"&gt;Wikipedia&lt;/a&gt;. Here&amp;rsquo;s the real world for reference:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/Koppen-Geiger_Map_v2_World_1991%E2%80%932020.svg.png" alt="A map of the actual Earth, with the same colour scheme" &gt;
&lt;br&gt;
&lt;em&gt;Source: &lt;a href="https://commons.wikimedia.org/wiki/File:Koppen-Geiger_Map_v2_World_1991%E2%80%932020.svg"&gt;Wikimedia Commons&lt;/a&gt;, CC-BY 4.0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You can see that my attempt is &lt;em&gt;far&lt;/em&gt; from perfect – in fact, it&amp;rsquo;s worse than what I got previously in my C# implementation. This is mostly because I simplified wind patterns, and I haven&amp;rsquo;t added noise to rain distribution yet either. But looking at the coasts (which is what the player will see), I think we&amp;rsquo;ve got good variety, which is the most important thing.&lt;/p&gt;
&lt;p&gt;The next step was to map Köppen climate classes like &amp;ldquo;temperate oceanic climate&amp;rdquo; (Cfb) and &amp;ldquo;cold-summer Mediterranean climate&amp;rdquo; (Csc) to a set of trees and plants that could plausibly grow there. And that&amp;rsquo;s where the real trouble began.&lt;/p&gt;
&lt;h2 id="to-köppen-or-not-to-köppen"&gt;To Köppen or not to Köppen&lt;/h2&gt;
&lt;p&gt;I had originally chosen the Köppen-Geiger scheme because, and I quote myself:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Köppen designed the system based on his experience as a botanist, so his classification is based on the vegetation we encounter in each climate class. That aligns perfectly with my goals.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So logically, these climate classes should cleanly map to particular biomes, right? I never checked this before, so I checked it now…&lt;/p&gt;
&lt;p&gt;… and it&amp;rsquo;s false.&lt;/p&gt;
&lt;p&gt;An awesome Wikipedia user has created the following map of the Earth&amp;rsquo;s biomes:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/Vegetation.png" alt="A map of biomes of the actual Earth" &gt;
&lt;br&gt;
&lt;em&gt;Source: &lt;a href="https://en.wikipedia.org/wiki/User:Berkserker"&gt;Berkserker&lt;/a&gt; on &lt;a href="https://en.wikipedia.org/wiki/File:Vegetation.png"&gt;Wikimedia Commons&lt;/a&gt;, CC-BY-SA 3.0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;These are biomes I can work with; even if I don&amp;rsquo;t manage to find any data sources, names like &amp;ldquo;dry steppe and thorn forest&amp;rdquo; contain enough information to start drawing something plausible. So the question is: how does this compare to the Köppen map? Which Köppen climate zones map to which biomes?&lt;/p&gt;
&lt;p&gt;Of course, being a programmer and map nerd, I wasn&amp;rsquo;t going to determine this by hand. So I figured out by trial and error that the above map was using the Robinson projection, used GDAL and QGIS to reproject the Köppen map into Robinson as well, aligned the two to the best of my abilities in GIMP, and then wrote some Python code to tally the corresponding pixels in each map. Note that the alignment isn&amp;rsquo;t perfect, so there will be some noise in the results, especially for Köppen zones that cover only a small part of the world. I&amp;rsquo;ll paste the full results here in case somebody has a use for it, but don&amp;rsquo;t bother reading the whole thing:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Af 89.0% tropical rainforest, 8.2% subtropical evergreen forest
Am 84.4% tropical rainforest, 6.3% grass savanna
Aw 29.0% dry forest and woodland savanna, 23.2% tropical rainforest, 23.0% tree savanna, 14.8% monsoon forests and mosaic, 5.7% grass savanna
As 100.0% monsoon forests and mosaic
BWh 44.1% xeric shrubland, 21.1% arid desert, 20.5% semiarid desert, 5.9% dry steppe and thorn forest
BWk 39.3% xeric shrubland, 25.3% dry steppe and thorn forest, 12.8% arid desert, 11.4% montane forests and grasslands, 5.5% semiarid desert
BSh 25.3% grass savanna, 22.8% tree savanna, 19.7% dry forest and woodland savanna, 17.9% dry steppe and thorn forest, 4.8% temperate steppe and savanna
BSk 57.8% temperate steppe and savanna, 10.2% dry steppe and thorn forest, 9.1% Mediterranean vegetation, 9.0% montane forests and grasslands, 6.3% xeric shrubland
Csa 63.9% Mediterranean vegetation, 14.5% temperate steppe and savanna, 11.8% temperate broadleaf forest
Csb 29.1% Mediterranean vegetation, 24.7% montane forests and grasslands, 24.1% temperate broadleaf forest, 9.2% dry steppe and thorn forest, 5.2% temperate steppe and savanna
Csc 38.5% Mediterranean vegetation, 30.8% temperate broadleaf forest, 30.8% montane forests and grasslands
Cwa 44.6% dry forest and woodland savanna, 19.4% temperate broadleaf forest, 14.1% monsoon forests and mosaic, 11.2% subtropical evergreen forest, 3.5% montane forests and grasslands
Cwb 32.3% montane forests and grasslands, 24.6% subtropical evergreen forest, 13.0% dry forest and woodland savanna, 12.2% temperate steppe and savanna, 8.8% tree savanna
Cwc no matches
Cfa 42.6% temperate broadleaf forest, 25.7% subtropical evergreen forest, 20.7% temperate steppe and savanna, 5.8% dry forest and woodland savanna
Cfb no matches
Cfc 62.5% taiga, 37.5% temperate broadleaf forest
Dsa 30.3% dry steppe and thorn forest, 29.8% montane forests and grasslands, 28.7% temperate broadleaf forest, 6.2% temperate steppe and savanna
Dsb 76.3% montane forests and grasslands, 13.5% dry steppe and thorn forest, 4.8% temperate steppe and savanna
Dsc 52.2% tundra, 36.0% taiga, 9.9% alpine tundra
Dsd 50.0% alpine tundra, 25.0% tundra, 25.0% taiga
Dwa 82.4% temperate broadleaf forest, 17.5% temperate steppe and savanna
Dwb 46.8% temperate broadleaf forest, 23.0% taiga, 16.3% temperate steppe and savanna, 13.7% montane forests and grasslands
Dwc 61.6% taiga, 17.9% alpine tundra, 15.4% montane forests and grasslands
Dwd 83.0% taiga, 16.0% alpine tundra
Dfa 55.2% temperate steppe and savanna, 44.7% temperate broadleaf forest
Dfb 46.3% temperate broadleaf forest, 44.1% taiga
Dfc 65.9% taiga, 29.3% tundra
Dfd 71.9% taiga, 26.1% tundra
ET 46.0% tundra, 44.8% montane forests and grasslands
EF 83.9% ice sheet and polar desert, 15.2% tundra
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The thing that you should be looking at is the first percentage on each line. For example, Köppen class &lt;code&gt;Af&lt;/code&gt; is covered for 89% by tropical rainforest – fine, we can just always plant tropical rainforest there. But the majority of climate zones aren&amp;rsquo;t nearly so clear cut; for example, what to make of &lt;code&gt;BSh&lt;/code&gt; (hot semi-arid)? One quarter of it is covered by grass savanna, almost a quarter by tree savanna, and several other biome types as well.&lt;/p&gt;
&lt;p&gt;At this point, I concluded that Köppen&amp;rsquo;s scheme did not fit my purposes as well as I had assumed, so I started looking at alternatives. And that&amp;rsquo;s when I was reminded of a blog post by the always extremely thorough World Building Pasta: &lt;a href="https://worldbuildingpasta.blogspot.com/2024/12/beyond-koppen-geiger-climate.html"&gt;Beyond the Köppen-Geiger Climate Classification System, Part I: Extensions and Alternatives&lt;/a&gt;. In it, Pasta describes several alternative climate classification systems with their pros and cons.&lt;/p&gt;
&lt;h2 id="biome1"&gt;BIOME1&lt;/h2&gt;
&lt;p&gt;The system that caught my attention was the BIOME1 model by Prentice et al., 1992 (&lt;a href="https://amu.hal.science/hal-01788308/document"&gt;PDF&lt;/a&gt;). This model fits my purposes very well, because it directly talks about plant types and in which climates they might grow, and offers a very data-driven approach to find out. The core of the paper is this one table:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/table1.png" alt="Screenshot of a table from the Prentice et al. paper" &gt;
&lt;/p&gt;
&lt;p&gt;For example, this says that the plant type &amp;ldquo;cool-temperate conifer&amp;rdquo; can grow in areas where the minimum temperature is at least −19 °C, the maximum temperature does not exceed 5 °C, there are at least 900 growing degree-days in a year, and the α coefficient is at least 0.65. Let&amp;rsquo;s unpack this one by one.&lt;/p&gt;
&lt;p&gt;Temperatures Tc and Tw are straightforward; Prentice et al. use the monthly average here, which I have readily available from my simulations.&lt;/p&gt;
&lt;p&gt;Growing degree-days are also easy to compute, but might warrant some explanation. The idea is that plants do not grow below some particular temperature, here taken as 5 °C, and they grow faster the higher the temperature goes. So to approximate the total amount of growth in a year, for each day we take the number of degrees above the 5 °C threshold, and add those up across the days in a year. For example, a 21 °C day is good for 21 - 5 = 16 growing degree-days, and a 2 °C day doesn&amp;rsquo;t contribute any growing degree-days at all. Some plants can start growing above 0 °C already, which is what the GDD₀ column is for.&lt;/p&gt;
&lt;p&gt;The Priestley-Taylor coefficient α is a bit more complicated to determine. Prentice et al. describe an iterative algorithm that models moisture storage in the soil, but I used a simpler approach: &lt;code&gt;α = precipitation / PET&lt;/code&gt;, where PET is the potential evapotransipiration computed using the outdated, but simple, Thornthwaite equation. I compute α separately for each month (assuming 10 in the case where PET is zero, because this empirically gave better results than 1), then take the average over the year.&lt;/p&gt;
&lt;p&gt;Finally, there&amp;rsquo;s the column &amp;ldquo;D&amp;rdquo;, the dominance class. This is needed because, even if a plant type could technically grow in a particular climate, other plants are better adapted and will outcompete it. This is why, for example, we won&amp;rsquo;t see any conifers in tropical rainforest. The rule is: if &lt;em&gt;any&lt;/em&gt; plant type with a particular dominance class can grow in a particular climate, all other types with higher-numbered dominance classes are excluded. Plants with the same dominance class can coexist though! This model is simple, straightforward, wrong, and oh so useful to me because it&amp;rsquo;s easy to implement and efficient to evaluate.&lt;/p&gt;
&lt;p&gt;With the list of plant types in hand, the BIOME1 model also specifies how to map these to biomes:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/table5.png" alt="A table mapping sets of plant types to biome names" &gt;
&lt;/p&gt;
&lt;p&gt;So the biomes are computed from the plants that can grow, rather than the other way round. Since I only want to plop down some plants, you might wonder why I&amp;rsquo;d bother with biomes at all. There are two reasons: one, I&amp;rsquo;d like to plot these on a map so I can compare it to the paper; and two, I want to assign different colours and textures to the base terrain depending on the biome.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the output computed by Prentice et al. themselves:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/BIOME1_predicted.jpg" alt="Map from Prentice et al. paper" &gt;
&lt;/p&gt;
&lt;p&gt;And, using the same colour scheme, here&amp;rsquo;s mine:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/biomes.png" alt="Map of the Earth showing biomes according to Prentice et al." &gt;
&lt;/p&gt;
&lt;p&gt;Again, it&amp;rsquo;s far from perfect, but this is clearly because of my simplistic climate model and not because of my implementation of the BIOME1 model. Most importantly, all classes &lt;em&gt;are&lt;/em&gt; represented here.&lt;/p&gt;
&lt;p&gt;Now, not to repeat my previous mistake with the Köppen model, I checked beforehand whether I could find a list of specific plant species for each of these biomes. That more or less worked by patching together various sources, and validating using the amazing &lt;a href="https://mapoflife.ai/dashboard/species"&gt;Map of Life&lt;/a&gt; whether their occurrence actually corresponds to that biome. In the end, I have a list of 2-3 distinct and characteristic species for each plant type, and since most biomes contain more than one plant type, this should give a decent impression of biodiversity on my world.&lt;/p&gt;
&lt;h2 id="colouring-terrain"&gt;Colouring terrain&lt;/h2&gt;
&lt;p&gt;Actually modelling, placing and rendering those plants is for a future blog post, because this one is getting plenty long already. So I&amp;rsquo;ll tackle the terrain colours first. I manually assigned each biome a plausible colour, and then mapped it to the terrain using the above biome map:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/square_biomes_terrain.jpg" alt="The same mountainous island as before, but now with coloured terrain" &gt;
&lt;/p&gt;
&lt;p&gt;It works exactly as designed! We&amp;rsquo;re looking at cool mixed forest (yellowish green), cool conifer forest (blueish green), tundra (yellowish) and ice/polar desert (white) here. This makes sense, because this island is situated at 56° north, and should have a climate comparable to the Hebrides off the coast of Scotland.&lt;/p&gt;
&lt;p&gt;But maybe, just maybe, real biomes don&amp;rsquo;t have such straight and hard borders? Actual biomes blend into each other. Interpolating between biomes is tricky though, because they are discrete classes. I could instead interpolate the output colours, but I think it would look more natural to add some domain warping using simplex noise instead:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/warped_biomes_terrain.jpg" alt="The same mountainous island as before, but now the biomes no longer have straight edges" &gt;
&lt;/p&gt;
&lt;p&gt;Better. Never mind that the snow line no longer corresponds properly to height; snow is something I&amp;rsquo;ll tackle later.&lt;/p&gt;
&lt;p&gt;One cheap trick to make every terrain instantly look much more varied is to have nothing grow on steep slopes, so let&amp;rsquo;s expose grey rock underneath:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/steep_slopes_terrain.jpg" alt="The same island again, but now steeper sections are coloured grey again" &gt;
&lt;/p&gt;
&lt;p&gt;Finally, terrain near the waterline will not be overgrown, or at least not by the same type of plants, because of tides and spray from the salty sea. So let&amp;rsquo;s expose the bare rock there too:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/26/around-the-world-26-biomes/bare_coastlines_terrain.jpg" alt="The same island yet again, but now coastlines are grey too" &gt;
&lt;/p&gt;
&lt;p&gt;The effect is subtle from this perspective, but it helps ground the terrain much better, and also makes the underwater bits less unnaturally green. (Underwater biomes and vegetation is something that should be handled separately, if needed.)&lt;/p&gt;
&lt;p&gt;Notice how there&amp;rsquo;s not a single texture in sight. I&amp;rsquo;m not great at drawing textures, and I hate UV unwrapping, so I&amp;rsquo;m trying to get away with a mostly texture-free art style in this game. We&amp;rsquo;ll see more of that style when I finish modelling my arboretum.&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 25: Placing ports</title><link>https://frozenfractal.com/blog/2025/9/21/around-the-world-25-placing-ports/</link><pubDate>Sun, 21 Sep 2025 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2025/9/21/around-the-world-25-placing-ports/</guid><description>&lt;p&gt;In the &lt;a href="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/"&gt;previous post&lt;/a&gt;, I promised to start placing some ports in our procedurally generated world. That turned out to be a bit more work than I expected.&lt;/p&gt;
&lt;p&gt;First off, what are our requirements?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ports must be on a coast.&lt;/li&gt;
&lt;li&gt;Two ports must not be too close to each other.&lt;/li&gt;
&lt;li&gt;Ports must be distributed around the world, according to some distribution that I can configure.&lt;/li&gt;
&lt;li&gt;Ports must be reachable by sea (do not put them on the shore of a lake).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As with terrain generation, my approach has two &amp;ldquo;levels&amp;rdquo;: the global and the local. The code decides the location of all ports in the world on the global map first, so that we can later create quests that involve ports that are very far away.&lt;/p&gt;
&lt;p&gt;(By the way, right now I&amp;rsquo;m talking only about ports, but besides harbour towns there could also be other points-of-interest on the coast, such as shipwrecks and buried treasure. Those have the same requirements, so they will be a straightforward extension later.)&lt;/p&gt;
&lt;h2 id="global-placement"&gt;Global placement&lt;/h2&gt;
&lt;p&gt;Recall that we previously figured out where the coasts are in our global map. We know the height of the terrain at each grid point, which can be either above or below sea level. We say that a grid cell is &amp;ldquo;coast&amp;rdquo; if some of its corners are above, and some are below sea level, coloured light blue here:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/21/around-the-world-25-placing-ports/cell_types.png" alt="A map showing orange blobs inside a dark blue area, with narrow areas of light blue in between" &gt;
&lt;/p&gt;
&lt;p&gt;Those cells will be candidates for containing a port. (Lakes have already been detected and filtered out at this point; they are classified as land instead.)&lt;/p&gt;
&lt;p&gt;To guarantee an even distribution of ports around the world, you might think that we could just put all coast cells in a list, and randomly pick elements from that list. However, that would only guarantee even distribution &lt;em&gt;along coastlines&lt;/em&gt;, not spatially. If we have a very wrinkly coastline (think fjords), it will contain a much higher density of ports than straight coastlines!&lt;/p&gt;
&lt;p&gt;The solution I came up with is to divide the global map up into grid squares:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/21/around-the-world-25-placing-ports/port_grid.jpg" alt="A global height map with a grid overlay" &gt;
&lt;/p&gt;
&lt;p&gt;Each of these squares will contain at most one port. For each square, we first check if it contains any coast at all; if not, we skip this square. If it does contain some coast cells, we pick the most &amp;ldquo;desirable&amp;rdquo; one; desirability is measured as the number of land cells within some radius of the port. This promotes spawning of ports at the end of sheltered bays, and discourages spawning of ports on tiny islands. The most desirable coast cell becomes the candidate port for that grid square.&lt;/p&gt;
&lt;p&gt;Next, all candidates are assigned a sample weight according to their latitude, fed through a curve that I can specify:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/21/around-the-world-25-placing-ports/port_distribution_curve.png" alt="Plot of a curve starting at 0.5, going up to 1.0, then gradually sloping downwards to 0.0" &gt;
&lt;/p&gt;
&lt;p&gt;So ports at the equator have a weight of 0.5, around 30° latitude the weight goes up to 1.0, sloping all the way down to 0.0 in the polar regions. The shape of the curve is my best guess at realism, considering habitability, but I might well need to tweak it for gameplay balancing purposes later. We then select a fixed number of candidates according to this weight:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/21/around-the-world-25-placing-ports/global_ports.png" alt="Orange and blue map, showing 200 tiny crosses where orange meets blue" &gt;
&lt;/p&gt;
&lt;p&gt;(The icons are very small; zoom in to see them.)&lt;/p&gt;
&lt;p&gt;For each of these ports, we also randomly select a population, also according to some distribution. I think I&amp;rsquo;ll later replace this by discrete size classes (hamlet, village, town, city, metropolis) but this&amp;rsquo;ll do for now.&lt;/p&gt;
&lt;p&gt;Smooth sailing so far!&lt;/p&gt;
&lt;h2 id="local-placement"&gt;Local placement&lt;/h2&gt;
&lt;p&gt;In the &lt;a href="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/"&gt;previous post&lt;/a&gt;, I was very happy because I&amp;rsquo;d found a solution to the port placement problem, by using a modified diamond-square algorithm. In short, if all four corners of a diamond or square are below sea level, the center is assigned a value below sea level as well, and vice versa.&lt;/p&gt;
&lt;p&gt;This works perfectly to preserve the &amp;ldquo;nature&amp;rdquo; of each cell in the global map: land, sea or coast. However, it does &lt;em&gt;not&lt;/em&gt; guarantee that all coast is reachable – I encountered fun situations like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/21/around-the-world-25-placing-ports/lakeside_port.jpg" alt="A port on a lake" &gt;
&lt;/p&gt;
&lt;p&gt;The red and green lines here mark out the edges of global map cells, whose corners have their heights taken from the global height map; everything in between is generated using diamond-square. As you can see, the cell containing (most of) the port has one corner that&amp;rsquo;s &lt;em&gt;ever so slightly&lt;/em&gt; below water, so this cell is a coast cell, and the algorithm correctly decided to spawn a port there.&lt;/p&gt;
&lt;p&gt;However, the cells connecting it to the sea are &lt;em&gt;also&lt;/em&gt; coast cells, so there are no constraints on the heights inside those cells, apart from the corners. In this case that terrain is fairly high, creating a tiny lake completely surrounded by hills; not a great spawn point for our intrepid fleet of player ships.&lt;/p&gt;
&lt;p&gt;At first I thought I could solve this by forcibly lowering the center of the port&amp;rsquo;s cell, pulling the neighbouring cells down along with it. But no:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/21/around-the-world-25-placing-ports/lakesside_port.jpg" alt="A port on two lakes" &gt;
&lt;/p&gt;
&lt;p&gt;This created a second tiny lake, but diamond-square still decided to block off the route to the sea.&lt;/p&gt;
&lt;p&gt;A second attempt had me force the height of the entire &lt;em&gt;quadrant&lt;/em&gt; connected to the sea corner, and you can guess how that worked out:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/21/around-the-world-25-placing-ports/square_lake_port.jpg" alt="A port on a square lake" &gt;
&lt;/p&gt;
&lt;p&gt;This problem took me way too long to figure out, but once I did, the solution seems obvious: we need to enforce connectivity between grid points. To be precise: if two adjacent corners of a cell are both below sea level, the local terrain generator &lt;em&gt;must&lt;/em&gt; ensure that there is a path by sea between the two. So I ended up forcing the terrain to be sea along entire edges, and that works great:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/21/around-the-world-25-placing-ports/coastal_port.jpg" alt="A port that&amp;rsquo;s now finally on the coast" &gt;
&lt;/p&gt;
&lt;p&gt;(For now, I&amp;rsquo;m ignoring both the width and the depth of this forced &amp;ldquo;channel&amp;rdquo;. The depth does not currently matter to gameplay; any place where the terrain is below sea level is accessible. The width can easily be extended by also forcing a minimum depth on cells adjacent to the edge.)&lt;/p&gt;
&lt;p&gt;Now that I had this functionality of forcing minimum and maximum height of particular points, I also used it to force the height of the &lt;em&gt;middle&lt;/em&gt; point to be at least zero. Usually this will cause some of the adjacent terrain to be above zero, creating some land for the port buildings to spawn on. The port is then always spawned right in the middle of the cell, without any local adjustments needed. Much simpler!&lt;/p&gt;
&lt;p&gt;The player&amp;rsquo;s ship will also need to be spawned near one of these ports. I previously had a complex dance at game startup to figure out where to spawn it, because even corner points wouldn&amp;rsquo;t necessarily be connected to open sea, but we couldn&amp;rsquo;t know until we started generating local terrain around the player, which we couldn&amp;rsquo;t do until we had placed the player. Now that chicken-and-egg problem is gone too: we simply pick a corner of the port&amp;rsquo;s cell that is below sea level, and the local terrain generator will guarantee a path from there to open sea.&lt;/p&gt;
&lt;h2 id="spawning-buildings"&gt;Spawning buildings&lt;/h2&gt;
&lt;p&gt;You&amp;rsquo;ve already seen how the ports currently look, using this &lt;del&gt;placeholder&lt;/del&gt; &lt;em&gt;absolutely stunning&lt;/em&gt; model of a building I created in Blender:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/9/21/around-the-world-25-placing-ports/building.png" alt="A grey building" &gt;
&lt;/p&gt;
&lt;p&gt;The radius in which buildings are spawned depends on the port&amp;rsquo;s population. Right now, the algorithm simply picks a uniformly random angle and distance from the center, and plops a building down at that point. This causes the building density in the centre to be greater, which is actually a feature – but really, this entire algorithm is a placeholder as well, until I have time to come up with something better.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s it for port placement… for now. I&amp;rsquo;m sure I&amp;rsquo;ll revisit this topic in the future, because there are more things I want to implement: a fair spatial distribution of small vs. large ports (larger ones having more services), a local language and culture, proper building placement in sensibly-looking districts, influence on the local vegetation, and maybe even non-placeholder building models as well. For now though, I &lt;em&gt;should&lt;/em&gt; be turning to gameplay, but I &lt;em&gt;might&lt;/em&gt; actually tackle some eyecandy first. We&amp;rsquo;ll see.&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 24: Local terrain</title><link>https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/</link><pubDate>Mon, 23 Jun 2025 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/</guid><description>&lt;p&gt;Now that we have a plausible looking height map for the entire world, it&amp;rsquo;s time to zoom in and see what the terrain looks like up close.&lt;/p&gt;
&lt;p&gt;I did this &lt;a href="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/"&gt;before&lt;/a&gt;, when I was still working on a sphere. I started out this time using the same approach, but the implementation I ended up with this time is quite different.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s start by simply interpolating the global height map using bilinear interpolation:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/base_height.jpg" alt="A grey terrain that clearly consists of square tiles" &gt;
&lt;/p&gt;
&lt;p&gt;The overall shape is plausible, but the terrain is way too smooth and obviously made of squares. I could fix the squares by using a more advanced interpolation method, but that would only make the smoothness worse. So I turned to my usual solution…&lt;/p&gt;
&lt;h2 id="more-noise"&gt;More noise&lt;/h2&gt;
&lt;p&gt;Adding some octaves of simplex noise gives a much better result:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/simplex_noise.jpg" alt="A grey terrain with simplex noise applied to it" &gt;
&lt;/p&gt;
&lt;p&gt;This is the minimum amount of noise necessary to hide the sharp creases at the cell edges. However, there is a problem. Consider this much flatter terrain:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/base_height_on_flats.jpg" alt="A grey terrain with no perceptible height difference" &gt;
&lt;/p&gt;
&lt;p&gt;What happens if we add simplex noise to this?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/simplex_noise_on_flats.jpg" alt="A grey terrain consisting of a large number of small islands" &gt;
&lt;/p&gt;
&lt;p&gt;Whoops! That doesn&amp;rsquo;t look realistic, and it&amp;rsquo;s also not going to be fun to navigate. The problem is that the simplex noise is now too strong, compared to the low height of the terrain.&lt;/p&gt;
&lt;p&gt;I tried several different approaches to solve this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Compute the slope of the global height map, and using that to modulate the amplitude of the local noise. Steeper regions get more noise, flatter regions get less. It worked, to some extent, but the effect was hard to control.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If the input height before noise is close to zero, modulate the noise to be close to zero as well. This had the unwanted effect of keeping the coastlines exactly straight, as in the situation without any noise.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Compute the distance to the nearest coastline, then modulate the noise based on that. The problem with that is, that the nearest coastline could be far away in some ungenerated chunk, and we&amp;rsquo;d still end up creating tons of islands in shallow water.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So none of these approaches was satisfactory.&lt;/p&gt;
&lt;h2 id="prelude-to-ports"&gt;Prelude to ports&lt;/h2&gt;
&lt;p&gt;The worst problem is that the simplex noise would arbitrarily move, add or remove coastlines. This is going to be a problem once we start placing port cities for the player to visit. In the global map, we classify each cell as &lt;code&gt;land&lt;/code&gt;, &lt;code&gt;sea&lt;/code&gt; or &lt;code&gt;coast&lt;/code&gt; based on the terrain height at the four corners:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If all corners are above sea level, the cell is &lt;code&gt;land&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If all corners are below sea level, the cell is &lt;code&gt;sea&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If some corners are above sea level and some are below, the cell is &lt;code&gt;coast&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using orange for &lt;code&gt;land&lt;/code&gt;, dark blue for &lt;code&gt;sea&lt;/code&gt; and light blue for &lt;code&gt;coast&lt;/code&gt;, the map around the above area looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/cell_types.png" alt="Classification of cell types into land, sea and coast" &gt;
&lt;/p&gt;
&lt;p&gt;Ports will be placed on this map only in &lt;code&gt;coast&lt;/code&gt; grid cells, which should make them accessible from the sea. However, if simplex noise raises that cell fully above sea level or lowers it fully below, where will the port go? Or what if the noise creates a land barrier in the sea surrounding the port, making it inaccessible?&lt;/p&gt;
&lt;p&gt;I initially tried to work around this by generating some extra local terrain around the current chunk, and creating the port in a suitable location once all surrounding local terrain was available. My use of the &lt;a href="https://runevision.github.io/LayerProcGen/"&gt;LayerProcGen&lt;/a&gt; algorithm made this pretty easy! Again it worked, to some extent, but it remained impossible to give any hard guarantees. On sufficiently tricky terrain, there would still be cases where no coastline could be found for the port to be placed, and it would end up landlocked or floating. (A city on water could still be a cool feature every once in a while; consider Venice. But a landlocked city being picked as a quest destination would be distinctly uncool.)&lt;/p&gt;
&lt;p&gt;What I really needed was a way to &lt;em&gt;guarantee&lt;/em&gt; that a &lt;code&gt;coast&lt;/code&gt; cell in the global map would remain a &lt;code&gt;coast&lt;/code&gt; cell in the local map, with a coastline bordering on the sea. It took me &lt;em&gt;way&lt;/em&gt; too long to realize that this would happen automatically if I kept the height of all four corners the same as in the global map, and only filled in the intermediate terrain. Ideally, &lt;code&gt;land&lt;/code&gt; should also remain above sea level and &lt;code&gt;sea&lt;/code&gt; should remain below. And I know just the algorithm for that! It&amp;rsquo;s fast, flexible, simple to implement, and predates simplex noise by about two decades! Enter…&lt;/p&gt;
&lt;h2 id="diamond-square"&gt;Diamond-square&lt;/h2&gt;
&lt;p&gt;The diamond-square algorithm is explained in many places around the web, so I&amp;rsquo;ll only give a brief summary.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We start with an array (height map) in which only the corner points have a known height. These points form a single square.&lt;/li&gt;
&lt;li&gt;For each square, fill in the center value as the average of its four corner values, plus some random value. This produces diamonds, and is therefore called the &amp;ldquo;diamond step&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;For each diamond, fill in the center value as the average of its four corner values, plus some random value. This produces squares again, and is therefore called the &amp;ldquo;square step&amp;rdquo;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;An illustration should make this clearer:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/Diamond_Square.svg" alt="The diamond-square algorithm illustrated" &gt;
&lt;br&gt;
&lt;em&gt;Source: Christopher Ewin via &lt;a href="https://commons.wikimedia.org/wiki/File:Diamond_Square.svg"&gt;Wikimedia Commons&lt;/a&gt;, CC-BY-SA 4.0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;(I think the naming is confusing. The step operating on squares should be called the &amp;ldquo;square step&amp;rdquo;, and the step operating on diamonds should be called the &amp;ldquo;diamond step&amp;rdquo;. Oh well.)&lt;/p&gt;
&lt;p&gt;On the edges, we only get half diamonds (i.e. triangles); the fourth corner is outside of the initial square. We can deal with those using LayerProcGen again: generate the neighbouring chunks as well, then read from those.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s see how this looks when applied to our steep coast:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/diamond_square_fixed_noise_range.jpg" alt="A grey terrain with diamond-square applied to it" &gt;
&lt;/p&gt;
&lt;p&gt;Okay, it&amp;rsquo;s rough now, and the creases are gone. What happens to the flatter coast?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/diamond_square_fixed_noise_range_on_flats.jpg" alt="A much flatter grey terrain with lots of tiny islands" &gt;
&lt;/p&gt;
&lt;p&gt;Well, it&amp;rsquo;s rough now too — but still a mess of islands, no better than our previous approach! What gives?&lt;/p&gt;
&lt;h2 id="random-tweaking"&gt;Random tweaking&lt;/h2&gt;
&lt;p&gt;The key is in the part &amp;ldquo;some random value&amp;rdquo; that I handwaved in the algorithm description. The terrains above were produced with uniformly random values between -100 and +100 meters, which is halved on each iteration of the algorithm. (It&amp;rsquo;s actually multiplied by √½ after the diamond step, and again by √½ after the square step.) This means the amplitude of the noise is the same on rough terrain as on smooth terrain, so we haven&amp;rsquo;t really gained much yet, compared to simplex noise.&lt;/p&gt;
&lt;p&gt;However, we now have a convenient place to tweak the randomness &lt;em&gt;dependent on the input&lt;/em&gt;. For instance, we could pick a random value uniformly between the minimum and the maximum of the height at the four surrounding points. This way, we could never get any local maxima or minima where previously there were none.&lt;/p&gt;
&lt;p&gt;In my case, I chose to compute the standard deviation of the surrounding four heights, and express the randomization strength relative to that standard deviation. For example, if the four corners are at heights 1.0, 2.0, 3.0 and 4.0, then the average is 2.5 and the (biased) standard deviation is about 1.12. Setting the strength factor to 1.0 would result in a center point between 1.38 and 3.62, which is safely in between the minimum and maximum. If the surrounding corners are at heights 1.0, 1.0, 1.0, 4.0 instead, then the central point would be between 0.45 and 3.05; you can see it shifts down along with the average, and it &lt;em&gt;can&lt;/em&gt; go below the minimum and above the maximum.&lt;/p&gt;
&lt;p&gt;How does this look when applied to the previous terrain?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/diamond_square_variable_noise_range.jpg" alt="A grey terrain with variance-dependent diamond square applied to it" &gt;
&lt;/p&gt;
&lt;p&gt;I think this already looks &lt;em&gt;far&lt;/em&gt; more interesting and realistic than the uniformly noisy previous attempt. The terrain is much more varied; there are clearly delineated steeper and flatter areas now. But the most important test of this algorithm: how does it fare on flat terrain?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/23/around-the-world-24-local-terrain/diamond_square_variable_noise_range_on_flats.jpg" alt="A much flatter grey terrain, this time without many islands" &gt;
&lt;/p&gt;
&lt;p&gt;Marvellous! Notice how we are getting only small bumps in the terrain now, and how the coastline has become more jagged and interesting, but the overall shape of the coastline has been preserved.&lt;/p&gt;
&lt;p&gt;There is one thing that still bothers me, though. All &lt;code&gt;coast&lt;/code&gt; cells are guaranteed to have some combination of land and water, but &lt;code&gt;land&lt;/code&gt; cells aren&amp;rsquo;t guaranteed to contain only land, and &lt;code&gt;sea&lt;/code&gt; cells aren&amp;rsquo;t guaranteed to contain only sea. In some cases, this could still cause land to form in &lt;code&gt;sea&lt;/code&gt; cells off the coast, blocking access to the port. Ideally, I&amp;rsquo;d like to keep all terrain in &lt;code&gt;sea&lt;/code&gt; cells below sea level, and in all &lt;code&gt;land&lt;/code&gt; cells above sea level. Fortunately, that&amp;rsquo;s now very easy to do: if all four corners are below sea level, limit the range of allowed random values to be below sea level as well, and vice versa. It didn&amp;rsquo;t visibly change the test terrains we were looking at, but I&amp;rsquo;ll trust that it does the right thing.&lt;/p&gt;
&lt;p&gt;All this together gives me an ironclad guarantee that coast cells will always contain a suitable spot to place a port, and the port will always be accessible from the sea!&lt;/p&gt;
&lt;h2 id="future-work"&gt;Future work&lt;/h2&gt;
&lt;p&gt;Instead of a uniformly random height change, it would be interesting to experiment with other random distributions, such as the normal (Gaussian) distribution. I&amp;rsquo;m putting that on the backlog because it&amp;rsquo;s not a priority right now, and easy to change in isolation later.&lt;/p&gt;
&lt;p&gt;Since I have an &lt;a href="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/"&gt;erosion algorithm&lt;/a&gt;, it would be cool to apply that after diamond-square, to make the result more believable. I have actually gotten that to work, and it looks a &lt;em&gt;bit&lt;/em&gt; better. The trouble is that erosion messes up all guarantees about coast/land/sea cells again. I might be able to fix that by locally raising or lowering the terrain after erosion, to restore those guarantees… but that&amp;rsquo;s something to tinker with later. For now, I&amp;rsquo;m keeping erosion turned off.&lt;/p&gt;
&lt;p&gt;For the next post, after all this talk about preparing the terrain for placing ports… let&amp;rsquo;s place some ports!&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 23: Hydraulic erosion</title><link>https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/</link><pubDate>Fri, 06 Jun 2025 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/</guid><description>&lt;p&gt;As I mentioned &lt;a href="https://frozenfractal.com/blog/2025/5/12/around-the-world-22-dropping-the-ball/"&gt;last time&lt;/a&gt;, I&amp;rsquo;m currently working on a full rewrite of the game, with a focus on building a solid technical foundation first. But because much of that is boring work, I allowed myself a fun side quest: hydraulic erosion.&lt;/p&gt;
&lt;p&gt;In nature, &lt;em&gt;erosion&lt;/em&gt; is a hugely important factor in shaping landscapes. And arguably the most significant form of erosion is &lt;em&gt;hydraulic erosion&lt;/em&gt;, that is, erosion by flowing water. It results in typical branching folds in the landscape:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/spoil_tip.jpg" alt="Spoil tip at Jägersfreude, Saarbrücken" &gt;
&lt;br&gt;
&lt;em&gt;Credit: OutcropWizard via &lt;a href="https://commons.wikimedia.org/wiki/File:Halde_J%C3%A4gersfreude.jpg"&gt;Wikimedia Commons&lt;/a&gt;, CC-BY-SA 4.0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Just so we&amp;rsquo;re on the same page, I&amp;rsquo;ll first describe my understanding of the hydraulic erosion process. Rain falls on the ground and flows downhill. It combines to form streams, which merge to form rivers, which merge into bigger rivers, until the water eventually flows into the sea. Along the way, it wears down the terrain that it flows over, lowering it. These loose rocks, pebbles and grains of sand, collectively known as &lt;em&gt;sediment&lt;/em&gt; are carried downstream. Where the terrain flattens out, the water flows less quickly, and the carried sediment settles down on the bottom, increasing the height of the terrain there.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s difficult to recreate the effects of hydraulic erosion directly through equations, although &lt;a href="https://www.shadertoy.com/view/7ljcRW"&gt;this amazing Shadertoy by Felix Westin&lt;/a&gt; does a very impressive job, at the expense of being pretty complicated. Instead, most people resort to some kind of simulation of the erosion process over multiple time steps, simulating water flow, sediment pickup and deposition, and evaporation. Those simulations can get pretty complex and time consuming, so I thought: maybe there&amp;rsquo;s a simpler way?&lt;/p&gt;
&lt;h2 id="attempt-1-blurosion"&gt;Attempt 1: blurosion&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s always worth starting out with &amp;ldquo;the simplest thing that could possibly work&amp;rdquo;. Because it&amp;rsquo;s simple, it doesn&amp;rsquo;t take much time to implement, and it may be good enough. Even if it isn&amp;rsquo;t, it&amp;rsquo;ll teach you something valuable.&lt;/p&gt;
&lt;p&gt;With that in mind, I tried the following idea. The steeper the terrain is, the faster the water will flow, and thus the more sediment it will carry downhill. So here&amp;rsquo;s the simplest possible erosion algorithm I could think of: for each grid cell, find its lowest neighbour, and compute the difference in height. Reduce my own height by some fixed fraction of that difference (erosion), and increase the neighbour&amp;rsquo;s height by the same amount (deposition). Repeat this a couple of times.&lt;/p&gt;
&lt;p&gt;For example, suppose the current cell&amp;rsquo;s height is 12, its lowest neighbour has height 4, and we&amp;rsquo;ve configured the erosion strength at 25%. The height difference is 8, so we&amp;rsquo;ll want to move 25% × 8 = 2 units of material. The current cell&amp;rsquo;s height then becomes 12 - 2 = 10, and the neighbour&amp;rsquo;s height is increased to 4 + 2 = 6.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s try it on this height map, which is mostly just simplex noise with some tectonic boundary effects on top:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/start.png" alt="Input height map" &gt;
&lt;/p&gt;
&lt;p&gt;And here&amp;rsquo;s the result after some iterations of the above algorithm:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/blurosion.png" alt="Blurred height map" &gt;
&lt;/p&gt;
&lt;p&gt;It looks like I just invented a rather slow blur algorithm instead. Not quite what I&amp;rsquo;d hoped for. In fact, it&amp;rsquo;s very similar to the &lt;a href="https://frozenfractal.com/blog/2023/11/20/around-the-world-3-hotspots-erosion/"&gt;thermal erosion algorithm&lt;/a&gt; I described over two years ago and apparently forgot about!&lt;/p&gt;
&lt;p&gt;But why didn&amp;rsquo;t it produce these typical ridges and valleys? Here comes the lesson: hydraulic erosion is, in some sense, self-reinforcing. If by random chance a slight valley forms, it&amp;rsquo;ll collect slightly more water from its surroundings, causing it to erode more quickly, and becoming more pronounced, which will cause it to collect even more water, and so on. In technical terms, the amount of erosion that happens to a point is strongly dependent on the &lt;em&gt;drainage area&lt;/em&gt; of that point, which is the total area that&amp;rsquo;s upstream from that point. My simplistic idea did not capture that.&lt;/p&gt;
&lt;h2 id="attempt-2-simulation"&gt;Attempt 2: simulation&lt;/h2&gt;
&lt;p&gt;Looking at the literature, I quickly found a well-known paper by Xing Mei et al., &lt;a href="https://hal.science/inria-00402079"&gt;Fast Hydraulic Erosion Simulation and Visualization on GPU&lt;/a&gt;. I&amp;rsquo;m not running my algorithms on the GPU at the moment, but that doesn&amp;rsquo;t matter; the algorithm works equally well on a CPU and can easily be parallelized.&lt;/p&gt;
&lt;p&gt;Mei models water flow as a column of water on top of each terrain cell, connected to its neighbours by virtual pipes. Water flow through these pipes is simulated, which results in a flow velocity field. The sediment transport capacity is then calculated for each cell, depending on the slope and velocity. Sediment is then picked up or deposited to bring the amount of dissolved sediment towards this target capacity. Finally, sediment is moved (advected) through the flow velocity field.&lt;/p&gt;
&lt;p&gt;The images in the paper aren&amp;rsquo;t very impressive, which may in part be due to limitations of the algorithm. Several people have taken the ideas and built upon them, for example Dylan Mcleod et al. in &lt;a href="https://huw-man.github.io/Interactive-Erosion-Simulator-on-GPU/"&gt;Interactive Hydraulic Erosion Simulator&lt;/a&gt; and Lan Lou in &lt;a href="https://github.com/LanLou123/Webgl-Erosion"&gt;Terrain erosion sandbox in WebGL&lt;/a&gt;. Those people &lt;em&gt;do&lt;/em&gt; get good results, so I knew it was possible.&lt;/p&gt;
&lt;p&gt;However, I failed to get good output from Mei&amp;rsquo;s algorithm. I&amp;rsquo;m not sure why – maybe my implementation had bugs, maybe the combination of (about ten) parameters was wrong, but I always got weird artifacts at best, and numerical instability at worst. Here&amp;rsquo;s the best result I managed to get:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/mei.png" alt="Output of my implementation of Mei et al." &gt;
&lt;/p&gt;
&lt;p&gt;It does show branching patterns, but they have a strong tendency to be strictly horizontal or vertical, which gives a very artificial look. You may need to zoom in to see it.&lt;/p&gt;
&lt;p&gt;There may have been ways to fix this, but the algorithm was already complex enough. So I started shopping around for something less complicated, but still sophisticated enough to do what I wanted.&lt;/p&gt;
&lt;h2 id="attempt-3-stream-power-law"&gt;Attempt 3: stream power law&lt;/h2&gt;
&lt;p&gt;While stumbling around, I found the blog post &lt;a href="https://davidar.io/post/sim-glsl"&gt;Simulating worlds on the GPU&lt;/a&gt; by David A Roberts. In it, he mentions the &lt;em&gt;stream power law&lt;/em&gt;, which is a very simple empirical equation that models how quickly terrain erodes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;E = K * A^m * S^n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;E&lt;/code&gt; is the rate of erosion,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A&lt;/code&gt; is the drainage area,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;S&lt;/code&gt; is the slope,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;K&lt;/code&gt;, &lt;code&gt;m&lt;/code&gt; and &lt;code&gt;n&lt;/code&gt; are constants.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A simple equation just two inputs, drainage area and slope, and only three parameters to tune (maybe just two because &lt;code&gt;n&lt;/code&gt; should be about twice as large as &lt;code&gt;m&lt;/code&gt;) – but it still involves that all-important factor, the drainage area. Did I hit gold?&lt;/p&gt;
&lt;p&gt;Slope is easy enough to compute; it&amp;rsquo;s just the length of the gradient vector. However, how do we find the drainage area? This is where I found another paper, by Schott et al. (2002), &lt;a href="https://hal.science/hal-04361019"&gt;Large-scale terrain authoring through interactive erosion simulation&lt;/a&gt;. They do a lot of work that I&amp;rsquo;m not interested in here, but also happen to present an algorithm to approximate the drainage area iteratively. It&amp;rsquo;s rather simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set the drainage area of each cell to 1.&lt;/li&gt;
&lt;li&gt;Repeat for some number of iterations:
&lt;ul&gt;
&lt;li&gt;Set the drainage area of each cell to 1 (the cell itself) plus the drainage area of all neighbouring cells that drain into the current cell (i.e. that have the current cell as their lowest neighbour).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This will eventually converge to the true drainage area, but we can stop it sooner if we want.&lt;/p&gt;
&lt;p&gt;A complication is that the drainage area depends on the shape of the terrain, and erosion will change that shape. So I decided to iteratively erode the terrain based on the stream power law in the same loop that also updates the drainage area.&lt;/p&gt;
&lt;p&gt;Does it work?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/stream_power_law.png" alt="Terrain eroded using stream power law" &gt;
&lt;/p&gt;
&lt;p&gt;It still has directional (horizontal/vertical) artifacts, but it&amp;rsquo;s getting somewhat acceptable.&lt;/p&gt;
&lt;p&gt;However…&lt;/p&gt;
&lt;p&gt;As you can see, the algorithm produces a large number of inland lakes below sea level. There are also many higher-up regions that are completely surrounded by higher terrain. These are known as &lt;em&gt;endorheic basins&lt;/em&gt;, and are quite rare in nature. The way I understand it, there are two reasons for this. Firstly, they would fill up with water, which carries sediment, eventually raising the basin floor until the point where it is no longer a basin. And secondly, because the water eventually overflows the edge of the basin, it will start carving out a path for the basin to drain into, turning it into a regular lake with an outflow, and eventually maybe just a riverbed.&lt;/p&gt;
&lt;p&gt;It makes sense that the algorithm produces basins, because they have a large drainage area, causing their bottom to sink. What the algorithm fails to capture, though, is that the stream power law only applies to &lt;em&gt;flowing&lt;/em&gt; water, and the water in a basin is standing still.&lt;/p&gt;
&lt;p&gt;I considered various approaches of detecting basins, and then either filling them or carving a way out, but decided that it would be too fiddly to get right, and possibly also too slow. Time for another tack.&lt;/p&gt;
&lt;h2 id="interlude-a-better-starting-point"&gt;Interlude: a better starting point&lt;/h2&gt;
&lt;p&gt;I decided to turn off the hydraulic erosion algorithm for the moment, and apply some long-wanted improvements to what I already had. This is basically just a rehash of &lt;a href="https://frozenfractal.com/blog/2023/11/2/around-the-world-1-continents/"&gt;previous stuff&lt;/a&gt; that I implemented in C# on a sphere, but now done in Rust on a plane.&lt;/p&gt;
&lt;p&gt;Firstly, we know that water runs downhill and eventually reaches the sea. So, &lt;em&gt;very&lt;/em&gt; broadly speaking, the closer to the coast a point is, the more water it receives, the more it erodes, and the lower it tends to be. So I computed the shortest distance to the coast at every point, mapped this through a configurable curve, and applied that to the terrain as a starting point:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/distance_transform.png" alt="Height computed by distance transform" &gt;
&lt;/p&gt;
&lt;p&gt;Of course, for various reasons, the coast isn&amp;rsquo;t equally steep everywhere. So let&amp;rsquo;s use some low-frequency simplex noise to modulate the distance by, before mapping it to a height:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/modulated_distance_transform.png" alt="Height computed by modulated distance transform" &gt;
&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s better. Now we have steep coasts, as well as flat regions that reach farther inland (hello Netherlands!).&lt;/p&gt;
&lt;p&gt;Next up was to improve the noise. What I had previously was just some octaves of simplex noise, which I&amp;rsquo;d hoped to make more interesting through the erosion algorithm:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/simplex.png" alt="Distance transform plus simplex noise" &gt;
&lt;/p&gt;
&lt;p&gt;(On top of the simplex noise, you&amp;rsquo;ll also notice some mountain ranges added by tectonic boundaries, which I &lt;a href="https://frozenfractal.com/blog/2023/11/13/around-the-world-2-plate-tectonics/"&gt;blogged about extensively&lt;/a&gt; before.)&lt;/p&gt;
&lt;p&gt;Inspired by the article on &lt;a href="https://iquilezles.org/articles/morenoise/"&gt;value noise derivatives&lt;/a&gt; by Inigo Quilez, I modified the noise according to a simple idea: mountains have steeper slopes, and also more rugged terrain. In terms of simplex noise octaves, this translates to: if the gradient of the noise is larger, subsequent smaller octaves of noise get bigger amplitude. I implemented a configurable curve so I can control this effect, which I call &amp;ldquo;slope dependent amplitude&amp;rdquo;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/slope_dependent_amplitude_curve.png" alt="Slope dependent amplitude curve" &gt;
&lt;/p&gt;
&lt;p&gt;With this curve applied, we can see that flatter regions become smoother, and steeper regions become rougher:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/slope_dependent_amplitude.png" alt="Slope dependent amplitude" &gt;
&lt;/p&gt;
&lt;p&gt;It makes the terrain much more varied and interesting.&lt;/p&gt;
&lt;p&gt;Because the slope is apparently not continuous, it also results in some weird artifacts in the form of straight lines… but I have an old trick up my sleeve: domain warping. If we modify the &lt;em&gt;input point&lt;/em&gt; of the noise by yet another layer of noise (or rather two, one for &lt;em&gt;x&lt;/em&gt; and one for &lt;em&gt;y&lt;/em&gt;), those straight lines get broken up nicely, and the overall terrain becomes even more interesting:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/domain_warping.png" alt="Domain warped noise effect" &gt;
&lt;/p&gt;
&lt;p&gt;This could &lt;em&gt;almost&lt;/em&gt; pass for something produced by erosion, without needing an actual erosion algorithm, but I wasn&amp;rsquo;t satisfied yet. There was one more idea I wanted to try.&lt;/p&gt;
&lt;h2 id="attempt-4-droplets"&gt;Attempt 4: droplets&lt;/h2&gt;
&lt;p&gt;So far, I&amp;rsquo;ve only talked about grid-based erosion algorithms. But there is another category, one that is at least as popular: particles. The idea is to simulate rainfall one &amp;ldquo;droplet&amp;rdquo; at a time, tracing its path downhill, picking up and depositing sediment as it moves over the terrain. Do that for enough random droplets, and you get quite a convincing landscape. For a more thorough introduction, check out the amazing video &lt;a href="https://www.youtube.com/watch?v=eaXk97ujbPQ"&gt;Coding Adventure: Hydraulic Erosion&lt;/a&gt; by Sebastian Lague. It&amp;rsquo;s so good I&amp;rsquo;ll just embed it here:&lt;/p&gt;
&lt;div class="youtubecontainer"&gt;
&lt;iframe class="youtube" width="640" height="360" src="https://www.youtube-nocookie.com/embed/eaXk97ujbPQ?rel=0" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;However, there is one major drawback to this approach: it&amp;rsquo;s relatively slow. To get some decent results, you need to simulate many thousands of droplets, each for several dozen time steps. Because each droplet affects the terrain in random places, it&amp;rsquo;s hard to parallelize this across multiple cores, and it&amp;rsquo;s also not very friendly to CPU caches.&lt;/p&gt;
&lt;p&gt;Another drawback is that a droplet can affect the terrain far away from where it&amp;rsquo;s initially spawned. This makes it difficult to apply erosion to individual terrain chunks, something I&amp;rsquo;ll want to do later.&lt;/p&gt;
&lt;p&gt;For these reasons, I initially didn&amp;rsquo;t consider using such a droplet algorithm. But maybe I could take some ideas from it, and from the grid-based methods, and come up with some hybrid approach that combines the best of both worlds? It turns out that I could.&lt;/p&gt;
&lt;p&gt;The idea is as follows. Instead of spawning one droplet at a time and simulating it, we&amp;rsquo;ll spawn all droplets simultaneously. And not just anywhere: we spawn exactly one droplet in each grid cell. And this remains the rule throughout the simulation; each grid cell contains exactly one droplet:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/droplet_grid.svg" alt="A grid of droplets" &gt;
&lt;/p&gt;
&lt;p&gt;This allows us to track the droplets&amp;rsquo; positions not in a flat, 1D array, but in a 2D array aligning exactly with the height map itself. For each droplet, we track:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the position as a floating-point number (rounding this down to the nearest integers gives the grid index)&lt;/li&gt;
&lt;li&gt;the speed at which it&amp;rsquo;s moving, starting out arbitrarily at 1 (notice that this isn&amp;rsquo;t really related to movement, and is more a measure of kinetic energy)&lt;/li&gt;
&lt;li&gt;the size (starting out at 1)&lt;/li&gt;
&lt;li&gt;the amount of sediment it&amp;rsquo;s carrying (starting out at 0)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next, we compute the terrain height at each droplet&amp;rsquo;s position using bilinear interpolation, as well as the gradient of the terrain. The gradient points uphill, so we want to move the droplet in the exact opposite direction. Regardless of its speed, we always move it over a distance of exactly 1 grid step. This guarantees that it&amp;rsquo;ll either remain in its current cell, or at worst in one of the 8 neighbouring cells:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/droplet_movement.svg" alt="Droplet movement" &gt;
&lt;/p&gt;
&lt;p&gt;We don&amp;rsquo;t yet move the droplet&amp;rsquo;s data into the new grid cell at this point!&lt;/p&gt;
&lt;p&gt;Next, we compute the terrain height at its &lt;em&gt;new&lt;/em&gt; position. Subtracting the previous height gives us a height &lt;em&gt;difference&lt;/em&gt;, i.e. how much the droplet has moved downwards (or, sometimes, upwards). We use this to calculate the droplet&amp;rsquo;s &lt;em&gt;sediment carrying capacity&lt;/em&gt;, which is the product of the height difference, speed, size and a configurable parameter, the sediment capacity factor.&lt;/p&gt;
&lt;p&gt;If the droplet can carry more sediment than it currently does, it picks some up from its previous location, lowering the terrain there, but never by so much that it would turn a downhill slope into an uphill one – erosion never does that. Conversely, if it carries more sediment than it can, it deposits some at its previous location, again never turning an uphill slope into a downhill one. The height change is applied to the terrain through bilinear interpolation, depending on the droplet&amp;rsquo;s position within its cell.&lt;/p&gt;
&lt;p&gt;Next, we update the droplet&amp;rsquo;s speed depending on how far it moved downhill, and we evaporate some of its water (multiply its size by something like 0.95).&lt;/p&gt;
&lt;p&gt;Now comes the secret sauce! We have updated each droplet&amp;rsquo;s position, but this means that some droplets (most, in fact) will have left the grid cell in which their data is stored:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/droplet_offgrid.svg" alt="Droplets moving out of their grid cell" &gt;
&lt;/p&gt;
&lt;p&gt;This leaves some cells overcrowded, with up to 9 droplets in them! My solution is to merge these droplets into one:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/droplet_merge.svg" alt="Merging droplets" &gt;
&lt;/p&gt;
&lt;p&gt;We average the position and speed, weighted by the size of the original droplets, but of course we sum up the sizes and sediments without weighting.&lt;/p&gt;
&lt;p&gt;Droplet movement also leaves some cells empty. One solution is to just spawn a new droplet with size 0 (essentially no droplet at all) in those cells. Another solution is to implement steady rainfall: in every time step, always spawn a small droplet in every cell. This is slightly more elegant because we don&amp;rsquo;t need to check for division by zero, and is the approach I ended up with.&lt;/p&gt;
&lt;p&gt;If you want to know the details, I&amp;rsquo;ll refer you to &lt;a href="https://github.com/SebLague/Hydraulic-Erosion/blob/master/Assets/Scripts/Erosion.cs"&gt;Sebastian Lague&amp;rsquo;s implementation&lt;/a&gt; on GitHub. Though my data structures and outer loops are different, the equations are pretty much the same. Though I do think Sebastian&amp;rsquo;s code has &lt;a href="https://github.com/SebLague/Hydraulic-Erosion/issues/15"&gt;some bugs&lt;/a&gt;…&lt;/p&gt;
&lt;p&gt;And now the moment you&amp;rsquo;ve been waiting for: let&amp;rsquo;s see if it works!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/side_by_side.png" alt="Side by side comparison" &gt;
&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s clear that the algorithm does what it should do, that it doesn&amp;rsquo;t introduce strange artifacts or numerical instabilities, and most importantly: that the result looks great! And it takes only 20 iterations, running in less than 1.5 seconds for a 2048×1024 map.&lt;/p&gt;
&lt;p&gt;As a finishing touch, I made the erosion strength dependent on tectonic region to randomly get more or less eroded terrain depending on location. The final result:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/6/6/around-the-world-23-hydraulic-erosion/droplet_erosion.png" alt="Terrain eroded by droplets" &gt;
&lt;/p&gt;
&lt;p&gt;Erosion strength is also dependent on latitude to get more hydraulic erosion towards the poles, resulting in more fjord-like structures on coastlines there (zoom in to see them).&lt;/p&gt;
&lt;p&gt;In nature, fjords occur on terrain closer to the poles because it was once covered in ice and therefore subjected to &lt;em&gt;glacial&lt;/em&gt; erosion. Which… I should probably not be getting into right now.&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 22: Dropping the ball</title><link>https://frozenfractal.com/blog/2025/5/12/around-the-world-22-dropping-the-ball/</link><pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2025/5/12/around-the-world-22-dropping-the-ball/</guid><description>&lt;p&gt;I&amp;rsquo;ve done something that common wisdom in software development says you should never ever do: I started over. Let me explain why. This might get a bit technical…&lt;/p&gt;
&lt;h2 id="stumbling-around"&gt;Stumbling around&lt;/h2&gt;
&lt;p&gt;Progress on the game had been rather slow for a few months. This was in part due to lack of time, but it was also because of the code itself, and my feelings about it. I was getting increasingly annoyed by the fact that &lt;a href="https://frozenfractal.com/blog/2024/1/8/around-the-world-11-everything-is-harder-on-a-sphere/"&gt;everything is harder on a sphere&lt;/a&gt;. The spherical shape of the world permeates every aspect of the code, from procedural generation to physics to navigation to rendering. It meant I had to develop bespoke solutions everywhere. Even if each of those solutions individually didn&amp;rsquo;t take up a lot of time, it was starting to add up, but I was nowhere near done dealing with this. And because the world geometry is so invasive, it&amp;rsquo;s hard to get rid of it without at least a partial rewrite.&lt;/p&gt;
&lt;p&gt;I also realized that I don&amp;rsquo;t like C#. I started out using it because I needed something more performant than GDScript, and also because I wanted a statically typed language to keep the codebase maintainable. C# is supported as a first-class citizen in Godot, and I had a little prior experience with it (though outdated), so it seemed the obvious choice. Even though I&amp;rsquo;d never been particularly fond of the language, I figured it would let me get the job done, and the language might grow on me.&lt;/p&gt;
&lt;p&gt;It did the opposite. Working in C# on the game, while using Rust for contract work, made me realize that I just like Rust much better. Despite Rust&amp;rsquo;s reputation of being hard to learn, the core language is actually simpler than C#, with fewer features that work together better. Moreover, C#&amp;rsquo;s documentation turned out to be mediocre at best – exceedingly verbose, but often failing to mention important details. Language features like &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#inline-arrays"&gt;inline arrays&lt;/a&gt;, which can be a huge performance boost in tight loops, are incompletely specified and the &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/inline-arrays"&gt;specification&lt;/a&gt; is still in draft after the feature has shipped. And the API docs don&amp;rsquo;t even consistently use a fixed-width font for code!&lt;/p&gt;
&lt;p&gt;A third factor that was holding me back was the &lt;a href="https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/"&gt;migration away from my custom ECS&lt;/a&gt;, which I never completed because I wanted to continue working on features. The codebase was messy and needed a significant amount of tedious work to get back to a good state. I would even get the occasional segmentation fault – something that should have been impossible using a managed language. (This very same error happened later with Rust as well, and turned out to be something that I &lt;em&gt;thought&lt;/em&gt; was thread safe in Godot not actually being thread safe, so C# isn&amp;rsquo;t to blame here. But I didn&amp;rsquo;t know that at the time.)&lt;/p&gt;
&lt;p&gt;All this was making me rather demotivated. If this were paid contract work, I would just power through, but for a hobby project that I&amp;rsquo;m doing for fun, it&amp;rsquo;s killing. If the fun is gone and unlikely to come back soon, the project is as good as dead.&lt;/p&gt;
&lt;h2 id="rust--gdscript--profit"&gt;Rust + GDScript = profit?&lt;/h2&gt;
&lt;p&gt;I eventually decided that the effort I was spending on the spherical world shape wasn&amp;rsquo;t worth it, considering that the improvement to gameplay would be small at best. I would be better off with a rectangular world, wrapping around at the east and west edges – essentially, a cylinder. But it was hard to make this happen without rewriting significant parts of code, and the game would not be runnable while I was working on this migration. It was an all or nothing affair.&lt;/p&gt;
&lt;p&gt;Also, the landscape has shifted since I started working on this incarnation of the game about two years ago. For one, the Rust bindings in the form of the &lt;a href="https://godot-rust.github.io/"&gt;godot-rust&lt;/a&gt; library have matured, and are quite stable and usable these days. On top of that, static typing in GDScript continues to improve, with features like &lt;a href="https://github.com/godotengine/godot/pull/78656"&gt;typed dictionaries&lt;/a&gt; being added to close the gaps.&lt;/p&gt;
&lt;p&gt;All these factors led me to the decision that a clean start would be a good idea. I started working on this on Valentine&amp;rsquo;s day this year, using Rust for the heavy lifting, and GDScript for glueing everything together. So far, this has been a productive combination, playing off each language&amp;rsquo;s strengths: Rust is performant (or rather, gives the programmer full control over performance) and scales well to complex codebases, whereas GDScript is quick to iterate on, and interfaces well with the engine.&lt;/p&gt;
&lt;h2 id="state-of-the-world"&gt;State of the world&lt;/h2&gt;
&lt;p&gt;Since I didn&amp;rsquo;t want to end up with a messy codebase once again, I spent the past three months working on a solid technical foundation. This effort is about halfway, but I want to finish it before building out more gameplay and content on top of it. That said, I couldn&amp;rsquo;t resist adding some nice new procedural generation features while porting, most notably erosion – but that&amp;rsquo;s still incomplete at this point, and warrants a blog post all of its own.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a screenshot of the current state of things:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/5/12/around-the-world-22-dropping-the-ball/snapshot.png" alt="A screenshot of the game showing a tiny ship, seen from a bird&amp;rsquo;s eye perspective, on a flat plane that might be water, mostly surrounded by some grey hills" &gt;
&lt;/p&gt;
&lt;p&gt;Procedural generation at the global scale is mostly done, with only tectonic hotspots and polar ice caps remaining on the list. Adding local noise and erosion to each chunk still needs to be done as well, as you can see from the grid artifacts. Other main missing features are ports and maps, both so essential to gameplay.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m holding off on porting my &lt;a href="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/"&gt;fancy water shader&lt;/a&gt; until I figure out the art style, since &lt;a href="https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/"&gt;painterly rendering turned out to be a non-starter&lt;/a&gt;. I think it&amp;rsquo;s going to be something more low-poly.&lt;/p&gt;
&lt;p&gt;But I do now have a nice interactive in-game world map viewer, which is a huge help for inspecting and debugging:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/5/12/around-the-world-22-dropping-the-ball/world_map.png" alt="A screenshot of the game showing a window with a map of the world" &gt;
&lt;/p&gt;
&lt;p&gt;Because this tooling is written in GDScript, it&amp;rsquo;s really easy and actually fun to quickly throw something like this together. Previously, I had built such tooling as an editor add-on. Having it in the game allows for interaction with the game itself, such as showing the ship&amp;rsquo;s position on the map, and right-clicking to teleport the ship to another location.&lt;/p&gt;
&lt;p&gt;And since the world is no longer spherical, the map is actually accurate and not distorted anywhere. (You might have noticed that the world wraps around north/south as well. It was just easier to code this way, and it won&amp;rsquo;t matter once I add impassable ice sheets to the poles.)&lt;/p&gt;
&lt;p&gt;With this reduction in scope, maybe, &lt;em&gt;just maybe&lt;/em&gt;, a third-person 3D perspective is back on the table? I haven&amp;rsquo;t decided yet…&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 21: Visibility</title><link>https://frozenfractal.com/blog/2025/2/4/around-the-world-21-visibility/</link><pubDate>Tue, 04 Feb 2025 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2025/2/4/around-the-world-21-visibility/</guid><description>&lt;p&gt;In a game focused on exploration, we need to have a way to decide what the player can and cannot see. With a first-person or third-person 3D view, visibility comes for free: anything below the horizon, and anything behind something else, is not visible. With a top-down perspective, we need to do some more work.&lt;/p&gt;
&lt;h2 id="visibility"&gt;Visibility&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s the test scene I&amp;rsquo;ll be using:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/2/4/around-the-world-21-visibility/all_visible.png" alt="A test scene without any visibility limitations" &gt;
&lt;/p&gt;
&lt;p&gt;Please ignore the black areas in the corners, which are due to a bug in my camera calculations that I&amp;rsquo;ll fix later. The player&amp;rsquo;s ship is in the centre, but it&amp;rsquo;s so small it&amp;rsquo;s barely visible. I&amp;rsquo;ll have to look into that later too.&lt;/p&gt;
&lt;p&gt;The simplest way to restrict visibility is to limit the camera zoom, so you simply cannot zoom out farther to see more. Since the view is always centered on the player&amp;rsquo;s ship, this effectively limits how far they can see. However, because the screen is typically not square, this restricts visibility more in the vertical direction than the horizontal; also, the amount of restriction would depend on the screen aspect ratio. Rather, we want some kind of &lt;em&gt;circular&lt;/em&gt; visibility.&lt;/p&gt;
&lt;p&gt;Of course, this is not a new problem, and games have been solving it at least since the early real-time strategy games like Dune II:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/2/4/around-the-world-21-visibility/dune2.png" alt="A screenshot of Dune II, showing the initial map" &gt;
&lt;/p&gt;
&lt;p&gt;The areas not visible to the player are simply rendered in black here. More precisely, in RTS games, areas &lt;em&gt;unexplored&lt;/em&gt; by the player are rendered in black, but that&amp;rsquo;s not what I want for my game. I&amp;rsquo;ll just make everything black that isn&amp;rsquo;t currently visible.&lt;/p&gt;
&lt;p&gt;Thanks to my newly gained experience with Godot&amp;rsquo;s compositor effects, it&amp;rsquo;s relatively simple to implement this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/2/4/around-the-world-21-visibility/no_height.png" alt="A screenshot with circular visibility" &gt;
&lt;/p&gt;
&lt;p&gt;However, this restricts visibility to a circle at the horizon. Wouldn&amp;rsquo;t it be fun if, just like in a first-person view, things &lt;em&gt;farther away&lt;/em&gt; than the horizon would also be visible, if they were tall enough, such as mountains and volcanic islands? So let&amp;rsquo;s add that! Using some vector algebra and trigonometry, it&amp;rsquo;s not too hard to compute whether a given point is above or below the horizon:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/2/4/around-the-world-21-visibility/with_height.png" alt="A screenshot with visibility depending on height" &gt;
&lt;/p&gt;
&lt;p&gt;And&amp;hellip; that&amp;rsquo;s cool, but there is a problem. The calculations are based on a typical carrack of the time, with a lookout standing in the crow&amp;rsquo;s nest 25 meters above the water level. The equation to compute the distance to the horizon is easy to derive using Pythagoras&amp;rsquo;s theorem:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;d = sqrt(2*R*h + h²)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where &lt;code&gt;R&lt;/code&gt; is the radius of the planet and &lt;code&gt;h&lt;/code&gt; is the height above sea level. (If &lt;code&gt;h&lt;/code&gt; is small relative to &lt;code&gt;R&lt;/code&gt;, we can omit the &lt;code&gt;h²&lt;/code&gt; term.) With our planet being 1% the size of Earth, this puts the horizon at 1.8 km away. That is, we can see the surface of the ocean up to 1.8 km away, but may be able to see taller things beyond that.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s say that a mountain can be at most 10 km tall. (The tallest mountain on Earth, Mount Everest, stands at 8.8 km.) The tip of such a mountain would then be visible from almost 39 km away! That means it would be very far outside our little horizon circle; you&amp;rsquo;d have to zoom out until the horizon circle occupies only a fraction of the screen, before you can spot that mountain in the distance. And the game would have to generate and render terrain at huge distances to make this work.&lt;/p&gt;
&lt;p&gt;Fortunately, when scaling down the planet to 1% the size of Earth, I also scaled terrain heights down to 10% size. (This should of course also have been 1%, but that would reduce mountains to mere hills in comparison to the ship.) So actually, our mountain is at most 1000 meters tall. This makes it visible from about 13 km away, which is already better, but still a lot compared to our 1.8 km horizon radius.&lt;/p&gt;
&lt;p&gt;So it&amp;rsquo;s time to cheat, and scale down terrain height &lt;em&gt;only&lt;/em&gt; in the visibilty calculations. But by what factor? Let&amp;rsquo;s take it from the other side: from how far away do we &lt;em&gt;want&lt;/em&gt; a 1 km tall mountain to be visible? We can make this value configurable directly for the game developer (me), and have the game code figure out the right scale factor. If we set the distance to a reasonable 5 km, that works out to a maximum mountain height of 81 meters, i.e. a scale factor of 0.081 on top of the 10% we have already:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/2/4/around-the-world-21-visibility/scaled_height.png" alt="Same, but with heights scaled down" &gt;
&lt;/p&gt;
&lt;p&gt;That seems a bit too tame, but maybe these mountains aren&amp;rsquo;t actually very tall; I haven&amp;rsquo;t checked. At least now I have a meaningful value to adjust. (And in the remainder of this post, you&amp;rsquo;ll see that I did adjust it.)&lt;/p&gt;
&lt;h2 id="blue-is-the-new-black"&gt;Blue is the new black&lt;/h2&gt;
&lt;p&gt;Even though you can&amp;rsquo;t go far wrong with black, it looks rather dull, especially if a large portion of the screen is filled with it. In reality, you would see the sky above the horizon. So how about using the sky colour instead of black?&lt;/p&gt;
&lt;p&gt;As it happens, during my excursion down the third-person rabbit hole in the &lt;a href="https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/"&gt;last post&lt;/a&gt;, I wrote some shader code to compute sky colours and atmospheric scattering. So I can reuse that here – it hasn&amp;rsquo;t been in vain after all!&lt;/p&gt;
&lt;p&gt;First, let&amp;rsquo;s compute the sky colour just above the horizon, and use that instead of black. This is a good opportunity to show off the day-night cycle as well, which has been in the game forever:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="horizon_color.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;I think this looks fairly good, but I also added some debanding noise after capturing this video.&lt;/p&gt;
&lt;p&gt;(You&amp;rsquo;ll notice that it&amp;rsquo;s not completely dark before sunrise and after sunset. That&amp;rsquo;s because there is also a moon, which is currently always full, and whose position is not overridden by the debug controls.)&lt;/p&gt;
&lt;h2 id="aerial-perspective"&gt;Aerial perspective&lt;/h2&gt;
&lt;p&gt;When looking at a distant object, such as a mountain, you&amp;rsquo;ll notice that it appears blueish and washed out. This phenomenon is called, somewhat confusingly, &lt;em&gt;aerial perspective&lt;/em&gt;. The washing out happens because the light from the mountain is partially absorbed and scattered before it reaches your eye. The blueish tint is caused by light from the sun being scattered &lt;em&gt;into&lt;/em&gt; the line of sight; light towards the blue end of the spectrum is more prone to scattering.&lt;/p&gt;
&lt;p&gt;Godot has fake aerial perspective built in, but the problem is that it works from the point of view of the camera, where we want it to work from the point of view of the ship. There&amp;rsquo;s no way to override that, so we have to build our own. Fortunately, with the sky shader, all the pieces are already there, so it&amp;rsquo;s just a few lines of code:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/2/4/around-the-world-21-visibility/aerial_perspective.png" alt="Screenshot with aerial perspective applied" &gt;
&lt;/p&gt;
&lt;p&gt;It adds a nice sense of distance. Because of the 1% planet scale, I had to strengthen the effect by a factor of 30 to make it look like this. Realistically that should have been a factor of 100, but that turns out to be too strong.&lt;/p&gt;
&lt;h2 id="shadows"&gt;Shadows&lt;/h2&gt;
&lt;p&gt;Godot offers shadow rendering by default, but I had to tweak the values quite a bit to make them work well at these scales. They make a big difference:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2025/2/4/around-the-world-21-visibility/shadows.png" alt="Screenshot with added shadows" &gt;
&lt;/p&gt;
&lt;p&gt;To wrap things up, here&amp;rsquo;s a video of exploring a bay in the early morning:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="sailing_around.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;I do see some problems already, such as that the aerial perspective effect is also applied at full strength in shadows, where inscattering should be less. I&amp;rsquo;m not yet sure how to fix that, so I&amp;rsquo;ll leave it at this for now. The game still looks better now than it did this morning!&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 20: Two steps forward, one step back</title><link>https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/</link><pubDate>Wed, 25 Dec 2024 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/</guid><description>&lt;p&gt;Work on the game has been slow due to lack of time, but worse, I&amp;rsquo;ve let it drift off in the wrong direction. I need to be more careful about scope creep. But first, let&amp;rsquo;s talk about the progress that I &lt;em&gt;am&lt;/em&gt; happy about.&lt;/p&gt;
&lt;h2 id="no-more-ecs"&gt;No more ECS&lt;/h2&gt;
&lt;p&gt;In &lt;a href="https://frozenfractal.com/blog/2024/4/11/around-the-world-14-floating-the-origin/"&gt;a previous post&lt;/a&gt;, I described my newly written entity-component system (ECS). I had some reasonable arguments for structuring the game as an ECS, most notably that it&amp;rsquo;s not possible to add functionality to all Godot nodes, regardless of type. You&amp;rsquo;d need multiple inheritance for this.&lt;/p&gt;
&lt;p&gt;I did mention the workaround of putting the functionality onto a child node, but that&amp;rsquo;s a fairly heavyweight solution. However, does that matter in practice? Perhaps not. Moreover, I&amp;rsquo;ve found another approach: put the nodes into a group, and have a separate node that applies some operation to all nodes in that group. This only works for code, not for data, but it turns out I don&amp;rsquo;t need that – I initially wanted to store a 64-bits node position, but storing a 32-bits position relative to the floating origin works just as well.&lt;/p&gt;
&lt;p&gt;And my ECS came with a number of drawbacks. Adding new node-based entities now required me to add code in five different files:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define a component to hold the data.&lt;/li&gt;
&lt;li&gt;Create a scene to represent the entity in the scene tree.&lt;/li&gt;
&lt;li&gt;Implement a system to work on that data and sync the changes to the scene.&lt;/li&gt;
&lt;li&gt;Register the system to be executed every frame.&lt;/li&gt;
&lt;li&gt;Register the scene to be added to the tree whenever the component is added.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This was just &lt;em&gt;way&lt;/em&gt; too cumbersome, for little benefit. With Godot&amp;rsquo;s node-based approach, I&amp;rsquo;d only need the scene and a script, and the link between the two is automatically managed.&lt;/p&gt;
&lt;p&gt;And because all the values were tucked away in components and systems in pure C# code, I lost the ability to modify scene properties in the editor while the game is running. I didn&amp;rsquo;t realize at the time how useful that feature is.&lt;/p&gt;
&lt;p&gt;Another argument for using an ECS was that it would simplify the implementation of saving and (especially) loading. I now realize that isn&amp;rsquo;t true. Yes, streaming a bunch of components from disk using some predefined serialization format is easier than reconstructing a scene tree from (e.g.) a JSON object. However, after loading those components, I&amp;rsquo;d &lt;em&gt;still&lt;/em&gt; need to reconstruct that scene tree anyway. The work is just moved into the systems that update nodes from components, but it still needs to be implemented.&lt;/p&gt;
&lt;p&gt;So in the end, I decided to throw out the ECS and port everything back to a more customary Godot scene tree. Everything? No, there were a few key benefits that I wanted to keep.&lt;/p&gt;
&lt;p&gt;First, there&amp;rsquo;s dependency injection. Systems allowed injecting resources and queries into their constructors, and this really helped to decouple the code. Since we&amp;rsquo;re now doing nodes again, I wrote a very simple &lt;code&gt;Injector&lt;/code&gt; node, which is triggered whenever a node is added to the scene tree. It uses reflection to scan the new node for any fields with the &lt;code&gt;[Inject]&lt;/code&gt; annotation, and provides them with values from an array of pre-registered injectable objects. Functionally it&amp;rsquo;s almost the same as Godot&amp;rsquo;s singletons, but makes the dependency more explicit, which I like.&lt;/p&gt;
&lt;p&gt;Second, there&amp;rsquo;s the global event bus. I haven&amp;rsquo;t actually implemented this yet, and maybe I won&amp;rsquo;t need to, but I&amp;rsquo;m definitely keeping it in mind.&lt;/p&gt;
&lt;h2 id="layerprocgen"&gt;LayerProcGen&lt;/h2&gt;
&lt;p&gt;Earlier this year, &lt;a href="https://runevision.com/"&gt;Rune Skovbo Johansen&lt;/a&gt; alias runevision released &lt;a href="https://runevision.github.io/LayerProcGen/"&gt;LayerProcGen&lt;/a&gt;, a principled framework for procedural generation of infinite worlds. My worlds aren&amp;rsquo;t infinite, but they are big enough that we can&amp;rsquo;t generate and store them in their entirety, so the same principles apply. The idea is that procedural generation happens in &lt;em&gt;layers&lt;/em&gt;, and each layer is generated in &lt;em&gt;chunks&lt;/em&gt; as usual. Each layer can only depend on the layers below it, but it can request chunks from a larger area so that it has some context to work with:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/ContextualLayers.png" alt="Example of layers" &gt;
&lt;br&gt;
&lt;em&gt;Source: the LayerProcGen documentation, licensed under &lt;a href="https://mozilla.org/MPL/2.0/"&gt;MPL 2.0&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I realized that I was essentially already doing some of the things LayerProcGen helps with, but in a more ad-hoc way. So I decided it would make sense to switch over to this framework. I couldn&amp;rsquo;t use Rune&amp;rsquo;s code directly even though it&amp;rsquo;s in C#, because it assumes a flat world (&lt;a href="https://frozenfractal.com/blog/2024/1/8/around-the-world-11-everything-is-harder-on-a-sphere/"&gt;curse those spheres!&lt;/a&gt;). Fortunately, the implementation isn&amp;rsquo;t rocket science so I just wrote my own.&lt;/p&gt;
&lt;p&gt;I now have layer-by-layer, chunk-by-chunk generation working, distributing the work over several threads to speed it up. The game looks exactly the same as before, but the code is better organized and easier to build on top of.&lt;/p&gt;
&lt;p&gt;Around this point, I got sidetracked a bit.&lt;/p&gt;
&lt;h2 id="full-3d"&gt;Full 3D&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;d &lt;a href="https://frozenfractal.com/blog/2024/3/19/around-the-world-12-2d-or-not-2d/"&gt;previously settled&lt;/a&gt; on 3D rendering, but with a mostly top-down camera:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/top_down.png" alt="Top-down perspective" &gt;
&lt;/p&gt;
&lt;p&gt;On a whim, with my newly found powers of editing the scene tree while the game is running, I moved the camera away from the top-down perspective, and put it in a third-person perspective behind the ship. And it looked… rather nice. Oh dear.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/third_person.png" alt="Third-person perspective behind the ship" &gt;
&lt;/p&gt;
&lt;p&gt;This screenshot doesn&amp;rsquo;t even have any land in it, but it already has a much more immersive feel than the top-down view. It would also add interesting gameplay elements, such as distant coasts actually being less clearly visible, and having to do more work to match your surroundings to a &lt;a href="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/"&gt;&lt;del&gt;map&lt;/del&gt; chart&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I figured that we have a full 3D scene already, so it shouldn&amp;rsquo;t be too much work to use this perspective instead of top-down, right? So down the rabbit hole I went, not realizing how deep it was.&lt;/p&gt;
&lt;h2 id="sky"&gt;Sky&lt;/h2&gt;
&lt;p&gt;We can now see the sky, and it looks rather drab and boring – not even properly blue. Indeed, that&amp;rsquo;s the best you can get with Godot out of the box, so I had to write my own sky shader. I did that, implementing a pretty standard path tracer with single scattering, which made sunsets about 100× prettier:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/sky.png" alt="Sky test scene with sunset" &gt;
&lt;/p&gt;
&lt;p&gt;It automatically works with moonlight too:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/moon.png" alt="Moonlit scene on the water" &gt;
&lt;/p&gt;
&lt;h2 id="aerial-perspective"&gt;Aerial perspective&lt;/h2&gt;
&lt;p&gt;Okay, we now have some atmospheric scattering going on, but it&amp;rsquo;s only applied to the sky and not to any other objects:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/without_aerial_perspective.png" alt="Without aerial perspective" &gt;
&lt;/p&gt;
&lt;p&gt;The faraway islands are still a harsh green, rather than fading into the distance. Fortunately, Godot has a checkbox to add some fake aerial perspective, which mimics the effect of light scattering into the ray between the camera and the distant mountains. This instantly made the scene look much better:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/with_aerial_perspective.png" alt="With aerial perspective" &gt;
&lt;/p&gt;
&lt;p&gt;The effect is fake and might be limiting later once I start adding fog, but it&amp;rsquo;s better than nothing. Good enough for now.&lt;/p&gt;
&lt;h2 id="painterly-rendering"&gt;Painterly rendering&lt;/h2&gt;
&lt;p&gt;With this new third-person perspective, the low-poly ship model contrasted weirdly with the highly detailed waves and terrain. I already had a solution in mind for that. There weren&amp;rsquo;t any cameras in the Age of Sail, so much of what we know has come to us in the form of paintings. So wouldn&amp;rsquo;t it be cool if the game looked like an oil painting as well? I&amp;rsquo;d been planning to apply a post-processing filter to do just that.&lt;/p&gt;
&lt;p&gt;I looked around in the literature, and found that there are essentially two ways to make images look like paintings. On the one hand, there are filters that modify the image in some clever way, so that the result looks somewhat like brush strokes. On the other hand, some techniques create and render actual brush strokes. The challenge with that is &lt;em&gt;temporal coherence&lt;/em&gt;: we render 60 frames per second, but we don&amp;rsquo;t want entirely new brush strokes to appear every frame, because that would cause way too much flicker.&lt;/p&gt;
&lt;p&gt;The anisotropic Kuwahara filter by Kyprianidis et al. is of the first category, which is easier to implement, so I wanted to try it first:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/12/25/around-the-world-20-two-steps-forward-one-step-back/anisotropic_kuwahara_filter.jpg" alt="Example of anisotropic Kuwahara filter" &gt;
&lt;/p&gt;
&lt;p&gt;This YouTube video shows an implementation in TouchDesigner, applied to some drone videos of a mountainous landscape, and it looks absolutely gorgeous (skip ahead to 1 minute):&lt;/p&gt;
&lt;div class="youtubecontainer"&gt;
&lt;iframe class="youtube" width="640" height="360" src="https://www.youtube-nocookie.com/embed/_Tz4NWz0SnA?rel=0" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;I got most of the way through implementing this, using Godot&amp;rsquo;s new compositor effects and compute shaders, when I realized that this rabbit hole was too deep. The somewhat naive, but still GPU-based implementation was taking almost 100 milliseconds per frame; I&amp;rsquo;d need to get it down to 3-4 ms to still run smoothly on older hardware. And some bug was causing it to look more like a bad JPEG than a painting.&lt;/p&gt;
&lt;p&gt;And even if I fixed the bugs and somehow made it 30 times faster, this filter might still not achieve the look I&amp;rsquo;m looking for, because the input doesn&amp;rsquo;t have nearly as much detail as a photograph or video. This filter is essentially about &lt;em&gt;removing&lt;/em&gt; detail, where I might be better off with something that &lt;em&gt;adds&lt;/em&gt; detail instead, i.e. individual brush strokes, with all the temporal stability issues that that entails.&lt;/p&gt;
&lt;h2 id="thump-thump"&gt;Thump! Thump!&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Alice felt that she was dozing off, when suddenly, thump! thump! down she came upon a heap of sticks and dry leaves, and the fall was over.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;At this point, I belatedly realized that going full 3D wasn&amp;rsquo;t as quick and easy as I&amp;rsquo;d originally estimated. The sky shader needed more work to get rid of the green horizon. The painterly rendering was the kind of stuff that academics build entire careers on, but without it, I&amp;rsquo;d need to craft more detailed models. And on top of all that, I only have clear skies so far – I haven&amp;rsquo;t even begun to implement real-time, dynamically changing clouds yet.&lt;/p&gt;
&lt;p&gt;It was becoming clear that I would have to cut scope, and put the camera back where I had planned it. I even considered going to 2D entirely, but that would essentially mean starting from scratch and wasting even more time. Instead, let&amp;rsquo;s climb back out of this hole to the surface, take a breath of fresh air, and forge ahead with what I&amp;rsquo;ve got already.&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Why my apps will soon be gone from the Google Play Store</title><link>https://frozenfractal.com/blog/2024/9/6/why-my-apps-will-soon-be-gone-from-google-play/</link><pubDate>Fri, 06 Sep 2024 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2024/9/6/why-my-apps-will-soon-be-gone-from-google-play/</guid><description>&lt;p&gt;The first real money I ever made from game development was on Android. It was in 2013, when Android was still the underdog compared to the iPhone, and was being touted as a great platform for developers. I&amp;rsquo;d taken two weeks to build &lt;a href="https://frozenfractal.com/games/patchy/"&gt;Patchy&lt;/a&gt;, a retro arcade game revamped for touch controls, and published it on the Google Play Store without any hassle. Since then, I&amp;rsquo;ve also published &lt;a href="https://frozenfractal.com/games/twistago/"&gt;Twistago&lt;/a&gt;, &lt;a href="https://frozenfractal.com/games/rocket-mail/"&gt;Rocket Mail&lt;/a&gt;, &lt;a href="https://frozenfractal.com/blog/2015/6/14/bigcanvas-released/"&gt;Bigcanvas&lt;/a&gt;, &lt;a href="https://frozenfractal.com/projects/radio-nul/"&gt;Radio Nul&lt;/a&gt; and &lt;a href="https://frozenfractal.com/projects/papageno/"&gt;Papageno&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With each next app, the bar for publishing kept getting higher. At some point, Google started publishing developers&amp;rsquo; full company address on the store alongside the app. That&amp;rsquo;s fine for a professional company, but quite problematic for a solo indie developer who works from home, especially if the app &lt;em&gt;or the developer&lt;/em&gt; is even remotely controversial. I worked around it at the time by just putting the name of the town instead of the full address, which is only right morally, but not legally.&lt;/p&gt;
&lt;p&gt;The technical requirements also kept going up. To publish an update to an app, it has to target the latest API version, which goes up once or twice a year. Each API update comes with a slew of deprecations, and breaking changes to the already inscrutable build system. And even if you don&amp;rsquo;t want to update the app, Google will eventually start hiding apps from users if the app doesn&amp;rsquo;t target some minimum API version. This means you can&amp;rsquo;t just publish an app and leave it at that; it&amp;rsquo;s several days of work per app per year to keep up with the latest rug-pulls from Google. Again this is no problem for a company for whom the app is their core business, but bad news for indie and hobbyist developers who just want to make something cool, put it out there, and move on to the next project.&lt;/p&gt;
&lt;p&gt;And then, there&amp;rsquo;s the latest increase in publishing requirements, the straw that broke the camel&amp;rsquo;s back, which made me decide to abandon the Google Play Store altogether.&lt;/p&gt;
&lt;h2 id="the-new-rules"&gt;The new rules&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://android-developers.googleblog.com/2023/07/boosting-trust-and-transparency-in-google-play.html"&gt;original announcement&lt;/a&gt; just mentioned that organizations will need to provide a D-U-N-S number. It was possible to choose your own deadline, so I set it as far in the future as I could. Now that deadline, 5 November, is getting close, and I can start the verification process. The email, however, makes it clear that they want much more than a D-U-N-S number:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What you need to provide to verify&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a D-U-N-S number for your organization&lt;br&gt;
If you don&amp;rsquo;t have a D-U-N-S number, request one at no cost from Dun &amp;amp; Bradstreet now. This process can take up to 30 days, so we recommend requesting a D-U-N-S number immediately. &lt;a href="https://support.google.com/googleplay/android-developer/answer/13628312#duns"&gt;Learn more about requesting a D-U-N-S number&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;a phone number and email address for Google Play users to contact you&lt;/li&gt;
&lt;li&gt;a phone number and email address for Google to contact you&lt;/li&gt;
&lt;li&gt;an official document to verify your identity&lt;/li&gt;
&lt;li&gt;an official document to verify your organization&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;There is only one of these items that I don&amp;rsquo;t have a problem with.&lt;/p&gt;
&lt;p&gt;The D-U-N-S number is only needed if your Play Store publisher account is for an organization, not an individual. But I&amp;rsquo;m registered as a sole proprietor, so the rule applies to me. You can &lt;a href="https://www.dnb.com/duns-number/lookup.html"&gt;look up&lt;/a&gt; your company&amp;rsquo;s D-U-N-S number, but the form doesn&amp;rsquo;t (currently?) allow selecting any country except the US. Dun &amp;amp; Bradstreet&amp;rsquo;s partner in the Netherlands, &lt;a href="https://www.altares.nl/en/our-data/duns-number/"&gt;Altares&lt;/a&gt;, does allow me to look up my own company, but charges 15 € for the privilege of seeing my own data, &lt;em&gt;including my own D-U-N-S number&lt;/em&gt;, which they&amp;rsquo;ve apparently already assigned when I registered with the Chamber of Commerce. If I didn&amp;rsquo;t already have one, I don&amp;rsquo;t see a way on their website to request one either, even though the FAQ mentions that you can (for a fee, of course).&lt;/p&gt;
&lt;p&gt;While that may be just some paperwork and a small expense, the next requirement is more insidious: &amp;ldquo;a phone number and email address for Google Play users to contact you&amp;rdquo;. I&amp;rsquo;m fine showing an email address, but I &lt;em&gt;absolutely do not want&lt;/em&gt; my phone number to be available to anyone on the internet. (Even for phone calls. But remember that a phone number is used for much more than phone calls these days.) And that&amp;rsquo;s just me, a privileged hetero white cis dude who is unlikely to be the target of harassment or doxxing.&lt;/p&gt;
&lt;p&gt;The requirement &amp;ldquo;a phone number and email address for Google to contact you&amp;rdquo; is the only one that sounds benign to me, although I have yet to see Google trying to contact small-fry developers like me by phone, instead of just reaching for the algorithmic ban hammer if I cross any line.&lt;/p&gt;
&lt;p&gt;An &amp;ldquo;official document to verify your identity&amp;rdquo; would presumably be a scan or photo of my passport. Now, I have reasonable confidence in the security of Google&amp;rsquo;s systems, but this is something very sensitive and can easily be abused if it fell into the wrong hands. And why is it necessary? I&amp;rsquo;ve done business with many other companies, where a lot more money was changing hands than I&amp;rsquo;m getting from my Play Store apps, but they never asked for my passport. Why does Google?&lt;/p&gt;
&lt;p&gt;Those other companies also never asked for &amp;ldquo;an official document to verify your organization&amp;rdquo;. Presumably this is a document I can request from the Chamber of Commerce to the tune of another 9 €, but again I&amp;rsquo;ve never had another company ask for this when doing business with them.&lt;/p&gt;
&lt;h2 id="in-conclusion"&gt;In conclusion&lt;/h2&gt;
&lt;p&gt;I sort of understand why Google is doing all this. It&amp;rsquo;s partly legal requirements (especially EU), partly an attempt to reduce spam and malicious stuff. Some of it might even be in the best interest of the end user.&lt;/p&gt;
&lt;p&gt;Larger companies have the resources to deal with all this, and for personal accounts the &lt;a href="https://support.google.com/googleplay/android-developer/answer/13628312"&gt;rules&lt;/a&gt; are much more lenient. Sadly, there has been little consideration for sole proprietorships like mine, which fall somewhere in between: bound by the same rules as big companies, but without the means to play by them.&lt;/p&gt;
&lt;p&gt;And this is why I&amp;rsquo;m just going to let my Play Store developer account expire. Starting on 5 November, you won&amp;rsquo;t be able to install any of my apps anymore. Most of them were not actively maintained anymore, &lt;em&gt;but they still worked&lt;/em&gt;, because the Android operating system itself is actually pretty good at backwards compatibility.&lt;/p&gt;
&lt;p&gt;Even though there was little remaining interest in my older apps, it makes me sad that people won&amp;rsquo;t be able to use them anymore. The one I&amp;rsquo;m most sad about, though, is &lt;a href="https://papageno.app"&gt;Papageno&lt;/a&gt;, an unfinished side project about bird sounds that I haven&amp;rsquo;t worked on for three years, but keeps popping into my mind as something I&amp;rsquo;d really like to finish some day. Maybe it could be in the form of a web app. We&amp;rsquo;ll see.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/9/6/why-my-apps-will-soon-be-gone-from-google-play/no_more_google_play.png" alt="The official &amp;ldquo;Get it on Google Play&amp;rdquo; button, but with the text &amp;ldquo;You can no longer&amp;rdquo; scribbled above it, probably flying in the face of official brand guidelines" &gt;
&lt;/p&gt;</description><category>business</category><category>Bigcanvas</category><category>Patchy</category><category>Twistago</category><category>Rocket Mail</category></item><item><title>Around The World, Part 19: Constructing languages</title><link>https://frozenfractal.com/blog/2024/8/9/around-the-world-19-constructing-languages/</link><pubDate>Fri, 09 Aug 2024 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2024/8/9/around-the-world-19-constructing-languages/</guid><description>&lt;p&gt;I&amp;rsquo;m having a bit of an &amp;ldquo;off&amp;rdquo; day today, so let&amp;rsquo;s do something fun, even if it&amp;rsquo;s rather low priority for the game: generating foreign languages.&lt;/p&gt;
&lt;p&gt;The idea is that the player starts out in a region where they speak the local language, which will be represented as English (or some other real-world language, if the game ever gets translated). However, as the player ventures out and discovers new civilizations, they&amp;rsquo;ll run into a language barrier. Text like quest descriptions and gossip will be shown in incomprehensible gibberish instead, until the player&amp;rsquo;s character learns the local language, or hires someone to translate. At that point, the foreign text is partially or entirely replaced by English. Of course, I don&amp;rsquo;t want to use real-world languages in my fictional world, so: we need an algorithm to produce languages!&lt;/p&gt;
&lt;p&gt;A more immediate reason to write a language generator is for naming cities, seas, coasts, islands and the like. Right now, city names are just drawn from a list of real-world ports, which is a bit weird at best.&lt;/p&gt;
&lt;h2 id="prior-work"&gt;Prior work&lt;/h2&gt;
&lt;p&gt;A fairly well known generator for &lt;em&gt;place names&lt;/em&gt; is due to Martin O&amp;rsquo;Leary. He has a &lt;a href="https://mewo2.com/notes/naming-language/"&gt;great description&lt;/a&gt; on his website, and published the source code &lt;a href="https://github.com/mewo2/naming-language"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve experimented with O&amp;rsquo;Leary&amp;rsquo;s algorithm in the past, but I was never entirely happy with the results. One problem might have been that I was trying to do two things at once:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generating languages&lt;/li&gt;
&lt;li&gt;Generating words in a generated language&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Repeatedly, I ran into the problem that as I added more diversity to words &lt;em&gt;within&lt;/em&gt; a language, each language became less distinctive and recognizable, resulting in a uniform soup of words.&lt;/p&gt;
&lt;p&gt;But a more fundamental issue was that generated words looked too artificial to me, which is probably due to the rigid phoneme structure that the algorithm uses. So instead, I&amp;rsquo;ll start with a more classic approach.&lt;/p&gt;
&lt;h2 id="markov-chains"&gt;Markov chains&lt;/h2&gt;
&lt;p&gt;Unlike Martin O&amp;rsquo;Leary&amp;rsquo;s generator, a Markov chain needs to be created from input data, for example a large amount of text. If we feed it English text, it&amp;rsquo;ll generate words that look more or less like English (or, in many cases, &lt;em&gt;are&lt;/em&gt; English). Since we don&amp;rsquo;t have any text in a language we haven&amp;rsquo;t yet generated, this sounds useless, but bear with me – I have some Ideas.&lt;/p&gt;
&lt;p&gt;For our purposes, you can think of a Markov chain as a table that contains, for each combination of preceding letters of some fixed length, the probability distribution of the letter that follows. For example, one row in the table could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
ba | 50% d, 20% g, 10% r, 10% t, 10% y
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This particular table is based on counting trigrams, i.e. combinations of three letters. In this case, the trigrams &lt;code&gt;bad&lt;/code&gt;, &lt;code&gt;bag&lt;/code&gt;, &lt;code&gt;bar&lt;/code&gt;, &lt;code&gt;bat&lt;/code&gt; and &lt;code&gt;bay&lt;/code&gt; were encountered in the input text. The left column contains the leftmost two letters of the trigram, and the right column indicates how frequently each next letters was encountered. There are special indicators for start-of-word and end-of-word, which can be treated just like other letters.&lt;/p&gt;
&lt;h2 id="creating-the-chain"&gt;Creating the chain&lt;/h2&gt;
&lt;p&gt;To produce the Markov chain, we need some input data. I want a diverse set of languages, so after some searching, I settled on the &lt;a href="https://research.ics.aalto.fi/cog/data/udhr/"&gt;UDHR corpus&lt;/a&gt;, which contains the Universal Declaration of Human Rights translated into 372 languages. This text is rather short: only 1783 words in English. For a normal Markov chain word generator, this would be pretty limiting. But for my Ideas, it shouldn&amp;rsquo;t matter.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the full table for the English version, if we count bigrams (combinations of two letters):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;^ | a 270, t 256, o 168, i 101, r 96, s 96, e 93, h 92, f 89, p 82, b 69, w 58, c 57, d 43, n 41, m 32, l 27, u 21, g 17, j 7, v 4, k 3
a | n 174, l 128, r 93, t 92, s 68, c 30, $ 20, m 17, v 16, g 16, i 13, d 9, w 9, b 8, y 7, u 2, f 1, p 1, k 1
b | e 52, l 18, y 13, i 7, a 5, o 4, j 4, s 3, u 2, r 1
c | l 45, o 43, e 43, t 38, h 32, i 30, a 19, $ 12, u 10, r 9, c 5, k 5, y 1
d | $ 181, e 45, i 34, o 23, u 15, a 12, l 3, s 3, v 2, g 2, r 2, y 1, h 1
e | $ 363, r 144, n 106, d 84, s 66, c 51, v 40, l 39, e 38, a 32, m 23, t 21, q 16, i 13, f 12, p 7, x 7, o 6, g 3, y 3, k 2, b 1
f | $ 98, r 39, o 31, e 16, u 16, a 11, f 9, i 4
g | h 61, $ 33, e 22, a 12, n 10, r 8, i 8, u 4, s 3, o 3, t 1
h | e 174, a 75, i 60, t 56, $ 36, o 25, u 14, r 3, m 1, y 1, n 1
i | n 135, o 109, t 92, g 71, s 69, c 66, e 25, m 24, v 22, a 22, l 19, r 14, f 8, d 6, b 6, p 5, z 4, $ 1, h 1
j | u 6, o 5, e 4
k | $ 7, i 4, e 3, s 3, n 1
l | $ 120, e 74, l 55, i 47, a 29, y 23, o 15, d 12, t 8, u 7, f 4, v 2, s 2
m | e 49, a 30, i 23, $ 21, o 17, s 17, p 13, m 8, b 7, u 4
n | $ 171, d 142, t 83, a 59, e 58, g 41, i 34, c 34, s 32, o 24, y 19, j 4, h 2, n 2, u 2, l 2, m 2, k 1, v 1, f 1
o | n 189, $ 97, f 95, r 94, m 48, u 36, t 35, c 22, p 19, l 13, d 9, o 8, s 8, y 7, g 6, w 6, v 6, b 4, i 2, h 1, k 1
p | r 43, e 43, l 16, o 12, u 11, a 8, i 6, m 4, $ 4, p 3, t 1, s 1
q | u 16
r | e 128, i 100, $ 85, t 53, y 53, o 43, a 42, s 25, d 11, n 11, m 9, r 8, v 6, b 5, g 5, k 5, u 4, f 4, p 4, c 4, l 2, h 2
s | $ 214, e 47, t 46, h 33, o 32, s 24, u 16, i 15, a 11, p 11, c 9, l 3, d 2, r 1, y 1
t | h 199, i 161, $ 121, o 90, e 75, y 37, a 35, s 33, r 24, l 11, u 10, t 5, w 1, m 1
u | n 42, r 29, m 17, a 17, l 16, t 16, c 14, s 11, b 9, d 6, i 6, p 5, g 4, e 3, f 1
v | e 76, i 10, a 8, o 5
w | h 22, i 18, o 12, $ 10, e 6, a 5, n 2
x | i 2, p 2, e 2, $ 1
y | $ 127, o 31, m 3, r 1, w 1, l 1, s 1, i 1
z | a 3, e 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;m using &lt;code&gt;^&lt;/code&gt; to indicate the start of the word and &lt;code&gt;$&lt;/code&gt; for the end of the word.&lt;/p&gt;
&lt;h2 id="generating-words"&gt;Generating words&lt;/h2&gt;
&lt;p&gt;Now let&amp;rsquo;s generate a word using this table, picking the most likely letters each time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;From the start-of-word indicator &lt;code&gt;^&lt;/code&gt;, the most likely next letter is &lt;code&gt;a&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;From &lt;code&gt;a&lt;/code&gt;, the most likely letter is &lt;code&gt;n&lt;/code&gt;, forming &lt;code&gt;an&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;From &lt;code&gt;n&lt;/code&gt;, the most likely letter is &lt;code&gt;$&lt;/code&gt;, so we are done.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Indeed, &lt;code&gt;an&lt;/code&gt; is a rather common English word. If we had started with &lt;code&gt;t&lt;/code&gt; instead, can you see where we are most likely to end up?&lt;/p&gt;
&lt;p&gt;Now let&amp;rsquo;s write some code to automate this process, and also randomly select the next letter according to the weighting, instead of just picking the most common one. Here are 10 words produced by the table above:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;no
ofessinyoory
tofen
thal
frtitopof
as
eraricolalees
con
berthigior
derthirghtiredendacorouly
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;hellip; Okay. Not exactly English. What if we use trigrams instead of bigrams? The table is too large to show in full, but here&amp;rsquo;s some output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;and
the
beitradefoull
mation
ance
weedomine
in
artion
torights
a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Better. Four of those are actual English words, the rest look like parts of words glued together, but that&amp;rsquo;s fine. Let&amp;rsquo;s try it on some more languages. I&amp;rsquo;ve added capitalization and punctuation manually.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Generated sentence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dutch&lt;/td&gt;
&lt;td&gt;Dat van welken eeft sond overwel echijn hetzen stikerrichtens opvolkel.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spanish&lt;/td&gt;
&lt;td&gt;Que la hagará destaral mérionesu plese humad nal humanacionalgual pentodada.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finnish&lt;/td&gt;
&lt;td&gt;Tei kaiskosa ja ja mihmisteettä arti la ja liseen ja.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serbian&lt;/td&gt;
&lt;td&gt;I člata privo poranjudje zemljšavo mednosti ste i ovešanda ne.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zulu&lt;/td&gt;
&lt;td&gt;No kuzwenhlama iswakekhelulundle nomba basivikathwazo olokwayimigazilele isizinke isizwe wona nombenza.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chinese&lt;/td&gt;
&lt;td&gt;育 方 他 努 所 普 照 定 家 任.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Looks quite reasonable to me. I don&amp;rsquo;t think I&amp;rsquo;ll use non-Latin-based alphabets, but it&amp;rsquo;s neat to see that the algorithm correctly inferred that Chinese words mostly consist of a single character.&lt;/p&gt;
&lt;p&gt;(Fun fact: DeepL translates the generated mock Chinese text as &amp;ldquo;The United States of America is the only country in the world that has a universal mandate for the protection of human rights.&amp;rdquo; So my code, in all its simplicity, is already on par with modern LLMs when it comes to hallucinating! But the sentence is clearly ambiguous, because Google instead translates it as &amp;ldquo;He strives to make the best use of his talents and to fulfill his family responsibilities&amp;rdquo;&amp;hellip; Chinese is as wonderful as it is mysterious to me.)&lt;/p&gt;
&lt;h2 id="the-first-idea"&gt;The first Idea&lt;/h2&gt;
&lt;p&gt;Now, how do we get from these Markov chains for &lt;em&gt;real&lt;/em&gt; languages to Markov chains for &lt;em&gt;generated&lt;/em&gt; languages, that don&amp;rsquo;t really exist? Here, my Idea is to not create a Markov chain of letters, but of letter &lt;em&gt;classes&lt;/em&gt;: vowels and different types of consonants. When generating words, we substitute each class with a letter from that class. This should reduce the resemblance to the input text, and hopefully combine the strengths of Martin O&amp;rsquo;Leary&amp;rsquo;s algorithm (pronouncable, plausible words) with those of Markov chains (natural word structures).&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll start with these letter classes, grouped roughly using my very limited knowledge of phonetics:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;Letters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;td&gt;vwf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M&lt;/td&gt;
&lt;td&gt;mnñ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;td&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P&lt;/td&gt;
&lt;td&gt;pb&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L&lt;/td&gt;
&lt;td&gt;l&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S&lt;/td&gt;
&lt;td&gt;szjščćž&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;K&lt;/td&gt;
&lt;td&gt;ckq&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;grhx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;aeiouyáéíóúàèìòùâêîôûäëöüï&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Linguists will recoil in horror at this table: I&amp;rsquo;m assuming a 1:1 correspondence between spelling and pronunciation here, which is &lt;em&gt;very wrong&lt;/em&gt; in general. But since the generated languages will be fictional, nobody will know how to pronounce them anyway.&lt;/p&gt;
&lt;p&gt;Now before we create the Markov chain from our input text, we first replace each letter in the text by its class label. This yields a much shorter table, which hopefully captures the word structure (phonotactics) of the input language. For English:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preceding&lt;/th&gt;
&lt;th&gt;Following&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;M 176, T 53, L 43, G 29, S 24, $ 22, K 11, P 11, V 10, A 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG&lt;/td&gt;
&lt;td&gt;A 168, $ 86, G 79, T 64, M 30, S 25, P 11, V 10, K 9, L 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AK&lt;/td&gt;
&lt;td&gt;A 81, L 39, T 36, G 23, $ 14, K 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;hellip;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;^S&lt;/td&gt;
&lt;td&gt;A 50, G 30, T 14, P 4, L 3, K 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;^T&lt;/td&gt;
&lt;td&gt;G 160, A 139&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;^V&lt;/td&gt;
&lt;td&gt;A 93, G 58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;^^&lt;/td&gt;
&lt;td&gt;A 653, T 299, G 205, P 151, V 151, S 103, M 73, K 60, L 27&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Of course, the quality of generated words is pretty hard to judge now:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GAPSAMKA
GAAT
AMT
TG
PGA
AVALTAM
TGALAALA
VAGA
AGAGTA
AVAL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To map these class labels back to actual letters, we use the occurrences of letters in the source language as weights. For example, in English, ‘e’ is the most common vowel, so it&amp;rsquo;ll have the largest probability of being chosen if class ‘A’ is encountered.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GAPSAMKA -&amp;gt; repsancu
GAAT -&amp;gt; hiet
AMT -&amp;gt; unt
TG -&amp;gt; dr
PGA -&amp;gt; pha
AVALTAM -&amp;gt; evaltom
TGALAALA -&amp;gt; thiloela
VAGA -&amp;gt; vere
AGAGTA -&amp;gt; ehirda
AVAL -&amp;gt; owel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It certainly doesn&amp;rsquo;t look like English anymore, but it does look somewhat like a real language.&lt;/p&gt;
&lt;p&gt;Can we still distinguish our input languages from each other?&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Translated sentence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;td&gt;Hepsanco ongs ofe pa ren rot reco beind elesois ba.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dutch&lt;/td&gt;
&lt;td&gt;Hovt fheegerelinoden ta vigste den sen en vaen ieetin iam.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spanish&lt;/td&gt;
&lt;td&gt;Pocrancoenicin proenpracon depdaruehosunil lo ensdes inena ne si o por.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finnish&lt;/td&gt;
&lt;td&gt;Sehviltaan jolnaisee soin ara aos vili alli votu takaon ioldervyte.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serbian&lt;/td&gt;
&lt;td&gt;Jvhća ci dake ejan tgutzeni jve prapag o jen avirlosovo.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zulu&lt;/td&gt;
&lt;td&gt;Zeantahla aselolalu na omgulinpe ekgemte nha ezujwezini oswancywy la wuntgokuto.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We still get some the double vowels typical of Dutch and Finnish, and the rhythm of the Spanish words also seems familiar. English is all over the place, but that&amp;rsquo;s fair, considering it&amp;rsquo;s something of a bastard language.&lt;/p&gt;
&lt;p&gt;However, I&amp;rsquo;m not thrilled about phrases like &amp;ldquo;pocrancoenicin proenpracon depdaruehosunil&amp;rdquo;. Those are altogether too many long words in a sequence.&lt;/p&gt;
&lt;h2 id="word-lengths"&gt;Word lengths&lt;/h2&gt;
&lt;p&gt;Fortunately, I don&amp;rsquo;t want to generate entirely random sentences; I want to &amp;ldquo;translate&amp;rdquo; English into these generated languages. That lets us use the original English sentence structure as a basis, which doesn&amp;rsquo;t have such word sequences. So let&amp;rsquo;s start with a famous English sentence:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It is a truth universally acknowledged, that a single man in possession of a good fortune must be in want of a wife.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For each word, we&amp;rsquo;ll generate a word in our target language that has the same length. I&amp;rsquo;m just doing this brute-force by generating words until one of the right length comes up (allowing a little leeway if this takes too many attempts):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Translated sentence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;td&gt;Dr bo en fenec tholerefend fritemyh, tach mah peamni tga uh hisedehtaos na li arer treseut, roun he ri thes en jer er.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dutch&lt;/td&gt;
&lt;td&gt;Ge in ca aweri everdied entermekgtih, tico en ainven ofd hol ollosejhan di av bisn injalto, acht an an folj in in eint.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spanish&lt;/td&gt;
&lt;td&gt;Ra lu ó cecom cenpinamenete esórvecre, dede o dochoi dos ce laprecopiál ta e ale vinstaz, heho se ne con la am lasal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finnish&lt;/td&gt;
&lt;td&gt;An os tä kakina parsikejta kiimeittevyson, esin ke käatan han nos tiklunnäsvyn hoam te etun ehdanta, ahten ta ut naat on dän keyn.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serbian&lt;/td&gt;
&lt;td&gt;I an i jvono drjulmarlen epstiti, nuvo u ojonsu sui vo sugide na i ali miluvok, slodo a eno nina si ta pidu.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zulu&lt;/td&gt;
&lt;td&gt;Le wu ki inhu ecazikekelwy kwytshakhabu, ongo eo akwale eenwu na unbhedhutvi kwy mu okga aswoche, aswa ma nha ano esi awo langi.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In real languages, of course, word lengths are not the same between languages. Some, like Greenlandic, are famous for their long words – the longest Greenlandic word in our input text is &amp;ldquo;suliffissaaruttoornaveersaartitaanissamut&amp;rdquo; at 41 letters. Other languages might consist mainly of shorter words. The distribution of word lengths is a significant part of each language&amp;rsquo;s characteristic look and feel.&lt;/p&gt;
&lt;p&gt;To incorporate word length into our generator, we&amp;rsquo;ll start by tallying how often each word length occurs in the input text:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/8/9/around-the-world-19-constructing-languages/word_lengths.svg" alt="Chart of word length for English and Greenlandic words" &gt;
&lt;/p&gt;
&lt;p&gt;Now, rather than just drawing random samples from this distribution, I would prefer that short words in the input sentence remain short in the translation, and long words remain long. So we want to somehow &amp;ldquo;remap&amp;rdquo; word lengths from English to Greenlandic. We can do that by looking at quantiles. For example, say we want to know the appropriate word length for the translation of the 5-letter word &amp;ldquo;truth&amp;rdquo; into Greenlandic. First we calculate: what &lt;em&gt;proportion&lt;/em&gt; of English words are shorter than 5 letters? Let&amp;rsquo;s say it&amp;rsquo;s 30%. We then generate a word such that (roughly) 30% of Greenlandic words are shorter than that word. Let&amp;rsquo;s see if it works:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Translated sentence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;td&gt;Dr bo en fenec tholerefend fritemyh, tach mah peamni tga uh hisedehtaos na li arer treseut, roun he ri thes en jer er.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dutch&lt;/td&gt;
&lt;td&gt;Ren sen ca tikgap werivelcanekeahenbeg entermekgtih, hienr ep oevenr eem ofd jirveredelazde rie lat vejn eateewd, vomjm wra ran oafel nen vom hemhe.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spanish&lt;/td&gt;
&lt;td&gt;Ra lu ó irduro cenpinamenete intesteea, qorda o dochoi baro on pehtelicil ne a toin dadroccóe, aserud la am tyine de i upah.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finnish&lt;/td&gt;
&lt;td&gt;Syan enui an eramion attinellodaselu ogtemkoättistima, seenoin ha sästaetta allaäte han tiklunnäsvyn hoam suen joenjam pätassaltta, ehdanta ahten oon teunnat naat on oulten.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serbian&lt;/td&gt;
&lt;td&gt;I an i iparat šadodisamevnu majbesnapejte, nuvo o ikasta jrinto ti etnujanajesi šo a potća bravirač, nina in am žlijo ku a moko.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zulu&lt;/td&gt;
&lt;td&gt;Nahle mhulule inhu ecazikekelwy mzamhaputhakwuna kwytshakhabu, enhylali lola oaudhule ajekwonka ikacge asuwezwumgle aakula axlu emebagte nokuthenpha, iswucusu lelolu bosanga kvemhike nhalu awo ecalwenhu.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;It seems to do the trick. In particular, mock Finnish and mock Zulu have become considerably more representative of their origins.&lt;/p&gt;
&lt;h2 id="supercalifragilisticexpialidocious"&gt;Supercalifragilisticexpialidocious&lt;/h2&gt;
&lt;p&gt;Oh, did someone say Greenlandic? Alright, let&amp;rsquo;s have it:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Translated sentence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Greenlandic&lt;/td&gt;
&lt;td&gt;Tiaqarnit inatenraaotila unat eijsataluunmuit alitiunimniasagsuritriluanakait qaemnetirsinasasesuitugsiitaissi, tinmgofvaimalli senuq pernuiffainmiinaqqanmaq iittiuvfaqkit qaniaremi inmappamnainnainniqalluat pississok pasat uppagaiteautaanulit ugniqissannassinnut, anmriunnatsiniq anuitirtaq pasat turanissiinmut tiavfakirat mutit pasjanikiaraaq.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;However, there&amp;rsquo;s a subtlety that I&amp;rsquo;m ignoring here: languages with long words typically pack more meaning into each single word. For example, the aforementioned &amp;ldquo;suliffissaaruttoornaveersaartitaanissamut&amp;rdquo; appears to mean &amp;ldquo;protection against unemployment&amp;rdquo;, so it does the work of three English words all by itself. Even then, Greenlandic is relatively verbose: the entire Universal Declaration of Human Rights text contains 58% more characters than its English counterpart.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t want to get into such merging of words, but I do want to scale the word length back a bit, so that &lt;em&gt;on average&lt;/em&gt; a generated Greenlandic word is 58% longer than the original English. It seems to work – words are still long, but no longer outrageously so:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Translated sentence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;td&gt;Dr bo en fenec tholerefend fritemyh, tach mah peamni tga uh hisedehtaos na li arer treseut, roun he ri thes en jer er.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Greenlandic&lt;/td&gt;
&lt;td&gt;Akanit kalla mi tigninnik eessutnakusinngat isunalilliirutonamu, aronnrit ania taqirfuk umagtat qigat tiqalarirnililli pasat taq pissutet anuitirtaq, anaila inniat mutit nakugeemut tiutat tiak samakcali.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="the-second-idea"&gt;The second Idea&lt;/h2&gt;
&lt;p&gt;For each input language, we now have:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a Markov chain for letter classes,&lt;/li&gt;
&lt;li&gt;the probability of each letter within each letter class,&lt;/li&gt;
&lt;li&gt;the distribution of word lengths.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The second Idea is that we can mix and match these between languages. For example, this could produce a language with the rhythm of Italian, the letters of Turkish, and the word lengths of Greenlandic.&lt;/p&gt;
&lt;p&gt;For that to work well, we need some more languages in the mix. So I hand-picked over 30 languages that can reasonably be rendered in an extended Latin alphabet, trying to cover a broad spectrum of language families as based on &lt;a href="https://www.omniglot.com/udhr/index.htm"&gt;this website&lt;/a&gt;. For Abkhaz, Arabic, Chinese, Greek, Japanese, Russian and Ukrainian, I used the &lt;code&gt;uroman&lt;/code&gt; Python package and cleaned up the output manually.&lt;/p&gt;
&lt;p&gt;After implementing the random mixing and matching, we have over 50,000 possible fictional languages. We can now also make each language generate its own name!&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Origin&lt;/th&gt;
&lt;th&gt;Translated sentence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sashhomuz&lt;/td&gt;
&lt;td&gt;Abkhaz + Hani + Uzbek&lt;/td&gt;
&lt;td&gt;Ae ti es eshi uaahahu uhutalido, aee yi dgoq ydeo cho onashulexds aqx ti aaio ilaqhu, ojny yim din oshi ea eb eliq.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drli&lt;/td&gt;
&lt;td&gt;Hakha Chin + Serbian + English&lt;/td&gt;
&lt;td&gt;Me um i orto rmokinraoldo domrmir, nuk or rnoika ćai lo rmecno li i lavr kaimok, airr li li duuk ng a nrad.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lotolis&lt;/td&gt;
&lt;td&gt;Hungarian + Russian + Finnish&lt;/td&gt;
&lt;td&gt;Nic mon vo elazuro otelidenecmeoll satihasapamily, bolicc a zsosihen tisnyn sa kudisetereht kit i viledes yllantaho, osatsyk ilu lin ssen lin suh ventab.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brvah&lt;/td&gt;
&lt;td&gt;Irish + Turkish + Uzbek&lt;/td&gt;
&lt;td&gt;Ir eys ü ardrem ırseedheas ütarrriihsethıç, krin ğü ketril bian emi bıekrenhse hkr t iıne ereeh, beıl lyn bra anti rus an fart.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nespasto&lt;/td&gt;
&lt;td&gt;Quechua + English + Lithuanian&lt;/td&gt;
&lt;td&gt;Osfes hoam as asllenam cicremaocope cieuacoscillis, llasko to vifehonto toenes com boscaaetetu tece os ronuma nanacal, cenan min cobi moiene es cona llince.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mahgi&lt;/td&gt;
&lt;td&gt;Edo + Xhosa + Kurdish&lt;/td&gt;
&lt;td&gt;Ei ao en wanfan nege agpanvan, kan e kaghe ohaen ni ohaqho kgy e uxeu inwigpo, uabe su ta awbe no u inwen.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tahiwk&lt;/td&gt;
&lt;td&gt;Hani + Huasteco + Kikongo&lt;/td&gt;
&lt;td&gt;Má nal jac baaik piulnákkoil noaanák, cyal bé céewik caaw ka jilpa bá bi pyjyl maéwc, lakhha ia la táawk i sa lakjek.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I like it! These languages are fairly plausible, yet reasonably distinctive.&lt;/p&gt;
&lt;p&gt;It would be possible to mix even harder, for example by choosing each character class from a different language, or by constructing a new Markov chain from the trigrams of multiple languages merged together. But I&amp;rsquo;ll leave it at this for now.&lt;/p&gt;
&lt;h2 id="place-names"&gt;Place names&lt;/h2&gt;
&lt;p&gt;Now that we can generate languages, and words within each language, it&amp;rsquo;s a small step to produce names of cities. Most of the time, these will be single words, but sometimes we&amp;rsquo;ll want to generate a composite name like &amp;ldquo;Dar es Salaam&amp;rdquo; or &amp;ldquo;The Hague&amp;rdquo;. I did have to impose a limit of 20 letters for this not to get out of hand.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Origin&lt;/th&gt;
&lt;th&gt;Generated city names&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sashhomuz&lt;/td&gt;
&lt;td&gt;Abkhaz + Hani + Uzbek&lt;/td&gt;
&lt;td&gt;Znexkha, Aiylolo, Omyiheop, Yeyhi, Amimeh, Esunsou, Aihegai-Amuihui, Ieno-Ilejhe, Om-Osamimaala, Ipshi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drli&lt;/td&gt;
&lt;td&gt;Hakha Chin + Serbian + English&lt;/td&gt;
&lt;td&gt;Rlenkrim, Tiasojal, Krot-Mre-Ninta, Kgaor-Denrkan, Liolla, Kaomrtir, Diulrni, Kop Jijela, Nintumr, Zoldin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lotolis&lt;/td&gt;
&lt;td&gt;Hungarian + Russian + Finnish&lt;/td&gt;
&lt;td&gt;Zsynintak, Zzihoga, Sayhek, Venkiom, Sasso, Va-Valtosas, Ellak, Zssat, Nentos, Tok-Kolehik&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brvah&lt;/td&gt;
&lt;td&gt;Irish + Turkish + Uzbek&lt;/td&gt;
&lt;td&gt;Rcrtriymta, Us-Rımtram, Kht-Iaimy, Ahkitr, Iıhfarb, Bhaapha, Deutr, Leene, Lianmü, Etr-Rğenna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nespasto&lt;/td&gt;
&lt;td&gt;Quechua + English + Lithuanian&lt;/td&gt;
&lt;td&gt;Sebayni, Ogane, Lloes, Cintoe-Ates, Ebeshiscoe, Lenes, Comeno Sas Cafpeo, Cgoon-Llonim, Mynocanaj, Chinemim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mahgi&lt;/td&gt;
&lt;td&gt;Edo + Xhosa + Kurdish&lt;/td&gt;
&lt;td&gt;Goaemwon, Idymnwyna, Fewon, Unnwun-Iwboe, Ovonwan, Uluhu, Agaunkufu, Aewba, Hhum Nizukheke, Igei-Iwenfaunae&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tahiwk&lt;/td&gt;
&lt;td&gt;Hani + Huasteco + Kikongo&lt;/td&gt;
&lt;td&gt;Jsal, Siak-Cáwk, Nekkawc, Xial, Kakyw-Naek-Keaow, Aac Lácáyl, Baol, Aí Keuw, Jakbail Taw Naáktak, Jjylcea&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I can just imagine the player finally learning Sashhomuz in the city of Aiylolo, only to be confounded when the next city over, Sayhek, turns out to speak Lotolis instead&amp;hellip;&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 18: Charting the seas</title><link>https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/</link><pubDate>Wed, 24 Jul 2024 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/</guid><description>&lt;p&gt;Maps will be an important part of the game. Let&amp;rsquo;s take a look at how we can draw those!&lt;/p&gt;
&lt;p&gt;By &amp;ldquo;maps&amp;rdquo;, I don&amp;rsquo;t mean your usual in-game map, which is always there in the corner of the screen, tracks your position and orientation, knows all parts of the world, and is never wrong. No, I&amp;rsquo;m talking about the kind of maps that sailors in the age of discovery actually used: sketchy, partly based on hearsay, woefully incomplete, often just plain wrong, and with no built-in GPS to tell you where on the map you are.&lt;/p&gt;
&lt;p&gt;In the real world, it takes a lot of effort to remove incompleteness and wrongness. In our perfect computer-generated world, it takes effort to &lt;em&gt;add&lt;/em&gt; them. So let&amp;rsquo;s first try to come up with a complete and correct map instead.&lt;/p&gt;
&lt;p&gt;(Aside: if you like reading about procedural map generation, I can &lt;em&gt;highly&lt;/em&gt; recommend &lt;a href="https://heredragonsabound.blogspot.com/"&gt;Here Dragons Abound&lt;/a&gt; by Scott Turner. Scott&amp;rsquo;s map generator does &lt;em&gt;way&lt;/em&gt; more than I can ever hope to achieve, and he blogs about it in detail.)&lt;/p&gt;
&lt;h2 id="maps-maps"&gt;Maps? Maps?!&lt;/h2&gt;
&lt;p&gt;Actually, using the word &amp;ldquo;map&amp;rdquo; is not quite right at best, and almost blasphemy at worst. The correct term is &amp;ldquo;chart&amp;rdquo;. NOAA&amp;rsquo;s National Ocean Service is &lt;a href="https://oceanservice.noaa.gov/facts/chart_map.html"&gt;almost condescending&lt;/a&gt; towards maps:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A chart is used by mariners to plot courses through open bodies of water as well as in highly trafficked areas. Because of its critical importance in promoting safe navigation, the nautical chart has a certain level of legal standing and authority. A map, on the other hand, is a reference guide showing predetermined routes like roads and highways.&lt;/p&gt;
&lt;p&gt;Nautical charts provide detailed information on hidden dangers to navigation. Maps provide no information of the condition of a road.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here&amp;rsquo;s an example of a typical chart. This one was drawn by Johannes Jansson around 1650:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/atlantic_jansson.webp" alt="Jansson map of the Atlantic" &gt;
&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a good example of the style I&amp;rsquo;ll be aiming for.&lt;/p&gt;
&lt;h2 id="charting-the-coast"&gt;Charting the coast&lt;/h2&gt;
&lt;p&gt;We have a height map of our world, so the easiest way to produce a topographical map (and yes, this is more like a map than a chart) is to apply a colour scale to these heights:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/height_map.png" alt="Height map" &gt;
&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s start, but it&amp;rsquo;s still a far cry from Jansson&amp;rsquo;s beautiful work. Our height map focuses on the land, whereas nautical charts usually had very little detail on land; only features that are helpful in navigation were drawn. So let&amp;rsquo;s start by detecting coastlines from the height map using a marching squares algorithm. Drawing the coastlines on a nice parchment-like texture:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/coastlines.png" alt="Coastlines" &gt;
&lt;/p&gt;
&lt;p&gt;Okay, that looks more like it. But without prior knowledge, it&amp;rsquo;s hard to distinguish land from water. The old charts address this in several ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Shade either land or sea, often only along the coast. Or shade both with a different colour.&lt;/li&gt;
&lt;li&gt;Draw rhumb lines (those criscrossing compass lines) only on the sea.&lt;/li&gt;
&lt;li&gt;Place city labels only on land.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I&amp;rsquo;d like my charts to have some variety, so I&amp;rsquo;ll implement at least the first two. Because I&amp;rsquo;m detecting contours as closed loops anyway, it&amp;rsquo;s easy to give each landmass its own distinct colour:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/land_color.png" alt="Coloured land" &gt;
&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s more readable, but perhaps a bit over the top. How about shading only near the coast? This requires me to implement a distance transform, which thankfully is &lt;a href="https://pure.rug.nl/ws/portalfiles/portal/14632734/c2.pdf"&gt;well documented&lt;/a&gt; (Meijster, Roerdink, Hesselink, 2002):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/land_edge_color.png" alt="Coloured land edges" &gt;
&lt;/p&gt;
&lt;p&gt;That looks a lot more professional. Let&amp;rsquo;s also add a windrose network to fill up all that empty space:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/windrose_network.png" alt="Windrose network" &gt;
&lt;/p&gt;
&lt;h2 id="lakes"&gt;Lakes&lt;/h2&gt;
&lt;p&gt;We still have a small problem:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/lakes.png" alt="Chart showing lakes" &gt;
&lt;/p&gt;
&lt;p&gt;The coastline detector detects &lt;em&gt;every&lt;/em&gt; transition between land and water, including ones inside a continent. These are lakes, and would historically not have been included in a coastal survey. More importantly, they make the chart look cluttered and aren&amp;rsquo;t reachable by the player anyway.&lt;/p&gt;
&lt;p&gt;Fortunately, it&amp;rsquo;s easy to detect whether a polygon is a lake or a coastline, because one will be oriented clockwise and the other counterclockwise, so we can just get rid of them:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/no_lakes.png" alt="Chart without lakes" &gt;
&lt;/p&gt;
&lt;p&gt;Unfortunately, this leaves us with another problem, because the land colouring is based on the original land/sea matrix, so it isn&amp;rsquo;t aware that we removed some polygon outlines. A second, smaller problem is that islands &lt;em&gt;inside&lt;/em&gt; lakes are still drawn; see the yellow spot just above the &amp;lsquo;k&amp;rsquo; in &amp;lsquo;Gdansk&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;A more robust approach is to fill lakes in the land/sea matrix, effectively replacing them by land &lt;em&gt;before&lt;/em&gt; we trace the coastlines. Let&amp;rsquo;s classify as a lake every water area that&amp;rsquo;s fully surrounded by land. This does mean that lakes touching the edge of the chart won&amp;rsquo;t be removed, but we&amp;rsquo;ll deal with that later. It nicely gets rid of lake polygons, islands inside lakes, and land colouring in one fell swoop:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/no_lake_islands.png" alt="Chart without lake islands" &gt;
&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s looking a lot cleaner.&lt;/p&gt;
&lt;h2 id="ports"&gt;Ports&lt;/h2&gt;
&lt;p&gt;So far, I&amp;rsquo;ve been using a tiny placeholder icon to represent cities:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/CityIcon.png" alt="Placeholder city icon" &gt;
&lt;/p&gt;
&lt;p&gt;Surely we can do better. Somebody by the name of Zarkonnen recently published a &lt;a href="https://zarkonnen.itch.io/extracted-1688-map-images"&gt;great asset pack&lt;/a&gt; of map icons extracted from a 1688 German map:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/city_icons.png" alt="City icons" &gt;
&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/town_icons.png" alt="Town icons" &gt;
&lt;/p&gt;
&lt;p&gt;The city icons are probably too big and detailed for our purposes, but the town icons fit nicely:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/map_with_city_icons.png" alt="Chart with port icons" &gt;
&lt;/p&gt;
&lt;p&gt;However, their placement leaves something to be desired. The icons currently overlap the coastline too much, showing half the city in the water.&lt;/p&gt;
&lt;p&gt;Right now, the icon sprite is centered on the location of the city. However, the icon is quite a bit bigger than the actual city would be on the chart, causing it to stick out into the sea. Let&amp;rsquo;s try moving each icon a little in each of 8 directions, and pick the one that gives the greatest overlap with land. To make it clear what&amp;rsquo;s happening, I added some debug drawing:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/icon_placement.png" alt="Chart with icon placement" &gt;
&lt;/p&gt;
&lt;p&gt;This shows the actual city location in red, and the candidate icon positions in blue. The green rectangle indicates the portion of the icon that we try to keep on land. The algorithm seems to be doing a reasonable job. It sometimes puts the icon in a slightly misleading position (most notably Zeebrugge), but this does not seem to happen a lot, and in-game we can just blame it on the chart author!&lt;/p&gt;
&lt;h2 id="name-labels"&gt;Name labels&lt;/h2&gt;
&lt;p&gt;You may already have spotted the other problem: the city name labels also overlap coastlines, and each other. Let&amp;rsquo;s tackle that next.&lt;/p&gt;
&lt;p&gt;In general, this is a hard problem to solve, because every adjustment you make to the placement of a single element might cause it to overlap other elements, which would then also need to be moved, which might have a cascading effect. The aforementioned Here Dragons Abound started with a &lt;a href="https://heredragonsabound.blogspot.com/2017/04/use-force-layout-luke.html"&gt;force layout&lt;/a&gt; algorithm to solve this, but eventually switched to &lt;a href="https://heredragonsabound.blogspot.com/2017/05/simulated-annealing.html"&gt;simulated annealing&lt;/a&gt;. That&amp;rsquo;s a rather big hammer though, so I&amp;rsquo;ll try something simpler first.&lt;/p&gt;
&lt;p&gt;First, I&amp;rsquo;ll give each label a few possible placement positions, and check which of those gives the least amount of overlap with the coastlines:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/labels_avoid_coastlines.png" alt="Labels avoiding coastlines" &gt;
&lt;/p&gt;
&lt;p&gt;Additionally, there is a weight factor assigned to each of the placements, to give some preference to the label being put to the right of the icon, instead of to the left, above or below.&lt;/p&gt;
&lt;p&gt;This algorithm makes a valiant attempt to avoid coastlines, but it doesn&amp;rsquo;t yet avoid overlap between labels. For that, we can simply add previously drawn labels to the area that should be avoided:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/labels_avoid_labels.png" alt="Labels avoiding labels" &gt;
&lt;/p&gt;
&lt;p&gt;You can see that the Qingdao label has moved to make room for Jubail. I&amp;rsquo;m not happy with the outcome yet, but with a smaller font size, hopefully such problems won&amp;rsquo;t occur frequently. So let&amp;rsquo;s increase the chart&amp;rsquo;s resolution, meaning all icons and labels get smaller:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/7/24/around-the-world-18-charting-the-seas/hi_res.png" alt="Higher-resolution chart" &gt;
&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s starting to look presentable. In the game, the player will be able to zoom in on these charts, so I&amp;rsquo;m not worried about drawing text at very small sizes.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m sure I&amp;rsquo;ll revisit chart drawing in the future, wherever the gameplay demands it: mountains, trade goods, hazards, names of regions and coastal features, and of course the mysterious X that marks the spot.&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 17: World generation in the prototype</title><link>https://frozenfractal.com/blog/2024/6/27/around-the-world-17-world-generation-in-the-prototype/</link><pubDate>Thu, 27 Jun 2024 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2024/6/27/around-the-world-17-world-generation-in-the-prototype/</guid><description>&lt;p&gt;The code repository in which I&amp;rsquo;m developing Around The World is called &lt;code&gt;aroundtheworld4&lt;/code&gt;. That might make you wonder: what happened to the first three? Today, let&amp;rsquo;s take a look at &lt;code&gt;aroundtheworld2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This was the version I made in 48 hours for the Alakajam game jam, and the only one that has been publically released. I&amp;rsquo;ve been referring to it on this blog as &amp;ldquo;the game jam prototype&amp;rdquo;. You can &lt;a href="https://aroundtheworld.frozenfractal.com/"&gt;play it in your browser&lt;/a&gt; right now, which I would recommend, but &lt;a href="https://alakajam.com/10th-alakajam/1014/around-the-world/"&gt;you don&amp;rsquo;t have to take my word for it&lt;/a&gt;. (I&amp;rsquo;m not sure it fully works on mobile, so use a computer with a mouse if you can.)&lt;/p&gt;
&lt;p&gt;In this post, I&amp;rsquo;ll explain the procedural generation of the world in that game. This is an updated version of an &lt;a href="https://alakajam.com/post/1382/world-generation-in-around-the-world"&gt;article&lt;/a&gt; I wrote earlier on the Alakajam website.&lt;/p&gt;
&lt;h2 id="generating-the-world-map"&gt;Generating the world map&lt;/h2&gt;
&lt;p&gt;In this early version, the world wasn&amp;rsquo;t a sphere; rather, it wrapped around left and right, and the poles were inaccessible, so it was technically a cylinder. Here&amp;rsquo;s the end result:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/6/27/around-the-world-17-world-generation-in-the-prototype/world-map.png" alt="Map of world 0 in the Around The World prototype" &gt;
&lt;/p&gt;
&lt;p&gt;The world size is 300×150 tiles; each pixel in this image represents one tile. I chose 300 because it gives some margins when displaying the map at the 320×200 resolution of the game. I chose the 2:1 ratio because it&amp;rsquo;s how an &lt;a href="https://en.wikipedia.org/wiki/Equirectangular_projection"&gt;equirectangular projection&lt;/a&gt; of a sphere (like the Earth) is usually displayed, with one degree of latitude being the same size on the map as one degree of longitude.&lt;/p&gt;
&lt;p&gt;Water depth is shown on the maps as four shades of blue, but in-game you can&amp;rsquo;t see depth; only one tile sprite is used for all water. This is because I had plans to make your ship run aground if you tried to enter shallows, so you had to use maps to avoid that. I&amp;rsquo;m glad I never got round to that, because it would probably have been too hard! But the different shades looked pretty on the maps, so I kept them.&lt;/p&gt;
&lt;p&gt;The base of the world generation is, as you might have guessed, simplex noise, powered by Godot&amp;rsquo;s &lt;a href="https://docs.godotengine.org/en/3.2/classes/class_opensimplexnoise.html"&gt;OpenSimplexNoise&lt;/a&gt; class. We can configure, among others:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the number of octaves: fewer octaves for a smoother result, more octaves for a more jagged result;&lt;/li&gt;
&lt;li&gt;the period: a larger period gives larger continents&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To make it wrap, we need to call &lt;code&gt;get_seamless_image&lt;/code&gt; to create a 300×300 image, then crop it to 300×150. The result is an image with monochrome pixel values between 0 and 255. Most of the values are around 128. We are going to interpret these values as a height map, larger values being higher.&lt;/p&gt;
&lt;h2 id="filling-the-oceans"&gt;Filling the oceans&lt;/h2&gt;
&lt;p&gt;If we simply used 128 as the threshold to decide between water and land, about half the map would be water and half would be land, and it almost certainly wouldn&amp;rsquo;t be circumnavigable! So in the code I have a variable &lt;code&gt;WATER_FRACTION&lt;/code&gt;, which lets me tweak what portion of the map should be water. In the end I set it to 0.75, which is slightly more than the 71% of our own planet, but always resulted in at least one possible route around the world in my tests, so I didn&amp;rsquo;t bother to actually code a check for this. This means there are probably seeds that are unwinnable!&lt;/p&gt;
&lt;p&gt;To figure out the desired water level, the code first loops through all pixels and creates a histogram, counting how often each of the 256 values occurs:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[ 0] 0
...
[126] 1894
[127] 2645
[128] 3642
[129] 3528
[130] 2974
[131] 1490
...
[255] 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then it runs through this array, adding each value to an accumulator. When the accumulator exceeds the desired number of water pixels, which is 300×150×0.75 = 33750, we have found our water level. Let&amp;rsquo;s say it&amp;rsquo;s 130 for this example.&lt;/p&gt;
&lt;h2 id="adding-ice-caps"&gt;Adding ice caps&lt;/h2&gt;
&lt;p&gt;Now we need to create the poles, because the top and bottom of the map must not be traversable (this world is a cylinder, after all). To do this, a bias is added to the height of each pixel, where the bias depends on the &lt;code&gt;y&lt;/code&gt; coordinate like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bias = 255 * pow((abs(2 * y / 149 - 1) - 1) * 10 + 1, 3)
if bias &amp;gt; 0:
pixel += bias
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Formulas like this are a very powerful tool in procedural generation, but they&amp;rsquo;re harder to read than they are to write (drawing graphs on paper helps!). Yet it&amp;rsquo;s built from a few basic primitives, so let&amp;rsquo;s break it down from the inside out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt; is between 0 and 149, inclusive. So &lt;code&gt;y / 149&lt;/code&gt; is between 0 and 1, inclusive.&lt;/li&gt;
&lt;li&gt;Multiply by 2, subtract 1, to get it between -1 (north pole) and 1 (south pole).&lt;/li&gt;
&lt;li&gt;Take the &lt;code&gt;abs&lt;/code&gt;olute value to get it between 0 (equator) and 1 (either pole).&lt;/li&gt;
&lt;li&gt;Subtract 1 again to get between -1 (equator) and 0 (pole).&lt;/li&gt;
&lt;li&gt;Multiply by 10 (a configurable constant which decides the size of the poles) to get between -10 (equator) and 0 (pole).&lt;/li&gt;
&lt;li&gt;Add 1 to get between -9 and +1.&lt;/li&gt;
&lt;li&gt;Raise to the power of 3 because it looked nicer that way? I forget.&lt;/li&gt;
&lt;li&gt;Multiply by 255 to make sure that we get a bias of 255 on the poles, which guarantees that they&amp;rsquo;ll be impassable.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="colouring-the-map"&gt;Colouring the map&lt;/h2&gt;
&lt;p&gt;Next, the colours are assigned. Level 130 has a height of 0 above water level, so we say that this is the beach (yellow). The three levels above this (131, 132, 133) become light green, the next three become middle green, and so on. A similar thing happens for negative heights, which are below water. As an exception, if the pixel is near the poles (&lt;code&gt;y&lt;/code&gt; close to 0 or to 149), it becomes ice (blueish white), but we add the height to &lt;code&gt;y&lt;/code&gt; to avoid a sharp horizontal line between ice and non-ice. The result is that inland ice occurs farther from the poles than coastal ice, which makes sense because the ocean has a warming effect. Ice is not just cosmetic but also serves a gameplay purpose: it lets the player know that they are getting close to the pole and will be blocked if they go much farther in that direction.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/6/27/around-the-world-17-world-generation-in-the-prototype/map-screen.png" alt="Map screen in the Around The World prototype" &gt;
&lt;/p&gt;
&lt;h2 id="placing-ports"&gt;Placing ports&lt;/h2&gt;
&lt;p&gt;That&amp;rsquo;s it for the terrain. Now let&amp;rsquo;s place the ports. There are up to 100 of them. I searched the web for a list of seaport names and found an Excel sheet with over 100 names, which I cleaned up a bit and copied into my code. (In jams especially, I often don&amp;rsquo;t bother opening files or parsing data, I just turn the data into a literal that can be pasted directly into a script. JSON in particular is valid source code in several languages!)&lt;/p&gt;
&lt;p&gt;For each of the 100 ports, first we generate a random &lt;code&gt;x, y&lt;/code&gt; coordinate pair as our starting point. If it&amp;rsquo;s land, it might be landlocked: too bad, try again! If it&amp;rsquo;s sea, we start a random walk. Each step, we move one pixel north, south, east or west. If it&amp;rsquo;s now on land, it must be coast because it was previously water. We&amp;rsquo;d like to place our port here, but first we check if there&amp;rsquo;s already another one within 4 tiles. If not, we place it, otherwise we give up and try again. If we haven&amp;rsquo;t found land after 75 steps, we give up and try again from a different starting point, up to 100 times. As an exception to avoid lots of polar cities (which would be unrealistic and might also be game-breaking), we also abort when we get close to the poles.&lt;/p&gt;
&lt;h2 id="adding-port-inventory"&gt;Adding port inventory&lt;/h2&gt;
&lt;p&gt;Finally, the port&amp;rsquo;s inventory is decided. One random cargo type is picked as supply (with 2-7 units) and two different ones are picked as demand (at twice the regular price). We need to make sure that these are all unique, because it makes no sense to both supply a good and demand it. Your first intuition might be to pick a random type until you&amp;rsquo;ve found one that&amp;rsquo;s not used yet, but there&amp;rsquo;s an easier way: simply create an array of all possible types, shuffle it, and pop items off of it. For large arrays and small numbers of samples, this is obviously rather inefficient, but for small arrays it&amp;rsquo;s fine, and it makes the code a lot easier to write.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/6/27/around-the-world-17-world-generation-in-the-prototype/city-screen.png" alt="City screen in the Around The World prototype" &gt;
&lt;/p&gt;
&lt;p&gt;As to equipment, in 60% of cases, a map of random size is offered for sale; in 40% of cases, one of the three powerups is offered. (In the game, you might encounter ports without any equipment. This is because when you buy the Binoculars, any Binoculars in other ports are upgraded to Telescopes, and when you buy the Telescope, all remaining Telescopes are deleted.)&lt;/p&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;As the very last step, the game decides what your starting port is going to be. This is not simply a random port! I wanted to make the player start in the &amp;ldquo;easiest&amp;rdquo; part of the map, so I added some code that counts for each port how many ports are within 15 tiles distance from it. The port with the most such neighbours becomes the starting port, and the player&amp;rsquo;s ship is placed in the middle of an adjacent sea tile.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it! I hope you enjoyed the read; now &lt;a href="https://aroundtheworld.frozenfractal.com/"&gt;go and play the prototype&lt;/a&gt; if you haven&amp;rsquo;t already! I&amp;rsquo;ll be back in the next post with more updates on the new, in-progress version of the game.&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 16: A matter of scale</title><link>https://frozenfractal.com/blog/2024/6/18/around-the-world-16-a-matter-of-scale/</link><pubDate>Tue, 18 Jun 2024 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2024/6/18/around-the-world-16-a-matter-of-scale/</guid><description>&lt;p&gt;All else being equal, I prefer games (and books, and movies) to be realistic, rather than making things up on the spot. But of course, all else is rarely equal. Today, I&amp;rsquo;ll be taking away some of the realism of my procedural world generator to accommodate gameplay.&lt;/p&gt;
&lt;h2 id="the-trouble"&gt;The trouble&lt;/h2&gt;
&lt;p&gt;Douglas Adams might have written:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Earth is big. You just won&amp;rsquo;t believe how vastly, hugely, mind-bogglingly big it is. I mean, you may think it&amp;rsquo;s a long way down the road to the chemist&amp;rsquo;s, but that&amp;rsquo;s just peanuts to Earth.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Planet Earth has a radius of 6371 km, a circumference of about 40,000 km. (That&amp;rsquo;s no coincidence: the metre was originally defined as 1/10,000,000th of the distance from the pole to the equator over Paris.) The first expedition to sail around the world was Magellan&amp;rsquo;s, and took this route:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/6/18/around-the-world-16-a-matter-of-scale/Magellan_Elcano_Circumnavigation-en.svg" alt="Map of Magellan&amp;rsquo;s expedition" &gt;
&lt;br&gt;
&lt;em&gt;Source: &lt;a href="https://commons.wikimedia.org/wiki/File:Magellan_Elcano_Circumnavigation-en.svg"&gt;Wikimedia Commons&lt;/a&gt; by Sémhur and Uxbona, CC-BY-SA 3.0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A 15th-century carrack like the ones Magellan used would be about 25 metres long and 25 metres tall:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/6/18/around-the-world-16-a-matter-of-scale/Nao_Victoria.jpg" alt="Photo of a replica of a sailing ship" &gt;
&lt;br&gt;
&lt;em&gt;Replica of the carrack (nao) &lt;em&gt;Victoria&lt;/em&gt;, the first ship ever to circumnavigate the world. Source: &lt;a href="https://en.wikipedia.org/wiki/File:Nao_Victoria.jpg"&gt;Wikimedia Commons&lt;/a&gt; by Gnsin, CC-BY-SA 3.0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;At sea, standing in the crow&amp;rsquo;s nest of such a ship on a clear day, the horizon &lt;a href="https://www.metabunk.org/curve/"&gt;would be&lt;/a&gt; about 20 km away. But interesting things like ships and islands are tall, and you&amp;rsquo;d be able to see them beyond the horizon as well, so let&amp;rsquo;s say your maximum view distance is 50 km. Those are the numbers I originally plugged into the game.&lt;/p&gt;
&lt;p&gt;However, remember that I &lt;a href="https://frozenfractal.com/blog/2024/3/19/around-the-world-12-2d-or-not-2d/"&gt;chose&lt;/a&gt; to show the world from a top-down, bird&amp;rsquo;s eye perspective. When fully zoomed out, at a common resolution of 1920×1080, one pixel on the screen would be about 50 metres in the world. The ship would just be half a pixel! You&amp;rsquo;d need to zoom in by a fair amount until you can even see that it&amp;rsquo;s actually a ship.&lt;/p&gt;
&lt;p&gt;It gets worse.&lt;/p&gt;
&lt;p&gt;Magellan&amp;rsquo;s fleet took about three years to circumnavigate the world, averaging less than 2.5 km/h. I&amp;rsquo;m aiming for a single run of the game to take about three &lt;em&gt;hours&lt;/em&gt; instead, so I&amp;rsquo;m compressing time by a factor of 10,000 (8,760 for the pedants, but this isn&amp;rsquo;t an exact science). To accomplish that, the player&amp;rsquo;s ship would need to move at a speed of 25,000 km/h — that&amp;rsquo;s 20 times the speed of sound, and well on its way to escape velocity towards outer space. In a single second, you&amp;rsquo;d be traversing 7 km. For a half-pixel ship, that would certainly look like &lt;a href="https://www.youtube.com/watch?v=ygE01sOhzz0&amp;amp;t=19s"&gt;ludicrous speed&lt;/a&gt;. I&amp;rsquo;m not trying to build Kerbal Sea Program here!&lt;/p&gt;
&lt;h2 id="the-solution"&gt;The solution&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m going to have to shrink the world. A lot. After some tinkering with a &lt;del&gt;spreadsheet&lt;/del&gt; highly advanced game design tool, I figured I&amp;rsquo;d make the world scale 1:100. A 400 km circumference might sound small, but it&amp;rsquo;s still &lt;em&gt;much&lt;/em&gt; bigger than most open-world games, so there&amp;rsquo;s plenty of space for adventure. This takes care of a factor of 100 out of that 10,000. (Somebody on Mastodon suggested I should just scale up the models instead, but it amounts to the same thing. Keeping the models scaled 1:1 makes authoring easier.)&lt;/p&gt;
&lt;p&gt;I also decided to significantly speed up the ship. Magellan&amp;rsquo;s fleet may have averaged 2.5 km/h, but under optimal conditions, they would have been doing about 7 knots, or 13 km/h. That means the 25 m ship would travel its own length in 7 seconds. I tried this in the game, and it feels really, painfully slow. Instead, I ended up with a ship that can do 90 km/h, which is pretty ludicrous for a sailing vessel, but &lt;em&gt;feels right&lt;/em&gt; in the game. This takes care of another factor of 7 out of that 10,000.&lt;/p&gt;
&lt;p&gt;That leaves a factor of about 15 still to be dealt with. For that, I&amp;rsquo;ve just added some buttons to speed up time. These will be necessary anyway: carefully navigating along a coast calls for a slower rate than crossing an ocean. At a 25× speedup, movement looks obviously fast, but not ludicrous.&lt;/p&gt;
&lt;p&gt;At a world scale of 1:100, a view range of 50 km would allow you to see all the way across the Atlantic ocean. Clearly that would take the fun out of discovery, and would make maps pointless. So I reduced the view range to 1 km; not scaled all the way down, but enough to make it feel constraining.&lt;/p&gt;
&lt;h2 id="scaling-the-heights"&gt;Scaling the heights&lt;/h2&gt;
&lt;p&gt;But now, major terrain features like mountains are much too small. An 8 km tall mountain like Mount Everest would stand only 80 m tall, which would be only a minor hill compared to the 25 m tall ship.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/6/18/around-the-world-16-a-matter-of-scale/before.png" alt="Screenshot of far too small mountains" &gt;
&lt;/p&gt;
&lt;p&gt;So we&amp;rsquo;ll have to scale up those &amp;ldquo;minor&amp;rdquo; topography features, keeping them closer to their real-life size, while retaining the &amp;ldquo;major&amp;rdquo; features like continents at 1:100 scale. Fortunately, I had seen this coming and kept all the scaling factors in the procedural generation easily configurable.&lt;/p&gt;
&lt;p&gt;Without further comment, here are some pictures of the state after about a day of tinkering:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/6/18/around-the-world-16-a-matter-of-scale/coastal_islands.png" alt="Coastal islands" &gt;
&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/6/18/around-the-world-16-a-matter-of-scale/mountainous_bay.png" alt="Mountainous bay" &gt;
&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/6/18/around-the-world-16-a-matter-of-scale/island.png" alt="Island" &gt;
&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/6/18/around-the-world-16-a-matter-of-scale/isthmus.png" alt="Isthmus" &gt;
&lt;/p&gt;
&lt;p&gt;The new world scale has some advantages. Remember that the global world map is generated on a 6×512×512 quad sphere. A single grid cell used to be about 20 km in size, so any small-scale features like rivers and barrier islands could not be generated at this level, and would have to be produced with more limited &amp;ldquo;local&amp;rdquo; functions like simplex noise. After downscaling, a grid cell is only 200 m (1/10th of the player&amp;rsquo;s view circle), so adding such features at the global level becomes feasible. I&amp;rsquo;ll have to see how much time I want to spend on that.&lt;/p&gt;
&lt;p&gt;Another advantage is that I no longer need double-precision floating point maths. A single-precision float has a precision of 3.8 mm at the surface of our 1:100 world, which turns out to be sufficient.&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 15: Making waves</title><link>https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/</link><pubDate>Fri, 31 May 2024 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/</guid><description>&lt;p&gt;I&amp;rsquo;ve been investing a lot of effort in the generation of plausible land. But the game is all about sailing, so most of the screen will be filled with water, not land. It&amp;rsquo;s time to take that smooth blue plane that served as the sea, and make it look better!&lt;/p&gt;
&lt;p&gt;The rendering of the water will largely be done in a shader. Godot shows changes to shader code live in the editor itself, but it doesn&amp;rsquo;t hot-reload modified shaders into the running game. So to shorten the feedback cycle, I created a little test scene in Blender and imported it into Godot:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/start.png" alt="The test scene, with a flat water surface" &gt;
&lt;/p&gt;
&lt;p&gt;This also reveals the carrack that I modeled earlier. For reference: it&amp;rsquo;s about 25 meters long.&lt;/p&gt;
&lt;h2 id="sines"&gt;Sines&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re even slightly mathematically inclined and you think of waves, you think of sines. So let&amp;rsquo;s start with that: a single sine wave, moving along the water. I&amp;rsquo;m using the very same mesh data as for the terrain to also render the water surface, but with a vertex shader that moves the vertices up and down. The wave propagation speed is determined from its wavelength a simple formula: &lt;code&gt;frequency = sqrt(gravity * 2 * pi / wavelength)&lt;/code&gt;. This is only valid in deep water, but I&amp;rsquo;m using it for shallows too. Here&amp;rsquo;s the result (I&amp;rsquo;m too lazy to create videos of every step, so you&amp;rsquo;ll have to make do with static images):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/sine.png" alt="A single sine wave" &gt;
&lt;/p&gt;
&lt;p&gt;The effect is… underwhelming. Part of the reason is that the mesh normals aren&amp;rsquo;t modified: they still point straight up. This affects things like shadows and reflections.&lt;/p&gt;
&lt;p&gt;Fortunately, it&amp;rsquo;s easy to compute the derivative of a sine wave, which can be used to compute the gradient, which in turn can be used to compute the normal.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/normals.png" alt="A sine wave, but with proper normals" &gt;
&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m doing all this in the fragment shader, so it&amp;rsquo;s done finely per pixel rather than coarsely per mesh vertex. This will make a big difference in visual quality later on, when we add finer details.&lt;/p&gt;
&lt;h2 id="transparency"&gt;Transparency&lt;/h2&gt;
&lt;p&gt;The shape of the waves is not yet great, but there&amp;rsquo;s a more pressing problem: this blue sheet doesn&amp;rsquo;t look like water, but more like a corrugated solid surface. It lacks the transparency and reflections that are characteristic of real water.&lt;/p&gt;
&lt;p&gt;Real water gets more opaque the deeper it gets, because more and more light is absorbed along the way to the bottom and back up again. We can emulate that: since we&amp;rsquo;re reusing the terrain mesh to render the water, each vertex already knows the local water depth from its &lt;em&gt;y&lt;/em&gt; coordinate. So let&amp;rsquo;s use that to create exponential attenuation:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/transparency.png" alt="With added transparency" &gt;
&lt;/p&gt;
&lt;p&gt;Note that this is only an approximation, because it&amp;rsquo;s not the water &lt;em&gt;depth&lt;/em&gt; that matters, but the length of the path that the light takes towards the bottom. If you&amp;rsquo;re looking at an oblique angle, that path might be (much) longer than the actual depth. But &lt;a href="https://frozenfractal.com/blog/2024/3/19/around-the-world-12-2d-or-not-2d/"&gt;we&amp;rsquo;re not using very oblique angles in this game&lt;/a&gt;, so the approximation works well enough.&lt;/p&gt;
&lt;p&gt;However, because shallow water is now almost fully transparent, it&amp;rsquo;s difficult to see where the water ends and the land begins. In reality, there would be some foam caused by breaking waves. We can easily add that: wherever the water depth (including wave height) is less than 1 meter, render the water as opaque white:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/surf.png" alt="With surf" &gt;
&lt;/p&gt;
&lt;p&gt;The surf definitely does not look amazing. It could do with a bit of texture and randomization. But it&amp;rsquo;s not awful and it serves the gameplay purpose, so it&amp;rsquo;s good enough for the moment.&lt;/p&gt;
&lt;h2 id="wave-shape"&gt;Wave shape&lt;/h2&gt;
&lt;p&gt;As I discovered while I was working on this, there is a &lt;em&gt;lot&lt;/em&gt; of scientific literature on waves. One thing that immediately becomes clear: ocean waves are &lt;em&gt;not&lt;/em&gt; sines. A better model is the so-called &lt;em&gt;trochoidal wave&lt;/em&gt; or &lt;em&gt;Gerstner wave&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/Trochoidal_wave.svg.png" alt="Trochoidal wave" &gt;
&lt;br&gt;
&lt;em&gt;Source: &lt;a href="https://en.wikipedia.org/wiki/File:Trochoidal_wave.svg"&gt;Wikipedia&lt;/a&gt; by Kraaiennest, CC-BY-SA 4.0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As you can see, each surface particle doesn&amp;rsquo;t just move up and down; it actually moves in a circle. It&amp;rsquo;s tempting to have our mesh vertices play the role of surface particles, and move the vertices horizontally as well as vertically. It looks good, but it has a significant drawback: the water surface is no longer a simple height field. In other words, given a coordinate in the world, it&amp;rsquo;s very hard to compute what the water height at that point is. And we&amp;rsquo;ll need that computation later, when we want the ship to float on the surface and move in response to it.&lt;/p&gt;
&lt;p&gt;So we&amp;rsquo;re going to settle for an approximation. The &lt;a href="https://developer.nvidia.com/gpugems/gpugems/part-i-natural-effects/chapter-1-effective-water-simulation-physical-models"&gt;very first chapter of the very first GPU Gems book&lt;/a&gt; describes one way to do this: shift the sine wave vertically to be between 0 and 1, raise it to some power, then shift it back. (Note that the web rendering of the book is slightly broken: the π symbol is missing from some equations.) A power of 2 looks good to me:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/pseudotrochoid.png" alt="With approximated trochoid shape" &gt;
&lt;/p&gt;
&lt;p&gt;Calculating the derivative is slightly more involved now, but it&amp;rsquo;s still just high school calculus. We can vary the exponent to make the waves look more or less pointy, but higher exponents flatten the valleys too much, so this model is not perfect. I can always revisit it if needed.&lt;/p&gt;
&lt;h2 id="more-waves"&gt;More waves&lt;/h2&gt;
&lt;p&gt;The corrugated solid look is gone, but the waves still look extremely straight and artificial. Real ocean waves are affected by all sorts of random processes, so they aren&amp;rsquo;t so regular. We&amp;rsquo;ll take another page out of the GPU Gems book, and add four such waves together, with different amplitudes, wavelengths and speeds – and most importantly, moving in slightly different directions. They add up to a much more irregular surface:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/multiple.png" alt="Multiple waves" &gt;
&lt;/p&gt;
&lt;p&gt;It may not be immediately clear from these static images, but the interaction between waves and land does look rather weird. Basically, there isn&amp;rsquo;t any: the waves simply pass through the land and disappear. In the above image, you can see a relatively tall wave cutting a straight path through the islands.&lt;/p&gt;
&lt;p&gt;In reality, waves tend to get taller in shallow water:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/Propagation_du_tsunami_en_profondeur_variable.gif" alt="Wave propagation animation" &gt;
&lt;br&gt;
&lt;em&gt;Source: &lt;a href="https://en.wikipedia.org/wiki/File:Propagation_du_tsunami_en_profondeur_variable.gif"&gt;Wikipedia&lt;/a&gt; by Régis Lachaume, CC-BY-SA 3.0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;However, that by itself will only make the problem worse. There&amp;rsquo;s another effect at play which counteracts the growing waves: when they get too tall, they break and topple forwards. Breaking waves are more difficult to model, so I don&amp;rsquo;t want to go there for the moment. Instead, I&amp;rsquo;ll just reduce the amplitude in shallows, all the way down to zero at the waterline:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/shallows.png" alt="Attenuating amplitude in shallows" &gt;
&lt;/p&gt;
&lt;p&gt;That gives a much calmer and more convincing look, especially in motion. The definition of &amp;ldquo;shallows&amp;rdquo;, by the way, is also in the literature: water is considered shallow if the depth is less than half the wavelength. This means that shorter waves will travel farther towards the waterline before getting extinguished.&lt;/p&gt;
&lt;h2 id="fine-details"&gt;Fine details&lt;/h2&gt;
&lt;p&gt;We&amp;rsquo;re getting there, but a real ocean surface doesn&amp;rsquo;t stop at a sum of just four waves. There are also waves of much shorter wavelengths running around and interfering with each other, giving water its characteristic sparkling and shimmering reflections:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/sparkles-235689_1280.jpg" alt="A real ocean surface" &gt;
&lt;br&gt;
&lt;em&gt;Source: &lt;a href="https://pixabay.com/photos/sparkles-water-surface-water-aqua-235689/"&gt;Pixabay&lt;/a&gt; by jingoba&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Since we&amp;rsquo;re having fun summing powers of sines anyway, why not sum some more, adding shorter and shorter waves to the mix? The effect is… not quite what I was hoping for.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/smaller_sines.png" alt="Smaller sines" &gt;
&lt;/p&gt;
&lt;p&gt;Due to the smaller wavelengths, there are clearly visible repeating patterns now. The same actually happens for the larger waves as well, but it&amp;rsquo;s not visible in this small-scale test scene.&lt;/p&gt;
&lt;p&gt;Can we fix this with more sine waves? Only up to a point, as it turns out. After over an hour of tinkering with parameters, I got somewhat better results:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/smaller_sines_improved.png" alt="Smaller sines, improved" &gt;
&lt;/p&gt;
&lt;p&gt;I might eventually have to throw in some simplex noise. What&amp;rsquo;s holding me back is a limitation in Godot: even though the simplex noise algorithm makes it trivial to compute the gradient of the noise alongside the actual value for little to no cost, Godot only exposes the value and not the gradient.&lt;/p&gt;
&lt;h2 id="on-a-sphere"&gt;On a sphere&lt;/h2&gt;
&lt;p&gt;Did I mention that &lt;a href="https://frozenfractal.com/blog/2024/1/8/around-the-world-11-everything-is-harder-on-a-sphere/"&gt;everything is harder on a sphere&lt;/a&gt;? This includes waves.&lt;/p&gt;
&lt;p&gt;First off, the &lt;em&gt;y&lt;/em&gt; coordinate of a vertex is no longer equal to the height of the terrain. We could do a calculation using the vertex position, but remember that I&amp;rsquo;m using a floating origin, so this is harder than it seems. Instead, I&amp;rsquo;m just passing the terrein height (negative for water depth) in a custom vertex attribute.&lt;/p&gt;
&lt;p&gt;A more subtle problem is how to represent waves. Imagine a single wave all the way around the equator, travelling north. At the equator, all looks good, and the wave crests will look nice and parallel. But the farther north you go, the more they are distorted; at the north pole, the waves would seem to converge inwards from all around you!&lt;/p&gt;
&lt;p&gt;A related problem, which also applies to flat planes, is: how to adjust the waves when wind speed changes? We can&amp;rsquo;t just change the wavelength of an existing wave, because as GPU Gems mentions: &amp;ldquo;Even if it were changed gradually, the crests of the wave would expand away from or contract toward the origin, a very unnatural look.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;But GPU Gems also has a solution: individual waves live only for a limited amount of time, fading in at the start of their lifetime and out at the end. &amp;ldquo;Therefore, we change the current average wavelength, and as waves die out over time, they will be reborn based on the new length. The same is true for direction.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;This also solves the problem of distorted waves on the sphere: as long as the player moves slowly enough that new waves are born around their point of view, they&amp;rsquo;ll never get to a point where the waves appear to converge or diverge significantly.&lt;/p&gt;
&lt;p&gt;Anyway, after getting all that sorted out, the water renders nicely in the game:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/5/31/around-the-world-15-making-waves/ingame.png" alt="Water in the game" &gt;
&lt;/p&gt;
&lt;p&gt;(This might be the first screenshot I&amp;rsquo;m sharing of the game itself! Please ignore the ugly HUD. It&amp;rsquo;s all placeholder.)&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m well aware that it doesn&amp;rsquo;t look &lt;em&gt;great&lt;/em&gt; yet. But at least it appears… watery?&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 14: Floating the origin</title><link>https://frozenfractal.com/blog/2024/4/11/around-the-world-14-floating-the-origin/</link><pubDate>Thu, 11 Apr 2024 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2024/4/11/around-the-world-14-floating-the-origin/</guid><description>&lt;p&gt;In the &lt;a href="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/"&gt;previous post&lt;/a&gt;, we got to see our generated world at close range. This revealed some precision problems that I knew I&amp;rsquo;d need to deal with eventually, but had been putting off for a rainy day. Now I could ignore them no longer.&lt;/p&gt;
&lt;h3 id="the-trouble-with-floats"&gt;The trouble with floats&lt;/h3&gt;
&lt;p&gt;Godot uses &lt;a href="https://en.wikipedia.org/wiki/Single-precision_floating-point_format"&gt;single-precision floating point&lt;/a&gt; numbers throughout the engine. I&amp;rsquo;ll call them &amp;ldquo;floats&amp;rdquo; for short. These numbers are stored in 32 bits. In order to allow both very large and very small values, these 32 bits are split into a mantissa and an exponent:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/4/11/around-the-world-14-floating-the-origin/float.svg" alt="32-bits floating point number" &gt;
&lt;br&gt;
&lt;em&gt;Source: &lt;a href="https://commons.wikimedia.org/wiki/File:Float_example.svg"&gt;Wikimedia Commons&lt;/a&gt; by Tanner, CC-BY-SA 3.0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The mantissa (&amp;ldquo;fraction&amp;rdquo; in the above image) stores the actual digits of the number, and the exponent indicates where the decimal (binary) point goes. The consequence is that floats don&amp;rsquo;t have the same precision throughout the number line: the larger the number, the larger the step becomes between two &lt;em&gt;consecutive&lt;/em&gt; numbers that can be represented.&lt;/p&gt;
&lt;p&gt;This is fine for most games, where the world is small. But as it turns out, a planet the size of the Earth is actually &lt;em&gt;not&lt;/em&gt; small. At Earth&amp;rsquo;s radius, 6371 km from the center of the planet, 32-bit floats have a precision of about 0.38 meters. It&amp;rsquo;s a kind of grid that every object will be snapped to, and any movement will have to take place in increments of this step size. And this grid isn&amp;rsquo;t nicely aligned with the planet surface either, so sideways movement will often also cause the object to move up or down at the same time.&lt;/p&gt;
&lt;p&gt;For most of the game, this amount of precision is actually still fine. I don&amp;rsquo;t plan to generate terrain triangles smaller than a few meters, and at that size any rounding problems will mostly be invisible. But for movement of the player&amp;rsquo;s ship, the lack of precision &lt;em&gt;is&lt;/em&gt; an issue. It seems to jitter as it moves around. (&amp;ldquo;Ship?&amp;rdquo;, you say? Yes, I have been working on gameplay systems behind the scenes as well. More about that later!)&lt;/p&gt;
&lt;h3 id="non-solutions"&gt;Non-solutions&lt;/h3&gt;
&lt;p&gt;Naively, you might think that we can just change the units of the scene to something smaller, like millimeters instead of meters. This doesn&amp;rsquo;t work because the numbers just get bigger – the digits of each number remain the same, the only thing that changes is the location of the decimal point in between them. Instead of a precision of 0.38 meters, you now have a precision of 380 millimeters.&lt;/p&gt;
&lt;p&gt;A working solution would be to use 64-bit floating point numbers, or &amp;ldquo;doubles&amp;rdquo;, instead. For games at the scale of the galaxy, this still isn&amp;rsquo;t enough, but at Earth scale it&amp;rsquo;s plenty: a double has a precision of 53 bits, allowing for a precision of &lt;em&gt;nanometers&lt;/em&gt; at the planet&amp;rsquo;s surface.&lt;/p&gt;
&lt;p&gt;Godot actually &lt;a href="https://docs.godotengine.org/en/stable/tutorials/physics/large_world_coordinates.html"&gt;supports&lt;/a&gt; using doubles instead of floats throughout the engine. There are a few reasons why I chose not to go this route. First, because it requires recompiling the engine from scratch – actually pretty easy for me, but a bit of a chore. Second, because it&amp;rsquo;s a fairly recent addition and probably not widely used, so I would expect to encounter bugs. And third, because it&amp;rsquo;s an all-or-nothing deal: &lt;em&gt;everything&lt;/em&gt; in the engine will then work with doubles. This means that basic types like &lt;code&gt;Vector3&lt;/code&gt; suddenly take up twice as much memory, which also means that mesh vertex data takes twice as much memory and twice as long to upload to the GPU. SIMD instructions can only process half as many doubles as floats simultaneously, so CPU efficiency also suffers.&lt;/p&gt;
&lt;p&gt;So, in the end I &lt;a href="https://frozenfractal.com/blog/2024/1/8/around-the-world-11-everything-is-harder-on-a-sphere/"&gt;once again&lt;/a&gt; chose to make life difficult for myself, and implemented a hybrid solution.&lt;/p&gt;
&lt;h3 id="floating-the-origin"&gt;Floating the origin&lt;/h3&gt;
&lt;p&gt;The key idea, which games like Kerbal Space Program also use, is that floating-point numbers actually &lt;em&gt;are&lt;/em&gt; very precise — as long as you don&amp;rsquo;t stray too far from the origin. So, what if instead of having the origin at the center of the planet, we just put it where we need it to be? Instead of moving the player through the universe, we move the universe around the player. Things far away from the player still won&amp;rsquo;t be precisely positioned, but they&amp;rsquo;re so far away that we don&amp;rsquo;t care. (This idea goes back at least as far as 1984, to the original Elite game on the BBC Micro. This &lt;a href="https://www.bbcelite.com/deep_dives/rotating_the_universe.html"&gt;amazing website&lt;/a&gt; by Mark Moxon explains how that – and all the rest of that game – works in extreme detail.)&lt;/p&gt;
&lt;p&gt;In fact I get to cut some corners here. Because doubles &lt;em&gt;are&lt;/em&gt; precise enough for my needs, I can just store each object&amp;rsquo;s position in 64-bit double precision relative to the world origin. When it&amp;rsquo;s time to render the frame, I convert this to a 32-bit float relative to the player, for consumption by the Godot engine. This way, all my game logic remains in global coordinates, making it easier to work with. Because these are discrete game objects, not big arrays of vertices, the added CPU and memory overhead should be minimal.&lt;/p&gt;
&lt;p&gt;However… now I need to put this 64-bit position on &lt;em&gt;every&lt;/em&gt; object in the world. And that&amp;rsquo;s a whole new can of worms.&lt;/p&gt;
&lt;h3 id="components"&gt;Components&lt;/h3&gt;
&lt;p&gt;Godot&amp;rsquo;s architecture is heavily based on object-oriented principles, and inheritance in particular. So if you need to have some functionality on &lt;em&gt;every&lt;/em&gt; object, regardless of its position in the class hierarchy, you&amp;rsquo;re out of luck. You&amp;rsquo;d have to add it to the root &lt;code&gt;Node3D&lt;/code&gt; class, but this is part of the engine itself and cannot be modified. (Granted, Godot is open source, you &lt;em&gt;can&lt;/em&gt; in fact modify the engine. But I&amp;rsquo;d prefer to keep using the official builds, so I can easily keep up to date with the latest version.)&lt;/p&gt;
&lt;p&gt;Fortunately, Godot&amp;rsquo;s node system is also very flexible. You can define the additional functionality as a separate node, which is attached as a child to every node that needs it. The new node is a kind of pluggable &amp;ldquo;component&amp;rdquo; that you can reuse wherever you need it. &lt;a href="https://www.gdquest.com/tutorial/godot/design-patterns/entity-component-system/"&gt;This article&lt;/a&gt; by GDQuest demonstrates that approach.&lt;/p&gt;
&lt;p&gt;However, it never felt quite right. You often need to interact with the sub-node that stores the position, which is tedious, error-prone and slow. What if the node isn&amp;rsquo;t there, or has been renamed, or if there are multiple? Something that should be trivial (looking up a value on an object) turns into a pile of error handling.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also the matter of memory efficiency. Once I&amp;rsquo;d started using such pluggable components, it made sense to consistently use them for everything. Tracking the health of an entity? A node. The speed? A node. The current camera focus? Another node. Most of these nodes contain very little data; a few numbers, maybe 16 bytes at most. Some are just markers that don&amp;rsquo;t store any data at all. But a Godot node by itself is a pretty heavyweight object: it stores a name, parent and owner pointers, a list &lt;em&gt;and&lt;/em&gt; a hash map of children, a hash map of signal connections, a list of groups, a bunch of flags, stuff related to networking… hundreds of bytes all together, and all that for storing a few bytes of data.&lt;/p&gt;
&lt;p&gt;An alternative approach is to have these pluggable components as plain C# objects, and put these in an array or in individual fields directly on each object. This is what I&amp;rsquo;d been doing so far. But it was a weird hybrid between Godot node scripts and pluggable objects, and it started to grate on me. It was time to go all the way and turn this into an ECS.&lt;/p&gt;
&lt;h3 id="the-ecs"&gt;The ECS&lt;/h3&gt;
&lt;p&gt;ECS stands for &lt;a href="https://en.wikipedia.org/wiki/Entity_component_system"&gt;entity component system&lt;/a&gt;, and it&amp;rsquo;s a popular pattern in some game development circles. The concept is explained very well in many other places, so I won&amp;rsquo;t rehash that here. Suffice to say that I chose to write my own, because it&amp;rsquo;s so fundamental to my game that I&amp;rsquo;d like to have full control over its features and implementation. And, as long as you don&amp;rsquo;t need squeeze out every last drop of performance, it&amp;rsquo;s not all that hard; it took me about a week fulltime to develop and port the existing game code over.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what it does, in a nutshell. This is going to get rather technical and requires some knowledge of C# and Godot to follow, so if you&amp;rsquo;re just here for procedural generation and/or pretty pictures, feel free to tune out.&lt;/p&gt;
&lt;p&gt;Entities are identified by 64-bit integers. Entity IDs are never reused, but 64 bits are enough to ensure that they won&amp;rsquo;t run out. Components can be any C# type, including structs and Godot nodes. The overarching &lt;code&gt;World&lt;/code&gt; class stores each component in a separate hash map, keyed on entity ID.&lt;/p&gt;
&lt;p&gt;Central to the &lt;code&gt;World&lt;/code&gt; is an event bus. Events can be any C# type, and objects can subscribe to events of a particular type. Systems are classes that subscribe to one or more events, by implementing the &lt;code&gt;IEventHandler&amp;lt;E&amp;gt;&lt;/code&gt; interface. The &lt;code&gt;World&lt;/code&gt; itself fires events when components are added and removed. Note that systems don&amp;rsquo;t implement a dedicated &lt;code&gt;Process(float delta)&lt;/code&gt; method called every tick; instead, there is a &lt;code&gt;Process&lt;/code&gt; event that they can subscribe to.&lt;/p&gt;
&lt;p&gt;Additionally, there are &amp;ldquo;resources&amp;rdquo;, which are singleton objects keyed on their type. My procedural generation services are added to the &lt;code&gt;World&lt;/code&gt; as resources, so that systems can access them. There is a simple dependency injection mechanism based on reflection, whereby systems can request resources through their constructor. (The new &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors"&gt;primary constructors&lt;/a&gt; introduced last November in C# 12 are great for this!) The event bus and the &lt;code&gt;World&lt;/code&gt; itself are also injectable in that way, to allow systems to send events and to create/destroy entities.&lt;/p&gt;
&lt;p&gt;Systems typically affect entities in the world through queries, which are also injectable. They are represented by the &lt;code&gt;IQuery&amp;lt;...&amp;gt;&lt;/code&gt; interface, where the &lt;code&gt;...&lt;/code&gt; indicates one or more components to query for. The &lt;code&gt;IQuery&lt;/code&gt; interface extends &lt;code&gt;IEnumerable&lt;/code&gt;, which lets the system iterate over all matching entities and components and manipulate them in whatever way needed.&lt;/p&gt;
&lt;p&gt;Right now I&amp;rsquo;ve only implemented &amp;ldquo;and&amp;rdquo; queries, where all requested components must be present. The actual iteration is implemented in the &lt;code&gt;World&lt;/code&gt; in a rather simple way: the hash map of the first component is iterated over in its entirety, and for each entry, the hash maps for the other requested component types are probed to see if they&amp;rsquo;re all present. (If this were a general-purpose library, I would at least iterate over the hash map of the &lt;em&gt;rarest&lt;/em&gt; component instead of the &lt;em&gt;first&lt;/em&gt;, which is more efficient. But because I&amp;rsquo;m writing both the ECS and the game, I know which component is rarest, and I just list that first.)&lt;/p&gt;
&lt;p&gt;The upshot of all this is that I no longer have node spaghetti. Everything is beautifully decoupled, there is very little boilerplate, and I feel much more comfortable and secure adding new functionality. Here&amp;rsquo;s an annotated example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#272822;background-color:#fafafa;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Class components are mutable, struct components are not, because&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// I failed to find a way to return structs by reference instead of value.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// So these have to be classes.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00a8c8"&gt;class&lt;/span&gt; &lt;span style="color:#75af00"&gt;Position&lt;/span&gt; &lt;span style="color:#111"&gt;{&lt;/span&gt; &lt;span style="color:#00a8c8"&gt;public&lt;/span&gt; &lt;span style="color:#111"&gt;Vector3&lt;/span&gt; &lt;span style="color:#111"&gt;Value&lt;/span&gt;&lt;span style="color:#111"&gt;;&lt;/span&gt; &lt;span style="color:#111"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00a8c8"&gt;class&lt;/span&gt; &lt;span style="color:#75af00"&gt;Velocity&lt;/span&gt; &lt;span style="color:#111"&gt;{&lt;/span&gt; &lt;span style="color:#00a8c8"&gt;public&lt;/span&gt; &lt;span style="color:#111"&gt;Vector3&lt;/span&gt; &lt;span style="color:#111"&gt;Value&lt;/span&gt;&lt;span style="color:#111"&gt;;&lt;/span&gt; &lt;span style="color:#111"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Using the primary constructor to inject the query.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// The World takes care of constructing it and passing it in.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00a8c8"&gt;class&lt;/span&gt; &lt;span style="color:#75af00"&gt;MovementSystem&lt;/span&gt;&lt;span style="color:#111"&gt;(&lt;/span&gt;&lt;span style="color:#111"&gt;IQuery&lt;/span&gt;&lt;span style="color:#111"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#111"&gt;Position&lt;/span&gt;&lt;span style="color:#111"&gt;,&lt;/span&gt; &lt;span style="color:#111"&gt;Velocity&lt;/span&gt;&lt;span style="color:#111"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#111"&gt;query&lt;/span&gt;&lt;span style="color:#111"&gt;)&lt;/span&gt; &lt;span style="color:#111"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#111"&gt;IEventHandler&lt;/span&gt;&lt;span style="color:#111"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#111"&gt;Process&lt;/span&gt;&lt;span style="color:#111"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#111"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00a8c8"&gt;public&lt;/span&gt; &lt;span style="color:#00a8c8"&gt;void&lt;/span&gt; &lt;span style="color:#111"&gt;Handle&lt;/span&gt;&lt;span style="color:#111"&gt;(&lt;/span&gt;&lt;span style="color:#111"&gt;Process&lt;/span&gt; &lt;span style="color:#111"&gt;processEvent&lt;/span&gt;&lt;span style="color:#111"&gt;)&lt;/span&gt; &lt;span style="color:#111"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00a8c8"&gt;foreach&lt;/span&gt; &lt;span style="color:#111"&gt;(&lt;/span&gt;&lt;span style="color:#00a8c8"&gt;var&lt;/span&gt; &lt;span style="color:#111"&gt;(&lt;/span&gt;&lt;span style="color:#111"&gt;position&lt;/span&gt;&lt;span style="color:#111"&gt;,&lt;/span&gt; &lt;span style="color:#111"&gt;velocity&lt;/span&gt;&lt;span style="color:#111"&gt;)&lt;/span&gt; &lt;span style="color:#00a8c8"&gt;in&lt;/span&gt; &lt;span style="color:#111"&gt;query&lt;/span&gt;&lt;span style="color:#111"&gt;)&lt;/span&gt; &lt;span style="color:#111"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#111"&gt;position&lt;/span&gt;&lt;span style="color:#111"&gt;.&lt;/span&gt;&lt;span style="color:#111"&gt;Value&lt;/span&gt; &lt;span style="color:#111"&gt;+=&lt;/span&gt; &lt;span style="color:#111"&gt;processEvent&lt;/span&gt;&lt;span style="color:#111"&gt;.&lt;/span&gt;&lt;span style="color:#111"&gt;Delta&lt;/span&gt; &lt;span style="color:#111"&gt;*&lt;/span&gt; &lt;span style="color:#111"&gt;velocity&lt;/span&gt;&lt;span style="color:#111"&gt;.&lt;/span&gt;&lt;span style="color:#111"&gt;Value&lt;/span&gt;&lt;span style="color:#111"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#111"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#111"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#111"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Add this to the world (somewhere in the main function).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#111"&gt;world&lt;/span&gt;&lt;span style="color:#111"&gt;.&lt;/span&gt;&lt;span style="color:#111"&gt;AddSystem&lt;/span&gt;&lt;span style="color:#111"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#111"&gt;MovementSystem&lt;/span&gt;&lt;span style="color:#111"&gt;&amp;gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#111"&gt;world&lt;/span&gt;&lt;span style="color:#111"&gt;.&lt;/span&gt;&lt;span style="color:#111"&gt;BuildEntity&lt;/span&gt;&lt;span style="color:#111"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#111"&gt;.&lt;/span&gt;&lt;span style="color:#111"&gt;Add&lt;/span&gt;&lt;span style="color:#111"&gt;(&lt;/span&gt;&lt;span style="color:#00a8c8"&gt;new&lt;/span&gt; &lt;span style="color:#111"&gt;Position&lt;/span&gt;&lt;span style="color:#111"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#111"&gt;.&lt;/span&gt;&lt;span style="color:#111"&gt;Add&lt;/span&gt;&lt;span style="color:#111"&gt;(&lt;/span&gt;&lt;span style="color:#00a8c8"&gt;new&lt;/span&gt; &lt;span style="color:#111"&gt;Velocity&lt;/span&gt;&lt;span style="color:#111"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#111"&gt;.&lt;/span&gt;&lt;span style="color:#111"&gt;Done&lt;/span&gt;&lt;span style="color:#111"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To integrate this ECS with Godot, I added a few special components and systems. There is the &lt;code&gt;InTree&lt;/code&gt; component, which contains a Godot &lt;code&gt;Node&lt;/code&gt;. The &lt;code&gt;InTreeSystem&lt;/code&gt; listens for &lt;code&gt;ComponentAdded&amp;lt;InTree&amp;gt;&lt;/code&gt; and &lt;code&gt;ComponentRemoved&amp;lt;InTree&amp;gt;&lt;/code&gt; events, and adds/removes the corresponding node from the component into Godot&amp;rsquo;s node tree. Additionally, there is a generic system called &lt;code&gt;NodeAddSystem&amp;lt;C&amp;gt;&lt;/code&gt; that takes a scene path in its constructor, and automatically instantiates that scene whenever a component of type &lt;code&gt;C&lt;/code&gt; is added to an entity. The idea here is that I don&amp;rsquo;t want to store any data on Godot nodes directly, because nodes are difficult to save and load. Instead, data flows only one way: from components to nodes.&lt;/p&gt;
&lt;h3 id="end-of-story"&gt;End of story&lt;/h3&gt;
&lt;p&gt;Okay, that&amp;rsquo;s enough talk about implementation details. I hope this makes it clear why I haven&amp;rsquo;t been posting any pretty pictures lately. I&amp;rsquo;ll try to make sure to have some more in the next post!&lt;/p&gt;</description><category>game development</category><category>Around The World</category><category>Godot</category></item><item><title>Around The World, Part 13: Zooming in</title><link>https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/</link><pubDate>Mon, 25 Mar 2024 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/</guid><description>&lt;p&gt;As mentioned in the &lt;a href="https://frozenfractal.com/blog/2024/3/19/around-the-world-12-2d-or-not-2d/"&gt;previous post&lt;/a&gt;, the player will never see our procedurally generated world all at a glance. They&amp;rsquo;ll be much closer to it, and seeing only a small part at any given time. Let&amp;rsquo;s see how we can go from coarse, global world maps to something that will actually look good at close range!&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a map of one of our generated worlds:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/coarse_height_map.png" alt="Coarse height map of a generated world" &gt;
&lt;/p&gt;
&lt;p&gt;The map is based on a quad sphere with six faces of 512×512 pixels. At Earth scale, that works out to about 20×20 kilometers per pixel. (I&amp;rsquo;m not certain that my world will actually be Earth-sized, because it might be too much work to fill such a big planet with enough interesting content — but I want to keep that option open for now.)&lt;/p&gt;
&lt;p&gt;How far can the player see? It depends how high above sea level they are: the higher up you are, the farther away the horizon is. Looking at some references, ships of the Age of Sail were no taller than 25 meters. We can then use &lt;a href="https://www.metabunk.org/curve/"&gt;this calculator&lt;/a&gt; to find that the horizon is about 20 km away. Taller objects like mountains and other ships can be seen beyond the horizon, and in theory Mount Everest could be seen from as far as 361 km away if it were on the coast, but I&amp;rsquo;ll settle for 50 km as an upper bound.&lt;/p&gt;
&lt;p&gt;That means that the player could see about 2.5 &amp;ldquo;world pixels&amp;rdquo; in any direction — &lt;em&gt;at best&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/coarse_height_map_zoomed.png" alt="Same as above, zoomed in" &gt;
&lt;/p&gt;
&lt;p&gt;Clearly, the current world resolution is not quite enough!&lt;/p&gt;
&lt;h3 id="making-a-mesh-of-things"&gt;Making a mesh of things&lt;/h3&gt;
&lt;p&gt;I haven&amp;rsquo;t settled on any particular art style yet, but it&amp;rsquo;s a safe bet that we&amp;rsquo;ll need a 3D mesh of the terrain. And if I settle on a low-poly style, it would be nice if that mesh looked good even if you can distinguish individual triangles.&lt;/p&gt;
&lt;p&gt;The quad sphere is great for the generative algorithms and simulations I&amp;rsquo;ve been doing so far, because it lets us pretend that we&amp;rsquo;re working with squares. But a mesh that the GPU can render is made up of triangles instead. We can split each square into two triangles, but they would not be nice equilateral triangles, so the mesh would not look so beautiful.&lt;/p&gt;
&lt;p&gt;Now it&amp;rsquo;s time to remember what I &lt;a href="https://frozenfractal.com/blog/2024/1/8/around-the-world-11-everything-is-harder-on-a-sphere/"&gt;wrote previously&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;But the subdivided icosahedron might make a comeback once we start building meshes!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Indeed, an icosahedron is made of 20 equilateral triangles, and we can subdivide each of these triangles recursively into smaller triangles to get an approximate sphere with almost exactly equilateral triangles:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/Geodesic_icosahedral_polyhedron_example.png" alt="Subdivided icosahedron" &gt;
&lt;br&gt;
&lt;em&gt;Source: &lt;a href="https://commons.wikimedia.org/wiki/File:Geodesic_icosahedral_polyhedron_example.png"&gt;Wikimedia Commons&lt;/a&gt; by Tomruen, CC-BY-SA 4.0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;GPU&amp;rsquo;s don&amp;rsquo;t much like individual triangles though; they like triangles in large batches. So instead of subdividing each face of the icosahedron into 4 triangles, we subdivide it into 64×64 triangles. At each corner, we interpolate the original quad sphere height map to get the height at that corner. This gives us our first approximation of a terrain mesh:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/terrain_mesh_zoomed_out.png" alt="Subdivided icosahedron terrain mesh" &gt;
&lt;/p&gt;
&lt;p&gt;Clearly this is no better than what we had before. But the beauty of it is, that we can subdivide each face into 4 smaller triangles, and generate a 64×64 triangle mesh for each of &lt;em&gt;those&lt;/em&gt;. And we can repeat this procedure indefinitely to get finer and finer meshes, as needed.&lt;/p&gt;
&lt;p&gt;If we did that for the whole world, it would take far too much memory and computing power. So we generate meshes only for the part of the world that&amp;rsquo;s visible, at the proper subdivision level to make it look detailed enough. No matter how far we zoom in, the number of visible triangles, and therefore the amount of work that needs to be done, is more or less the same. Nice!&lt;/p&gt;
&lt;p&gt;Now those chunky pixels from before resolve into something much smoother:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/terrain_mesh_zoomed_in.png" alt="Subdivided icosahedron terrain mesh, zoomed in" &gt;
&lt;/p&gt;
&lt;p&gt;Whoops, that&amp;rsquo;s a little &lt;em&gt;too&lt;/em&gt; smooth. That makes sense, because the original large-scale world map simply doesn&amp;rsquo;t have this amount of detail.&lt;/p&gt;
&lt;h3 id="adding-fine-details"&gt;Adding fine details&lt;/h3&gt;
&lt;p&gt;What we need here is our trusty friend simplex noise to add some smaller features. Remember that this noise is added during the generation of meshes, so it doesn&amp;rsquo;t need to be computed for the whole world. The drawback is that we can&amp;rsquo;t easily use any fancy algorithms like erosion simulation, which require information from nearby points; each mesh vertex must be computable independently of all the others. So at this scale level, we&amp;rsquo;re limited to relatively simple algorithms.&lt;/p&gt;
&lt;p&gt;The noise is scaled such that the largest (lowest frequency) octave is roughly the size of one pixel in the global world map. That gives us this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/mesh_with_noise.png" alt="Terrain mesh with noise" &gt;
&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s looking better already, but most coastlines end up looking like this instead:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/too_many_islands.png" alt="Too many islands" &gt;
&lt;/p&gt;
&lt;p&gt;Unlike the steep volcanic island we looked at above, this is a coastline with a much shallower slope, where the noise adds way too many small islands and inlets. This is because the noise amplitude is the same everywhere, but what looks good on mountains is way too rough for lowlands. In real life though, not all terrain is equally rough.&lt;/p&gt;
&lt;h3 id="modulating-the-noise"&gt;Modulating the noise&lt;/h3&gt;
&lt;p&gt;What we need is a system that adds more noise in areas where we&amp;rsquo;d expect rough terrain, such as mountainous areas, and less noise in smoother terrain like lowlands. We already have a system to add height based on tectonic forces, which is how we create mountain ranges, so let&amp;rsquo;s bolt it onto that. Let&amp;rsquo;s also add noise to hotspot volcanoes. Our previously messy coastline now looks much smoother and resolves into an isthmus:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/not_too_many_islands.png" alt="Not too many islands" &gt;
&lt;/p&gt;
&lt;p&gt;Meanwhile, our hotspot volcano remains pleasingly rough:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/hotspot_volcano.png" alt="Hotspot volcano" &gt;
&lt;/p&gt;
&lt;p&gt;The noise itself could be improved, though. Right now it&amp;rsquo;s just 4 octaves of simplex noise layered on top of each other. While working on the large-scale terrain, I noticed that ridge noise looks much better: the valleys between the ridges almost look like rivers, resembling the effect of hydraulic erosion (something we don&amp;rsquo;t simulate). Seen here at an angle to better show the relief:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/ridged_noise.png" alt="Ridged noise" &gt;
&lt;/p&gt;
&lt;p&gt;Here I also lowered the frequency (increased the scale) of the noise to span several pixels in the world map. This seems to be better for tying those disparate pixels together.&lt;/p&gt;
&lt;p&gt;On mountain ranges, the noise looks perhaps a bit too uniform:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/mountain_range.png" alt="Mountain range" &gt;
&lt;/p&gt;
&lt;p&gt;Warping the domain of the ridge noise using three octaves of yet another simplex noise fixes this nicely, making the result look pleasingly irregular:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/mountain_range_domain_warped.png" alt="Mountain range with domain warping" &gt;
&lt;/p&gt;
&lt;p&gt;As you can see in the corners, there&amp;rsquo;s also something wrong with my calculation of the visible area. We seem to be missing some terrain meshes here. I&amp;rsquo;ll fix that later.&lt;/p&gt;
&lt;p&gt;To wrap up this post, let&amp;rsquo;s add a simple water surface. The same subdivision algorithm is used as for the land, but I just apply a height of zero everywhere, and use a different material for rendering. When a triangle is entirely covered by land, we don&amp;rsquo;t need to generate any water mesh and vice versa, but if a triangle is only partly covered by water, we need to render both so we get nice sharp coastlines.&lt;/p&gt;
&lt;p&gt;Some pretty islands:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/islands.png" alt="Islands" &gt;
&lt;/p&gt;
&lt;p&gt;An interesting-looking bay:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/25/around-the-world-13-zooming-in/bay.png" alt="Bay" &gt;
&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s obvious that more work is needed. For example, you can still see some artifacts of the underlying square grid, and the coastlines are much too smooth here. But we&amp;rsquo;re getting there!&lt;/p&gt;</description><category>game development</category><category>Around The World</category></item><item><title>Around The World, Part 12: 2D or not 2D</title><link>https://frozenfractal.com/blog/2024/3/19/around-the-world-12-2d-or-not-2d/</link><pubDate>Tue, 19 Mar 2024 00:00:00 +0000</pubDate><author><name>Thomas ten Cate</name></author><guid>https://frozenfractal.com/blog/2024/3/19/around-the-world-12-2d-or-not-2d/</guid><description>&lt;p&gt;So far, I&amp;rsquo;ve been talking about generating a world at a very large scale for the game. But the aim of the game is exploration, so the player will rarely, if ever, get to see the entire planet at once. Which raises the question: what &lt;em&gt;will&lt;/em&gt; the player see?&lt;/p&gt;
&lt;p&gt;The game jam prototype that started it all looked like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/19/around-the-world-12-2d-or-not-2d/alakajam_prototype.png" alt="Screenshot of Around The World prototype" &gt;
&lt;/p&gt;
&lt;p&gt;Its visuals were designed to mimic the EGA era, with low resolution and a limited colour palette. A top-down two-dimensional view perfectly fit into that. Of course, it&amp;rsquo;s also easier to build, which made it the obvious choice for a 48-hour game jam.&lt;/p&gt;
&lt;p&gt;However, this prototype took place on a rectangular map that wraps around at the left and right edges – essentially a cylinder. This made it possible to represent it as a grid of square tiles. The full game that I&amp;rsquo;m working on now will take place on an actual sphere, where such a thing &lt;a href="https://frozenfractal.com/blog/2024/1/8/around-the-world-11-everything-is-harder-on-a-sphere/"&gt;is not possible&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="making-it-2d"&gt;Making it 2D?&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve thought long and hard if I could use some kind of hack to use an entirely flat 2D view in the new game as well. Then I could use pixel art, which is relatively quick to produce. If the player only sees a small part of the world at the same time, surely it&amp;rsquo;s possible to flatten that to a plane without too much distortion?&lt;/p&gt;
&lt;p&gt;And yes, this is possible. But it&amp;rsquo;s not enough. Even if we have zero distortion at the player&amp;rsquo;s location, distortion inevitably increases towards the edges of the view. That&amp;rsquo;s still fine, except that the player can move towards these edges, and far beyond them. As the player moves further away from their starting point, distortion would inevitably increase. What should happen then?&lt;/p&gt;
&lt;p&gt;One option is to reproject when the distortion becomes too large. At that moment, the center of the projection would again snap to the player&amp;rsquo;s position. This would lead to jarring jumps, where the entire world suddenly appears to change around the player. That&amp;rsquo;s clearly not awesome, and we want this game to be awesome, right?&lt;/p&gt;
&lt;p&gt;I could try to hide those jumps in some artificial way, e.g. by showing a fullscreen end-of-day summary at the end of every in-game day. But I&amp;rsquo;m worried that this would interrupt the flow of the game.&lt;/p&gt;
&lt;p&gt;To entirely get rid of these sudden jumps, we could instead reproject continuously. The location of the player is always used as the center of the projection, so it has zero distortion, and the edges of the view are never distorted too much. So now we have a system that, in real time, transforms a 3D scene into a 2D image. Does that sound familiar? At this point, &lt;em&gt;we have essentially reinvented a 3D rendering engine&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id="making-it-3d"&gt;Making it 3D?&lt;/h3&gt;
&lt;p&gt;After this realization, I wondered if I should go all-in and make the game fully 3D. After all, wouldn&amp;rsquo;t it be awesome if you viewed the world from a first-person perspective, standing on the deck of your ship and seeing a strip of long-awaited land appearing from the haze on the horizon? This would make the game much more immersive.&lt;/p&gt;
&lt;p&gt;I have even been tinkering with this in another early prototype:&lt;/p&gt;
&lt;video controls width="756"&gt;&lt;source src="prototype_3d.mp4" type="video/mp4"&gt;
Sorry, your browser does not support embedded videos.
&lt;/video&gt;
&lt;p&gt;The prospect is extremely tempting even now, but I have to be realistic: I&amp;rsquo;m just one guy doing game design, coding and artwork, and I have neither the time nor the skills to model 3D objects to the detail needed to make this work.&lt;/p&gt;
&lt;p&gt;Also, from a gameplay perspective, it might make the game very difficult: this third-person camera makes it very hard to match the shape of a coastline to what you see on a map. Of course navigators needed to deal with this in real life too, but they used a sextant to take painstaking measurements. Also, they could draw on years of experience and even then sometimes got it wrong. Not something I&amp;rsquo;d want to inflict on my players!&lt;/p&gt;
&lt;h3 id="middle-ground"&gt;Middle ground&lt;/h3&gt;
&lt;p&gt;So we have to use 3D, but we don&amp;rsquo;t want a first or third person perspective. The sensible remaining option is to use a top-down perspective, but rendered using 3D meshes and models.&lt;/p&gt;
&lt;p&gt;I was initially worried that this would look very bland and boring. You would just see the deck of the ship, with some narrow lines across it representing the beams and sails. Trees would look like circles, buildings would look like squares. Even 2D games from the previous century would rarely do this; rather, they would have some oblique perspective baked into their sprites:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/19/around-the-world-12-2d-or-not-2d/simcity.png" alt="SimCity Classic screenshot" &gt;
&lt;br&gt;
&lt;em&gt;SimCity Classic (1994)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Oddly, it wasn&amp;rsquo;t until I bought the newly released Pioneers of Pagonia, a real-time strategy game by the makers of the old Settlers games, that I saw the extremely obvious solution:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://frozenfractal.com/blog/2024/3/19/around-the-world-12-2d-or-not-2d/pioneers_of_pagonia.jpg" alt="Screenshot of Pioneers of Pagonia" &gt;
&lt;br&gt;
&lt;em&gt;Pioneers of Pagonia, screenshot by the developers&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The camera used in modern RTS games like this could work for me as well: a mostly top-down view, but with the camera tilted slightly from the vertical. This would show objects slightly from the side, giving a better sense of their shape, while still letting me get away with low-detail, low-poly 3D models because the camera never gets too close.&lt;/p&gt;
&lt;p&gt;Also, it saves me from having to procedurally generate and render good-looking skies and clouds, which would be a rabbit hole that I would absolutely &lt;em&gt;love&lt;/em&gt; to go down, but would keep me busy for months.&lt;/p&gt;
&lt;p&gt;As a bonus, the circle of viewable area is squashed into a wide ellipse, better fitting the aspect ratio of a PC screen.&lt;/p&gt;
&lt;p&gt;So, that&amp;rsquo;s what we&amp;rsquo;re working with. In the next post, let&amp;rsquo;s see what our generated worlds might look like from this perspective!&lt;/p&gt;</description><category>game design</category><category>Around The World</category></item></channel></rss>