#include #include #include #include #include #include #include #include #include using namespace psemek; template auto moore_neighbourhood() { if constexpr (D == 2) { std::array, 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, 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 util::ndarray generate_perlin(std::array 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> 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::ndarray, 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::ndarray result_float(size); math::interval value_range; for (auto idx : result_float.indices()) { math::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::ndarray 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::clamp(v * 255.f, 0.f, 255.f)); } return result; } template util::ndarray generate_worley(std::array 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, D>> octave_points; std::vector octave_weights; float sum_octave_weights = 0.f; for (int o = minoctave; o < minoctave + octaves; ++o) { std::array octave_size; for (int d = 0; d < D; ++d) octave_size[d] = (1 << o); util::ndarray, 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(rng); } octave_points.push_back(std::move(octave)); 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; util::ndarray result_float(size); math::interval value_range; for (auto idx : result_float.indices()) { math::point 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 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::infinity(); for (auto n : moore_neighbourhood()) { math::point 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 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 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::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 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::size_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[]) { util::hash_map 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] << " [ 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 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 (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 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(result.data()), result.size()); } } if (*dim == 3) { util::ndarray 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(result.data()), result.size()); } }