#pragma once
#include <assert.h>
#include <cstdint>
#include <map>
#include <vector>
#include "vertex.hpp"
/**
* @brief Linear Congruential Generator
*
* Once seeded, gives stable pseudo-random numbers.
* Concept: https://en.cppreference.com/w/cpp/named_req/UniformRandomBitGenerator
*/
class PRNG {
public:
typedef uint32_t result_type;
static constexpr result_type min() {
return 0;
}
static constexpr result_type max() {
return UINT32_MAX;
}
result_type operator()() {
return get();
}
private:
result_type seed;
/** @brief Pop a new pseudo-random value. */
result_type get();
public:
PRNG(result_type seed) : seed(seed) {}
/** @brief Sub-generator derived once for arbitrary number of follow-up calls. */
PRNG(PRNG& parent) : PRNG(~parent.get()) {}
/** @brief max (exclusive) wrapper for convenience. */
result_type get(result_type max);
/** @brief min/max (inclusive) wrapper for convenience. */
result_type get(result_type min, result_type max);
/** @brief min/max (inclusive) wrapper for signed integers (still unsigned results). */
int get(int min, int max);
/** @brief float values in [0.0, 1.0] */
fvalue_t getf();
result_type last() const; ///< current state
static result_type hash(result_type); ///< underlying permutation
};
/** @brief 2D perlin noise implementation. */
class Perlin2D final {
private:
const vertex2_t<fcoord_t> viewport;
Buffer2D<vertex2_t<fcoord_t>> buf;
/** @brief Avoid blocky artifacts by smoothing polynomial instead of linear boundaries. */
static INLINE constexpr fvalue_t ease(fvalue_t t) {
#if USE_PERLIN_INTERPOLATE
return t * t * t * (t * (t * 6.0F - 15.0F) + 10.0F); // 6t^5 - 15t^4 + 10t^3
#else
return t;
#endif
}
/** @brief Linear or polynomial interpolation between the two values. */
static INLINE constexpr fvalue_t interpolate(fvalue_t a, fvalue_t b, fvalue_t v) {
return (b - a) * ease(v) + a;
}
/** @brief Dot product as weight for one of the random vectors. */
INLINE constexpr fvalue_t dot_gradient(dcoord_t x, dcoord_t y, fvalue_t wx, fvalue_t wy) const {
const vertex2_t<fcoord_t> gradient = buf.at(x, y);
return (gradient.x * wx) + (gradient.y * wy);
}
/** @brief Perlin noise implementation, yielding [-1, 1] for float coordinates. */
INLINE constexpr fcoord_t at(fcoord_t x, fcoord_t y) const {
const dcoord_t x0 = (dcoord_t)std::floor(x);
const dcoord_t x1 = x0 + 1;
const dcoord_t y0 = (dcoord_t)std::floor(y);
const dcoord_t y1 = y0 + 1;
assert(0 <= x0 && x1 < buf.width());
assert(0 <= y0 && y1 < buf.height());
const fcoord_t dist_x0 = x - (fcoord_t)x0;
const fcoord_t dist_x1 = x - (fcoord_t)x1;
const fcoord_t dist_y0 = y - (fcoord_t)y0;
const fcoord_t dist_y1 = y - (fcoord_t)y1;
const fcoord_t a = dot_gradient(x0, y0, dist_x0, dist_y0);
const fcoord_t b = dot_gradient(x1, y0, dist_x1, dist_y0);
const fcoord_t c = dot_gradient(x0, y1, dist_x0, dist_y1);
const fcoord_t d = dot_gradient(x1, y1, dist_x1, dist_y1);
return interpolate(interpolate(a, b, dist_x0), interpolate(c, d, dist_x0), dist_y0);
}
public:
Perlin2D(PRNG&& rnd, res_t res, vertex2_t<fcoord_t> viewport) : viewport(viewport), buf(res.w, res.h) {
for (dcoord_t x = 0; x < buf.width(); ++x) {
for (dcoord_t y = 0; y < buf.height(); ++y) {
buf.at(x, y) = {rnd.getf() * 2.0F - 1.0F, rnd.getf() * 2.0F - 1.0F};
}
}
}
/** @brief Perlin noise interface, yielding [-1, 1] for the given viewport x/y coordinates. */
INLINE constexpr fcoord_t at(const vertex_t& pos) const {
return at(pos.x / viewport.x * (fcoord_t)(buf.width() - 2), pos.y / viewport.y * (fcoord_t)(buf.height() - 2));
}
};
/** @brief Abstract base for all textures, which yield RGB values for (u/v or absolute) scene hits. */
class Texture {
public:
/** @brief @ref Texture interface, [0, 1] RGB at given u/v coordinates or world coordinate. */
virtual vertex_t get_color(const uv_t&, const vertex_t&) const = 0;
};
/** @brief Unconditionally give a predefined color. */
class ColorTexture final : public Texture {
private:
const vertex_t color;
public:
ColorTexture(vertex_t color) : color(color) {}
INLINE vertex_t get_color(const uv_t& uv, const vertex_t& pos) const override {
return color;
}
};
/** @brief Interpolate between two colors by perlin noise value. */
class NoiseTexture final : public Texture {
private:
const vertex_t color_lo;
const vertex_t color_hi;
const vertex_t spread; // min, max, factor
const bool iso; ///< Isobar at 0.5 instead of min/max interpolation.
const Perlin2D& noise;
public:
NoiseTexture(vertex_t color_lo, vertex_t color_hi, fvalue_t max, bool iso, const Perlin2D& noise)
: color_lo{color_lo},
color_hi{color_hi},
spread{iso ? -max : -max / 2.0F, iso ? max : max / 2.0F, iso ? 1.0F / max : 2.0F / max},
iso(iso),
noise(noise) {
assert(0.0F < max && max <= 0.5F);
}
INLINE vertex_t get_color(const uv_t& uv, const vertex_t& pos) const override {
fvalue_t noise_val = std::max(spread.v[0], std::min(spread.v[1], noise.at(pos))) * spread.v[2];
if (iso) {
noise_val = 1.0F - std::abs(noise_val); // zero gradient isobar
} else {
noise_val = noise_val / 2.0F + 0.5F; // [-1,1] -> [0, 1]
}
return ((color_lo * (1.0F - noise_val)) + (color_hi * noise_val)) / 2.0F;
}
};
/**
* @brief Interpolate between two other @ref Texture instances along the given axis.
*
* Used between 'rooms' of a level, for a smoother 'door' transition.
*/
class GradientTexture final : public Texture {
private:
const Texture* lo;
const Texture* hi;
const int component;
public:
GradientTexture(const Texture* lo, const Texture* hi, int component) : lo(lo), hi(hi), component(component) {
assert(0 <= component && component <= 1);
}
INLINE vertex_t get_color(const uv_t& uv, const vertex_t& pos) const override {
assert(0.0F <= uv.u && uv.u <= 1.0F);
assert(0.0F <= uv.v && uv.v <= 1.0F);
#if USE_TEXTURE_GRADIENT
return (lo->get_color(uv, pos) * (1.0F - uv.V[component])) + (hi->get_color(uv, pos) * (uv.V[component]));
#else
return uv.v[component] < 0.5 ? lo->get_color(uv, pos) : hi->get_color(uv, pos);
#endif
}
};
/** @brief Light as emitted from a point source, the only implementation atm. */
class PointLight final {
private:
fcoord_t rad;
fcoord_t sqrad;
vertex_t color;
public:
PointLight(fcoord_t rad, vertex_t color) : rad(rad), sqrad(rad * rad), color(color) {}
/** @brief radius */
INLINE constexpr fcoord_t get_max_dist() const {
return rad;
}
/** @brief radius^2, for optimized comparisons that don't need sqrt() */
INLINE constexpr fcoord_t get_max_sqdist() const {
return sqrad;
}
/** @brief color value as [0, 1] RGB */
INLINE constexpr const vertex_t& get_color() const {
return color;
}
};
/** @brief Generate and maintain @ref Texture instances in different 'variants', acc. to @ref LevelGenerator. */
class TextureFactory {
private:
const PRNG::result_type seed;
const Perlin2D noise; // global noise
std::vector<std::unique_ptr<const Texture>> textures{};
std::map<std::pair<int, int>, const Texture*> cache{}; // caches more for locality than memory
public:
TextureFactory(PRNG&& rnd, res_t res, vertex2_t<fcoord_t> viewport);
size_t size() const;
const Texture* make(const std::pair<int, int>& variant);
const Texture* make(const std::pair<int, int>& variant_lo, const std::pair<int, int>& variant_hi, int component);
};
/** @brief Generate and maintain @ref PointLight instances in different 'variants', acc. to @ref LevelGenerator. */
class LightFactory {
private:
const PRNG::result_type seed;
const fcoord_t radius;
std::vector<std::unique_ptr<const PointLight>> lights{};
std::map<std::pair<int, int>, const PointLight*> cache{};
public:
LightFactory(PRNG&& rnd, fcoord_t radius);
fcoord_t get_radius() const;
const PointLight* make(const std::pair<int, int>& variant);
const PointLight* make(fcoord_t rad, const vertex_t& color);
};