Procedural Map Prototyping Tool

In a previous set of posts I showed a small 2D procedural map generating tool I made. The idea was to create a tool that allowed for fast iteration on different noise generation techniques.

Welp. That didn’t workout as well I thought it would. I thought using Lua scripting would make it really flexible, and it did, but it was also really laggy despite running the generation on 4 different threads. Also, just using FastNoise means I have to do all the noise combining myself in the scripts. Not really ideal for a prototyping tool!

What I need is speed, flexibility and fast iteration.

So I’m making another tool.

This one is based on libnoise. Libnoise is a portable C++ library for generating coherent noise. Libnoise has a number of different modules that can be chained together to generate and modify noise maps.

From the libnoise tutorials:

Image not found!

Modules can be combined in various ways to achieve different result. But. Libnoise on its doesn’t really allow for fast iteration. You’d have to change values and re-compile each time you want to try something different. As well as it being tedious to rearrange modules. So this tool needs to provide a layer for managing all the different noise modules.

Initial Concept

Image not found!

Above is the initial block diagram I came up with for mapgen2. I’m going for a Model-View-Controller type pattern. Modules (models) are managed in the backend. Views and Controllers are for viewing and modifing data in the backend.

Image not found!

A node graph editor is a good choice for this type of editor.

I want all the noise module nodes to display a preview of the noise map that they generate. As you change the modules parameters you get a live preview update. I felt that this was critical as it helps to visualize how the different parameters change the final output.

For the GUI I’m going with a combination of Magnum and ImGui. In constrast with the last tool I used SFML + ImGui. I felt Magnum was exactly what I was looking for in terms of a graphic library and decided to try it out. On that note I also needed to use a Magnum ImGui binding and ImGui Addons for the node graph editor and tabs.

Also looks like I can make a number of contributions to those projects so I’m excited to dive into open source!

Noise Module Wrapper

I needed a common unit for interacting with noise modules. So I decided to wrap libnoise modules in a class that knows how to interactive with the different them.

Looks something like this.

class NoiseModule
{
public:
    enum class Type
    {
        Billow,
        Perlin,
        RidgedMulti,
        ScaleBias,
        Select,
        ...
    };

    NoiseModule(Type type)
    {
        //...
    }
};

The underlying libnoise module can be one of 30 options (some examples listed in the enum). I store these options as a variant and use a factory to create them.

    ...
    using ModuleVariant = boost::variant<
        noise::module::Billow,
        noise::module::Perlin,
        noise::module::RidgedMulti,
        noise::module::ScaleBias,
        noise::module::Select
    >;
    ...

class ModuleFactory
{
public:
    static NoiseModule::ModuleVariant createModule(NoiseModule::Type type)
    {
        switch (type)
        {
        case NoiseModule::Type::Billow:
            return { noise::module::Billow() };
        case NoiseModule::Type::Perlin:
            return { noise::module::Perlin() };
        case NoiseModule::Type::RidgedMulti:
            return { noise::module::RidgedMulti() };
        case NoiseModule::Type::ScaleBias:
            return { noise::module::ScaleBias() };
        case NoiseModule::Type::Select:
            return { noise::module::Select() };
        default:
            throw std::runtime_error("Invalid noise type");
            break;
        }
    }
    ...

Parameters needed a single unit to interact with as well, so I also stored those in variants.

    using ParameterVariant = boost::variant<
        int,
        float,
        RangedInt,
        RangedFloat,
    >;
    using ParameterMap = std::map<std::string, ParameterVariant>;
    using ParameterMapPtr = std::shared_ptr<ParameterMap>;

Parameters need to be interacted with by other components in the system. Returning the parameter map as a reference was too prone to errors as I discovered last time. So here I pass them back as a std::shared_ptr. These are also created with a factory method.

    static NoiseModule::ParameterMap createParams(NoiseModule::Type type)
    {
        switch (type)
        {
        case NoiseModule::Type::Billow:
            return {
                { "seed", 1337 },
                { "frequency", (float)noise::module::DEFAULT_BILLOW_FREQUENCY },
                { "octaves", RangedInt(1, 25, noise::module::DEFAULT_BILLOW_OCTAVE_COUNT) },
                { "persistence", RangedFloat(0.f, 1.f, noise::module::DEFAULT_BILLOW_PERSISTENCE) },
                { "lacunarity", RangedFloat(1.f, 2.f, noise::module::DEFAULT_BILLOW_LACUNARITY) },
            };
        case NoiseModule::Type::Perlin:
            return {
                {"seed", 1337},
                {"frequency", (float)noise::module::DEFAULT_PERLIN_FREQUENCY},
                {"octaves", RangedInt(1, 25, noise::module::DEFAULT_PERLIN_OCTAVE_COUNT)},
                {"persistence", RangedFloat(0.f, 1.f, noise::module::DEFAULT_PERLIN_PERSISTENCE)},
                {"lacunarity", RangedFloat(1.f, 4.f, noise::module::DEFAULT_PERLIN_LACUNARITY)},
            };
        case NoiseModule::Type::RidgedMulti:
            return {
                { "seed", 1337 },
                { "frequency", (float)noise::module::DEFAULT_RIDGED_FREQUENCY },
                { "octaves", RangedInt(1, 25, noise::module::DEFAULT_RIDGED_OCTAVE_COUNT) },
                { "lacunarity", RangedFloat(1.f, 4.f, noise::module::DEFAULT_RIDGED_LACUNARITY) },
            };
        case NoiseModule::Type::ScaleBias:
            return {
                {"bias", 0.0f},
                {"scale", 1.0f}
            };
        case NoiseModule::Type::Select:
            return {
                {"lower_bound", (float)noise::module::DEFAULT_SELECT_LOWER_BOUND},
                {"upper_bound", (float)noise::module::DEFAULT_SELECT_UPPER_BOUND},
                {"fall_off", (float)noise::module::DEFAULT_SELECT_EDGE_FALLOFF}
            };
        default:
            throw std::runtime_error("Invalid noise type");
            break;
        }
    }

C++ initializer lists make this pretty slick!

The nice part about using boost::variant is visitors. To set parameters in the different noise modules I created a SetParamsVisitor.

struct SetParamsVisitor : public boost::static_visitor<>
{
public:
    SetParamsVisitor(NoiseModule::ParameterMap& params)
        : params_{params}
    {

    }

    void operator()(noise::module::Billow& module) const
    {
        module.SetSeed(boost::get<int>(params_["seed"]));
        module.SetFrequency(boost::get<float>(params_["frequency"]));
        module.SetOctaveCount(boost::get<RangedInt>(params_["octaves"]).value);
        module.SetPersistence(boost::get<RangedFloat>(params_["persistence"]).value);
        module.SetLacunarity(boost::get<RangedFloat>(params_["lacunarity"]).value);
    }

    void operator()(noise::module::Perlin& module) const
    {
        module.SetSeed(boost::get<int>(params_["seed"]));
        module.SetFrequency(boost::get<float>(params_["frequency"]));
        module.SetOctaveCount(boost::get<RangedInt>(params_["octaves"]).value);
        module.SetPersistence(boost::get<RangedFloat>(params_["persistence"]).value);
        module.SetLacunarity(boost::get<RangedFloat>(params_["lacunarity"]).value);
    }

    void operator()(noise::module::RidgedMulti& module) const
    {
        module.SetSeed(boost::get<int>(params_["seed"]));
        module.SetFrequency(boost::get<float>(params_["frequency"]));
        module.SetOctaveCount(boost::get<RangedInt>(params_["octaves"]).value);
        module.SetLacunarity(boost::get<RangedFloat>(params_["lacunarity"]).value);
    }

    void operator()(noise::module::ScaleBias& module) const
    {
        module.SetBias(boost::get<float>(params_["bias"]));
        module.SetBias(boost::get<float>(params_["scale"]));
    }

    void operator()(noise::module::Select& module) const
    {
        module.SetBounds(boost::get<float>(params_["lower_bound"]), boost::get<float>(params_["upper_bound"]));
        module.SetEdgeFalloff(boost::get<float>(params_["fall_off"]));
    }
private:
    NoiseModule::ParameterMap& params_;
};

Modules are created and remove by a ModuleManager class.

Node Editor

When all the pieces are put together I get a node editor like this:

Image not found!

This is the setup from libnoise tutorial 5!

In action:

Image not found! Image not found! Image not found!