From 94ea4cf9323332ba1e4c5168bfb1a503664a45c5 Mon Sep 17 00:00:00 2001 From: lisyarus Date: Sat, 23 Dec 2023 15:24:08 +0300 Subject: [PATCH] Add noise-generator tool --- tools/noise-generator/CMakeLists.txt | 1 + tools/noise-generator/generator.cpp | 356 +++++++++++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 tools/noise-generator/CMakeLists.txt create mode 100644 tools/noise-generator/generator.cpp diff --git a/tools/noise-generator/CMakeLists.txt b/tools/noise-generator/CMakeLists.txt new file mode 100644 index 00000000..ad1c6f8e --- /dev/null +++ b/tools/noise-generator/CMakeLists.txt @@ -0,0 +1 @@ +psemek_add_build_tool(psemek-noise-generator TRUE generator.cpp) diff --git a/tools/noise-generator/generator.cpp b/tools/noise-generator/generator.cpp new file mode 100644 index 00000000..7d5e1663 --- /dev/null +++ b/tools/noise-generator/generator.cpp @@ -0,0 +1,356 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace psemek; + +template +util::array generate(std::array const & size, int octaves, int minoctave, float power, + bool tile, float gamma, bool remap, std::uint64_t seed) +{ + std::cout << "Generating " << D << "D 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 << "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> octave_noise; + std::vector octave_weights; + float sum_octave_weights = 0.f; + + random::uniform_sphere_vector_distribution random_gradient; + + for (int o = minoctave; o < (minoctave + octaves); ++o) + { + std::array octave_size; + for (int d = 0; d < D; ++d) + octave_size[d] = (1 << o) + (tile ? 0 : 1); + + util::array, 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(1 << o), - power); + octave_weights.push_back(weight); + sum_octave_weights += weight; + } + + for (auto & w : octave_weights) + w /= sum_octave_weights; + + pcg::fractal> 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(largest_size, size[d]); + + util::array result_float(size); + geom::interval value_range; + + for (auto idx : result_float.indices()) + { + geom::point 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::array result(size); + + for (auto idx : result.indices()) + { + float v = result_float(idx); + if (remap) + v = geom::unlerp(value_range, v); + v = std::pow(v, gamma); + result(idx) = static_cast(std::clamp(v * 255.f, 0.f, 255.f)); + } + + return result; +} + +char const options_usage[] = +R"( +Available options: + + 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 + + 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 parse_int(std::string const & str) +{ + try + { + return std::stoi(str); + } + catch (...) + { + return std::nullopt; + } +} + +std::optional parse_seed(std::string const & str, int base) +{ + try + { + std::uint64_t unused; + return std::stoull(str, &unused, base); + } + catch (...) + { + return std::nullopt; + } +} + +std::optional parse_float(std::string const & str) +{ + try + { + return std::stof(str); + } + catch (...) + { + return std::nullopt; + } +} + +std::optional 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[]) +{ + std::unordered_map values; + values["dim"] = "2"; + values["sizex"] = "256"; + values["sizey"] = "256"; + values["sizez"] = "256"; + values["octaves"] = "8"; + values["minoctave"] = "0"; + values["power"] = "1.0"; + 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] << " [ 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 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 tile = parse_bool(values["tile"]); + auto gamma = parse_float(values["gamma"]); + auto remap = parse_bool(values["remap"]); + + std::optional seed; + if (values["seed"] == "random") { + random::device device{}; + seed = (static_cast(device()) << 32) | static_cast(device()); + } + if (!seed) seed = parse_seed(values["seed"], 10); + if (!seed) seed = parse_seed(values["seed"], 16); + + 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 (!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) + { + auto result = generate<2>({*sizex, *sizey}, *octaves, *minoctave, *power, *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(result.data()), result.size()); + } + } + + if (*dim == 3) + { + auto result = generate<3>({*sizex, *sizey, *sizez}, *octaves, *minoctave, *power, *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(result.data()), result.size()); + } +} +