Search code examples
unity-game-enginegeometrycontourterrain

Creating a 2D circle with a flood approach


I'm trying to generate topographical contour lines looking something like this: enter image description here

My eventual goal is to generate contour lines based on a few user-defined points (ex: this hill is at 100m elevation, and has a slope of X).

For now, my algorithm for generating a heightmap is a kind of radial flood fill, where each point sets its current height, then enqueues its 8 neighbours (including diagonals) to have their height set to the current height - the value of the slope. heightmap is a 2D array of doubles representing the height at each x/y point on the map. sqrt_2 is the value of square root of 2, which we multiply the slope value of diagonal neighbours to represent their true distance from the current point. Each point also propagates outwards its slope (the rate at which the height moves towards the default height. EnqueueIfValidPoint simply adds a point to the points_to_assign queue. The idea is to start at certain points that we KNOW the height of, and slowly slope/gradient towards a default height (0 in this case). points_to_assign is a regular FIFO Queue.

This code is written in C# in Unity, but the language does not change the logic behind it.

// Continue the flood fill until we're out of points to assign
while (points_to_assign.Count > 0)
{

    PointToAssign p = points_to_assign.Dequeue();

    // Check if we have already assigned a height to this point
    if (heightmap[p.x_pos, p.y_pos] == unassigned_height)
    {
        assigned_points++;
        // Assign a height to this point
        heightmap[p.x_pos, p.y_pos] = p.height;


        // Height to assign neighbours to, moved towards default floor value
        double slope = p.slope;//GetRandomSlope(p.x_pos, p.y_pos, p.slope);

        double orthogonal_heights = 0;
        if (p.height >= 0)
            orthogonal_heights = Math.Max(0, p.height - (slope));
        else
            orthogonal_heights = Math.Min(0, p.height + (slope));
        // Enqueue neighbours of this point to assign a new height to
        // Below
        EnqueueIfValidPoint(points_to_assign, heightmap, p.x_pos - 1, p.y_pos, orthogonal_heights, p.slope);
        // Above
        EnqueueIfValidPoint(points_to_assign, heightmap, p.x_pos + 1, p.y_pos, orthogonal_heights, p.slope);
        // Left
        EnqueueIfValidPoint(points_to_assign, heightmap, p.x_pos, p.y_pos - 1, orthogonal_heights, p.slope);
        // Right
        EnqueueIfValidPoint(points_to_assign, heightmap, p.x_pos, p.y_pos + 1, orthogonal_heights, p.slope);

        double diagonal_heights = 0;
        if (p.height >= 0)
            diagonal_heights = Math.Max(0, p.height - (slope * sqrt_2));
        else
            diagonal_heights = Math.Min(0, p.height + (slope * sqrt_2));
        // Below and left
        EnqueueIfValidPoint(points_to_assign, heightmap, p.x_pos - 1, p.y_pos - 1, diagonal_heights, p.slope);
        // Below and right
        EnqueueIfValidPoint(points_to_assign, heightmap, p.x_pos + 1, p.y_pos - 1, diagonal_heights, p.slope);
        // Above and left
        EnqueueIfValidPoint(points_to_assign, heightmap, p.x_pos - 1, p.y_pos + 1, diagonal_heights, p.slope);
        // Above and right
        EnqueueIfValidPoint(points_to_assign, heightmap, p.x_pos + 1, p.y_pos + 1, diagonal_heights, p.slope);
    }
}

Height values are then passed to a colour function that assigns a colour, ex: if the height is between 0 and 1, assign it white, if it's between 1 and 2, assign it light green, etc.

Unfortunately this code doesn't quite produce circle, and instead produces a octagon. I imagine the problem has to do with encoding the diagonal neighbour values enter image description here

Does anyone have any idea how to produce a circle, instead of an octagon using my flood fill strategy?


Solution

  • The problem is that you are getting wrong distances when you mix your two step types (diagonal and orthogonal). E.g., an orthogonal step + a diagonal step result in an actual distance of sqrt(5) ~ 2.24. But your algorithm gives you 1 + sqrt(2) ~ 2.41. That's why the circle is cut off.

    What you need to do is calculate the actual distance from the seed point. Simply store the seed point with the items in the queue (or if there is only one, then use this one) and calculate the height from the distance. Something along:

    heightmap[p.x_pos, p.y_pos] = distance(p.seedPoint, p) * p.slope + p.seedPoint.height;
    

    You could also store the seed point and its slope externally and just reference it in the queue to save some memory.

    It is also possible to incrementally calculate the Euclidean distance by accumulating the x-difference and the y-difference and then just calculate sqrt(x-difference^2 + y-difference^2). But this is probably not worth the effort.