Procedural river generation

So I am creating procedural terrain using perlin noise and it’s going fairly well so far. I can create shorelines and mountain ranges. I can’t attach more than one photo apparently, so if you can imagine the image at the bottom without the rivers then that’s what I have at the moment.

At the moment my shoreline and mountain ranges work by generating a width x height (eg 200 x 200) map and setting each value through a function:

coastline::

float coastGen(int x, int y, float fB, float fS, float fG, Vector2 fO)
        {
            float retValue;
            float mapDiagonal = Mathf.Sqrt((mapWidth * mapWidth) + (mapHeight * mapHeight));
            Vector2 shorePoint;
            shorePoint.x = Mathf.Sin(fB) * fS;
            shorePoint.y = Mathf.Cos(fB) * fS;
            float X = x - fO.x;
            float Y = y - fO.y;
            float A = Mathf.Sqrt(((X - shorePoint.x) * (X - shorePoint.x)) + ((Y - shorePoint.y) * (Y - shorePoint.y)));
            float C = Mathf.Sqrt((X * X) + (Y * Y));
            float B = fS;
            float angFromOrigin = Mathf.Acos((C * C - A * A - B * B) / (-2 * A * B));
            float angFromShore = angFromOrigin - 90 * Mathf.Deg2Rad;

            float distFromShore = Mathf.Sin(angFromShore) * A;
            if (distFromShore >= 0)
            {
                retValue = 1 - (distFromShore / (mapDiagonal - fS)) * fG;
                if (retValue < 0.2f)
                {
                    retValue = 0.2f;
                }
            }
            else
            {
                retValue = 1;
            }
            return retValue;
        }

mountain range::

float mountGen(int x, int y, float fS, float fG, Vector2 fO)
        {
            float X = x - fO.x;
            float Y = y - fO.y;
            float dist = (fS - Mathf.Sqrt((X * X) + (Y * Y))) * fG/100;
            if (dist < 1)
            {
                dist = 1;
            }
            return dist;
        }

map generation::

for (int y = 0; y < mapHeight; y++)
        {
            for (int x = 0; x < mapWidth; x++)
            {
                featureMap[x, y] = 1;
                for (int i = 0; i < terrainFeatures.Length; i++)
                {
                    float fG = 1;
                    if (terrainFeatures[i].featureType == featureOptions.Coast)
                    {
                        fG = coastGen(x, y, terrainFeatures[i].featureBearing, terrainFeatures[i].featureShore, terrainFeatures[i].featureGradient, terrainFeatures[i].featureOffset);
                    }
                    else if (terrainFeatures[i].featureType == featureOptions.Mountains)
                    {
                        fG = mountGen(x, y, terrainFeatures[i].featureShore, terrainFeatures[i].featureGradient, terrainFeatures[i].featureOffset);
                    }
                    else if (terrainFeatures[i].featureType == featureOptions.River)
                    {
                        fG = riverGen(x, y, riverMap);
                    }
                    if (fG != 1)
                    {
                        featureMap[x, y] = featureMap[x, y] * fG;
                    }
                }
            }
        }

Sorry for the long chunks of code!
Anyway, my question is : how can I create a river feature, ideally using “spaghetti” perlin noise? I’d like it to follow similar methods to the other ones (i.e. I can apply it to the general feature map).
So far what I have creates a kind of river system, but I can’t isolate one strand or control it at all.

float riverGen(int x, int y, float[,] riverMap)
        {
            float height = riverMap[x,y];
            float lowBound = 0.45f;
            float topBound = 0.55f;
            if (height >= lowBound && height <= topBound)
            {
                return 0;
            }
            else
            {
                return 1;
            }
        }

Again, sorry for code dumps!
Any help is appreciated

I saw this tutorial about Voronoi polygon terrain a while ago. So for rivers it picks a point and uses the terrain slope to go downhill to a lake or ocean. If you want it to moving in a general direction then give it an extra force in that direction.


Thanks for the suggestion, I tried to implement this but the way I have my terrain set up means that unless I start the river on a mountain, it is very short and doesn’t meander anywhere. While this method seems good for whole landmasses or larger scales of terrain, I’m trying to take samples of terrain and passing them parameters to create rivers, mountains etc.
My other terrain features were also based off passing coordinates (x,y) through a function to apply a modifier to that location, so this kind of noise didn’t fit the rest of the system either.

For anyone looking for a similar solution, here’s what I ended up with:

  1. Pass parameters in; x and y coordinates, river bearing, river width, river offset.
  2. Imagine a “centre line” for the river, basically a line at the angle of the river bearing, which passes through the river offset.
  3. Find the smallest distance from this line to the x,y coordinates using trigonometry.
  4. If this distance is lower than the river width the apply a x0 modifier to the terrain map.
  5. Otherwise apply a x1 modifier.

This results in a line gouged into the terrain at the parameters I pass in.
Now to give the river a bed:

  1. Add another parameter to the function, a spline ranging from 0 to 1 in both axes. This looks like the bottom half of a x^2 + y^2 = z graph in my project, i.e. a 180* curve.
  2. Instead of applying a x0 modifier in step 4 of the original method, get the y value at the x position of this new curve, with x being the distance from the bearing line divided by the overall river width. Essentially being close to the line results in a low / high value, meaning a lower point of the spline is sampled, and being close to the width from the line has the opposite effect.
  3. Take this value and apply it to the terrain map.
  4. Step 5 of the original method still applies; if the distance from the bearing line is larger than the river width the terrain map is left alone.

And now to add meanders to the river:

  1. Create a global 1-dimensional noise map, i.e. a noise line, ranging from 0 to 1.
  2. In between Steps 3 and 4 in the original method, find the distance “along” the bearing line, i.e. between points on the bearing line perpendicular to the x,y coordinate and the river’s offset point.
  3. Use this distance “along” the bearing line to get a value out of the noise line from step 1.
  4. Multiply this noise line value by a new parameter, river meander distance.
  5. Add this value onto the value from Step 3 of the original method, basically offsetting the river central line using noise.

And that’s it. I’ve put the relevant code and what the river looks like on terrain below.


        float riverGen(int x, int y, float fB, float fS, float fG, Vector2 fO, AnimationCurve fP, float[] riverMap)
        {
            float retValue;
            float A;
            float B;
            float X = x - fO.x;
            float Y = y - fO.y;
            int quadrant = quad((int)X, (int)Y);
            if (quadrant == 1 || quadrant == 3)
            {
                A = Mathf.Sqrt(Y * Y);
            }
            else
            {
                A = Mathf.Sqrt(X * X);
            }
            float rawDist = Mathf.Sqrt((X * X) + (Y * Y));
            float angFromOrigin = Mathf.Asin(A / rawDist);
            angFromOrigin = angFromOrigin + (quadrant * 90 * Mathf.Deg2Rad);

            float distAlongBearing = Mathf.Cos(angFromOrigin - (fB * Mathf.Deg2Rad)) * rawDist;
            float rawDistFromBearing = Mathf.Sin(angFromOrigin - (fB * Mathf.Deg2Rad)) * rawDist;
            
            float distFromBearing = rawDistFromBearing - riverMap[(int)(mapWidth*2+distAlongBearing)]*fG;

            if (distFromBearing <= fS && distFromBearing >= fS * -1)
            {
                retValue = fP.Evaluate(distFromBearing/fS);
                if (retValue < minimumFeatureMultiplier)
                {
                    retValue = minimumFeatureMultiplier;
                }
                if (retValue > 1)
                {
                    retValue = 1;
                }
            }
            else
            {
                retValue = 1;
            }
            return retValue;
        }