Noise Utils

36 min read

Coherent, cellular, and per-pixel noise with fractal layering, domain warping, and curl flows, so you can generate terrain heightmaps, cloud textures, cave masks, and particle flow fields without writing the math yourself.

Summary

The Scylla noise utilities cover every routine source of procedural randomness like coherent (smoothly varying) noise for terrain and clouds, cellular (Voronoi-style) noise for cracks and stones, domain warping for organic distortions, curl noise for divergence-free vector fields, and statistically independent per-pixel noise for film grain and dithering. NoiseGenerator is the core entry point, providing an instance-based class with a seed property, sampling methods (Evaluate2D, Evaluate3D), fractal samplers (Fractal2D, Fractal3D), bulk fillers (Fill2D, Fill3D), and a parallel cellular family (EvaluateCellular2D/3D, FractalCellular2D/3D, FillCellular2D/3D). All sampling is allocation-free after construction and deterministic from the seed, so that the same seed always produces the same output on every platform.

Four coherent algorithms are featured in NoiseAlgorithm:

  • OpenSimplex2 (the default, isotropic, no axis-aligned bias, fast)
  • Improved Perlin (Ken Perlin 2002, quintic fade, requires a one-time 2 KB permutation table)
  • Value (hash-based with bilinear interpolation, simpler and faster than Perlin)
  • ValueCubic (the smoother cubic-interpolated variant of Value)

Cellular noise lives in its own family because its parameters (distance metric, return type, jitter) differ; you choose between Euclidean, Manhattan, and Chebyshev distance, between F1/F2/F2-F1 return types, and tune jitter and frequency through CellularSettings. Five fractal types (None, FBM (Fractional Brownian Motion), Turbulence, Ridged, PingPong) layer multiple octaves of the chosen algorithm with configurable lacunarity (frequency multiplier per octave) and persistence (amplitude multiplier per octave) to produce the natural-looking patterns that single-octave noise cannot.

NoiseSettings, CellularSettings, WarpSettings, and PixelNoiseSettings are immutable readonly structs with named presets covering the most common shapes: Terrain, Clouds, Ridges, and Caves for the coherent settings; Voronoi, Cracks, Stone, and Cells for cellular; Subtle and Extreme for warping; Default, FilmGrain, Heavy, SubtleMono, and Dither for pixel noise. Use the presets directly or construct a custom instance for any one-off shape. NoiseDomainWarper distorts input coordinates using another noise sample for fluid-looking patterns (single warp roughly 3x base cost, double warp roughly 5x). NoiseCurlSampler computes the curl of a scalar noise field to produce a divergence-free vector field (2D curl: 4 noise evaluations; 3D curl: 12), the canonical source for particle flow and fluid simulation. PixelNoise is the separate static-class subsystem for statistically independent per-pixel noise with uniform or Gaussian distribution, RNG-based (sequential) or hash-based (coordinate-addressable) call shapes, and Color/Color32 output formats.

Procedural content is the difference between hand-authored worlds and infinite variation. A terrain heightmap from NoiseSettings.Terrain, a cloud density volume from NoiseSettings.Clouds, a stone wall texture from CellularSettings.Stone, particle flow from NoiseCurlSampler, and film grain from PixelNoise.FilmGrain are all one-line calls. The same seed always produces the same output, so serialized worlds round-trip cleanly without storing the bitmap. The same generator can be sampled millions of times per frame without allocation, so on-the-fly evaluation (sampling around the camera as the player moves) is realistic. The pluggable algorithm enum and the preset families together cover almost every shape commercial games need, and a custom settings instance covers the rest.

Use the noise utilities for:

  • Terrain heightmaps and biome distribution (multi-octave FBM with OpenSimplex2).
  • Procedural textures: clouds, marble, wood, stone, cracks (turbulence, ridged, cellular variants).
  • Organic motion: camera shake, foliage sway, banner ripple (low-frequency noise sampled over time).
  • Mask generation: caves, vegetation density, weather coverage (threshold a fractal noise field).
  • Particle flow and fluid-like motion (curl noise on a 2D or 3D field).
  • Film grain, dithering, and image roughening (statistically independent per-pixel noise).
  • Worley-cell decorations: stone tiles, Voronoi shatter patterns, cell-based biomes (cellular noise with the right distance metric and return type).
  • Distorting coordinates before sampling another noise source (domain warping) for fluid, organic shapes.

Features

  • Four coherent algorithms. OpenSimplex2 (default, isotropic, fast), Perlin (improved Perlin 2002 with quintic fade), Value (hash-based, fastest), ValueCubic (smoother cubic-interpolated variant). Switch algorithms through the Algorithm setting; the rest of the API is identical.
  • Five fractal types layer multi-octave detail. None, FBM, Turbulence, Ridged, PingPong. Each layers the base algorithm at increasing frequencies and decreasing amplitudes; Lacunarity and Persistence tune the spread.
  • Cellular noise family. EvaluateCellular2D/3D, FractalCellular2D/3D, FillCellular2D/3D for Voronoi-style patterns. Euclidean, Manhattan, or Chebyshev distance; F1, F2, or F2-F1 return type; configurable jitter and frequency through CellularSettings.
  • Domain warping. NoiseDomainWarper distorts input coordinates using a second noise sample for fluid, organic shapes. Single warp costs roughly 3x the base sample; double warp roughly 5x.
  • Curl noise for divergence-free vector fields. NoiseCurlSampler.Curl2D and Curl3D compute the curl of a scalar noise field. The canonical source for particle flow, fluid-like motion, and any vector field that needs to be divergence-free.
  • Per-pixel grain. PixelNoise is the separate static-class subsystem for statistically independent per-pixel noise: uniform or Gaussian distribution, RNG-based (sequential) or hash-based (coordinate-addressable), Color and Color32 output. Five presets cover film grain, heavy noise, subtle mono, and dithering.
  • Allocation-free after construction. Every sampling call (Evaluate2D, Evaluate3D, Fractal2D, Fractal3D, EvaluateCellular*, FractalCellular*) is allocation-free. Bulk fillers (Fill2D, Fill3D) write into caller-supplied arrays.
  • Deterministic from the seed. The same seed produces the same output on every platform and runtime. Serialized worlds round-trip cleanly without storing the bitmap; the same generator can be reproduced from a single integer.
  • Immutable settings structs with named presets. NoiseSettings (Terrain, Clouds, Ridges, Caves), CellularSettings (Voronoi, Cracks, Stone, Cells), WarpSettings (Subtle, Extreme), PixelNoiseSettings (FilmGrain, Heavy, SubtleMono, Dither). Use a preset directly or build a custom struct for one-off shapes.
  • Bulk 2D and 3D fillers. Fill2D(array, ...) and Fill3D(array, ...) evaluate the noise across a rectangular or volumetric grid in one call, the right primitive for terrain heightmaps, density volumes, and texture pre-bakes.
  • Tuneable octave layering. OctaveCount, Lacunarity (frequency multiplier per octave), and Persistence (amplitude multiplier per octave) compose the fractal stack. Defaults match the typical “terrain or clouds” shape; override per call for richer or simpler patterns.
  • Pluggable seed and frequency. Seed, Frequency, and Algorithm are mutable on the generator. Re-seeding is one assignment; switching algorithms produces a different look without re-allocating the generator.

Algorithms and fractal types

Coherent noise is “smoothly varying random”: nearby input coordinates map to nearby output values. Pure random numbers (see Random Utils) have no such property; sampling them at adjacent pixels produces high-frequency static. The Scylla noise utilities provide four coherent algorithms plus the cellular family and the per-pixel grain subsystem, layered by five fractal types when richer patterns are needed.

A few terms worth defining if you are new to the area. Octave is one layer of a fractal stack: each subsequent octave samples the algorithm at a higher frequency (typically 2x, set by Lacunarity) and a lower amplitude (typically 0.5x, set by Persistence). Lacunarity is the frequency multiplier per octave; values above 2 make each octave more detailed; values below 2 make octaves overlap more. Persistence is the amplitude multiplier per octave; the typical 0.5 means coarser layers dominate the final result. Frequency is the input scaling factor: high frequency produces small-scale, busy detail; low frequency produces broad continental shapes. Jitter (cellular only) is how far cellular feature points can wander from their canonical lattice positions; zero produces a regular grid; one produces fully random placement.

AlgorithmRange (approx.)CostNotes
NoiseAlgorithm.OpenSimplex2[-1, 1]FastDefault and recommended. Hash-based, isotropic (no axis-aligned bias), allocation-free. Best general-purpose pick for terrain, clouds, masks, and motion. Faster than Perlin on most hardware.
NoiseAlgorithm.Perlin[-1, 1]FastImproved Perlin (2002), quintic fade. Requires a 2 KB permutation table allocated lazily on first use. Slightly stronger axis-aligned bias than OpenSimplex2 visible at low frequencies; useful for legacy compatibility and historical look.
NoiseAlgorithm.Value[-1, 1]FastestHash-based with bilinear interpolation. Simpler than Perlin, blocky-looking at low octave counts. Use when the visual character (slight visible interpolation pattern) is desirable, or when raw speed matters more than smoothness.
NoiseAlgorithm.ValueCubic[-1, 1]FastSmoother variant of Value using cubic interpolation. Removes the visible bilinear pattern; cost is between Value and OpenSimplex2.
Cellular (EvaluateCellular*)[~0, ~1] (F1)SlowerVoronoi-style noise driven by CellularSettings. Returns distance to the nearest feature point (F1), to the second-nearest (F2), or their difference (F2-F1). Distance metric is configurable. Best for stones, cracks, biome cells.
PixelNoiseper-channel grainVery fastStatistically independent per-pixel noise (no spatial correlation). Uniform or Gaussian distribution, monochromatic or color, RNG-based or hash-based. Best for film grain, dithering, and image roughening.

The five fractal types apply on top of the coherent algorithms (and on cellular, for the fractal-cellular methods).

Fractal typeOutput rangeEffect
FractalType.Nonebase algorithm rangeSingle-octave evaluation; no fractal layering. The fastest path and the right pick when the natural algorithm output is already the desired shape.
FractalType.FBM[-1, 1] (normalized)FBM (Fractional Brownian Motion). Sum of detuned octaves. The default look for terrain, masks, and smooth continuous fields.
FractalType.Turbulence[0, 1]Absolute value per octave produces folded, billowy shapes. Suitable for cloud density, flame, and any “puffy” pattern. Output is always positive.
FractalType.Ridged[~0, ~2] (unnormalized)Inverted absolute value produces sharp peaks and smooth valleys. Suitable for mountain spines, canyon walls, and river channel edges. May exceed [0, 1].
FractalType.PingPong[0, 1]Triangle-wave layering produces banded patterns. Useful for striped marble, wood grain, and similar layered materials.

Five named NoiseSettings presets cover the most common shapes: Default (single-octave OpenSimplex2), Terrain (6-octave FBM at 0.005 frequency), Clouds (4-octave Turbulence at 0.01), Ridges (5-octave Ridged at 0.008), and Caves (4-octave FBM at 0.05 with persistence 0.45). Four CellularSettings presets cover Voronoi-style use cases: Default (Euclidean F1 jitter 1.0), Voronoi (F2-F1 for visible cell borders), Cracks (F2-F1 with Manhattan distance), Stone (Euclidean F2 with smaller jitter), and Cells (Euclidean F1 with full jitter for irregular cells). Five PixelNoiseSettings presets cover the per-pixel grain shapes: Default, FilmGrain (Gaussian monochromatic), Heavy (50% uniform), SubtleMono (5% Gaussian monochromatic), and Dither (2% uniform monochromatic). Construct a custom instance through the struct constructor when none of the presets matches.

Recipes

Each recipe below is a self-contained answer to one game-development goal – scan the names, find the one that matches what you are building, and copy the solution. The first recipe creates the NoiseGenerator that the remaining ones pass to Fractal2D, Fill2D, and the other sampling calls, so if you are new to the API, start there.

Create a seeded noise generator

Problem. Every sample, every heightmap, and every animated flow field begins with a NoiseGenerator. You want the same seed to reproduce the same terrain across runs, so a player who saves mid-dungeon comes back to the same cave layout, and a procedural world shared between players matches on every machine.

Solution. Construct one with an explicit integer seed for full reproducibility. You can also derive the seed from an IRandomSource if the project already has a master RNG, which keeps every system’s randomness tied back to a single per-run seed.

using Scylla.Core.Util.Noise;
using Scylla.Core.Util.Random;

/* Default seed (0). Fine for prototypes where determinism does not matter yet. */
var defaultGen = new NoiseGenerator();

/* Explicit seed. Two generators with the same seed produce identical output for
 * identical input coordinates, on every platform and runtime. The same cave map
 * shows up on every machine when the seed comes from the same source. */
var worldGen = new NoiseGenerator(seed: 42);

/* Derive the seed from an existing IRandomSource. The master RNG is sampled once;
 * the generator never references it again. Useful when the project drives all
 * randomness from one seeded source and each noise generator should get a
 * deterministic sub-seed that flows from the same per-run value. */
IRandomSource masterRng = RandomUtil.CreateXoshiro256(seed: 12345UL);
var noiseFromRng = new NoiseGenerator(masterRng);

/* The seed is exposed for serialization or logging. Store it alongside any
 * generated content so you can reproduce the output later. */
long seedSnapshot = worldGen.Seed;
Log.Info($"Noise generator seed: {seedSnapshot}", LogCategory.Core);

Sample terrain elevation at a single point

Problem. You are building an open-world game and you need to query the terrain height at an arbitrary world-space position – at runtime, without a pre-baked array. A character stepping onto a slope, a projectile checking whether it hits the ground, or a foliage-placement pass asking “what is the height here?” all need this one call.

Solution. Evaluate2D and Evaluate3D sample one octave of the chosen algorithm at a single coordinate. The output is in the approximate range [-1, 1]; remap it to your world-space height range after sampling. Scale the input coordinates with a small frequency multiplier to control feature size – raw world-space values change too fast to produce visible terrain features.

/* Default algorithm is OpenSimplex2: isotropic, fast, no axis-aligned grid lines. */
float v = worldGen.Evaluate2D(x: 12f, y: 34f);

/* Explicit algorithm: pick by visual character, not just speed.
 * OpenSimplex2 is the best general default.
 * Perlin matches legacy assets authored against the classic Perlin look.
 * Value/ValueCubic produce a simpler interpolation character for retro styles. */
float os   = worldGen.Evaluate2D(x: 12f, y: 34f, NoiseAlgorithm.OpenSimplex2);
float perl = worldGen.Evaluate2D(x: 12f, y: 34f, NoiseAlgorithm.Perlin);
float val  = worldGen.Evaluate2D(x: 12f, y: 34f, NoiseAlgorithm.Value);
float vcub = worldGen.Evaluate2D(x: 12f, y: 34f, NoiseAlgorithm.ValueCubic);

/* 3D evaluation: useful for volumetric clouds, ore deposits, and animated
 * 2D noise (sample at z = Time.time for a smoothly evolving 2D field). */
float v3 = worldGen.Evaluate3D(x: 12f, y: 34f, z: 56f);

/* Scale the input. Raw world coordinates change too fast; multiply by a small
 * frequency so features are visible at gameplay scale. 0.02 produces features
 * roughly 50 units across, which reads as "rolling hills" at human scale. */
const float TERRAIN_FREQ = 0.02f;
float h = worldGen.Evaluate2D(worldX * TERRAIN_FREQ, worldZ * TERRAIN_FREQ);

/* Remap from [-1, 1] to [0, 1] for use as a normalized height or density value. */
float h01 = (h + 1f) * 0.5f;

Generate a terrain heightmap with FBM

Problem. Single-octave noise looks too smooth for almost any game terrain: the crags of a mountain, the rolling hills of a countryside, the jagged edge of a cliff all need multiple frequencies layered on top of each other. You want continental-scale shape from the coarse layers and fine rock detail from the finer ones, the same way a real heightmap looks when you zoom out from fine detail to the whole continent.

Solution. Fractal2D and Fractal3D sample multiple octaves and combine them according to a FractalType. The NoiseSettings struct bundles the algorithm, fractal type, frequency, octave count, lacunarity, and persistence into one immutable value. Five named presets cover the common shapes; build a custom instance through the constructor for anything else.

/* Presets cover the common shapes. Pass them directly to Fractal2D/3D.
 * Terrain is 6-octave FBM at 0.005 frequency - continental shapes with fine detail.
 * Clouds is 4-octave Turbulence at 0.01 - billowy, always-positive output.
 * Ridges is 5-octave Ridged at 0.008 - sharp mountain spines and canyon walls.
 * Caves is 4-octave FBM at 0.05 - threshold this value to get cave cross-sections. */
float terrain = worldGen.Fractal2D(worldX, worldZ, NoiseSettings.Terrain);
float clouds  = worldGen.Fractal2D(worldX, worldZ, NoiseSettings.Clouds);
float ridges  = worldGen.Fractal2D(worldX, worldZ, NoiseSettings.Ridges);
float caves   = worldGen.Fractal3D(worldX, worldY, worldZ, NoiseSettings.Caves);

/* Custom settings when the presets do not match. Constructor order:
 * algorithm, fractal type, frequency, octaves, lacunarity, persistence. */
var custom = new NoiseSettings(
    algorithm:   NoiseAlgorithm.OpenSimplex2,
    fractal:     FractalType.FBM,
    frequency:   0.015f,  /* mid-scale features */
    octaves:     5,       /* 5 layers of detail */
    lacunarity:  2.0f,    /* each octave doubles frequency */
    persistence: 0.55f    /* slightly heavier coarse layers */
);

float bumpy = worldGen.Fractal2D(worldX, worldZ, custom);

/* PingPong produces banded patterns - the right look for striped marble,
 * wood grain, or layered geological strata on a cave wall. */
var marble = new NoiseSettings(
    NoiseAlgorithm.OpenSimplex2, FractalType.PingPong,
    frequency: 0.03f, octaves: 3, lacunarity: 2.0f, persistence: 0.5f
);
float stripe = worldGen.Fractal2D(x, y, marble);

Fill a heightmap or texture buffer in one call

Problem. You are building a terrain mesh that needs a 1024 x 1024 heightmap, or a cloud density volume at 256 x 256 x 64, or a 512 x 512 noise texture baked to a render target. Looping over Evaluate2D manually for each cell works but leaves performance on the table. You want the fastest path from settings to array.

Solution. Fill2D and Fill3D write noise values directly into a caller-provided float[] in row-major order, bypassing the per-cell method-call overhead of a manual loop. The array must be sized to width * height (2D) or width * height * depth (3D). Both methods accept optional coordinate offsets so you can re-fill the same buffer for different world regions without reallocating.

/* 2D heightmap. Cell (col, row) lands at output[row * width + col]. */
const int W = 256, H = 256;
var heights = new float[W * H];

worldGen.Fill2D(heights, W, H, NoiseSettings.Terrain,
    offsetX: 0f, offsetY: 0f);

/* Re-fill the same buffer for a neighboring tile by changing the offsets.
 * Same array, same generator, same settings - just a different world region.
 * The result is seamlessly tileable as long as the seed and settings match. */
worldGen.Fill2D(heights, W, H, NoiseSettings.Terrain,
    offsetX: W, offsetY: 0f); /* tile to the right */

/* 3D density volume for clouds or caves.
 * Slice s, row r, column c lands at output[s * (W * H) + r * W + c]. */
const int D = 32;
var density = new float[W * H * D];

worldGen.Fill3D(density, W, H, D, NoiseSettings.Clouds,
    offsetX: 0f, offsetY: 0f, offsetZ: 0f);

/* Apply a custom transform after the fill.
 * Here we remap [-1, 1] to a [0, peak] world-space height range. */
float peakHeight = 100f;
for (int i = 0; i < heights.Length; i++)
{
    heights[i] = (heights[i] + 1f) * 0.5f * peakHeight;
}

Texture stone, cracks, and biome borders with cellular noise

Problem. You are tiling a stone dungeon floor and the smooth gradients from coherent noise look wrong – stone has discrete blocks with hard edges. Or you are building a fracture system for a breakable surface and you need lines that radiate outward like cracks, not gradients. Cellular noise (Voronoi-style) is the right algorithm when the art calls for discrete-cell feel rather than flowing gradients.

Solution. The cellular family on NoiseGenerator scatters feature points across the input space and returns a value based on distance to one or more of those points. CellularSettings bundles the distance metric, return type, jitter, and fractal parameters. Pick the preset that matches the desired character, or construct custom settings for a one-off shape.

/* Single-octave cellular: pick a preset that matches the visual character.
 * Voronoi highlights cell boundaries with F2-F1 (the value peaks at borders).
 * Cracks uses Manhattan distance for angular crack lines in stone.
 * Stone uses F2 with smaller jitter for a smoother tile-like appearance.
 * Cells gives filled irregular regions - the right base for biome maps. */
float voronoi = worldGen.EvaluateCellular2D(x, y, CellularSettings.Voronoi);
float cracks  = worldGen.EvaluateCellular2D(x, y, CellularSettings.Cracks);
float stone   = worldGen.EvaluateCellular2D(x, y, CellularSettings.Stone);
float cells   = worldGen.EvaluateCellular2D(x, y, CellularSettings.Cells);

/* 3D cellular: ore vein placement, gas cloud density, volumetric cave structures. */
float cells3D = worldGen.EvaluateCellular3D(x, y, z, CellularSettings.Default);

/* Fractal cellular: stack multiple octaves for more visual richness.
 * Cost scales linearly with octave count. */
float layered = worldGen.FractalCellular2D(x, y, CellularSettings.Stone);

/* Bulk fill - same row-major layout as Fill2D. Use for texture pre-bakes. */
var stoneMap = new float[256 * 256];
worldGen.FillCellular2D(stoneMap, 256, 256, CellularSettings.Stone,
    offsetX: 0f, offsetY: 0f);

/* 3D bulk fill for volumetric patterns. */
var oreVolume = new float[64 * 64 * 64];
worldGen.FillCellular3D(oreVolume, 64, 64, 64, CellularSettings.Voronoi,
    offsetX: 0f, offsetY: 0f, offsetZ: 0f);

/* Custom cellular for a one-off shape. Constructor order:
 * returnType, distanceType, jitter, frequency, fractal, octaves,
 * lacunarity, persistence.
 * Here: large irregular cells via Manhattan distance for a political-map biome look. */
var biome = new CellularSettings(
    returnType:   CellularReturnType.F1,
    distanceType: CellularDistanceType.Manhattan,
    jitter:       0.85f,  /* mostly random placement */
    frequency:    0.015f, /* large cells */
    fractal:      FractalType.None,
    octaves:      1,
    lacunarity:   2.0f,
    persistence:  0.5f
);
float biomeCell = worldGen.EvaluateCellular2D(x, y, biome);

Break up noise patterns with domain warping

Problem. Anyone who has stared at fractal noise long enough notices a faintly axis-aligned grid pattern bleeding through the texture. The algorithm’s simplicity is the price. You want the fluid, organic shapes seen in painted lava flows, swirling cloud formations, and procedural marble slabs, and single-octave or even multi-octave noise alone cannot produce them.

Solution. NoiseDomainWarper distorts the input coordinates using another noise sample before passing them through to the base evaluation. Construct the warper around an existing NoiseGenerator; it reuses the generator’s seed. Single warp costs roughly 3x a plain evaluation; double warp (warping the warp coordinates again, recursively) costs roughly 5x but produces visibly richer organic shapes.

/* Construct the warper around the generator.
 * The warper reuses the generator's seed and algorithm settings. */
var warper = new NoiseDomainWarper(worldGen);

/* Single warp: distort once, then sample. WarpSettings controls strength,
 * frequency, and the warp algorithm. Three presets cover the common ranges.
 * Subtle (0.5 strength) barely shifts the pattern; Extreme (3.0) produces
 * heavy fluid distortion. */
float v1 = warper.Warp2D(x, y, NoiseSettings.Terrain, WarpSettings.Default);
float v2 = warper.Warp2D(x, y, NoiseSettings.Clouds, WarpSettings.Subtle);
float v3 = warper.Warp2D(x, y, NoiseSettings.Ridges, WarpSettings.Extreme);

/* 3D warp. Same shape as 2D plus a Z coordinate. */
float v4 = warper.Warp3D(x, y, z, NoiseSettings.Caves, WarpSettings.Default);

/* Double warp: warp the warp coordinates before sampling. Roughly 5x base
 * cost but produces the richest organic shapes - the look of painted marble,
 * turbulent smoke, or an alien landscape. */
float dbl2 = warper.WarpDouble2D(x, y, NoiseSettings.Terrain, WarpSettings.Default);
float dbl3 = warper.WarpDouble3D(x, y, z, NoiseSettings.Clouds, WarpSettings.Default);

/* Fractal warp: warp coordinates with a multi-octave warp field rather than
 * a single-octave sample. Cost scales with octave count; use when the warp
 * field itself needs to carry high-frequency detail. */
float frac2 = warper.WarpFractal2D(x, y, NoiseSettings.Terrain, WarpSettings.Default);
float frac3 = warper.WarpFractal3D(x, y, z, NoiseSettings.Caves, WarpSettings.Default);

/* Custom warp settings. Constructor: strength, frequency, offsetX, offsetY,
 * offsetZ, algorithm.
 * Animate the warp over time by biasing offsetX/Y with Time.time. */
var custom = new WarpSettings(
    strength:  2.0f,
    frequency: 0.4f,
    offsetX:   0f, offsetY: 0f, offsetZ: 0f,
    algorithm: NoiseAlgorithm.OpenSimplex2
);
float distorted = warper.Warp2D(x, y, NoiseSettings.Terrain, custom);

Drive particle flow with curl noise

Problem. Particles seeded into a plain random vector field accumulate in dead zones and clump at saddle points, which looks nothing like the swirling motion that smoke, fire, water, and crowds actually exhibit. You want particles that flow in continuously turning, looping paths without piling up anywhere, the kind of motion that reads immediately as “fluid” even from a distance.

Solution. The curl of a scalar noise field is divergence-free by construction, which is the math behind the “particles flow in swirling, fluid-like patterns without piling up” effect. NoiseCurlSampler computes this curl via finite differences. 2D curl uses 4 noise evaluations per sample; 3D curl uses 12.

/* Construct the sampler around a generator. The optional epsilon controls the
 * step size used for the finite-difference derivative; the default (0.001)
 * works for most cases. Smaller values produce more accurate curls at the
 * cost of float precision; larger values smooth out the field. */
var curl = new NoiseCurlSampler(worldGen);
var curlPrecise = new NoiseCurlSampler(worldGen, epsilon: 0.0001f);

/* 2D curl: returns a Vector2 perpendicular to the gradient of the noise field.
 * Use NoiseSettings to pick the underlying field's frequency and fractal layering. */
Vector2 flow = curl.SampleCurl2D(x, y, NoiseSettings.Default);

/* Apply the result as a velocity offset on a particle - the only math you
 * need on the gameplay side. The curl field handles the swirling. */
particle.position += flow * speed * Time.deltaTime;

/* 3D curl: returns a Vector3 perpendicular to the 3D gradient. 12 noise
 * evaluations per sample; profile before sampling per-particle per-frame
 * on a large particle system with thousands of active particles. */
Vector3 flow3D = curl.SampleCurl3D(x, y, z, NoiseSettings.Default);

/* Sampling the curl field over time produces continuously-evolving flow.
 * Pass Time.time as a bias on the input coordinates so the field slowly
 * shifts without snapping or repeating within a play session. */
Vector2 evolving = curl.SampleCurl2D(x + Time.time * 0.1f, y, NoiseSettings.Default);

Add film grain and dithering to rendered output

Problem. You want to composite film grain over a rendered frame without visible spatial patterns and without any relationship between adjacent pixels. A smooth gradient after a dithering pass should break into the canonical “ordered noise” look; a dark fantasy game’s rendered frames should carry subtle monochrome grain that reads as film without looking like compression artifacts. The coherent noise algorithms are the wrong tool here because you explicitly do not want spatial correlation.

Solution. PixelNoise is the separate static-class subsystem for statistically independent per-pixel noise. Two API styles are available: RNG-based methods that consume sequential values from an IRandomSource (for one-off generation where reproducibility does not matter), and hash-based methods that take a seed and (x, y) coordinates (for procedural textures that must reproduce bit-exactly across runs).

using Scylla.Core.Util.Random;

/* RNG-based fill: write a fresh independent grain field into the buffer.
 * Each pixel gets a completely independent sample; no spatial pattern. */
var pixels32 = new Color32[256 * 256];
IRandomSource rng = RandomUtil.CreateXoshiro256(seed: 1UL);
PixelNoise.Fill(pixels32, PixelNoiseSettings.FilmGrain, rng);

/* Float-channel variant for 16-bit or float textures. */
var pixelsF = new Color[256 * 256];
PixelNoise.Fill(pixelsF, PixelNoiseSettings.FilmGrain, rng);

/* Apply variant: add grain on top of an existing image rather than overwriting.
 * The destination pixels are modified in place by the noise amount; alpha is
 * preserved. Useful for compositing grain over a rendered frame each tick. */
PixelNoise.Apply(pixels32, PixelNoiseSettings.SubtleMono, rng);
PixelNoise.Apply(pixelsF, PixelNoiseSettings.SubtleMono, rng);

/* Hash-based fill: deterministic for a given seed + coordinate pair. Two
 * runs with the same seed produce bit-exact identical output. Best for
 * procedural textures that must round-trip across builds or machines. */
const long SEED = 0xCAFEBABE;
PixelNoise.Fill(pixels32, width: 256, height: 256, SEED, PixelNoiseSettings.Default);
PixelNoise.Fill(pixelsF, width: 256, height: 256, SEED, PixelNoiseSettings.Default);

/* Hash-based apply: composite deterministic grain over an existing image. */
PixelNoise.Apply(pixels32, width: 256, height: 256, SEED, PixelNoiseSettings.Dither);
PixelNoise.Apply(pixelsF, width: 256, height: 256, SEED, PixelNoiseSettings.Dither);

/* Single-pixel hash evaluation. Returns the Color32 for one (x, y) coordinate
 * without needing a buffer at all. Useful for shader-equivalent CPU evaluations
 * where you just need the grain value at one known screen position. */
Color32 grain = PixelNoise.Evaluate(seed: SEED, x: 10, y: 20, PixelNoiseSettings.FilmGrain);

/* Custom settings. Constructor: amount, distribution, monochromatic.
 * Here: 15% Gaussian monochrome grain, the typical "film burn" strength. */
var custom = new PixelNoiseSettings(
    amount:        0.15f,                        /* 15% around mid-gray */
    distribution:  PixelNoiseDistribution.Gaussian,
    monochromatic: true                           /* one value applied to R, G, B */
);
PixelNoise.Apply(pixels32, custom, rng);

API Sketch

The public surface is split across three sealed classes (NoiseGenerator, NoiseDomainWarper, NoiseCurlSampler), one static class (PixelNoise), four immutable readonly structs (NoiseSettings, CellularSettings, WarpSettings, PixelNoiseSettings), and five enums. Every coherent and cellular sample flows through a NoiseGenerator instance; pixel noise is standalone because it has no spatial coherence to maintain.

namespace Scylla.Core.Util.Noise
{
    /* Coherent noise generator: one instance per seed. */
    public sealed class NoiseGenerator
    {
        public long Seed { get; }

        public NoiseGenerator(long seed = 0);
        public NoiseGenerator(IRandomSource rng);

        /* Single-octave gradient noise (OpenSimplex2 / Perlin / Value / ValueCubic). */
        public float Evaluate2D(float x, float y, NoiseAlgorithm algorithm = NoiseAlgorithm.OpenSimplex2);
        public float Evaluate3D(float x, float y, float z, NoiseAlgorithm algorithm = NoiseAlgorithm.OpenSimplex2);

        /* Fractal layering (FBM / Turbulence / Ridged / PingPong). */
        public float Fractal2D(float x, float y, NoiseSettings settings);
        public float Fractal3D(float x, float y, float z, NoiseSettings settings);

        /* Bulk fill into a caller-provided row-major float[]. */
        public void Fill2D(float[] output, int width, int height, NoiseSettings settings,
            float offsetX = 0f, float offsetY = 0f);
        public void Fill3D(float[] output, int width, int height, int depth, NoiseSettings settings,
            float offsetX = 0f, float offsetY = 0f, float offsetZ = 0f);

        /* Cellular (Voronoi-style) noise. */
        public float EvaluateCellular2D(float x, float y, CellularSettings settings);
        public float EvaluateCellular3D(float x, float y, float z, CellularSettings settings);
        public float FractalCellular2D(float x, float y, CellularSettings settings);
        public float FractalCellular3D(float x, float y, float z, CellularSettings settings);
        public void FillCellular2D(float[] output, int width, int height,
            CellularSettings settings, float offsetX = 0f, float offsetY = 0f);
        public void FillCellular3D(float[] output, int width, int height, int depth,
            CellularSettings settings, float offsetX = 0f, float offsetY = 0f, float offsetZ = 0f);
    }

    /* Domain-warped sampler: distorts input coordinates with another noise sample. */
    public sealed class NoiseDomainWarper
    {
        public NoiseDomainWarper(NoiseGenerator generator);

        public float Warp2D       (float x, float y, NoiseSettings noiseSettings, WarpSettings warpSettings);
        public float Warp3D       (float x, float y, float z, NoiseSettings noiseSettings, WarpSettings warpSettings);
        public float WarpDouble2D (float x, float y, NoiseSettings noiseSettings, WarpSettings warpSettings);
        public float WarpDouble3D (float x, float y, float z, NoiseSettings noiseSettings, WarpSettings warpSettings);
        public float WarpFractal2D(float x, float y, NoiseSettings noiseSettings, WarpSettings warpSettings);
        public float WarpFractal3D(float x, float y, float z, NoiseSettings noiseSettings, WarpSettings warpSettings);
    }

    /* Divergence-free vector field via curl of a scalar noise field. */
    public sealed class NoiseCurlSampler
    {
        public NoiseCurlSampler(NoiseGenerator generator, float epsilon = 0.001f);

        public Vector2 SampleCurl2D(float x, float y, NoiseSettings settings);
        public Vector3 SampleCurl3D(float x, float y, float z, NoiseSettings settings);
    }

    /* Per-pixel grain (statistically independent samples, no spatial coherence). */
    public static class PixelNoise
    {
        /* RNG-based: consume sequential values from an IRandomSource. */
        public static void Fill (Color32[] output, PixelNoiseSettings settings, IRandomSource rng);
        public static void Fill (Color[]   output, PixelNoiseSettings settings, IRandomSource rng);
        public static void Apply(Color32[] pixels, PixelNoiseSettings settings, IRandomSource rng);
        public static void Apply(Color[]   pixels, PixelNoiseSettings settings, IRandomSource rng);

        /* Hash-based: seed + (x, y) yields deterministic random-access output. */
        public static Color32 Evaluate(long seed, int x, int y, PixelNoiseSettings settings);
        public static void Fill (Color32[] output, int width, int height, long seed, PixelNoiseSettings settings);
        public static void Fill (Color[]   output, int width, int height, long seed, PixelNoiseSettings settings);
        public static void Apply(Color32[] pixels, int width, int height, long seed, PixelNoiseSettings settings);
        public static void Apply(Color[]   pixels, int width, int height, long seed, PixelNoiseSettings settings);
    }

    /* Settings (immutable readonly structs). */
    public readonly struct NoiseSettings
    {
        public static readonly NoiseSettings Default; /* OpenSimplex2 single-octave at freq 1.0 */
        public static readonly NoiseSettings Terrain; /* FBM 6 octaves, OpenSimplex2, freq 0.005 */
        public static readonly NoiseSettings Clouds;  /* Turbulence 4 octaves, freq 0.01 */
        public static readonly NoiseSettings Ridges;  /* Ridged 5 octaves, freq 0.008 */
        public static readonly NoiseSettings Caves;   /* FBM 4 octaves, freq 0.05, persistence 0.45 */

        public readonly NoiseAlgorithm Algorithm;
        public readonly FractalType    Fractal;
        public readonly float          Frequency;
        public readonly int            Octaves;
        public readonly float          Lacunarity;
        public readonly float          Persistence;

        public NoiseSettings(NoiseAlgorithm algorithm, FractalType fractal,
            float frequency, int octaves, float lacunarity, float persistence);
    }

    public readonly struct CellularSettings
    {
        public static readonly CellularSettings Default;  /* Euclidean F1, jitter 1.0 */
        public static readonly CellularSettings Voronoi;  /* Euclidean F2-F1 for visible borders */
        public static readonly CellularSettings Cracks;   /* Manhattan F2-F1 */
        public static readonly CellularSettings Stone;    /* Euclidean F2, smaller jitter */
        public static readonly CellularSettings Cells;    /* Euclidean F1, full jitter */

        public readonly CellularReturnType   ReturnType;
        public readonly CellularDistanceType DistanceType;
        public readonly float                Jitter;
        public readonly float                Frequency;
        public readonly FractalType          Fractal;
        public readonly int                  Octaves;
        public readonly float                Lacunarity;
        public readonly float                Persistence;

        public CellularSettings(CellularReturnType returnType, CellularDistanceType distanceType,
            float jitter, float frequency, FractalType fractal, int octaves,
            float lacunarity, float persistence);
    }

    public readonly struct WarpSettings
    {
        public static readonly WarpSettings Default; /* strength 1.0 */
        public static readonly WarpSettings Subtle;  /* strength 0.5 */
        public static readonly WarpSettings Extreme; /* strength 3.0 */

        public readonly float          Strength;
        public readonly float          Frequency;
        public readonly float          OffsetX;
        public readonly float          OffsetY;
        public readonly float          OffsetZ;
        public readonly NoiseAlgorithm Algorithm;

        public WarpSettings(float strength, float frequency,
            float offsetX, float offsetY, float offsetZ, NoiseAlgorithm algorithm);
    }

    public readonly struct PixelNoiseSettings
    {
        public static readonly PixelNoiseSettings Default;    /* 10 % uniform color */
        public static readonly PixelNoiseSettings FilmGrain;  /* 8 % Gaussian monochrome */
        public static readonly PixelNoiseSettings Heavy;      /* 50 % uniform color */
        public static readonly PixelNoiseSettings SubtleMono; /* 5 % Gaussian monochrome */
        public static readonly PixelNoiseSettings Dither;     /* 2 % uniform monochrome */

        public readonly float                  Amount;
        public readonly PixelNoiseDistribution Distribution;
        public readonly bool                   Monochromatic;

        public PixelNoiseSettings(float amount, PixelNoiseDistribution distribution, bool monochromatic);
    }

    /* Enums. */
    public enum NoiseAlgorithm           { OpenSimplex2 = 0, Perlin = 1, Value = 2, ValueCubic = 3 }
    public enum FractalType              { None = 0, FBM = 1, Turbulence = 2, Ridged = 3, PingPong = 4 }
    public enum CellularDistanceType     { Euclidean = 0, Manhattan = 1, Chebyshev = 2 }
    public enum CellularReturnType       { F1 = 0, F2 = 1, F2MinusF1 = 2 }
    public enum PixelNoiseDistribution   { Uniform, Gaussian }
}

See the API reference for full signatures, remarks, and exception contracts on every member.

Settings reference

The four settings structs cover the full configuration surface. Every preset is in the table; build a custom instance with the constructor for any other shape.

NoiseSettings properties

PropertyTypeNotes
AlgorithmNoiseAlgorithmWhich coherent algorithm to use per octave. OpenSimplex2 is the recommended default.
FractalFractalTypeLayering method. None for single octave; FBM / Turbulence / Ridged / PingPong for the multi-octave shapes.
FrequencyfloatInput scaling. Typical world-space terrain values are 0.001..0.05. Constructor clamps to 0.01 if non-positive (with warning log).
OctavesintNumber of layered octaves. Ignored when Fractal == None. Typical range is 2..8.
LacunarityfloatFrequency multiplier per octave. Typical value 2.0 (each octave doubles frequency). Constructor clamps to 2.0 if non-positive.
PersistencefloatAmplitude multiplier per octave. Typical value 0.5 (each octave halves amplitude). Higher values produce noisier output.

NoiseSettings presets

PresetAlgorithmFractalFrequencyOctavesUse case
DefaultOpenSimplex2None1.01Neutral starting point. Single-octave noise at base frequency.
TerrainOpenSimplex2FBM0.0056Continental terrain heightmaps. Smooth large shapes with fine detail.
CloudsOpenSimplex2Turbulence0.014Cloud density. Folded, billowy character. Output in [0, 1].
RidgesOpenSimplex2Ridged0.0085Mountain spines, canyon walls, river edges. Output unnormalized.
CavesOpenSimplex2FBM0.054Threshold to produce 2D cave cross-sections or 3D cave volumes.

CellularSettings properties and presets

PropertyTypeNotes
ReturnTypeCellularReturnTypeF1 (nearest), F2 (second nearest), or F2MinusF1 (their difference, for cell borders).
DistanceTypeCellularDistanceTypeEuclidean (smooth round cells), Manhattan (block-shaped), or Chebyshev (square cells).
JitterfloatHow far feature points wander from lattice positions. 0 = grid; 1 = full random.
FrequencyfloatCell size control. Smaller values produce larger cells.
FractalFractalTypeSet to None for single-octave; otherwise layered cellular.
OctavesintOctave count for fractal cellular.
LacunarityfloatFrequency multiplier per octave.
PersistencefloatAmplitude multiplier per octave.
PresetReturnDistanceJitterUse case
DefaultF1Euclidean1.0General-purpose cellular field.
VoronoiF2MinusF1Euclidean1.0Visible cell borders (lines on boundaries).
CracksF2MinusF1Manhattan1.0Angular crack patterns for stone walls.
StoneF2Euclidean~0.5Smoother stone-tile patterns.
CellsF1Euclidean1.0Irregular cell regions for biome maps.

WarpSettings properties and presets

PropertyTypeNotes
StrengthfloatHow far input coordinates are displaced. Typical 0.5..3.0.
FrequencyfloatFrequency of the warp field. Smaller values produce broader distortion.
OffsetX/Y/ZfloatPer-axis bias added to the warp sample. Useful for animating warps over time.
AlgorithmNoiseAlgorithmWhich algorithm to use for the warp field itself. OpenSimplex2 by default.
PresetStrengthUse case
Default1.0Balanced organic distortion.
Subtle0.5Light fluid character for subtle terrain or texture work.
Extreme3.0Heavy distortion for stylized clouds or fluid effects.

PixelNoiseSettings properties and presets

PropertyTypeNotes
AmountfloatStandard deviation around mid-gray. Range [0, 1]. Typical film grain 0.05..0.15.
DistributionPixelNoiseDistributionUniform (square distribution) or Gaussian (Irwin-Hall approximation, more natural).
MonochromaticboolWhen true, one value is applied to R/G/B uniformly. When false, channels independent.
PresetAmountDistributionMonochromeUse case
Default0.10UniformnoGeneral-purpose color noise.
FilmGrain0.08GaussianyesRealistic film-style grain over a rendered image.
Heavy0.50UniformnoHeavy color noise; stylized “broken signal” effect.
SubtleMono0.05GaussianyesLight monochromatic grain; barely visible at full size.
Dither0.02UniformyesAnti-banding dither pattern for low-bit-depth output.

Best Practices

  • Default to OpenSimplex2 for new content. It has no axis-aligned bias, is faster than Perlin on most hardware, requires no permutation table, and works identically at every input scale. Switch to Perlin only when matching a legacy asset that was authored against the classic Perlin look.
  • Scale input coordinates before sampling. Raw world-space coordinates change too fast; multiply by a small frequency (0.005 to 0.05 is typical for terrain) so the noise produces visible features at the right spatial scale. NoiseSettings bakes the frequency in via the struct so you do not have to remember the multiplier at every call site.
  • Use the named NoiseSettings presets for the obvious shapes. Terrain, Clouds, Ridges, and Caves cover the most common procedural shapes with sensible defaults; reach for a custom constructor only when the presets do not match.
  • Reuse one NoiseGenerator instance for many samples. Allocation is the only real cost (the Perlin permutation table is 2 KB on first use); sampling is allocation-free. A single instance can comfortably serve every gameplay system that samples the same seed.
  • Construct a separate generator per logical seed. Combat, AI, loot, and terrain often want independent noise fields; each should hold its own NoiseGenerator(seed) instance so changes in one do not affect the others.
  • Pre-bake static terrain into a buffer with Fill2D/Fill3D. Bulk fill is significantly faster than per-cell Evaluate loops for large arrays, and the result can be reused without re-evaluation. For a streaming world, fill one tile at a time and cache the result; the generator is cheap to re-instantiate with the same seed when cache pressure forces an eviction.
  • Threshold a fractal field to produce masks. Cave generation, vegetation density, and biome boundaries are usually noise > threshold operations on a single multi-octave field. The threshold value controls coverage; play with octave count and persistence to control feature size. You can combine Procedural Map Generation algorithms with a noise-threshold pass to get the best of both – algorithmic connectivity guarantees with natural-looking boundaries.
  • Pair NoiseAlgorithm.OpenSimplex2 with CellularSettings for layered biomes. A low-frequency OpenSimplex2 layer for continents, a higher-frequency cellular layer with F2MinusF1 for biome boundaries, and a ridged fractal layer for mountains together produce richer terrain than any single algorithm.
  • Restrict NoiseDomainWarper to areas the player can see. Single warp triples cost; fractal warp adds another integer multiplier. Pre-bake static warped terrain; reserve runtime warping for dynamic effects (camera shake, fluid distortion) that genuinely need it.
  • Use NoiseCurlSampler for visual flow, not physical simulation. The discrete curl approximation has small numerical divergence; particles in a curl field move convincingly but the field is not divergence-free in a strict sense. For physics-grade fluid simulation, use a dedicated fluid solver instead.
  • Pick the cellular return type by what should be visible. F1 for filled cells, F2MinusF1 for visible cell boundary lines, F2 for smoother fields. Manhattan distance for angular cracks, Euclidean for natural-looking cells, Chebyshev for square cells.
  • Prefer hash-based PixelNoise for procedural textures. The hash variant is deterministic for any seed plus coordinate pair, so two runs (or two machines) produce bit-exact identical output without storing the bitmap. The RNG variant is for one-off generation where reproducibility does not matter.
  • Lock NoiseSettings values used for shipped content. Procedural content authored with one set of values will not match content authored with even slightly different values; lacunarity 2.0 vs 1.95 produces visibly different terrain. Commit them as a configuration asset or named preset so the same seed reproduces the same world across builds.

Pitfalls