psemek/tools/noise-generator/generator.cpp

553 lines
14 KiB
C++

#include <psemek/pcg/perlin.hpp>
#include <psemek/pcg/fractal.hpp>
#include <psemek/random/device.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform.hpp>
#include <psemek/random/uniform_sphere.hpp>
#include <psemek/gfx/pixmap.hpp>
#include <psemek/util/hash_table.hpp>
#include <psemek/io/file_stream.hpp>
using namespace psemek;
template <int D>
auto moore_neighbourhood()
{
if constexpr (D == 2)
{
std::array<math::vector<int, D>, 9> result;
for (int dy = -1; dy <= 1; ++dy)
for (int dx = -1; dx <= 1; ++dx)
result[(dy + 1) * 3 + (dx + 1)] = {dx, dy};
return result;
}
else if constexpr (D == 3)
{
std::array<math::vector<int, D>, 27> result;
for (int dz = -1; dz <= 1; ++dz)
for (int dy = -1; dy <= 1; ++dy)
for (int dx = -1; dx <= 1; ++dx)
result[(dz + 1) * 9 + (dy + 1) * 3 + (dx + 1)] = {dx, dy, dz};
return result;
}
}
template <int D>
util::ndarray<std::uint8_t, D> generate_perlin(std::array<std::size_t, D> const & size, int octaves, int minoctave, float power,
bool invert, bool tile, float gamma, bool remap, std::uint64_t seed)
{
std::cout << "Generating " << D << "D perlin noise\n";
std::cout << "Size ";
for (int d = 0; d < D; ++d)
{
if (d > 0)
std::cout << "x";
std::cout << size[d];
}
std::cout << "\nOctaves: " << minoctave << ".." << (minoctave + octaves - 1) << "\n";
std::cout << "Power: " << power << "\n";
std::cout << "Invert: " << (invert ? "true" : "false") << "\n";
std::cout << "Tile: " << (tile ? "true" : "false") << "\n";
std::cout << "Gamma: " << gamma << "\n";
std::cout << "Remap: " << (remap ? "true" : "false") << "\n";
std::cout << "Seed: " << std::hex << std::setw(16) << std::setfill('0') << seed << "\n";
std::cout << std::flush;
random::generator rng{seed, 0};
std::vector<pcg::perlin<float, D>> octave_noise;
std::vector<float> octave_weights;
float sum_octave_weights = 0.f;
random::uniform_sphere_vector_distribution<float, D> random_gradient;
for (int o = minoctave; o < (minoctave + octaves); ++o)
{
std::array<std::size_t, D> octave_size;
for (int d = 0; d < D; ++d)
octave_size[d] = (1 << o) + (tile ? 0 : 1);
util::ndarray<math::vector<float, D>, D> gradients(octave_size);
for (auto & g : gradients)
g = random_gradient(rng);
if (tile)
octave_noise.emplace_back(std::move(gradients), pcg::seamless);
else
octave_noise.emplace_back(std::move(gradients));
float weight = std::pow(static_cast<float>(1 << o), - power);
octave_weights.push_back(weight);
sum_octave_weights += weight;
}
for (auto & w : octave_weights)
w /= sum_octave_weights;
pcg::fractal<pcg::perlin<float, D>> fractal_noise(std::move(octave_noise), std::move(octave_weights));
int largest_size = size[0];
for (int d = 0; d < D; ++d)
largest_size = std::max<int>(largest_size, size[d]);
util::ndarray<float, D> result_float(size);
math::interval<float> value_range;
for (auto idx : result_float.indices())
{
math::point<float, D> p;
for (int d = 0; d < D; ++d)
{
float unused;
p[d] = std::modf((idx[d] + 0.5f) / largest_size, &unused);
}
float v = fractal_noise(p);
result_float(idx) = v;
value_range |= v;
}
util::ndarray<std::uint8_t, D> result(size);
for (auto idx : result.indices())
{
float v = result_float(idx);
if (remap)
v = math::unlerp(value_range, v);
if (invert)
v = 1.f - v;
v = std::pow(v, gamma);
result(idx) = static_cast<std::uint8_t>(std::clamp(v * 255.f, 0.f, 255.f));
}
return result;
}
template <int D>
util::ndarray<std::uint8_t, D> generate_worley(std::array<std::size_t, D> const & size, int octaves, int minoctave, float power,
bool invert, bool tile, float gamma, bool remap, std::uint64_t seed)
{
std::cout << "Generating " << D << "D worley noise\n";
std::cout << "Size ";
for (int d = 0; d < D; ++d)
{
if (d > 0)
std::cout << "x";
std::cout << size[d];
}
std::cout << "\nOctaves: " << minoctave << ".." << (minoctave + octaves - 1) << "\n";
std::cout << "Power: " << power << "\n";
std::cout << "Invert: " << (invert ? "true" : "false") << "\n";
std::cout << "Tile: " << (tile ? "true" : "false") << "\n";
std::cout << "Gamma: " << gamma << "\n";
std::cout << "Remap: " << (remap ? "true" : "false") << "\n";
std::cout << "Seed: " << std::hex << std::setw(16) << std::setfill('0') << seed << "\n";
std::cout << std::flush;
random::generator rng{seed, 0};
std::vector<util::ndarray<math::point<float, D>, D>> octave_points;
std::vector<float> octave_weights;
float sum_octave_weights = 0.f;
for (int o = minoctave; o < minoctave + octaves; ++o)
{
std::array<std::size_t, D> octave_size;
for (int d = 0; d < D; ++d)
octave_size[d] = (1 << o);
util::ndarray<math::point<float, D>, D> octave(octave_size);
for (auto idx : octave.indices())
{
auto & p = octave(idx);
for (int d = 0; d < D; ++d)
p[d] = idx[d] + random::uniform<float>(rng);
}
octave_points.push_back(std::move(octave));
float weight = std::pow(static_cast<float>(1 << o), - power);
octave_weights.push_back(weight);
sum_octave_weights += weight;
}
for (auto & w : octave_weights)
w /= sum_octave_weights;
util::ndarray<float, D> result_float(size);
math::interval<float> value_range;
for (auto idx : result_float.indices())
{
math::point<float, D> p;
for (int d = 0; d < D; ++d)
p[d] = (idx[d] + 0.5f) / size[d] * (1 << minoctave);
float v = 0.f;
for (int o = 0; o < octaves; ++o)
{
math::point<int, D> i;
for (int d = 0; d < D; ++d)
i[d] = std::floor(p[d]);
int octave_size = 1 << (o + minoctave);
float closest_distance = std::numeric_limits<float>::infinity();
for (auto n : moore_neighbourhood<D>())
{
math::point<int, D> j = i + n;
if (tile)
{
for (int d = 0; d < D; ++d)
j[d] = (j[d] + octave_size) % octave_size;
}
else
{
bool good = true;
for (int d = 0; d < D; ++d)
{
good &= (j[d] >= 0);
good &= (j[d] < octave_size);
}
if (!good)
continue;
}
math::vector<float, D> r = octave_points[o](j) - p;
if (tile)
{
for (int d = 0; d < D; ++d)
{
if (i[d] + n[d] == -1)
r[d] -= octave_size;
else if (i[d] + n[d] == octave_size)
r[d] += octave_size;
}
}
math::make_min(closest_distance, math::length(r));
}
v += closest_distance * octave_weights[o] / std::sqrt(2.f);
for (int d = 0; d < D; ++d)
p[d] *= 2.f;
}
result_float(idx) = v;
value_range |= v;
}
util::ndarray<std::uint8_t, D> result(size);
for (auto idx : result.indices())
{
float v = result_float(idx);
if (remap)
v = math::unlerp(value_range, v);
if (invert)
v = 1.f - v;
v = std::pow(v, gamma);
result(idx) = static_cast<std::uint8_t>(std::clamp(v * 255.f, 0.f, 255.f));
}
return result;
}
char const options_usage[] =
R"(
Available options:
type
Noise type (perlin or worley)
Default = worley
dim
Noise dimension (2 or 3)
Default = 2
sizex
Size along X coordinate, in pixels
Default = 256
sizey
Size along Y coordinate, in pixels
Default = 256
sizez
Size along Z coordinate, in pixels (for dim = 3)
Default = 256
octaves
Number of noise octaves (higher octaves have higher frequency)
Default = 8
minoctave
First octave index (0-th octave is square with the size of largest output dimension)
Default = 0
power
Octaves weight power (0.0 means all octaves have equal contributions), can be negative
Default = 1.0
invert
Replace noise values with 1-value (makes sense for worley noise only)
Default = false
tile
Whether to make the noise tileable (seamless)
Default = true
gamma
The gamma value to apply to the output (gamma = 2.2 means sRGB-like conversion)
Default = 1.0
remap
Whether to remap noise values to [0, 1] range (adding many actaves tends to average out the noise)
Default = true
seed
Random seed (a 64-bit decimal or hex integer, or "random")
Default = random
)";
std::optional<int> parse_int(std::string const & str)
{
try
{
return std::stoi(str);
}
catch (...)
{
return std::nullopt;
}
}
std::optional<std::uint64_t> parse_seed(std::string const & str, int base)
{
try
{
std::size_t unused;
return std::stoull(str, &unused, base);
}
catch (...)
{
return std::nullopt;
}
}
std::optional<float> parse_float(std::string const & str)
{
try
{
return std::stof(str);
}
catch (...)
{
return std::nullopt;
}
}
std::optional<bool> parse_bool(std::string const & str)
{
if (str == "true")
return true;
else if (str == "false")
return false;
else
return std::nullopt;
}
int main(int argc, char * argv[])
{
util::hash_map<std::string, std::string> values;
values["type"] = "perlin";
values["dim"] = "2";
values["sizex"] = "256";
values["sizey"] = "256";
values["sizez"] = "256";
values["octaves"] = "8";
values["minoctave"] = "0";
values["power"] = "1.0";
values["invert"] = "false";
values["tile"] = "true";
values["gamma"] = "1.0";
values["remap"] = "true";
values["seed"] = "random";
std::string output_path;
if (argc < 2)
{
std::cerr << "Usage: " << argv[0] << " <output-path> [ key1=value1 [ key2=value2 ... ] ]\n";
std::cerr << options_usage;
return EXIT_FAILURE;
}
output_path = argv[1];
for (int i = 2; i < argc; ++i)
{
std::string arg = argv[i];
auto divider = arg.find('=');
if (divider == std::string::npos) {
std::cerr << "Failed to parse argument \"" << arg << "\"\n";
return EXIT_FAILURE;
}
auto key = arg.substr(0, divider);
auto value = arg.substr(divider + 1);
if (!values.contains(key)) {
std::cerr << "Unknown key \"" << key << "\"\n";
return EXIT_FAILURE;
}
values[key] = value;
}
auto type = values["type"];
auto dim = parse_int(values["dim"]);
auto sizex = parse_int(values["sizex"]);
auto sizey = parse_int(values["sizey"]);
auto sizez = parse_int(values["sizez"]);
auto octaves = parse_int(values["octaves"]);
auto minoctave = parse_int(values["minoctave"]);
auto power = parse_float(values["power"]);
auto invert = parse_bool(values["invert"]);
auto tile = parse_bool(values["tile"]);
auto gamma = parse_float(values["gamma"]);
auto remap = parse_bool(values["remap"]);
std::optional<std::size_t> seed;
if (values["seed"] == "random") {
random::device device{};
seed = (static_cast<std::uint64_t>(device()) << 32) | static_cast<std::uint64_t>(device());
}
if (!seed) seed = parse_seed(values["seed"], 10);
if (!seed) seed = parse_seed(values["seed"], 16);
if (type != "perlin" && type != "worley")
{
std::cerr << "Unknown noise type: " << values["type"] << "\n";
return EXIT_FAILURE;
}
if (!dim || (*dim != 2 && *dim != 3)) {
std::cerr << "Unknown noise dimension: " << values["dim"] << "\n";
return EXIT_FAILURE;
}
if (!sizex || *sizex <= 0) {
std::cerr << "Size must be a positive integer: " << values["sizex"] << "\n";
return EXIT_FAILURE;
}
if (!sizey || *sizey <= 0) {
std::cerr << "Size must be a positive integer: " << values["sizey"] << "\n";
return EXIT_FAILURE;
}
if (!sizez || *sizez <= 0) {
std::cerr << "Size must be a positive integer: " << values["sizez"] << "\n";
return EXIT_FAILURE;
}
if (!octaves || *octaves <= 0) {
std::cerr << "Number of octaves must be a positive integer: " << values["octaves"] << "\n";
return EXIT_FAILURE;
}
if (!minoctave || *minoctave < 0) {
std::cerr << "First octave must be a nonnegative integer: " << values["minoctave"] << "\n";
return EXIT_FAILURE;
}
if (!power) {
std::cerr << "Power must be a floating-point number: " << values["power"] << "\n";
return EXIT_FAILURE;
}
if (!invert) {
std::cerr << "Invert must be true or false: " << values["invert"] << "\n";
return EXIT_FAILURE;
}
if (!tile) {
std::cerr << "Tile must be true or false: " << values["tile"] << "\n";
return EXIT_FAILURE;
}
if (!gamma) {
std::cerr << "Gamma must be a floating-point number: " << values["gamma"] << "\n";
return EXIT_FAILURE;
}
if (!remap) {
std::cerr << "Remap must be true or false: " << values["remap"] << "\n";
return EXIT_FAILURE;
}
if (!seed) {
std::cerr << "Seed must be a decimal or hex integer: " << values["seed"] << "\n";
return EXIT_FAILURE;
}
if (*dim == 2)
{
util::ndarray<std::uint8_t, 2> result;
if (type == "perlin")
result = generate_perlin<2>({*sizex, *sizey}, *octaves, *minoctave, *power, *invert, *tile, *gamma, *remap, *seed);
else if (type == "worley")
result = generate_worley<2>({*sizex, *sizey}, *octaves, *minoctave, *power, *invert, *tile, *gamma, *remap, *seed);
io::file_ostream out{std::filesystem::path(output_path)};
if (output_path.ends_with(".png"))
{
std::cout << "Saving PNG image to " << output_path << std::endl;
gfx::write_image_png(result, std::move(out));
}
else if (output_path.ends_with(".tga"))
{
std::cout << "Saving TGA image to " << output_path << std::endl;
gfx::write_image_tga(result, std::move(out));
}
else if (output_path.ends_with(".pgm"))
{
std::cout << "Saving PGM image to " << output_path << std::endl;
gfx::write_image_pgm(result, std::move(out));
}
else
{
std::cout << "Saving raw image to " << output_path << std::endl;
out.write(reinterpret_cast<char const *>(result.data()), result.size());
}
}
if (*dim == 3)
{
util::ndarray<std::uint8_t, 3> result;
if (type == "perlin")
generate_perlin<3>({*sizex, *sizey, *sizez}, *octaves, *minoctave, *power, *invert, *tile, *gamma, *remap, *seed);
else if (type == "worley")
result = generate_worley<3>({*sizex, *sizey, *sizez}, *octaves, *minoctave, *power, *invert, *tile, *gamma, *remap, *seed);
io::file_ostream out{std::filesystem::path(output_path)};
std::cout << "Saving raw image to " << output_path << std::endl;
out.write(reinterpret_cast<char const *>(result.data()), result.size());
}
}