noise/noise_layers.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /// @file | ||
| 2 | /// @brief Stacks of fractal noise layers with domain warps and blend modes. | ||
| 3 | #pragma once | ||
| 4 | |||
| 5 | #include "terraingen/noise/fractal.h" | ||
| 6 | |||
| 7 | namespace terraingen { | ||
| 8 | |||
| 9 | /// How a noise layer combines with the height accumulated from the layers before it. | ||
| 10 | enum class LayerBlend : uint32_t { | ||
| 11 | Add = 0, ///< height + layer | ||
| 12 | Max = 1, ///< max(height, layer) | ||
| 13 | Min = 2, ///< min(height, layer) | ||
| 14 | Mask = 3, ///< height * (layer / amplitude + 1) / 2: the layer modulates roughness by region. | ||
| 15 | }; | ||
| 16 | |||
| 17 | /// Number of `LayerBlend` values. | ||
| 18 | static const uint32_t kLayerBlendCount = 4; | ||
| 19 | |||
| 20 | /// Most layers a synthetic terrain may stack; a fixed bound keeps layers uploadable to GPUs. | ||
| 21 | static const uint32_t kMaxNoiseLayers = 8; | ||
| 22 | |||
| 23 | /// One layer of a synthetic terrain: a fractal, an optional domain warp and a blend mode. | ||
| 24 | struct NoiseLayer { | ||
| 25 | 21 | bool enabled = true; ///< Whether the layer contributes. | |
| 26 | 20 | FractalParams fractal; ///< The layer's fractal; its amplitude scales the layer. | |
| 27 | 19 | LayerBlend blend = LayerBlend::Add; ///< How it combines with the layers before it. | |
| 28 | 17 | float warpAmplitude = 0.0f; ///< Domain warp displacement in metres; 0 disables warping. | |
| 29 | 16 | float warpWavelength = 500.0f; ///< Wavelength of the warp field in metres. | |
| 30 | |||
| 31 | #ifndef __SLANG__ | ||
| 32 | 21 | bool operator==(const NoiseLayer&) const = default; | |
| 33 | #endif | ||
| 34 | }; | ||
| 35 | |||
| 36 | /// Up to `kMaxNoiseLayers` layers in a fixed-size block, the form GPU kernels receive. | ||
| 37 | struct NoiseLayers { | ||
| 38 | NoiseLayer layers[kMaxNoiseLayers]; ///< The layers; only the first `count` are used. | ||
| 39 | uint32_t count = 0; ///< Number of layers used, at most `kMaxNoiseLayers`. | ||
| 40 | }; | ||
| 41 | |||
| 42 | /// A 2D domain warp offset. | ||
| 43 | struct WarpOffset2 { | ||
| 44 | float dx = 0.0f; ///< Offset in x, in metres. | ||
| 45 | float dy = 0.0f; ///< Offset in y, in metres. | ||
| 46 | }; | ||
| 47 | |||
| 48 | namespace detail { | ||
| 49 | |||
| 50 | /// Mixed into a layer's seed to seed its warp fields apart from its own noise. Frozen for | ||
| 51 | /// reproducibility. | ||
| 52 | static const uint32_t kWarpSeedSalt = 0x5bd1e995u; | ||
| 53 | |||
| 54 | } // namespace detail | ||
| 55 | |||
| 56 | /// The fractal of the first warp field of `layer`: 4-octave Perlin fBm of the layer's warp | ||
| 57 | /// wavelength and amplitude. Each further field is seeded with `PcgHash` of the one before. | ||
| 58 | 838181 | inline FractalParams WarpParams(NoiseLayer layer) { | |
| 59 | 838181 | FractalParams warp = {}; | |
| 60 | 838181 | warp.wavelength = layer.warpWavelength; | |
| 61 | 838181 | warp.amplitude = layer.warpAmplitude; | |
| 62 | 838181 | warp.octaves = 4; | |
| 63 | 838181 | warp.seed = PcgHash(layer.fractal.seed ^ detail::kWarpSeedSalt); | |
| 64 | 838181 | return warp; | |
| 65 | } | ||
| 66 | |||
| 67 | /// A layer's domain warp offset from two independent warp fields (`WarpParams`). | ||
| 68 | /// @param x, y World coordinates in metres. | ||
| 69 | /// @return The offset, each component bounded by `layer.warpAmplitude` metres. | ||
| 70 | 837920 | inline WarpOffset2 DomainWarp2(float x, float y, NoiseLayer layer) { | |
| 71 | 837920 | FractalParams warp = WarpParams(layer); | |
| 72 | 837920 | WarpOffset2 offset = {}; | |
| 73 | 837920 | offset.dx = Fractal2(x, y, warp); | |
| 74 | 837920 | warp.seed = PcgHash(warp.seed); | |
| 75 | 837920 | offset.dy = Fractal2(x, y, warp); | |
| 76 | 837920 | return offset; | |
| 77 | } | ||
| 78 | |||
| 79 | /// A layer's normalized value, warped when `warpAmplitude` > 0, before amplitude and | ||
| 80 | /// blending. | ||
| 81 | /// @param x, y World coordinates in metres. | ||
| 82 | /// @return The value in [-1, 1]. | ||
| 83 | 81934947 | inline float EvaluateNoiseLayer(float x, float y, NoiseLayer layer) { | |
| 84 | 81934947 | FractalParams unit = layer.fractal; | |
| 85 | 81934947 | unit.amplitude = 1.0f; | |
| 86 | 81934947 | if (layer.warpAmplitude > 0.0f) { | |
| 87 | 837918 | const WarpOffset2 offset = DomainWarp2(x, y, layer); | |
| 88 | 837918 | return Fractal2(x + offset.dx, y + offset.dy, unit); | |
| 89 | } | ||
| 90 | 81097029 | return Fractal2(x, y, unit); | |
| 91 | } | ||
| 92 | |||
| 93 | namespace detail { | ||
| 94 | |||
| 95 | /// The height of a stack of layers so far. | ||
| 96 | struct NoiseStack { | ||
| 97 | float height = 0.0f; ///< Height so far, in metres. | ||
| 98 | bool started = false; ///< Whether any layer has set the height yet. | ||
| 99 | }; | ||
| 100 | |||
| 101 | /// Combines an enabled layer with a stack; the first layer sets the height. | ||
| 102 | /// @param value The layer's normalized value, in [-1, 1]. | ||
| 103 | 83542653 | inline NoiseStack BlendNoiseLayer(NoiseStack stack, NoiseLayer layer, float value) { | |
| 104 | 83542653 | const float scaled = layer.fractal.amplitude * value; | |
| 105 | 83542653 | if (!stack.started) { | |
| 106 | 81085539 | stack.height = scaled; | |
| 107 | 81085539 | stack.started = true; | |
| 108 | 81085539 | return stack; | |
| 109 | } | ||
| 110 | 2457114 | switch (layer.blend) { | |
| 111 | 4098 | case LayerBlend::Max: | |
| 112 | 4098 | stack.height = max(stack.height, scaled); | |
| 113 | 4098 | break; | |
| 114 | 8194 | case LayerBlend::Min: | |
| 115 | 8194 | stack.height = min(stack.height, scaled); | |
| 116 | 8194 | break; | |
| 117 | 829641 | case LayerBlend::Mask: | |
| 118 | 829641 | stack.height *= 0.5f * (value + 1.0f); | |
| 119 | 829641 | break; | |
| 120 | 1615181 | default: | |
| 121 | 1615181 | stack.height += scaled; | |
| 122 | 1615181 | break; | |
| 123 | } | ||
| 124 | 2457114 | return stack; | |
| 125 | } | ||
| 126 | |||
| 127 | /// Adds a layer at a 2D point to a stack, if the layer is enabled. | ||
| 128 | /// @param x, y World coordinates in metres. | ||
| 129 | 81906372 | inline NoiseStack AddNoiseLayer2(NoiseStack stack, float x, float y, NoiseLayer layer) { | |
| 130 | 81906372 | if (!layer.enabled) { | |
| 131 | 4202 | return stack; | |
| 132 | } | ||
| 133 | 81902170 | return BlendNoiseLayer(stack, layer, EvaluateNoiseLayer(x, y, layer)); | |
| 134 | } | ||
| 135 | |||
| 136 | } // namespace detail | ||
| 137 | |||
| 138 | /// Height of stacked layers: the first enabled layer sets it (`amplitude * value`), each | ||
| 139 | /// later enabled layer combines by its `blend` mode. | ||
| 140 | /// @param x, y World coordinates in metres. | ||
| 141 | /// @return Height in metres; 0 if no layer is enabled. | ||
| 142 | 4117 | inline float EvaluateNoiseLayers(float x, float y, NoiseLayers layers) { | |
| 143 | 4117 | detail::NoiseStack stack = {}; | |
| 144 | 36945 | for (uint32_t i = 0; i < layers.count; ++i) { | |
| 145 | 32828 | stack = detail::AddNoiseLayer2(stack, x, y, layers.layers[i]); | |
| 146 | } | ||
| 147 | 4117 | return stack.height; | |
| 148 | } | ||
| 149 | |||
| 150 | #ifndef __SLANG__ | ||
| 151 | /// `EvaluateNoiseLayers` of any number of layers, including more than a GPU block holds. | ||
| 152 | /// @param x, y World coordinates in metres. | ||
| 153 | /// @param layers The first of `count` layers. | ||
| 154 | /// @return Height in metres; 0 if no layer is enabled. | ||
| 155 | 79954502 | inline float EvaluateNoiseLayers(float x, float y, const NoiseLayer* layers, std::size_t count) { | |
| 156 | 79954502 | detail::NoiseStack stack; | |
| 157 | 161828046 | for (std::size_t i = 0; i < count; ++i) { | |
| 158 | 81873544 | stack = detail::AddNoiseLayer2(stack, x, y, layers[i]); | |
| 159 | } | ||
| 160 | 79954502 | return stack.height; | |
| 161 | } | ||
| 162 | |||
| 163 | /// A 3D domain warp offset. | ||
| 164 | struct WarpOffset3 { | ||
| 165 | double dx = 0.0; ///< Offset in x, in metres. | ||
| 166 | double dy = 0.0; ///< Offset in y, in metres. | ||
| 167 | double dz = 0.0; ///< Offset in z, in metres. | ||
| 168 | }; | ||
| 169 | |||
| 170 | /// `DomainWarp2` in 3D: three warp fields (`WarpParams`) evaluated with `Fractal3`. | ||
| 171 | /// @param x, y, z World coordinates in metres. | ||
| 172 | /// @return The offset, each component bounded by `layer.warpAmplitude` metres. | ||
| 173 | 260 | inline WarpOffset3 DomainWarp3(double x, double y, double z, NoiseLayer layer) { | |
| 174 | 260 | FractalParams warp = WarpParams(layer); | |
| 175 | 260 | WarpOffset3 offset; | |
| 176 | 260 | offset.dx = static_cast<double>(Fractal3(x, y, z, warp)); | |
| 177 | 260 | warp.seed = PcgHash(warp.seed); | |
| 178 | 260 | offset.dy = static_cast<double>(Fractal3(x, y, z, warp)); | |
| 179 | 260 | warp.seed = PcgHash(warp.seed); | |
| 180 | 260 | offset.dz = static_cast<double>(Fractal3(x, y, z, warp)); | |
| 181 | 520 | return offset; | |
| 182 | } | ||
| 183 | |||
| 184 | /// `EvaluateNoiseLayer` in 3D, with `Fractal3` and `DomainWarp3`. | ||
| 185 | /// @param x, y, z World coordinates in metres. | ||
| 186 | /// @return The layer's normalized value in [-1, 1]. | ||
| 187 | 1640484 | inline float EvaluateNoiseLayer3(double x, double y, double z, NoiseLayer layer) { | |
| 188 | 1640484 | FractalParams unit = layer.fractal; | |
| 189 | 1640484 | unit.amplitude = 1.0f; | |
| 190 | 1640484 | if (layer.warpAmplitude > 0.0f) { | |
| 191 | 259 | const WarpOffset3 offset = DomainWarp3(x, y, z, layer); | |
| 192 | 259 | return Fractal3(x + offset.dx, y + offset.dy, z + offset.dz, unit); | |
| 193 | } | ||
| 194 | 1640225 | return Fractal3(x, y, z, unit); | |
| 195 | } | ||
| 196 | |||
| 197 | namespace detail { | ||
| 198 | |||
| 199 | /// Adds a layer at a 3D point to a stack, if the layer is enabled. | ||
| 200 | /// @param x, y, z World coordinates in metres. | ||
| 201 | 1640485 | inline NoiseStack AddNoiseLayer3(NoiseStack stack, double x, double y, double z, NoiseLayer layer) { | |
| 202 | 1640485 | if (!layer.enabled) { | |
| 203 | 2 | return stack; | |
| 204 | } | ||
| 205 | 1640483 | return BlendNoiseLayer(stack, layer, EvaluateNoiseLayer3(x, y, z, layer)); | |
| 206 | } | ||
| 207 | |||
| 208 | } // namespace detail | ||
| 209 | |||
| 210 | /// `EvaluateNoiseLayers` in 3D with `EvaluateNoiseLayer3`, for noise on a sphere. | ||
| 211 | /// @param x, y, z World coordinates in metres. | ||
| 212 | /// @param layers The first of `count` layers. | ||
| 213 | /// @return Height in metres; 0 if no layer is enabled. | ||
| 214 | 1126923 | inline float EvaluateNoiseLayers3(double x, double y, double z, const NoiseLayer* layers, | |
| 215 | std::size_t count) { | ||
| 216 | 1126923 | detail::NoiseStack stack; | |
| 217 | 2767408 | for (std::size_t i = 0; i < count; ++i) { | |
| 218 | 1640485 | stack = detail::AddNoiseLayer3(stack, x, y, z, layers[i]); | |
| 219 | } | ||
| 220 | 1126923 | return stack.height; | |
| 221 | } | ||
| 222 | #endif | ||
| 223 | |||
| 224 | } // namespace terraingen | ||
| 225 |