on
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:
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
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.
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:
This is the setup from libnoise tutorial 5!
In action: