Finishing the CPU code

In the last couple of lessons, we looked at the library scripts Utils.gd and RiverGenerator.gd. In this lesson, we’ll put everything together to get our final result.

We need to calculate and process our rivers, heatmap, and moisture map before passing them to the shader.

Open WorldMap.gd. In generate() function, after the line height_map = domain_warp(height_map, 10, 0.15), add the following:

var rivers_level := Vector2(color_map.gradient.offsets[1], color_map.gradient.offsets[-2])
var rivers_map := RiverGenerator.generate_rivers(_rng, height_map, 10, rivers_level)

This section prepares the rivers_map image texture by calling on RiverGenerator.generate_rivers() with a maximum of ten rivers. We use color_map.gradient.offsets to construct rivers_level, the threshold values for available_start_positions and available_end_positions respectively:

For the rivers, it means that available_start_positions are all positions in the deepwater range, and available_end_positions are all positions in the ice biome range as defined by the height_map values. This range makes our river flow from the top of the mountains down to the ocean.

Next, we calculate heat_map_minmax and moisture_map_minmax. We use them in the shader to normalize the respective noise texture values. Remember that Utils.get_min_max_noise() returns values in the interval [-1, 1] and that’s why we need to normalize them using Utils.normalize_noise_vector2(). Doing so squashes the values in the [0, 1] interval:

var heat_map_minmax := Utils.get_minmax_noise(heat_map)
var moisture_map_minmax := Utils.get_minmax_noise(moisture_map)
heat_map_minmax = Utils.normalize_noise_vector2(heat_map_minmax)
moisture_map_minmax = Utils.normalize_noise_vector2(moisture_map_minmax)

At the end of generate(), add the following code to pass our rivers, heat, and moisture data to the shader:

viewer.material.set_shader_param("rivers_map", rivers_map)
viewer.material.set_shader_param("heat_map_minmax", heat_map_minmax)
viewer.material.set_shader_param("moisture_map_minmax", moisture_map_minmax)

This concludes our work on the CPU’s side. At this point, running the project should result in an image like the one below.

It doesn’t seem like we accomplished much as we still have to finish the shader implementation. We’ll do that in the next lesson.

References

See the full WorldMap.gd code in the next listing.

extends Control


var _rng := RandomNumberGenerator.new()

onready var viewer: ColorRect = $Viewer


func _ready() -> void:
    _rng.randomize()
    generate()


# Generates a new world map and feeds textures into the viewer's shader.
func generate() -> void:
    var color_map: GradientTexture = viewer.material.get_shader_param("color_map")
    var height_map: Texture = viewer.material.get_shader_param("height_map")
    var heat_map: NoiseTexture = viewer.material.get_shader_param("heat_map")
    var moisture_map: NoiseTexture = viewer.material.get_shader_param("moisture_map")

    height_map.noise.seed = _rng.randi()
    heat_map.noise.seed = _rng.randi()
    moisture_map.noise.seed = _rng.randi()

    height_map = domain_warp(height_map, 10, 0.15)
    var rivers_level := Vector2(color_map.gradient.offsets[1], color_map.gradient.offsets[-2])
    var rivers_map := RiverGenerator.generate_rivers(_rng, height_map, 10, rivers_level)
    var heat_map_minmax := Utils.get_minmax_noise(heat_map)
    var moisture_map_minmax := Utils.get_minmax_noise(moisture_map)
    heat_map_minmax = Utils.normalize_noise_vector2(heat_map_minmax)
    moisture_map_minmax = Utils.normalize_noise_vector2(moisture_map_minmax)


    viewer.material.set_shader_param("color_map", discrete(color_map))
    viewer.material.set_shader_param("color_map_offsets", to_sampler2D(color_map.gradient.offsets))
    viewer.material.set_shader_param("color_map_offsets_n", color_map.gradient.offsets.size())
    viewer.material.set_shader_param("height_map", height_map)
    viewer.material.set_shader_param("rivers_map", rivers_map)
    viewer.material.set_shader_param("heat_map_minmax", heat_map_minmax)
    viewer.material.set_shader_param("moisture_map_minmax", moisture_map_minmax)



# Uses the 2D noise value as z-axis in the 3D noise function to generate a more realistic
# height map.
func domain_warp(nt: NoiseTexture, strength: float, size: float) -> ImageTexture:
    var out := ImageTexture.new()
    strength = max(0, strength)
    size = max(0, size)

    var data := []
    var minmax := Vector2(INF, -INF)
    for x in range(nt.width):
        for y in range(nt.height):
            var value := strength * nt.noise.get_noise_2d(size * x, size * y)
            value = nt.noise.get_noise_3d(x, y, value)
            minmax.x = min(minmax.x, value)
            minmax.y = max(minmax.y, value)
            data.push_back(value)

    var bytes = StreamPeerBuffer.new()
    for d in data:
        bytes.put_float(range_lerp(d, minmax.x, minmax.y, 0, 1))

    var image := Image.new()
    image.create_from_data(nt.width, nt.height, false, Image.FORMAT_RF, bytes.data_array)
    out.create_from_image(image, 0)

    return out


# Converts a smooth gradient to a discrete texture, that is to say,
# a texture with hard color transitions.
func discrete(gt: GradientTexture) -> ImageTexture:
    var out := ImageTexture.new()
    var image := Image.new()

    image.create(gt.width, 1, false, Image.FORMAT_RGBA8)
    var point_count := gt.gradient.get_point_count()

    image.lock()
    for index in (point_count - 1) if point_count > 1 else point_count:
        var offset1: float = gt.gradient.offsets[index]
        var offset2: float = gt.gradient.offsets[index + 1] if point_count > 1 else 1
        var color: Color = gt.gradient.colors[index]
        for x in range(gt.width * offset1, gt.width * offset2):
            image.set_pixel(x, 0, color)
    image.unlock()
    out.create_from_image(image, 0)

    return out


# Converts an array of floating point values to an ImageTexture, to sample with a shader.
func to_sampler2D(array: PoolRealArray) -> ImageTexture:
    var bytes := StreamPeerBuffer.new()
    for x in array:
        bytes.put_float(x)

    var image := Image.new()
    image.create_from_data(array.size(), 1, false, Image.FORMAT_RF, bytes.data_array)

    var out := ImageTexture.new()
    out.create_from_image(image, 0)
    return out