Skip to content

Introduction to Procedural Generation with Perlin Noise for Game Development

Welcome, game developers, to an exciting journey into the world of procedural generation using Perlin noise—a technique that’s not just a cornerstone of modern game development but a magic wand for creating endless, dynamic worlds. From the sprawling landscapes of open-world adventures to the intricate patterns of a rogue-like dungeon, procedural generation can transform your game from a static canvas to a living, breathing universe with unlimited replayability.

Ken Perlin, while working on the visual effects for Tron, introduced Perlin noise in the 1980s. It has since become the gold standard for generating natural-looking textures and terrains. This tutorial will guide you through leveraging Perlin noise to craft visually stunning game environments, ensuring each player’s experience is unique.

In this series, we start by constructing a height map for terrain generation. As we progress, we’ll explore simulating diverse biomes and creating a dynamic 2D dungeon generator. Whether you’re building the next big RPG or a serene exploration game, understanding Perlin noise will equip you with the tools to add depth and realism to your worlds.

The Basics of Perlin Noise

At its heart, Perlin noise is about creating coherent randomness. Unlike the stark unpredictability of raw random numbers, Perlin noise ensures smooth transitions between values, mimicking the gentle variations of natural landscapes. It’s this smoothness that makes Perlin noise invaluable for:

  • Terrain Generation: Crafting landscapes that feel alive, from serene hills to daunting mountain ranges.
  • Texture Creation: Generating realistic textures for surfaces, enhancing visual fidelity.
  • Environmental Effects: Simulating natural phenomena like smoke, fire, or flowing water, adding immersion to your game environments.

Key Features for Game Developers:

  • Smoothness: Seamless transitions between values for natural-looking variations.
  • Control: Fine-tune parameters to sculpt your world, from gentle plains to chaotic terrains.
  • Repeatability: Generate the same world twice or create new ones with a simple seed change.

Behind the Scenes: The Math of Perlin Noise

Imagine a grid where each intersection has a random vector. Perlin noise calculates how much influence each of these vectors has on a point in space, smoothly interpolating between them. This process creates a gradient effect, where changes are gradual and organic, much like the real world.

For those who love diving into the technicalities, think of Perlin noise as blending random gradients in a way that results in a smooth, coherent pattern across a grid. It’s a symphony of dot products and interpolations, orchestrated to generate the illusion of randomness without the chaos of pure chance.

For an in depth explanation of the how it works bookmark this Coding Train Youtube playlist

Practical Implementation for Game Terrain

Setting the Stage with Seeded Randomness

Before we delve into the noise itself, let’s tackle randomness in games. Consistency in randomness is vital, especially when generating worlds you want to revisit or share. Our SeededRandom class uses a seed to ensure that ‘random’ numbers aren’t so random after all—they’re predictable, allowing for the same world to be generated every time that seed is used.

Commented Code
/**
 * Defines a SeededRandom class for generating pseudo-random numbers based on an initial seed.
 * This is useful for situations where you need reproducible sequences of random numbers.
 */
class SeededRandom {
    /**
     * Constructor for the SeededRandom class.
     * Initializes a new instance with a seed value.
     * 
     * @param {number} seed - The initial seed value for the pseudo-random number generator.
     *                        The seed ensures the sequence of generated numbers is reproducible.
     */
    constructor(seed) {
        // The modulus operator ensures the seed value stays within the range of positive 32-bit integers.
        // 2147483647 is the largest 32-bit prime number, commonly used in linear congruential generators for better randomness.
        this.seed = seed % 2147483647;
        
        // If the seed is not positive, adjust it to ensure it is within the range 1 to 2147483646.
        // This adjustment is necessary because a seed of 0 would produce a sequence of zeros, reducing randomness.
        if (this.seed <= 0) this.seed += 2147483646;
    }

    /**
     * Generates the next pseudo-random number in the sequence.
     * 
     * @returns {number} The next pseudo-random number, ensuring it falls within the 32-bit integer range.
     */
    next() {
        // Multiplies the current seed by 16807 (a primitive root modulo 2147483647, which ensures a full cycle of numbers)
        // and takes the modulus to ensure the result remains within 32-bit integer size. 
        // This specific multiplier is part of the minimal standard random number generator.
        return this.seed = this.seed * 16807 % 2147483647;
    }

    /**
     * Generates a pseudo-random floating-point number between 0 (inclusive) and 1 (exclusive).
     * 
     * @returns {number} The next pseudo-random floating-point number.
     */
    nextFloat() {
        // Calls next() to get a new pseudo-random integer, subtracts 1 to make it start from 0,
        // and divides by 2147483646 to normalize the result to the range [0,1).
        return (this.next() - 1) / 2147483646;
    }
}
class SeededRandom {
    constructor(seed) {
        this.seed = seed % 2147483647;
        if (this.seed <= 0) this.seed += 2147483646;
    }

    next() {
        return this.seed = this.seed * 16807 % 2147483647;
    }

    nextFloat() {
        return (this.next() - 1) / 2147483646;
    }
}
Commented Code
using System;

/// <summary>
/// A pseudo-random number generator class that uses a linear congruential generator (LCG) algorithm,
/// allowing for reproducible sequences of random numbers given an initial seed.
/// </summary>
public class SeededRandom
{
    // The current state of the generator, which will evolve as new numbers are generated.
    private long seed;

    // Constants for the linear congruential generator algorithm. The values of these constants are crucial
    // for the quality of the pseudo-random numbers generated.

    // The multiplier value 'a'. This particular value is part of the minimal standard set by Park and Miller.
    private const long a = 48271;

    // The increment value 'c'. It is set to 0 for this implementation, defining it as a multiplicative LCG.
    private const long c = 0;

    // The modulus value 'm'. It's set to 2^31 - 1, which is a Mersenne prime that helps achieve a full period for the LCG.
    private const long m = 2147483647;

    /// <summary>
    /// Constructs a new instance of the SeededRandom class with a given initial seed.
    /// </summary>
    /// <param name="seed">The initial seed value for the pseudo-random number generator.</param>
    public SeededRandom(int seed)
    {
        this.seed = seed;
    }

    /// <summary>
    /// Generates the next pseudo-random number in the sequence as an integer.
    /// </summary>
    /// <returns>A pseudo-random integer.</returns>
    public int Next()
    {
        // Updates the seed by applying the linear congruential generator formula
        // and returns the new seed as the next number in the sequence.
        seed = (a * seed + c) % m;
        return (int)seed;
    }

    /// <summary>
    /// Generates a pseudo-random floating-point number between 0 (inclusive) and 1 (exclusive).
    /// </summary>
    /// <returns>A pseudo-random float.</returns>
    public float NextFloat()
    {
        // Calls Next() to generate a new pseudo-random integer, then divides it by 'm'
        // to normalize the result to a floating-point number in the range [0, 1).
        return Next() / (float)m;
    }
}
using System;

public class SeededRandom
{
    private long seed;
    private const long a = 48271;
    private const long c = 0;
    private const long m = 2147483647;

    public SeededRandom(int seed)
    {
        this.seed = seed;
    }

    public int Next()
    {
        seed = (a * seed + c) % m;
        return (int)seed;
    }

    public float NextFloat()
    {
        return Next() / (float)m;
    }
}
Commented Code
#include <algorithm>
#include <cmath>
#include <iostream>
#include <vector>

/// A pseudo-random number generator class based on a linear congruential generator (LCG) algorithm.
/// This class allows generating a sequence of pseudo-random numbers from a seed value,
/// ensuring reproducible outcomes which are useful for simulations, testing, and gaming.
class SeededRandom {
private:
  // Current seed value, evolves with each call to Next() or NextFloat().
  long seed;

  // The multiplier 'a'. This value is part of the parameters that define the quality and characteristics
  // of the linear congruential generator. 48271 is a commonly used value in LCG algorithms for its good properties.
  static const long a = 48271;

  // The increment 'c'. It is set to 0 in this implementation, defining it as a multiplicative LCG.
  static const long c = 0;

  // The modulus 'm'. Using 2^31 - 1 (a Mersenne prime) as the modulus helps in achieving a full period
  // for the generated pseudo-random sequence, maximizing the sequence's length before it repeats.
  static const long m = 2147483647;

public:
  /// Constructor that initializes the pseudo-random number generator with a seed value.
  /// @param seed Initial seed value for generating numbers.
  SeededRandom(int seed) : seed(seed) {}

  /// Generates the next number in the sequence of pseudo-random numbers.
  /// @return The next pseudo-random number as an int.
  int Next() {
    // Calculates the next seed value using the linear congruential generator formula.
    // The use of static_cast<int> ensures the result is properly typed as an integer.
    seed = (a * seed + c) % m;
    return static_cast<int>(seed);
  }

  /// Generates a floating-point number between 0 (inclusive) and 1 (exclusive) based on the pseudo-random sequence.
  /// @return A pseudo-random float between 0 and 1.
  float NextFloat() {
    // Utilizes the Next() function to obtain a pseudo-random integer, then divides it by 'm'
    // to normalize the result to a floating-point number in the range [0, 1).
    return Next() / static_cast<float>(m);
  }
};
#include <algorithm>
#include <cmath>
#include <iostream>
#include <vector>

class SeededRandom {
private:
  long seed;
  static const long a = 48271;
  static const long c = 0;
  static const long m = 2147483647;

public:
  SeededRandom(int seed) : seed(seed) {}

  int Next() {
    seed = (a * seed + c) % m;
    return static_cast<int>(seed);
  }

  float NextFloat() { return Next() / static_cast<float>(m); }
};
// Defines a class named SeededRandom for generating pseudo-random numbers based on an initial seed value.
// This class uses a linear congruential generator (LCG) algorithm to produce a sequence of pseudo-random numbers,
// which is useful for scenarios requiring reproducible randomness, such as simulations or game logic.

CLASS SeededRandom
  // Constructor method initializes a new instance of SeededRandom with a given seed.
  // The seed value is used to start the sequence of pseudo-random numbers.
  METHOD constructor(seed)
    // Adjusts the seed to ensure it is within the range of positive 32-bit integers,
    // using 2147483647, the largest 32-bit prime number, as the modulus.
    this.seed = seed MOD 2147483647
    // Ensures the seed value is positive, adjusting it if necessary.
    // This adjustment is crucial because a non-positive seed would impair the randomness.
    IF this.seed <= 0 THEN
      this.seed = this.seed + 2147483646
    ENDIF
  ENDMETHOD

  // Generates the next number in the pseudo-random sequence.
  METHOD next()
    // Updates the seed using a specific multiplier (16807) and modulus (2147483647),
    // which are common parameters for LCG algorithms to ensure good distribution and cycle length.
    this.seed = (this.seed * 16807) MOD 2147483647
    // Returns the updated seed as the next number in the sequence.
    RETURN this.seed
  ENDMETHOD

  // Generates a floating-point number between 0 (inclusive) and 1 (exclusive).
  METHOD nextFloat()
    // Calls the next() method to generate a new pseudo-random integer, then normalizes
    // this value to a floating-point number in the range [0, 1) by dividing by the maximum possible value.
    // Subtracting 1 from the next() result before the division ensures the result is strictly less than 1.
    RETURN (this.next() - 1) / 2147483646
  ENDMETHOD
ENDCLASS

Crafting Worlds with Perlin Noise

Moving on to the star of our show—the Perlin class encapsulates the essence of Perlin noise. It initializes with a seed for consistent world generation and employs a permutation array to introduce randomness without losing repeatability.

  • Initialization with a Seed: This ensures the ability to reproduce the noise pattern by using the same seed value, making the noise deterministic.
  • Permutation Array: A shuffled array of integers that determines the randomness in the generated noise, ensuring variety in the output.
  • Gradient Function: Determines the slope at each point in the noise, contributing to the smoothness of the final output.
  • Fade Function: A smoothing function that eases the transitions between points, giving the noise its characteristic “smooth” appearance.
  • Linear Interpolation (Lerp) Function: Used to interpolate between values, which helps in creating a continuous, seamless noise pattern.
  • Noise Function: The core function that calculates the noise value for a given point in space by combining the effects of the permutation array, gradient, fade, and lerp functions.
Commented Code
// Define the Perlin class for generating Perlin noise.
class Perlin {
  // Constructor initializes a new instance of the Perlin noise generator with a given seed.
  constructor(seed) {
    this.p = []; // Initialize permutation array.
    let random = new SeededRandom(seed); // Create a seeded random generator for consistent shuffling.
    let permutation = []; // Temporary array to hold the initial permutation of values.
    for (let i = 0; i < 256; i++) {
      permutation[i] = i; // Fill permutation array with values from 0 to 255.
    }

    // Shuffle the permutation array using the seeded random generator to ensure repeatability.
    for (let i = 255; i > 0; i--) {
      let j = Math.floor(random.nextFloat() * (i + 1));
      // Swap the elements at indices i and j.
      [permutation[i], permutation[j]] = [permutation[j], permutation[i]];
    }

    // Duplicate the shuffled permutation to avoid overflow when accessing elements.
    this.p = permutation.concat(permutation);
  }

  // The fade function smoothens the transition between coordinate points.
  fade(t) {
    // Polynomial fade function for smoother transitions.
    return t * t * t * (t * (t * 6 - 15) + 10);
  }

  // The lerp function performs linear interpolation between two points.
  lerp(t, a, b) {
    // Calculate the interpolated value.
    return a + t * (b - a);
  }

  // Gradient function calculates a dot product based on a hashed value and the x, y, z coordinates.
  // This contributes to the final noise value based on the spatial position.
  grad(hash, x, y, z) {
    const h = hash & 15; // Hash value constrained to 16 possibilities (0-15).
    const u = h < 8 ? x : y; // Select x or y based on hash.
    const v = h < 4 ? y : h === 12 || h === 14 ? x : z; // Select y or z based on hash.
    return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v); // Calculate dot product based on hash.
  }

  // Generates Perlin noise for a given point in 3D space (x, y, z).
  noise(x, y, z) {
    // Find unit cube that contains the point and wrap the coordinates.
    const X = Math.floor(x) & 255;
    const Y = Math.floor(y) & 255;
    const Z = Math.floor(z) & 255;
    // Find relative x, y, z of the point in the cube.
    x -= Math.floor(x);
    y -= Math.floor(y);
    z -= Math.floor(z);
    // Compute fade curves for x, y, z.
    const u = this.fade(x);
    const v = this.fade(y);
    const w = this.fade(z);
    // Hash coordinates of the 8 cube corners.
    const A = this.p[X] + Y;
    const AA = this.p[A] + Z;
    const AB = this.p[A + 1] + Z;
    const B = this.p[X + 1] + Y;
    const BA = this.p[B] + Z;
    const BB = this.p[B + 1] + Z;

    // Add blended results from 8 corners of the cube and interpolate the results.
    return this.lerp(w, this.lerp(v, this.lerp(u, this.grad(this.p[AA], x, y, z), // Blend from the bottom front.
      this.grad(this.p[BA], x - 1, y, z)), // to the bottom back.
      this.lerp(u, this.grad(this.p[AB], x, y - 1, z), // Then blend from the top front.
        this.grad(this.p[BB], x - 1, y - 1, z))), // to the top back.
      this.lerp(v, this.lerp(u, this.grad(this.p[AA + 1], x, y, z - 1), // Repeat for the lower floor.
        this.grad(this.p[BA + 1], x - 1, y, z - 1)),
        this.lerp(u, this.grad(this.p[AB + 1], x, y - 1, z - 1), // And then for the upper floor.
          this.grad(this.p[BB + 1], x - 1, y - 1, z - 1))));
  }
}
class Perlin {
  constructor(seed) {
    this.p = [];
    let random = new SeededRandom(seed);
    let permutation = [];
    for (let i = 0; i < 256; i++) {
      permutation[i] = i;
    }

    for (let i = 255; i > 0; i--) {
      let j = Math.floor(random.nextFloat() * (i + 1));
      [permutation[i], permutation[j]] = [permutation[j], permutation[i]];
    }

    this.p = permutation.concat(permutation);
  }

  fade(t) {
    return t * t * t * (t * (t * 6 - 15) + 10);
  }

  lerp(t, a, b) {
    return a + t * (b - a);
  }

  grad(hash, x, y, z) {
    const h = hash & 15;
    const u = h < 8 ? x : y;
    const v = h < 4 ? y : h === 12 || h === 14 ? x : z;
    return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
  }

  noise(x, y, z) {
    const X = Math.floor(x) & 255;
    const Y = Math.floor(y) & 255;
    const Z = Math.floor(z) & 255;
    x -= Math.floor(x);
    y -= Math.floor(y);
    z -= Math.floor(z);
    const u = this.fade(x);
    const v = this.fade(y);
    const w = this.fade(z);
    const A = this.p[X] + Y;
    const AA = this.p[A] + Z;
    const AB = this.p[A + 1] + Z;
    const B = this.p[X + 1] + Y;
    const BA = this.p[B] + Z;
    const BB = this.p[B + 1] + Z;

    return this.lerp(w, this.lerp(v, this.lerp(u, this.grad(this.p[AA], x, y, z),
      this.grad(this.p[BA], x - 1, y, z)),
      this.lerp(u, this.grad(this.p[AB], x, y - 1, z),
        this.grad(this.p[BB], x - 1, y - 1, z))),
      this.lerp(v, this.lerp(u, this.grad(this.p[AA + 1], x, y, z - 1),
        this.grad(this.p[BA + 1], x - 1, y, z - 1)),
        this.lerp(u, this.grad(this.p[AB + 1], x, y - 1, z - 1),
          this.grad(this.p[BB + 1], x - 1, y - 1, z - 1))));
  }
}
Commented Code
// Existing implementation

// Define the Perlin class for generating Perlin noise.
public class PerlinNoise
{
    // Array to store the permutation table, which is essential for the noise generation algorithm.
    private int[] permutationTable;
    // Constant defining the size of the permutation table.
    private const int tableSize = 256;
    // Bitmask used for wrapping indices to table size, facilitating seamless tiling of noise.
    private const int tableSizeMask = tableSize - 1;
    // Instance of a seeded random number generator to shuffle the permutation table.
    private SeededRandom seededRandom;

    // Constructor that initializes the Perlin noise generator with a specific seed.
    public PerlinNoise(int seed)
    {
        seededRandom = new SeededRandom(seed);
        permutationTable = new int[tableSize * 2];

        // Temporary array to initialize a sequence of values.
        int[] tempTable = new int[tableSize];
        for (int i = 0; i < tableSize; i++)
            tempTable[i] = i;

        // Shuffle the temporary array using the seeded random number generator to ensure repeatability.
        for (int i = 0; i < tableSize; i++)
        {
            int j = seededRandom.Next() % tableSize;
            // Swap elements at indices i and j.
            int temp = tempTable[i];
            tempTable[i] = tempTable[j];
            tempTable[j] = temp;
        }

        // Duplicate the permutation table to avoid overflow when accessing indices.
        for (int i = 0; i < tableSize; i++)
        {
            permutationTable[i] = tempTable[i];
            permutationTable[tableSize + i] = tempTable[i];
        }
    }

    // The Fade function smooths coordinate inputs to ease the transition between values.
    private float Fade(float t)
    {
        // Uses Ken Perlin's fade function (6t^5 - 15t^4 + 10t^3) for smoothing.
        return t * t * t * (t * (t * 6 - 15) + 10);
    }

    // Linearly interpolates between two values a and b using a blend factor t.
    private float Lerp(float a, float b, float t)
    {
        // Simple linear interpolation.
        return a + t * (b - a);
    }

    // Calculates a gradient vector from a hash value and adds contributions from x, y, and z.
    private float Grad(int hash, float x, float y, float z)
    {
        // Calculate gradient vector based on hashed value.
        int h = hash & 15;
        float u = h < 8 ? x : y;
        float v = h < 4 ? y : h == 12 || h == 14 ? x : z;
        return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
    }

    // Generates Perlin noise for a given point (x, y, z) in 3D space.
    public float Noise(float x, float y, float z)
    {
        // Wrap the integer parts of x, y, and z to [0, 255] range.
        int X = (int)Math.Floor(x) & tableSizeMask;
        int Y = (int)Math.Floor(y) & tableSizeMask;
        int Z = (int)Math.Floor(z) & tableSizeMask;

        // Subtract the integer part to get fractional coordinates, which are used for interpolation.
        x -= (int)Math.Floor(x);
        y -= (int)Math.Floor(y);
        z -= (int)Math.Floor(z);

        // Calculate smooth fade curves for each of x, y, z.
        float u = Fade(x);
        float v = Fade(y);
        float w = Fade(z);

        // Hash coordinates of the cube corners and add blended results from the corners.
        int A = permutationTable[X] + Y;
        int AA = permutationTable[A] + Z;
        int AB = permutationTable[A + 1] + Z;
        int B = permutationTable[X + 1] + Y;
        int BA = permutationTable[B] + Z;
        int BB = permutationTable[B + 1] + Z;

        // Blend the noise contributions from each of the eight cube corners.
        float res = Lerp(w, Lerp(v, Lerp(u, Grad(permutationTable[AA], x, y, z),  // The lower bound
                                         Grad(permutationTable[BA], x - 1, y, z)), // The upper bound
                                   Lerp(u, Grad(permutationTable[AB], x, y - 1, z),  // The lower bound
                                         Grad(permutationTable[BB], x - 1, y - 1, z))), // The upper bound
                             Lerp(v, Lerp(u, Grad(permutationTable[AA + 1], x, y, z - 1),  // The lower bound
                                         Grad(permutationTable[BA + 1], x - 1, y, z - 1)), // The upper bound
                                   Lerp(u, Grad(permutationTable[AB + 1], x, y - 1, z - 1),  // The lower bound
                                         Grad(permutationTable[BB + 1], x - 1, y - 1, z - 1)))); // The upper bound

        // Normalize to [0, 1]
        return (res + 1.0f) / 2.0f;
    }
}
// Existing implementation

public class PerlinNoise
{
    private int[] permutationTable;
    private const int tableSize = 256;
    private const int tableSizeMask = tableSize - 1;
    private SeededRandom seededRandom;

    public PerlinNoise(int seed)
    {
        seededRandom = new SeededRandom(seed);
        permutationTable = new int[tableSize * 2];

        int[] tempTable = new int[tableSize];
        for (int i = 0; i < tableSize; i++)
            tempTable[i] = i;

        for (int i = 0; i < tableSize; i++)
        {
            int j = seededRandom.Next() % tableSize;

            int temp = tempTable[i];
            tempTable[i] = tempTable[j];
            tempTable[j] = temp;
        }

        for (int i = 0; i < tableSize; i++)
        {
            permutationTable[i] = tempTable[i];
            permutationTable[tableSize + i] = tempTable[i];
        }
    }

    private float Fade(float t)
    {
        return t * t * t * (t * (t * 6 - 15) + 10);
    }

    private float Lerp(float a, float b, float t)
    {
        return a + t * (b - a);
    }

    private float Grad(int hash, float x, float y, float z)
    {
        int h = hash & 15;
        float u = h < 8 ? x : y;
        float v = h < 4 ? y : h == 12 || h == 14 ? x : z;
        return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
    }

    public float Noise(float x, float y, float z)
    {
        int X = (int)Math.Floor(x) & tableSizeMask;
        int Y = (int)Math.Floor(y) & tableSizeMask;
        int Z = (int)Math.Floor(z) & tableSizeMask;

        x -= (int)Math.Floor(x);
        y -= (int)Math.Floor(y);
        z -= (int)Math.Floor(z);

        float u = Fade(x);
        float v = Fade(y);
        float w = Fade(z);

        int A = permutationTable[X] + Y;
        int AA = permutationTable[A] + Z;
        int AB = permutationTable[A + 1] + Z;
        int B = permutationTable[X + 1] + Y;
        int BA = permutationTable[B] + Z;
        int BB = permutationTable[B + 1] + Z;

        float res = Lerp(w, Lerp(v, Lerp(u, Grad(permutationTable[AA], x, y, z), 
                                         Grad(permutationTable[BA], x - 1, y, z)),
                                   Lerp(u, Grad(permutationTable[AB], x, y - 1, z),
                                         Grad(permutationTable[BB], x - 1, y - 1, z))),
                             Lerp(v, Lerp(u, Grad(permutationTable[AA + 1], x, y, z - 1),
                                         Grad(permutationTable[BA + 1], x - 1, y, z - 1)),
                                   Lerp(u, Grad(permutationTable[AB + 1], x, y - 1, z - 1),
                                         Grad(permutationTable[BB + 1], x - 1, y - 1, z - 1))));

        return (res + 1.0f) / 2.0f;
    }
}
Commented Code
// Existing implementation

// A class for generating Perlin noise, a procedural technique used in computer graphics to produce natural-looking textures.
class PerlinNoise {
private:
  // A table used for permutation in the Perlin noise algorithm. It's doubled to avoid overflow in indexing.
  std::vector<int> permutationTable;
  // The size of the permutation table.
  static const int tableSize = 256;
  // A bitmask used for wrapping indices to remain within the permutation table's bounds.
  static const int tableSizeMask = tableSize - 1;
  // Seeded random number generator to shuffle the permutation table in a reproducible way.
  SeededRandom seededRandom;

  // The Fade function smooths the input values to ease transitions, as described by Ken Perlin.
  float Fade(float t) { return t * t * t * (t * (t * 6 - 15) + 10); }

  // Linear interpolation between two values a and b using the blend factor t.
  float Lerp(float a, float b, float t) { return a + t * (b - a); }

  // Calculates a gradient based on a hash value and the x, y, z coordinates. 
  // This contributes to the pseudo-randomness of the noise.
  float Grad(int hash, float x, float y, float z) {
    int h = hash & 15;
    float u = h < 8 ? x : y;
    float v = h < 4 ? y : h == 12 || h == 14 ? x : z;
    return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
  }

public:
  // Constructor initializes the Perlin noise generator with a specific seed.
  PerlinNoise(int seed) : seededRandom(seed) {
    permutationTable.resize(tableSize * 2);
    std::vector<int> tempTable(tableSize);
    for (int i = 0; i < tableSize; i++)
      tempTable[i] = i;

    // Shuffle the temporary permutation table using the seeded random generator.
    for (int i = 0; i < tableSize; i++) {
      int j = seededRandom.Next() % tableSize;
      std::swap(tempTable[i], tempTable[j]);
    }

    // Duplicate the shuffled permutation table to avoid overflow.
    for (int i = 0; i < tableSize; i++) {
      permutationTable[i] = tempTable[i];
      permutationTable[tableSize + i] = tempTable[i];
    }
  }

  // Generates Perlin noise for a given point (x, y, z) in 3D space.
  float Noise(float x, float y, float z) {
    // Find the unit cube containing the point and wrap the integer parts to the table size.
    int X = static_cast<int>(std::floor(x)) & tableSizeMask;
    int Y = static_cast<int>(std::floor(y)) & tableSizeMask;
    int Z = static_cast<int>(std::floor(z)) & tableSizeMask;

    // Calculate the relative position of the point in the cube.
    x -= std::floor(x);
    y -= std::floor(y);
    z -= std::floor(z);

    // Compute fade curves for x, y, z.
    float u = Fade(x);
    float v = Fade(y);
    float w = Fade(z);

    // Hash coordinates of the cube's eight corners.
    int A = permutationTable[X] + Y;
    int AA = permutationTable[A] + Z;
    int AB = permutationTable[A + 1] + Z;
    int B = permutationTable[X + 1] + Y;
    int BA = permutationTable[B] + Z;
    int BB = permutationTable[B + 1] + Z;

    // Add blended results from the eight corners of the cube.
    float res =
        Lerp(w,
             Lerp(v,
                  Lerp(u, Grad(permutationTable[AA], x, y, z),
                       Grad(permutationTable[BA], x - 1, y, z)),
                  Lerp(u, Grad(permutationTable[AB], x, y - 1, z),
                       Grad(permutationTable[BB], x - 1, y - 1, z))),
             Lerp(v,
                  Lerp(u, Grad(permutationTable[AA + 1], x, y, z - 1),
                       Grad(permutationTable[BA + 1], x - 1, y, z - 1)),
                  Lerp(u, Grad(permutationTable[AB + 1], x, y - 1, z - 1),
                       Grad(permutationTable[BB + 1], x - 1, y - 1, z - 1))));

    // Normalize the result to be within the range [0, 1].
    return (res + 1.0f) / 2.0f;
  }
};
// Existing implementation

class PerlinNoise {
private:
  std::vector<int> permutationTable;
  static const int tableSize = 256;
  static const int tableSizeMask = tableSize - 1;
  SeededRandom seededRandom;

  float Fade(float t) { return t * t * t * (t * (t * 6 - 15) + 10); }

  float Lerp(float a, float b, float t) { return a + t * (b - a); }

  float Grad(int hash, float x, float y, float z) {
    int h = hash & 15;
    float u = h < 8 ? x : y;
    float v = h < 4 ? y : h == 12 || h == 14 ? x : z;
    return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
  }

public:
  PerlinNoise(int seed) : seededRandom(seed) {
    permutationTable.resize(tableSize * 2);
    std::vector<int> tempTable(tableSize);
    for (int i = 0; i < tableSize; i++)
      tempTable[i] = i;

    for (int i = 0; i < tableSize; i++) {
      int j = seededRandom.Next() % tableSize;
      std::swap(tempTable[i], tempTable[j]);
    }

    for (int i = 0; i < tableSize; i++) {
      permutationTable[i] = tempTable[i];
      permutationTable[tableSize + i] = tempTable[i];
    }
  }

  float Noise(float x, float y, float z) {
    int X = static_cast<int>(std::floor(x)) & tableSizeMask;
    int Y = static_cast<int>(std::floor(y)) & tableSizeMask;
    int Z = static_cast<int>(std::floor(z)) & tableSizeMask;

    x -= std::floor(x);
    y -= std::floor(y);
    z -= std::floor(z);

    float u = Fade(x);
    float v = Fade(y);
    float w = Fade(z);

    int A = permutationTable[X] + Y;
    int AA = permutationTable[A] + Z;
    int AB = permutationTable[A + 1] + Z;
    int B = permutationTable[X + 1] + Y;
    int BA = permutationTable[B] + Z;
    int BB = permutationTable[B + 1] + Z;

    float res =
        Lerp(w,
             Lerp(v,
                  Lerp(u, Grad(permutationTable[AA], x, y, z),
                       Grad(permutationTable[BA], x - 1, y, z)),
                  Lerp(u, Grad(permutationTable[AB], x, y - 1, z),
                       Grad(permutationTable[BB], x - 1, y - 1, z))),
             Lerp(v,
                  Lerp(u, Grad(permutationTable[AA + 1], x, y, z - 1),
                       Grad(permutationTable[BA + 1], x - 1, y, z - 1)),
                  Lerp(u, Grad(permutationTable[AB + 1], x, y - 1, z - 1),
                       Grad(permutationTable[BB + 1], x - 1, y - 1, z - 1))));

    return (res + 1.0f) / 2.0f;
  }
};
// Defines a class for generating Perlin noise, a procedural texture primitive, useful in computer graphics.
// Perlin noise is generated at any point in n-dimensional space, and it produces a smooth, natural-looking texture.
CLASS Perlin
  // Constructor initializes a new instance of the Perlin class with a given seed for pseudo-randomness.
  METHOD constructor(seed)
    // Initialize an empty array for permutation table.
    this.p = []
    // Create an instance of the SeededRandom class with the provided seed for deterministic randomness.
    DECLARE random = NEW SeededRandom(seed)
    // Initialize a permutation array with values from 0 to 255, in order.
    DECLARE permutation = ARRAY[0..255] INITIALLY EACH i = i

    // Shuffle the permutation array using the seeded random generator to randomize the values without repetition.
    FOR i FROM 255 DOWNTO 1
      DECLARE j = FLOOR(random.nextFloat() * (i + 1)) // Calculate a random index to swap with.
      SWAP permutation[i] WITH permutation[j] // Swap elements at indices i and j.
    END FOR

    // Duplicate the permutation array to avoid overflow when accessing it with indices beyond 255.
    this.p = permutation + permutation
  END METHOD

  // Smoothens the input value t, enhancing the interpolation quality of the noise.
  METHOD fade(t)
    // Polynomial fade function improves the transition smoothness.
    RETURN t^3 * (t * (t * 6 - 15) + 10)
  END METHOD

  // Linearly interpolates between two values, a and b, by a factor of t.
  METHOD lerp(t, a, b)
    // Computes the linear interpolation, which is essential for noise smoothness.
    RETURN a + t * (b - a)
  END METHOD

  // Calculates gradient vectors and determines their influence on the specified coordinates.
  METHOD grad(hash, x, y, z)
    // Determine the gradient direction based on the hash value.
    DECLARE h = hash AND 15
    // Select the appropriate gradients based on the lower 4 bits of the hash.
    DECLARE u = IF h < 8 THEN x ELSE y END IF
    DECLARE v = IF h < 4 THEN y ELSE IF h == 12 OR h == 14 THEN x ELSE z END IF
    // Compute the dot product of the gradient vector with the distance vector.
    RETURN (IF h AND 1 == 0 THEN u ELSE -u END IF) + (IF h AND 2 == 0 THEN v ELSE -v END IF)
  END METHOD

  // Computes the Perlin noise value at a specific point in 3D space.
  METHOD noise(x, y, z)
    // Convert the floating-point coordinates to integers and apply bitwise AND to constrain them to 0-255 range.
    DECLARE X = FLOOR(x) AND 255
    DECLARE Y = FLOOR(y) AND 255
    DECLARE Z = FLOOR(z) AND 255
    // Fractional part of the coordinates for interpolation.
    x -= FLOOR(x)
    y -= FLOOR(y)
    z -= FLOOR(z)
    // Apply fade function to each coordinate to smooth transitions.
    DECLARE u = this.fade(x)
    DECLARE v = this.fade(y)
    DECLARE w = this.fade(z)
    // Hash coordinates of the 8 cube corners and add offsets.
    DECLARE A = this.p[X] + Y
    DECLARE AA = this.p[A] + Z
    DECLARE AB = this.p[A + 1] + Z
    DECLARE B = this.p[X + 1] + Y
    DECLARE BA = this.p[B] + Z
    DECLARE BB = this.p[B + 1] + Z

    // Interpolate between the gradients at the corners of the cube that contains the input point.
    // This interpolation gives the final noise value, which is smooth and appears more natural.
    RETURN this.lerp(w, this.lerp(v, this.lerp(u, this.grad(this.p[AA], x, y, z),
                   this.grad(this.p[BA], x - 1, y, z)),
               this.lerp(u, this.grad(this.p[AB], x, y - 1, z),
                   this.grad(this.p[BB], x - 1, y - 1, z))),
           this.lerp(v, this.lerp(u, this.grad(this.p[AA + 1], x, y, z - 1),
                   this.grad(this.p[BA + 1], x - 1, y, z - 1)),
               this.lerp(u, this.grad(this.p[AB + 1], x, y - 1, z - 1),
                   this.grad(this.p[BB + 1], x - 1, y - 1, z - 1))))
  END METHOD
END CLASS

To use this class, you would create an instance of Perlin and call the noise method with the coordinates for which you want to generate noise:

const seed = 123456; // Example seed
const perlin = new Perlin(seed);
const noiseValue = perlin.noise(0.5, 0.5, 0);
console.log("Noise Value: "+noiseValue);
class Program
{
    public static void Main(string[] args)
    {
        // Initialize the PerlinNoise instance with a seed
        int seed = 12345; // Example seed
        PerlinNoise perlinNoise = new PerlinNoise(seed);
        
        // Define parameters for the FractalNoise call
        float x = 1.5f; // Example X coordinate
        float y = 1.5f; // Example Y coordinate
        float z = 0.0f; // Example Z coordinate (can be 0 for 2D noise)

        // Call the Noise method and print the result
        float noiseValue = perlinNoise.Noise(x, y, z);
        Console.WriteLine($"Noise Value: {noiseValue}");
    }
}
int main() {
  int seed = 12345;
  PerlinNoise perlinNoise(seed);

  float x = 1.5f;
  float y = 1.5f;
  float z = 0.0f;

  float noiseValue =
      perlinNoise.Noise(x, y, z);
  std::cout << "Noise Value: " << noiseValue << std::endl;

  return 0;
}
DECLARE seed = 123456
// Create a new Perlin noise object
CREATE perlin AS NEW Perlin(seed)

// Generate a noise value using the Perlin noise object with coordinates (0.5, 0.5, 0.5)
SET noiseValue TO perlin.noise(0.5, 0.5, 0)

// Print the generated noise value
PRINT noiseValue

Implement the Fractal Noise Method

Adding more layers, or “octaves,” to a noise map enhances its complexity and visual appeal. This approach, known as “fractal noise” or “octave noise,” combines multiple layers of noise functions at varying frequencies and amplitudes to create more natural and detailed patterns. This method is widely used in procedural terrain generation, cloud simulation, and other graphical effects in video games and simulations.

Key Parameters

  • X, Y, Z: The location on the noise map. X is horizontal (1D), Y vertical (2D) and Z is depth (3D).
  • Octaves: The number of noise layers combined to create the final pattern. Increasing the number of octaves adds detail but can also increase computation time.
  • Persistence: Controls the decrease in amplitude of each successive octave, affecting the “roughness” of the final output. A lower persistence value makes the noise smoother. It’s typically a value between 0 and 1.
  • Lacunarity: Determines how the frequency of each octave changes relative to the previous one. Higher lacunarity increases the “gap” between octaves, introducing more variation. A common choice is 2, doubling the frequency at each step.
  • Frequency: The initial frequency of the first octave. Higher frequencies produce finer details in the noise pattern.
  • Amplitude: The initial amplitude of the first octave. This controls the initial impact of the noise, with higher amplitudes producing more pronounced features.

Practical Application

In procedural terrain generation, for example, these parameters can be adjusted to simulate various natural features. A lower number of octaves with high persistence may create gently rolling hills, while a higher number of octaves with low persistence and high lacunarity could simulate rugged mountains.

Now let’s add the fractalNoise method to our Perlin class.

Commented Code
class Perlin {
  // Existing implementation of the Perlin noise class.
  
  /**
   * Generates fractal noise by layering multiple octaves of Perlin noise.
   * Each octave contributes to the final noise with its own frequency and amplitude,
   * creating a more complex and natural-looking pattern.
   * 
   * @param {number} x - The x coordinate in 3D space.
   * @param {number} y - The y coordinate in 3D space.
   * @param {number} z - The z coordinate in 3D space, kept constant for 2D noise.
   * @param {number} octaves - The number of layers of noise to generate.
   * @param {number} persistence - Determines how the amplitude of octaves decreases.
   * @param {number} lacunarity - Determines how the frequency of octaves increases.
   * @returns {number} A single floating-point value representing the generated noise at the given coordinates.
   */
  fractalNoise(x, y, z, octaves = 4, persistence = 0.5, lacunarity = 2.0) {
    let total = 0; // Accumulates the total noise value.
    let frequency = 1; // The starting frequency of the first octave.
    let amplitude = 1; // The starting amplitude of the first octave.
    let maxValue = 0; // Used for normalizing the result to a range of [0.0, 1.0].

    for (let i = 0; i < octaves; i++) {
      // Offset the coordinates for each octave to introduce variation.
      let xOffset = i * 2;
      let yOffset = i * 2;
      // Calculate the noise value for this octave and add it to the total, adjusted by its frequency and amplitude.
      total += this.noise((x + xOffset) * frequency, (y + yOffset) * frequency, z) * amplitude;

      // Accumulate the maximum possible value for normalization purposes.
      maxValue += amplitude;

      // Adjust the amplitude and frequency for the next octave according to the persistence and lacunarity.
      amplitude *= persistence; // Persistence decreases amplitude with each octave, making higher octaves contribute less.
      frequency *= lacunarity; // Lacunarity increases frequency with each octave, adding more detail with each layer.
    }

    // Normalize the total noise value to be within [0.0, 1.0] and return it.
    return total / maxValue;
  }
}
class Perlin {
  // Existing Perlin noise class implementation
  
  fractalNoise(x, y, z, octaves = 4, persistence = 0.5, lacunarity = 2.0) {
    let total = 0;
    let frequency = 1;
    let amplitude = 1;
    let maxValue = 0;

    for (let i = 0; i < octaves; i++) {
      let xOffset = i * 2;
      let yOffset = i * 2;
      total += this.noise((x + xOffset) * frequency, (y + yOffset)  * frequency, z) * amplitude;
      
      maxValue += amplitude;
      
      amplitude *= persistence;
      frequency *= lacunarity;
    }

    return total / maxValue;
  }
}
Commented Code
public class PerlinNoise
{
  // Existing Perlin noise class implementation
  
  /// <summary>
  /// Generates fractal noise by layering multiple octaves of Perlin noise. This method enhances the visual complexity
  /// and natural appearance of the noise, making it suitable for generating more detailed and varied textures or terrains.
  /// </summary>
  /// <param name="x">The x coordinate in the noise space.</param>
  /// <param name="y">The y coordinate in the noise space.</param>
  /// <param name="z">The z coordinate in the noise space. For 2D noise, this can be kept constant.</param>
  /// <param name="octaves">The number of iterations of noise to layer. More octaves create more detail, at the cost of performance.</param>
  /// <param name="persistence">Controls the decrease in amplitude of each octave. Lower values make the noise smoother.</param>
  /// <param name="lacunarity">Controls the increase in frequency of each octave. Higher values create more detailed noise patterns.</param>
  /// <returns>A single float value representing the generated fractal noise, normalized between 0.0 and 1.0.</returns>
  public float FractalNoise(float x, float y, float z, int octaves, float persistence, float lacunarity)
  {
      float total = 0; // Total noise accumulated across all octaves.
      float frequency = 1; // Initial frequency of the noise.
      float amplitude = 1; // Initial amplitude of the noise.
      float maxValue = 0;  // Accumulator to track the maximum possible value for normalization.

      // Loop through each octave, applying and accumulating Perlin noise based on current amplitude and frequency.
      for (int i = 0; i < octaves; i++)
      {
          // Calculate an offset to ensure each octave samples a different part of the noise space.
          int offset = i * 2;
          // Accumulate noise value, scaled by amplitude, and adjust frequency and amplitude for the next octave.
          total += Noise((x + offset) * frequency, (y + offset) * frequency, z * frequency) * amplitude;

          // Accumulate the max possible value to use for normalization later.
          maxValue += amplitude;

          // Adjust amplitude by persistence (reduces each octave's influence) and frequency by lacunarity (increases detail).
          amplitude *= persistence;
          frequency *= lacunarity;
      }

      // Normalize the total noise value to a range of 0.0 to 1.0 before returning.
      return total / maxValue;
  }
}
public class PerlinNoise
{
  // Existing Perlin noise class implementation
  
  public float FractalNoise(float x, float y, float z, int octaves, float persistence, float lacunarity)
    {
        float total = 0;
        float frequency = 1;
        float amplitude = 1;
        float maxValue = 0;

        for (int i = 0; i < octaves; i++)
        {
            int offset = i * 2;
            total += Noise((x + offset) * frequency, (y  + offset) * frequency, z * frequency) * amplitude;

            maxValue += amplitude;

            amplitude *= persistence;
            frequency *= lacunarity;
        }

        return total / maxValue;
    }
}
Commented Code
class PerlinNoise
{
  // Existing Perlin noise class implementation

public:
  
  /// Generates fractal noise by combining multiple octaves of Perlin noise.
  /// Each octave contributes to the final noise output with its own frequency and amplitude,
  /// creating a more complex and visually interesting result. This method is especially useful
  /// for generating realistic textures or terrain in game development.
  ///
  /// @param x The x coordinate in the noise space.
  /// @param y The y coordinate in the noise space.
  /// @param z The z coordinate in the noise space, useful for 3D noise but can be kept constant for 2D.
  /// @param octaves The number of iterations or layers of noise to combine for the final output.
  ///                More octaves lead to more detail at the cost of computation time.
  /// @param persistence Determines how much each subsequent octave contributes to the total noise.
  ///                    A lower persistence value makes the noise smoother.
  /// @param lacunarity Controls the frequency growth for each octave. Higher lacunarity values
  ///                   increase the complexity of the noise pattern.
  /// @return A single float value representing the fractal noise at the given coordinates,
  ///         normalized to the range [0, 1].
  float FractalNoise(float x, float y, float z, int octaves, float persistence,
                     float lacunarity) {
    float total = 0; // Accumulates the total noise value from all octaves.
    float frequency = 1; // The starting frequency of the noise.
    float amplitude = 1; // The starting amplitude of the noise.
    float maxValue = 0; // Used for normalizing the result to the range [0, 1].

    // Iterate through each octave to layer the noise.
    for (int i = 0; i < octaves; i++) {
      int offset = i * 2; // Apply an offset to each octave to vary the noise pattern.
      // Add the noise value, scaled by the current amplitude and frequency, to the total.
      total += Noise((x + offset) * frequency, (y + offset) * frequency, z * frequency) * amplitude;
      // Accumulate the maximum possible value to normalize the result later.
      maxValue += amplitude;
      // Decrease the amplitude by the persistence factor for each subsequent octave.
      amplitude *= persistence;
      // Increase the frequency by the lacunarity factor for each subsequent octave.
      frequency *= lacunarity;
    }

    // Normalize the total noise value to fall within the range [0, 1] before returning it.
    return total / maxValue;
  }
};
class PerlinNoise
{
  // Existing Perlin noise class implementation
  public:
  
  float FractalNoise(float x, float y, float z, int octaves, float persistence,
                     float lacunarity) {
    float total = 0;
    float frequency = 1;
    float amplitude = 1;
    float maxValue = 0;

    for (int i = 0; i < octaves; i++) {
      int offset = i * 2;
      total += Noise((x+offset) * frequency, (y+offset) * frequency, z * frequency) * amplitude;
      maxValue += amplitude;
      amplitude *= persistence;
      frequency *= lacunarity;
    }

    return total / maxValue;
  }
}
CLASS Perlin
  // Existing implementation of the Perlin noise class.
  
  // Method to generate fractal noise, which enhances basic Perlin noise by layering multiple octaves of noise.
  // This method results in noise that appears more complex and natural, suitable for textures, terrain, and other procedural content.
  METHOD fractalNoise(x, y, z, octaves = 4, persistence = 0.5, lacunarity = 2.0)
    // Initialize total to accumulate noise values across all octaves.
    DECLARE total = 0
    // Starting frequency for the first octave.
    DECLARE frequency = 1
    // Starting amplitude for the first octave.
    DECLARE amplitude = 1
    // Used to normalize the result to a range of [0.0, 1.0]. This is important to ensure the final noise value
    // is within a predictable range, making it easier to use in applications.
    DECLARE maxValue = 0

    // Iterate over each octave, adding layered noise values.
    FOR i FROM 0 TO octaves - 1
      // Calculate an offset for each octave to ensure varied and interesting results.
      // This offset helps in avoiding repetitive patterns in the generated noise.
      DECLARE offset = i * 2
      // Accumulate the noise value, adjusted by the current frequency and amplitude, into the total.
      // The use of an offset here helps in ensuring that each octave contributes unique characteristics to the overall noise.
      total += this.noise((x + offset) * frequency, (y + offset) * frequency, z) * amplitude
      // Accumulate the maximum possible value to use for normalization later.
      maxValue += amplitude
      // Adjust amplitude by the persistence for the next octave. Persistence controls the decrease in amplitude
      // with each octave, affecting the "roughness" of the noise. Lower persistence values produce smoother noise.
      amplitude *= persistence
      // Adjust frequency by the lacunarity for the next octave. Lacunarity controls the frequency increase
      // with each octave, affecting the detail of the noise. Higher lacunarity values produce more detailed noise.
      frequency *= lacunarity
    END FOR

    // Normalize the total noise value to be within the range [0.0, 1.0] by dividing by maxValue.
    // This normalization step is crucial for ensuring the output is consistently scaled across different inputs and noise configurations.
    RETURN total / maxValue
  END METHOD
END CLASS

To use this method, you would create an instance of Perlin and call the fractalNoise method with the coordinates for which you want to generate noise:

const seed = 123456; // Example seed
const octaves = 4;
const persistence = 0.5;
const lacunarity = 2.0;
const x = 1.5;
const y = 1.5;
const z = 0.0;
const perlin = new Perlin(seed);
const noiseValue = perlin.fractalNoise(x, y, z, octaves, persistence, lacunarity);
console.log("Noise Value: "+noiseValue);
class Program
{
    public static void Main(string[] args)
    {
        // Initialize the PerlinNoise instance with a seed
        int seed = 12345; // Example seed
        PerlinNoise perlinNoise = new PerlinNoise(seed);

        // Define parameters for the FractalNoise call
        float x = 1.5f; // Example X coordinate
        float y = 1.5f; // Example Y coordinate
        float z = 0.0f; // Example Z coordinate (can be 0 for 2D noise)
        int octaves = 4;
        float persistence = 0.5f;
        float lacunarity = 2.0f; // Example lacunarity value

        // Call the FractalNoise method and print the result
        float noiseValue = perlinNoise.FractalNoise(x, y, z, octaves, persistence, lacunarity);
        Console.WriteLine($"Fractal Noise Value: {noiseValue}");
    }
}
int main() {
  int seed = 12345;
  PerlinNoise perlinNoise(seed);

  float x = 1.5f;
  float y = 1.5f;
  float z = 0.0f;
  int octaves = 4;
  float persistence = 0.5f;
  float lacunarity = 2.0f;

  float noiseValue =
      perlinNoise.FractalNoise(x, y, z, octaves, persistence, lacunarity);
  std::cout << "Fractal Noise Value: " << noiseValue << std::endl;

  return 0;
}
DECLARE seed = 123456
DECLARE octaves = 4
DECLARE persistence = 0.5
DECLARE lacunarity = 2.0
DECLARE x = 1.5
DECLARE y = 1.5
DECLARE z = 0.0

// Create a new Perlin noise object
CREATE perlin AS NEW Perlin(seed)

DECLARE noiseValue = perlin.fractalNoise(x, y, z, octaves, persistence, lacunarity);

// Print the generated noise value
PRINT noiseValue

By understanding and manipulating these fundamental blocks, you’re equipped to generate infinite, diverse landscapes that can serve as the backbone of your game’s world.

Visualization

Now we focus on visualizing Perlin noise. Visualization is essential for testing and demonstrating how the noise function works. This section covers how to create one-dimensional (1D) and two-dimensional (2D) visual representations using JavaScript, crucial for game development applications like textures and terrains.

1D Visualization

For 1D visualization, we’ll use JavaScript to draw on a canvas with Perlin noise values along the X axis. This demonstrates the smooth transitions characteristic of Perlin noise. We start by creating the HTML canvas elements.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Perlin Noise</title>
</head>
<body>
    <canvas id="canvas1D" width="800" height="150"></canvas>
    <canvas id="canvas2D" width="400" height="400"></canvas>
</body>
</html>

Next we program the 1D visualization function using the fractalNoise method of the Perlin class.

function visualize1D(perlin, octaves, persistence, lacunarity, xIncrement) {
  const canvas = document.getElementById('canvas1D');
  const ctx = canvas.getContext('2d');
  const width = canvas.width;
  const height = canvas.height;

  ctx.clearRect(0, 0, width, height);
  ctx.beginPath();
  ctx.strokeStyle = '#FF6F61'; // Set the line color
  for (let i = 0; i < width; i++) {
    const noiseVal = perlin.fractalNoise(i * xIncrement, 0, 0, octaves, persistence, lacunarity);
    const y = height - (noiseVal + 1) * height / 2; // Scale noise to canvas height
    if (i === 0) ctx.moveTo(i, y);
    else ctx.lineTo(i, y);
  }
  ctx.stroke();
}

//Call function on load
window.onload = function() {
  const seed = 777;
  const octaves = 4; //Between 1-8
  const persistence = .5; //Between 0-1
  const lacunarity = 2.0; //Between 1.0-3.0
  const xIncrement = .03; //Increment scales the noise - Between .01-.2

  // Initialize Perlin noise with the new seed
  const perlin = new Perlin(seed);
  
  visualize1D(perlin, octaves, persistence, lacunarity, xIncrement);
}

2D Visualization

Now we extend the concept to render Perlin noise across a grid on a canvas, simulating real-world applications like terrain generation. This involves mapping noise values to pixel colors to create a visual output.

function visualize2D(perlin, octaves, persistence, lacunarity, xIncrement, yIncrement) {
  const canvas = document.getElementById('canvas2D');
  const ctx = canvas.getContext('2d');
  const width = canvas.width;
  const height = canvas.height;
  const imageData = ctx.createImageData(width, height);

  for (let x = 0; x < width; x++) {
    for (let y = 0; y < height; y++) {
      const noiseVal = perlin.fractalNoise(x * xIncrement, y * yIncrement, 0, octaves, persistence, lacunarity);
      const color = Math.floor((noiseVal + 1) * 128);
      const index = (x + y * width) * 4;
      imageData.data[index] = imageData.data[index + 1] = imageData.data[index + 2] = color;
      imageData.data[index + 3] = 255; // Alpha channel
    }
  }
  ctx.putImageData(imageData, 0, 0);
}

//Call function on load
window.onload = function() {
  const seed = 777;
  const octaves = 4; //Between 1-8
  const persistence = .5; //Between 0-1
  const lacunarity = 2.0; //Between 1.0-3.0
  const xIncrement = .03; //Increment scales the noise - Between .01-.2
  const yIncrement = .03; //Increment scales the noise - Between .01-.2

  // Initialize Perlin noise with the new seed
  const perlin = new Perlin(seed);
  
  visualize2D(perlin, octaves, persistence, lacunarity, xIncrement, yIncrement);
}

These examples are intended to help you understand and apply Perlin noise in your game development projects, enabling the creation of natural-looking patterns and textures.

Here is an interactive demo that I put together for you to experiment with.

Generating and Visualizing Terrain with A Height Map

To generate and visualize a height map using the fractalNoise method from the provided Perlin class, we can follow a two-step approach:

  • Generate the Height Map: We will create a function to generate a 2D array representing the height map. Each cell in the array will contain a value between -1 and 1, produced by the fractalNoise method, representing different terrains based on the height.
  • Visualize the Height Map: For visualization, we will map the height values to different colors representing various terrains (e.g., water, beaches, forests, mountains, snow). Although this example will be conceptual (since we can’t directly render images in code here), I’ll describe how you can use HTML canvas or a similar graphics library to visualize the map.

First, let’s write the function to generate the height map:

Commented Code
// Generates a height map using fractal noise based on Perlin noise.
// This method is particularly useful for simulating realistic terrain in computer graphics.
function generateHeightMap(width, height, perlinSeed, octaves = 4, persistence = 0.5, lacunarity = 2.0, xIncrement = .03, yIncrement = .03) {
  // Instantiate the Perlin class with the given seed to generate consistent noise patterns.
  const perlin = new Perlin(perlinSeed);
  // Initialize the height map array with the specified height.
  let map = new Array(height);

  // Iterate over each row of the map.
  for (let y = 0; y < height; y++) {
    // Initialize each row with an array of the specified width.
    map[y] = new Array(width);
    for (let x = 0; x < width; x++) {
      // Normalize x and y coordinates to a range of [0, 1] to allow for seamless tiling of the noise pattern.
      const nx = x / width, ny = y / height;

      // Generate fractal noise for each point on the map using the normalized coordinates,
      // adjusted by the increment values to control the scale of the noise features.
      // Increment is * 100 to use the increment values from earlier
      // The fractalNoise method is expected to take 3D coordinates, but here z is omitted for 2D noise generation.
      map[y][x] = perlin.fractalNoise(nx * (xIncrement * 100), ny * (yIncrement * 100), 0, octaves, persistence, lacunarity);
    }
  }

  // Return the populated height map array, where each value represents the elevation at that point.
  return map;
}
function generateHeightMap(width, height, seed, octaves = 4, persistence = 0.5, lacunarity = 2.0, xIncrement = .03, yIncrement = .03) {
  const perlin = new Perlin(seed);
  let map = new Array(height);

  for (let y = 0; y < height; y++) {
    map[y] = new Array(width);
    for (let x = 0; x < width; x++) {
      const nx = x / width, ny = y / height;
      map[y][x] = perlin.fractalNoise(nx * (xIncrement * 100), ny * (yIncrement * 100), 0, octaves, persistence, lacunarity);
    }
  }

  return map;
}
Commented Code
// Existing implementation

// Defines a class responsible for generating height maps using Perlin noise.
// Height maps are two-dimensional arrays where each value represents the height at that point, 
// which is useful for terrain generation in game development.
public class HeightMapGenerator
{
    // Holds an instance of the PerlinNoise class, which is used to generate the noise values.
    private PerlinNoise perlinNoise;

    // Constructor that initializes the HeightMapGenerator with a specific seed for the Perlin noise.
    // This seed ensures that the generated noise (and thus the terrain) is reproducible.
    public HeightMapGenerator(int seed)
    {
        this.perlinNoise = new PerlinNoise(seed);
    }

    // Generates a height map based on Perlin noise with specified parameters.
    // Parameters allow customization of the noise to achieve different visual effects.
    public float[,] GenerateHeightMap(int width, int height, int octaves = 4, float persistence = 0.5f, float lacunarity = 2.0f, float xIncrement = 0.05f, float yIncrement = 0.05f)
    {
        // Initialize the height map array with dimensions specified by width and height.
        float[,] map = new float[height, width];

        // Iterate over each cell in the map array to calculate its height.
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                // Normalize x and y coordinates to a range of [0, 1] to ensure that
                // the noise function works uniformly across the height map.
                float nx = x / (float)width, ny = y / (float)height;

                // Calculate the height value using fractal noise, which combines multiple
                // octaves of Perlin noise for more complex patterns. The xIncrement and yIncrement
                // values scale the coordinates to adjust the frequency of the noise, affecting
                // the "zoom level" of the terrain features.
                map[y, x] = perlinNoise.FractalNoise(nx * (xIncrement * 100), ny * (yIncrement * 100), 0, octaves, persistence, lacunarity);
            }
        }

        // Return the populated height map, with each value representing the elevation at that point.
        return map;
    }
}
// Existing implementation

public class HeightMapGenerator
{
    private PerlinNoise perlinNoise;

    public HeightMapGenerator(int seed)
    {
        this.perlinNoise = new PerlinNoise(seed);
    }

    public float[,] GenerateHeightMap(int width, int height, int octaves = 4, float persistence = 0.5f, float lacunarity = 2.0f, float xIncrement = 0.05f, float yIncrement = 0.05f)
    {
        float[,] map = new float[height, width];

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                float nx = x / (float)width, ny = y / (float)height;

                map[y, x] = perlinNoise.FractalNoise(nx * (xIncrement * 100), ny * (yIncrement * 100), 0, octaves, persistence, lacunarity);
            }
        }

        return map;
    }
}
Commented Code
// Existing implementation

// A function that generates a 2D height map using Perlin noise.
// The height map is represented as a 2D vector of floats, where each float represents the height at that coordinate.
std::vector<std::vector<float>> generateHeightMap(
    int width, // The width of the height map in units.
    int height, // The height of the height map in units.
    float xOffset, // Horizontal offset to apply to the noise function, allows for shifting the noise horizontally.
    float yOffset, // Vertical offset to apply to the noise function, allows for shifting the noise vertically.
    float zOffset, // Offset in the z-axis for the noise function, useful for generating different slices of 3D noise.
    int perlinSeed, // Seed value for the Perlin noise generator, ensures reproducible results.
    int octaves = 4, // Number of noise layers to combine, affects the complexity of the noise.
    float persistence = 0.5f, // Controls the amplitude decrease for each octave, affecting the "roughness" of the terrain.
    float lacunarity = 2.0f, // Controls the frequency increase for each octave, affecting the detail of the terrain.
    float xIncrement = 0.05f, // Scale factor for the x-coordinate, controls the horizontal scale of the noise patterns.
    float yIncrement = 0.05f // Scale factor for the y-coordinate, controls the vertical scale of the noise patterns.
) {
    // Initialize the PerlinNoise object with the given seed.
    PerlinNoise perlin(perlinSeed);
    // Create a 2D vector to hold the height values of the map.
    std::vector<std::vector<float>> map(height, std::vector<float>(width));

    // Iterate over every cell in the map to calculate its height.
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            // Normalize the current coordinates to a range of [0, 1] to ensure consistent noise scaling.
            float nx = static_cast<float>(x) / static_cast<float>(width);
            float ny = static_cast<float>(y) / static_cast<float>(height);

            // Generate fractal noise for the current point using the normalized coordinates,
            // adjusted by the provided offsets and scale factors. This noise value represents
            // the height at this point on the map.
            map[y][x] = perlin.FractalNoise(
                (nx + xOffset) * (xIncrement * 100),
                (ny + yOffset) * (yIncrement * 100),
                zOffset, octaves, persistence, lacunarity
            );
        }
    }

    // Return the populated height map.
    return map;
}
// Existing implementation

std::vector<std::vector<float>> generateHeightMap(int width, int height, float xOffset, float yOffset, float zOffset, int perlinSeed, int octaves = 4, float persistence = 0.5f, float lacunarity = 2.0f, float xIncrement = 0.05f, float yIncrement = 0.05f) {
    PerlinNoise perlin(perlinSeed);
    std::vector<std::vector<float>> map(height, std::vector<float>(width));

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            float nx = static_cast<float>(x) / static_cast<float>(width);
            float ny = static_cast<float>(y) / static_cast<float>(height);

            map[y][x] = perlin.FractalNoise((nx + xOffset) * (xIncrement * 100), (ny + yOffset) * (yIncrement * 100), zOffset, octaves, persistence, lacunarity);
        }
    }

    return map;
}
// Initializes variables for the dimensions of the height map, offsets in each dimension, and parameters for noise generation.
DECLARE width, height, xOffset, yOffset, zOffset, perlinSeed, octaves = 4, persistence = 0.5, lacunarity = 2.0, xIncrement = 0.05, yIncrement = 0.05

// Create an instance of the Perlin class with the provided seed to generate consistent, pseudo-random noise.
CREATE perlin AS NEW Perlin(perlinSeed)

// Initialize an empty array to store the height map data.
DECLARE map AS NEW Array(height)

// Loop through each cell in the height map array to calculate its height based on Perlin noise.
FOR y FROM 0 TO height - 1 DO
    // Initialize the current row in the map.
    map[y] = NEW Array(width)
    FOR x FROM 0 TO width - 1 DO
        // Normalize the x and y coordinates to a range of [0, 1] to allow for seamless scaling and tiling of the noise.
        DECLARE nx = x / width
        DECLARE ny = y / height

        // Apply offsets and increments to the normalized coordinates to control the "look" and "scale" of the terrain.
        // The offsets allow for scrolling through the noise space, while the increments adjust the frequency of the noise.
        // Generating fractal noise using the adjusted coordinates and the specified noise parameters.
        // This combines multiple layers of Perlin noise to create a more complex and natural-looking height map.
        map[y][x] = perlin.fractalNoise((nx + xOffset) * (xIncrement * 100), (ny + yOffset) * (yIncrement * 100), zOffset, octaves, persistence, lacunarity)
    END FOR
END FOR

// Return the fully populated height map, now containing the generated terrain heights.
RETURN map

Next, let’s add a new HTML canvas element:

<canvas id="terrainCanvas" width="600" height="400"></canvas>

Next, to visualize this height map, the following code outlines a strategy for mapping height values to colors and rendering them in Javascript. 

function visualizeHeightMap(map, canvasId) {
  const canvas = document.getElementById(canvasId);
  if (!canvas) return;

  const ctx = canvas.getContext('2d');
  const width = map[0].length;
  const height = map.length;

  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const value = map[y][x];
      
      ctx.fillStyle = getColorForHeight(value);
      ctx.fillRect(x, y, 1, 1); // Drawing a 1x1 pixel rectangle for each point
    }
  }
}

function getColorForHeight(value) {
  if (value < -0.45) return '#204D75'; // Deep Ocean
  else if (value < -0.25) return '#367ABD'; // Shallow Water
  else if (value < -0.1) return '#76A5AF'; // Beach
  else if (value < 0) return '#C2B280'; // Sand
  else if (value < 0.2) return '#8DB255'; // Grassland
  else if (value < 0.3) return '#228B22'; // Forest Green
  else if (value < 0.45) return '#7E5E60'; // Mountain Range
  else return '#FFF8F0'; // Snow Peaks
}

//Call function on load
window.onload = function() {
  const seed = 777;
  const octaves = 4; //Between 1-8
  const persistence = .5; //Between 0-1
  const lacunarity = 2.0; //Between 1.0-3.0
  const xIncrement = .03; //Increment scales the noise - Between .01-.2
  const yIncrement = .03; //Increment scales the noise - Between .01-.2
  
  const map = generateHeightMap(600, 400, seed, octaves, persistence, lacunarity, xIncrement, yIncrement);
  visualizeHeightMap(map, 'terrainCanvas');
}

This process will generate a unique terrain height map based on the provided seed and visualize it with colors on the canvas, creating a visually appealing representation of different terrains.

Remember, you can adjust the octaves, persistence, and lacunarity parameters in the generateHeightMap function to experiment with different looks for your terrain. Additionally, the color thresholds in getColorForHeight can be tweaked to refine how each terrain type is represented visually.

Here is an interactive demo using the height map functions that I put together for you to experiment with.

Next Steps: From Height Maps to Living Worlds

What we’ve covered is just the beginning. With the foundation laid, we’ll explore transforming these height maps into vibrant ecosystems, setting the stage for adventures and stories to unfold. Stay tuned as we delve deeper into turning mathematical concepts into the sprawling terrains and intricate dungeons that form the heart of memorable games.

This Post Has 0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top