Procedural generation of marbled paper

Seeing beautiful marbled papers in a documentary about Venice triggered an immediate desire to try to write some code to procedurally recreate them. In this article I'll share how I did a small CLI app using LibCapy and examples of the result it produces.

First, the usual boilerplate for the app. Including LibCapy and defining the command line arguments. I've choosen to allow selection of the image dimensions (in cm with dpi selection), number of colors, number of pass (cf later), output path of course, RNG seed for reproducibility, and a pastel mode for the random colors (just cause I like it).

The app structure's properties are just what it takes to memorize the command line arguments, and are initialised as follow

A few user-defined exceptions to manage invalid arguments:

Then we're ready for the interesting stuff. I need a random number generator, a Gaussian blur kernel (to smooth the image and somehow replicate the mixing of pigments in the real process), a palette of random colors, and an image to draw on. The image size is initially a bit larger than the result one to accomodate the Gaussian blur kernel. The image background is arbitrarily assigned the first color in the palette.

As for the real marble paper, I start with some random drops on a surface. I want them to be placed randomly but nicely spread on the whole image. That's a perfect use case for Poisson disk sampling. For the sampling parameter I choose 2cm as the minimum distance between disk. It gives results I like for A4 size paper, but not so much for other sizes. So I tried to parameterized the distance with the paper size. I'm not convinced with the result but I kept it like this anyway. Getting satisfying results for A4 is sufficient for now as I'm just having fun here. The blurring strength is then set equal to the sampling distance.

For drawing, I use a circle (that doesn't matter much if the real drop are not perfect circles), and a 9B CapyPen (almost no bleeding of colors between drop but a little bit). The radius of each drop is randomly set somewhere between 0.75 and 1.0 of the sampling distance, and that range decreases proportional to the golden ratio (because why not) for each color in the palette. Reducing the range avoids the last drawn color to overwhelm the first ones. I also skip the first color in the palette, which has been used for the background.

This gives something like this:

Then I reproduce the stretching of colors with a comb like for the real paper. The number of times ('pass') the drops are stretched is set by the user, and all other parameters are randomised: the number of teeth, their distribution, the direction of stretching, and the stretching's strength and shape.

The stretching axis (x or y) is randomly choosen at each pass.

While we are at playing with Poisson disk sampling, lets use it for the teeth distribution too. The distance in this case is randomly choosen in a range proportional to the distance of the drops sampling, and increasing with each pass. I also ensure that the first and last teeth are at the borders of the paper for convenience.

The stretching itself consists of shifting pixels along the stretching axis, warping around the edges of the image if needed. The shifting amount follows a \(|sin|\) pattern controlled by the stretching direction \(d\) (randomly set to -1 or 1), strength \(s\) (starting within [2, 7] times the distance in the drop sampling and decreased accordingly to the golden ratio at each pass) and power \(p\) (randomly set within [0.25, 0.75] at each pass). That pattern applies between each teeth: I prepare an array of shifting value for each pixel, with the value equal to \(d.s.sin(t\pi)^p\) where \(t\) (in [0, 1]) is the relative position of the pixel between the two nearest teeth of the comb.

Finally I apply blurring between each pass, crop the provisional border at the end, and save the result.

The effect of 3 successive passes on the example given above are shown below:

Some more examples:

And the complete code source:

Compile with gcc -o main main.c `pkg-config --cflags gtk+-3.0` -lcapy -lm -lpng (need LibCapy version v0.9.0 or higher). Comments or questions are always welcome by email.