Domain warping: an interactive introduction

Domain warping is one of my favorite techniques to use when creating generative art. It’s an extremely versatile and flexible approach that can be applied in so many different situations, often leading to unexpectedly cool results. I discovered it by accident shortly after I started my journey into generative art, back in 2020. I had just finished my master’s thesis on spatial statistics, and was excited to find a creative outlet where I could apply methods I had been working on for a long time (leading to the R package GRFics).

At the time, I thought I had come up with something new and original. However, domain warping has been in use for decades, and this 2002 article by Inigo Quilez sums things up nicely. While its definition varies from one source to another, I like to think of it as any method that involves using noise functions (like Perlin and simplex noise) to displace positions. Mathematically, this can be expressed as

(x, y)\mapsto(x + f(x, y), y + g(x, y)),

where f and g are noise functions. (Note: Domain warping can be applied in any number of dimensions, but we’ll focus on the two-dimensional case.)

To give you an idea of what this looks like, here’s what happens when you apply domain warping to a nice, regular grid of points:

Before/after

It’s easy to see where the name of the technique comes from: it warps the underlying space/domain!

Codewise, the above setup is not very complicated. We want to warp a position (x, y) using two noise functions, one for each coordinate:

function getWarpedPosition(x, y) {
  const dx = noiseFunctionX(x, y);
  const dy = noiseFunctionY(x, y);

  x += dx;
  y += dy;

  return [x, y];
}

The noise functions can be generated in any way you desire. For the grid warping above, the package open-simplex-noise was used. Users of Processing and p5.js can access Perlin noise directly through the “noise” function. For those working in other languages, there are plenty of implementations available for both Perlin and simplex noise.

Let’s assume that our noise functions return values between -1 and 1, and introduce some parameters to our function:

let numWarps = 2;
let warpSizeX = warpSizeY = 0.1;
let falloff = 0.5; // Should be between 0 and 1

function getWarpedPosition(x, y) {
  let scale = 1;

  for (let i = 0; i < numWarps; i++) {
    // Scale from [-1, 1] to [-warpSize, warpSize]
    const dx = warpSizeX * noiseFunctionX(x, y);
    const dy = warpSizeY * noiseFunctionY(x, y);

    x += scale * dx;
    y += scale * dy;

    scale *= falloff;
  }

  return [x, y];
}

The changes are:

  • The procedure is repeated a certain number of times, specified by numWarps. Applying domain warping repeatedly, or warping positions that have already been warped, often leads to interesting results.
  • The parameters warpSizeX and warpSizeY control the amount of warping that is applied to each coordinate.
  • falloff is used to ensure that the amount of warping decreases from one iteration to the next. It should therefore have a value between 0 and 1.

Below, you can explore how different parameter values affect the regular grid. The grid covers a square with side length 2, and clicking “Generate new” generates a new set of noise functions.

Neat! Playing around with this should give some useful intuition, but it doesn’t fully demonstrate the beauty of domain warping. For our next example, we’ll need a couple more functions. The first, getColorAtPosition, maps positions to colors by feeding a noise function into a colormap:

function getColorAtPosition(x, y) {
  // (1 + value)/2 maps from [-1, 1] to [0, 1]
  const value = (1 + noiseFunctionColor(x, y)) / 2;
  const color = colormap(value);
  return color;
}

A colormap is a function that interpolates between colors, like in a gradient. In exact terms, it takes in a number between 0 and 1 and returns a color. This is useful for showing how the value of a function changes. In JS, colormaps can be created with culori and RGBetween (a compact colormap package made by yours truly).

Combining this with domain warping, we end up with the function getWarpedColor:

function getWarpedColor(x, y) {
  [x, y] = getWarpedPosition(x, y);
  const warpedColor = getColorAtPosition(x, y);
  return warpedColor;
}

Below, the output from getWarpedColor is drawn over a rectangle of width 2. As in the grid demo, the parameters can be adjusted, and the un-warped image can be shown by toggling “Disable warp”.

By increasing the number of warps and the warp sizes you’ll get complex, painting-like pieces with beautiful, marbled features. For good reason, this is probably the most popular way to use domain warping in the generative arts.

As mentioned in the first paragraph, domain warping is highly versatile, and this article barely scratches the surface of what’s possible, both in how the method can be modified and how it can be applied. This is something we’ll have to explore further in the next article. Nevertheless, I believe that you are ready to start experimenting with domain warping! Feel free to share your work with me on Twitter or Instagram, I would love to see what you come up with.

Also, requests and suggestions for topics to write about are greatly appreciated, and can be left in the comment section below. We’ll round things off with “Mulholland”, which combines Poisson disk sampling and domain warping for a very interesting effect:

4 comments

  1. Very interesting, especially the Mulholland piece. Makes me want to explore domain wrapping again. Thanks a lot for the write-up!

  2. Mathis, could you please clarify two things:
    – Whats the difference between two noise fx for each coordinate (x, y)? I guess it should have different implementation to return different values with the same input.
    – Why I assumed that noise fx returns -1 to 1 result? Im using p5.js and it says in docs it always returns 0 to 1 values.

    Any advice would be appreciated!

  3. Gm Mathias,
    Thank you for detailed explanation, you getting great results!
    I am totally new to generated art, trying to find out how this beautiful method works. In theory everything sound clear (formula and approach). But when I am trying to apply DW to regular points grid, the algorithm gives me different result. It seems like neighbour warped coordinates not related to each other, so the output looks pretty messed.
    What could be wrong?

    Would be much appreciate to any advice from you

Leave a comment

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