packages feed

pure-noise-0.3.0.0: src/Numeric/Noise/Fractal.hs

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE UnboxedTuples #-}

-- |
-- Maintainer: Jeremy Nuttall <jeremy@jeremy-nuttall.com>
-- Stability : experimental
module Numeric.Noise.Fractal (
  -- * Configuration
  FractalConfig (..),
  defaultFractalConfig,
  PingPongStrength (..),
  defaultPingPongStrength,

  -- * 2D Noise
  fractal2,
  billow2,
  ridged2,
  pingPong2,

  -- * 3D Noise
  fractal3,
  billow3,
  ridged3,
  pingPong3,

  -- * Custom fractals

  --

  -- |
  -- The building blocks 'fractal2', 'billow2', 'ridged2', and 'pingPong2'
  -- are assembled from a shared per-octave loop ('fractal2With' /
  -- 'fractal3With') and a per-variant 'FractalStep'.
  --
  -- You can provide your own step to build custom fractal variants with the
  -- __same specialization behavior as the built-ins__.
  FractalStep,
  fractal2With,
  fractal3With,
  fbmStep,
  billowStep,
  ridgedStep,
  pingPongStep,
) where

import GHC.Generics
import Numeric.Noise.Internal

-- | Configuration for fractal noise generation.
--
-- Fractal noise combines multiple octaves (layers) of noise at different
-- frequencies and amplitudes to create more complex, natural-looking patterns.
data FractalConfig a = FractalConfig
  { octaves :: Int
  -- ^ Number of noise layers to combine. More octaves create more detail
  -- but are more expensive to compute. Must be \( >= 1 \).
  , lacunarity :: a
  -- ^ Frequency multiplier between octaves. Each octave's frequency is
  -- the previous octave's frequency multiplied by lacunarity.
  , gain :: a
  -- ^ Amplitude multiplier between octaves. Each octave's amplitude is
  -- the previous octave's amplitude multiplied by gain.
  -- Values \( < 1 \) create smoother noise, values \( > 1 \) create rougher noise.
  , weightedStrength :: a
  -- ^ Controls how much each octave's amplitude is influenced by the
  -- previous octave's value. At 0, octaves have independent amplitudes.
  -- At 1, lower-valued areas in previous octaves reduce the amplitude
  -- of subsequent octaves. Range: \( [0, 1] \).
  }
  deriving (Generic, Read, Show, Eq)

-- | Default configuration for fractal noise generation.
defaultFractalConfig :: (RealFrac a) => FractalConfig a
defaultFractalConfig =
  FractalConfig
    { octaves = 7
    , lacunarity = 2
    , gain = 0.5
    , weightedStrength = 0
    }
{-# INLINEABLE defaultFractalConfig #-}

-- | Apply Fractal Brownian Motion (FBM) to a 2D noise function.
--
-- FBM combines multiple octaves of noise at increasing frequencies and
-- decreasing amplitudes to create natural-looking, multi-scale patterns.
-- This is the standard fractal noise implementation.
--
-- @
-- fbm :: Noise2 Float
-- fbm = fractal2 defaultFractalConfig perlin2
-- @
fractal2 :: (RealFrac a) => FractalConfig a -> Noise2 a -> Noise2 a
fractal2 config = mkNoise2 . fractal2With (fbmStep (weightedStrength config)) config . noise2At
{-# INLINE [2] fractal2 #-}

-- | Apply billow fractal to a 2D noise function.
--
-- Billow creates a cloud-like or billowy appearance by taking the absolute
-- value of each octave. This produces sharp ridges in the negative regions
-- of the noise, creating a distinct puffy or cloudy look.
--
-- @
-- clouds :: Noise2 Float
-- clouds = billow2 defaultFractalConfig perlin2
-- @
billow2 :: (RealFrac a) => FractalConfig a -> Noise2 a -> Noise2 a
billow2 config = mkNoise2 . fractal2With (billowStep (weightedStrength config)) config . noise2At
{-# INLINE [2] billow2 #-}

-- | Apply ridged fractal to a 2D noise function.
--
-- Ridged creates sharp ridges by inverting and taking the absolute value
-- of each octave. This is particularly useful for terrain generation,
-- creating mountain ridges and valleys.
--
-- @
-- mountains :: Noise2 Float
-- mountains = ridged2 defaultFractalConfig perlin2
-- @
ridged2 :: (RealFrac a) => FractalConfig a -> Noise2 a -> Noise2 a
ridged2 config = mkNoise2 . fractal2With (ridgedStep (weightedStrength config)) config . noise2At
{-# INLINE [2] ridged2 #-}

-- | Apply ping-pong fractal to a 2D noise function.
--
-- Ping-pong creates a wave-like pattern by folding the noise values back
-- and forth within a range, creating a distinctive undulating appearance.
-- The strength parameter controls the intensity of the ping-pong effect.
--
-- @
-- waves :: Noise2 Float
-- waves = pingPong2 defaultFractalConfig defaultPingPongStrength perlin2
-- @
pingPong2 :: (RealFrac a) => FractalConfig a -> PingPongStrength a -> Noise2 a -> Noise2 a
pingPong2 config strength =
  mkNoise2 . fractal2With (pingPongStep strength (weightedStrength config)) config . noise2At
{-# INLINE [2] pingPong2 #-}

-- | Per-octave fractal step: from the raw octave noise, produce
-- @(# term added to the sum (pre-amplitude), amplitude weight factor #)@.
--
-- The weight factor is multiplied into the amplitude after each octave
-- (before the 'gain' multiply), so it already incorporates
-- 'weightedStrength' — see 'fbmStep' for the canonical shape.
--
-- Consuming this type needs no extensions; /writing/ a custom step requires
-- @{-\# LANGUAGE UnboxedTuples \#-}@:
--
-- @
-- -- fBm with unweighted octaves (weight factor 1)
-- flatStep :: FractalStep Float
-- flatStep raw = (# raw, 1 #)
--
-- custom :: Noise2 Float
-- custom = mkNoise2 (fractal2With flatStep defaultFractalConfig (noise2At perlin2))
-- @
type FractalStep a = a -> (# a, a #)

fractal2With
  :: (RealFrac a)
  => FractalStep a
  -> FractalConfig a
  -> (Seed -> a -> a -> a)
  -> Seed
  -> a
  -> a
  -> a
fractal2With step FractalConfig{..} noise2 seed x0 y0
  | octaves < 1 = 0
  | otherwise =
      let !bounding = fractalBounding FractalConfig{..}
       in go octaves 0 seed x0 y0 bounding
 where
  -- Mirrors FNL's GenFractal* loops, including rounding order:
  -- sum += v * amp; amp *= w; amp *= gain; x *= lacunarity. Carrying the
  -- scaled coordinates (rather than an accumulated freq) matches FNL's
  -- loop and is one multiply cheaper per axis. Caveat: for bases with a
  -- coordinate transform (OpenSimplex2/2S), FNL scales the *transformed*
  -- coordinates while we re-transform the scaled raw ones — identical
  -- rounding only at power-of-two lacunarity, ulps apart otherwise.
  go 0 !acc !_ !_ !_ !_ = acc
  go !o !acc !s !x !y !amp =
    case step (noise2 s x y) of
      (# v, w #) ->
        let !acc' = acc + v * amp
            !amp' = amp * w * gain
         in go (o - 1) acc' (s + 1) (x * lacunarity) (y * lacunarity) amp'
{-# INLINE [1] fractal2With #-}

-- | Apply Fractal Brownian Motion (FBM) to a 3D noise function.
--
-- 3D version of 'fractal2'. See 'fractal2' for details.
fractal3 :: (RealFrac a) => FractalConfig a -> Noise3 a -> Noise3 a
fractal3 config = mkNoise3 . fractal3With (fbmStep (weightedStrength config)) config . noise3At
{-# INLINE [2] fractal3 #-}

-- | Apply billow fractal to a 3D noise function.
--
-- 3D version of 'billow2'. See 'billow2' for details.
billow3 :: (RealFrac a) => FractalConfig a -> Noise3 a -> Noise3 a
billow3 config = mkNoise3 . fractal3With (billowStep (weightedStrength config)) config . noise3At
{-# INLINE [2] billow3 #-}

-- | Apply ridged fractal to a 3D noise function.
--
-- 3D version of 'ridged2'. See 'ridged2' for details.
ridged3 :: (RealFrac a) => FractalConfig a -> Noise3 a -> Noise3 a
ridged3 config = mkNoise3 . fractal3With (ridgedStep (weightedStrength config)) config . noise3At
{-# INLINE [2] ridged3 #-}

-- | Apply ping-pong fractal to a 3D noise function.
--
-- 3D version of 'pingPong2'. See 'pingPong2' for details.
pingPong3 :: (RealFrac a) => FractalConfig a -> PingPongStrength a -> Noise3 a -> Noise3 a
pingPong3 config strength =
  mkNoise3 . fractal3With (pingPongStep strength (weightedStrength config)) config . noise3At
{-# INLINE [2] pingPong3 #-}

fractal3With
  :: (RealFrac a)
  => FractalStep a
  -> FractalConfig a
  -> (Seed -> a -> a -> a -> a)
  -> Seed
  -> a
  -> a
  -> a
  -> a
fractal3With step FractalConfig{..} noise3 seed x0 y0 z0
  | octaves < 1 = 0
  | otherwise =
      let !bounding = fractalBounding FractalConfig{..}
       in go octaves 0 seed x0 y0 z0 bounding
 where
  go 0 !acc !_ !_ !_ !_ !_ = acc
  go !o !acc !s !x !y !z !amp =
    case step (noise3 s x y z) of
      (# v, w #) ->
        let !acc' = acc + v * amp
            !amp' = amp * w * gain
         in go (o - 1) acc' (s + 1) (x * lacunarity) (y * lacunarity) (z * lacunarity) amp'
{-# INLINE [1] fractal3With #-}

fractalBounding :: (RealFrac a) => FractalConfig a -> a
fractalBounding FractalConfig{..} = recip (go 1 g 1)
 where
  -- FNL's CalculateFractalBounding, exactly: gain is abs'd, the sum has
  -- octaves terms (1 + g + ... + g^(octaves-1)), and it accumulates
  -- left-associated: ((1 + g) + g^2) + ... Preserve the shape; sum/take
  -- round differently. octaves = 1 gives 1: a single-octave fractal is
  -- its base noise.
  g = abs gain
  go !i !amp !ampFractal
    | i >= octaves = ampFractal
    | otherwise = go (i + 1) (amp * g) (ampFractal + amp)
{-# INLINE [2] fractalBounding #-}

-- | Step for fractal Brownian motion
--
-- Linear interpolation that maps the octave value from \( [-1, 1] \) into
-- \( [0, 1] \).
--
-- Equivalent to FNL's @GenFractalFBm@, with the exception that it is clamped
-- for both 2D and 3D functions, and so will not exceed \( +1 \).
fbmStep :: (RealFrac a) => a -> FractalStep a
fbmStep wS raw = (# raw, lerp 1 (min (raw + 1) 2 * 0.5) wS #)
{-# INLINE fbmStep #-}

-- | Step for billow fractals
--
-- Creates puffy looking noise. Feels something like clouds.
--
-- See: https://ambient.data-imaginist.com/reference/billow.html
--
-- No direct FastNoiseLite equivalent.
billowStep :: (RealFrac a) => a -> FractalStep a
billowStep wS raw =
  let !b = abs raw * 2 - 1
   in (# b, lerp 1 (min (b + 1) 2 * 0.5) wS #)
{-# INLINE billowStep #-}

-- | Step for ridged fractals
--
-- Creates angular/sharp noise. Feels something like a mountain range.
--
-- Equivalent to FNL's @GenFractalRidged@.
ridgedStep :: (RealFrac a) => a -> FractalStep a
ridgedStep wS raw =
  let !n = abs raw
   in (# n * (-2) + 1, lerp 1 (1 - n) wS #)
{-# INLINE ridgedStep #-}

-- | Strength parameter for ping-pong fractal noise.
--
-- Controls the intensity of the ping-pong folding effect.
-- Higher values create more frequent oscillations.
newtype PingPongStrength a = PingPongStrength a
  deriving (Generic)

-- | Default ping-pong strength value.
defaultPingPongStrength :: (RealFrac a) => PingPongStrength a
defaultPingPongStrength = PingPongStrength 2
{-# INLINE defaultPingPongStrength #-}

-- | Step for ping-pong fractal,
--
-- Creates wavy, intense noise.
--
-- Equivalent to FNL's @GenFractalPingPong@
pingPongStep :: (RealFrac a) => PingPongStrength a -> a -> FractalStep a
pingPongStep (PingPongStrength strength) wS raw =
  let !n = pingPong ((raw + 1) * strength)
   in (# (n - 0.5) * 2, lerp 1 n wS #)
{-# INLINE pingPongStep #-}

pingPong :: (RealFrac a) => a -> a
pingPong t0 =
  let !t = t0 - fromIntegral @Int (truncate (t0 * 0.5) * 2)
   in 1 - abs (t - 1)
{-# INLINE pingPong #-}