Blomst : procedural planets & shaders!
Hey! Last time around I wrote a bunch of posts about shader stuff in our LD37 entry, LOCK AND RELOAD, which can be found on the old website:
This time around, I thought I'd do something similar for our LD38 game, Blomst 🌻, which also featured some procedural generation in it—something I've rarely done—so there's a lot of new concepts this time! c:
Go here if you want to play Blomst 🌻!
Procedural planets

The main mechanic of the game is to sow seeds to let different kinds of plants spring up and build up an atmosphere to terraform the planet. 🌱
We wanted to associate different kinds of plant with different "biomes", so that some could only be placed on the green greens, others only in the sandy deserts and the rest in watery puddles. So I had to make all of these areas show up on the planet!
Generating the sphere
First things first. There are different ways to go about producing a round 3D object. We could use an icosphere/polyhedron (left) or we could use a UV sphere (right).

There are pros and cons to both depending on the application. Without really considering the other option at the time, I just went ahead and generated a UV sphere, but in retrospect an icosphere would probably have been better for the tiled nature of the planets, to keep each tile about the same size, either as hexagons by joining five triangles, or as quads by joining two. UV spheres have a problem of pinching at the poles, with faces getting increasingly smaller the closer to the poles they are.
As a nice touch, to make the planets not look so perfectly round, I also displaced random vertices a bit to give a rougher, more potato-like shape to it! 🍠
Dealing with pinched poles
Well, I didn't, really! To cheat around the problematic poles, I ended up just covering them entirely in ice, much like the poles on Earth, and made it so that nothing could grow on those areas. One of them later became the designated spawn point for the main character, and the other one the designated spot for the shop, so it worked out in the end! c;

But of course if this kind of cheating doesn't work for your application, just do the smart thing, unlike me, and use an icosphere! I should have also probably used fewer slices horizontally than vertically to give the tiles more of a square size.
Generating the biomes
So the next thing to do was to figure out how to create the different areas on the planet. I wanted it to be procedural and randomised each time. I ended up generating a very low-resolution texture (if the planet had 20x20 slices, I made the texture 20x20 pixels, so as to assign each pixel on the texture to a tile on the sphere) by use of a seamless simplex noise algorithm so that it would loop nicely around the planet, and assigned different colours to the pixels depending on the noise values, while also hardcoding in the icy poles.
I only needed four types of terrain (greens, sand, water and ice), so I decided to encode each as a full RGBA value in one of the four channels of each pixel, leaving the other channels zeroed. So for example the sand would have an RGBA colour value of (1, 0, 0, 0) (or (255, 0, 0, 0)), i.e. fully transparent red. This way I would only have to read one value for each type in the shader.
A resulting texture looks something like this, with the alpha values filled in again and poles represented by white; in reality of course most pixels were fully transparent:

And on the planet, with the usual interpolation of textures, it would look less pixellated and more rounded (note that this was before I added poles):

Rendering higher-resolution terrain
So now both I and my shader code could see where the different biomes were on the planet, but of course I wanted it to look nicer than a bunch of smudged pixels. Instead of actually displaying the terrain texture on the sphere, I used it to determine which high-res texture to use in a particular spot on it. Again the interpolation would make sure that it didn't look blocky.

Essentially I would read the colour of the terrain texture at the current fragment, something like so:
fixed4 terrain = tex2D(texTerrain, UV);
Then I would read each high-res texture, like so:
fixed3 colSand = tex2D(texSand, UV).rgb;
fixed3 colGreen = tex2D(texGreen, UV).rgb;
fixed3 colWater = tex2D(texWater, UV).rgb;
fixed3 colIce = tex2D(texIce, UV).rgb;
I could then separate the terrain values into one intensity for each biome at the current fragment:
fixed valSand = terrain.r;
fixed valGreen = terrain.g;
fixed valWater = terrain.b;
fixed valIce = terrain.a;
As you can see, which colour channel I chose to use for which type of terrain was somewhat symbolic. Then it was just a matter of multiplication, multiplying intensities by colours to get at the final result!
Optimising and enabling palettes
Four different RGB images really weren't necessary to get the desired results, however, so I decided to optimise things a bit. Instead I turned each of the three first textures (excluding ice) into grayscale images, and encoded each as a single channel into one RGB image, so that the red channel represented the sand pattern, the green the grassy pattern and the blue the watery pattern. I didn't read the ice from a texture at all in the end, but just used pure white, which I didn't need an image for.

Of course, as you can see, the water texture ended up being completely unnecessary since it was just one colour too, but at the time I had planned to actually put some kind of pattern in there as well and reserved the space for it. In the end I actually alpha blended the grass pattern on top in a parallax manner instead, to give the water a little sense of depth as well as reflection.
Now the sampling could be simplified to just one:
fixed3 patterns = tex2D(texPatterns, UV).rgb;
fixed intSand = patterns.r;
fixed intGreen = patterns.g;
fixed intWater = patterns.b;
So now I could read the intensities of each channel, rather than the colours of the whole texture, for each type of terrain, to get its pattern. That would make everything turn black and white, though. To get colours back in, I created a palette texture where the UV position read would correspond to the intensity of the pattern on the horizontal axis and the channel on the vertical one, and so the correct pixel colour would be red.
Here's what a palette looks like:

The left side corresponds to pixels with no intensity ("black") in the pattern texture, "white" on the right, and "greys" in the middle. For a smoother transition between colours I could've used a gradient instead.
In the end we didn't actually end up using different palettes for different planets, like we had initially planned on, however.
Moving on
There's more shader stuff to look at, like the parallax water, the atmosphere around the planet, or how the planet turns from sepia to full colour as the atmosphere grows! But I don't want this one post to get too lengthy, so I'll save it for the next one! Until then! 🎮