2

I'm trying to render the Mandelbrot set in color and make it look good. I'm want to replicate the image from the Wikipedia page. The Wikipedia image also included an Ultra Fractal 3 parameter file.

mandelZoom00MandelbrotSet {
fractal:
  title="mandel zoom 00 mandelbrot set" width=2560 height=1920 layers=1
  credits="WolfgangBeyer;8/21/2005"
layer:
  method=multipass caption="Background" opacity=100
mapping:
  center=-0.7/0 magn=1.3
formula:
  maxiter=50000 filename="Standard.ufm" entry="Mandelbrot" p_start=0/0
  p_power=2/0 p_bailout=10000
inside:
  transfer=none
outside:
  density=0.42 transfer=log filename="Standard.ucl" entry="Smooth"
  p_power=2/0 p_bailout=128.0
gradient:
  smooth=yes rotation=29 index=28 color=6555392 index=92 color=13331232
  index=196 color=16777197 index=285 color=43775 index=371 color=3146289
opacity:
  smooth=no index=0 opacity=255
}

I've been trying to decipher this file and write a program to reproduce the Wikipedia image. This is my best attempt:

Mandelbrot set

This is part of the renderer. It's kind of messy because I was fiddling around and rewriting large chunks trying to figure this thing out.

using real = double;
using integer = long long;

struct complex {
  real r, i;
};

using grey = unsigned char;

struct color {
  grey r, g, b;
};

struct real_color {
  real r, g, b;
};

grey real_to_grey(real r) {
  // converting to srgb didn't help much
  return std::min(std::max(std::round(r), real(0.0)), real(255.0));
}

color real_to_grey(real_color c) {
  return {real_to_grey(c.r), real_to_grey(c.g), real_to_grey(c.b)};
}

real lerp(real a, real b, real t) {
  return std::min(std::max(t * (b - a) + a, real(0.0)), real(255.0));
}

real_color lerp(real_color a, real_color b, real t) {
  return {lerp(a.r, b.r, t), lerp(a.g, b.g, t), lerp(a.b, b.b, t)};
}

complex plus(complex a, complex b) {
  return {a.r + b.r, a.i + b.i};
}

complex square(complex n) {
  return {n.r*n.r - n.i*n.i, real{2.0} * n.r * n.i};
}

complex next(complex z, complex c) {
  return plus(square(z), c);
}

real magnitude2(complex n) {
  return n.r*n.r + n.i*n.i;
}

real magnitude(complex n) {
  return std::sqrt(magnitude2(n));
}

color lerp(real_color a, real_color b, real t) {
  return real_to_grey(lerp(a, b, t));
}

struct result {
  complex zn;
  integer n;
};

result mandelbrot(complex c, integer iterations, real bailout) {
  complex z = {real{0.0}, real{0.0}};
  integer n = 0;
  real bailout2 = bailout * bailout;
  for (; n < iterations && magnitude2(z) <= bailout2; ++n) {
    z = next(z, c);
  }
  return {z, n};
}

struct table_row {
  real index;
  real_color color;
};

real invlerp(real value, real min, real max) {
  return (value - min) / (max - min);
}

color lerp(table_row a, table_row b, real index) {
  return lerp(a.color, b.color, invlerp(index, a.index, b.index));
}

color mandelbrot_color(complex c, integer iterations, real bailout) {
  const result res = mandelbrot(c, iterations, bailout);
  if (res.n == iterations) {
    // in the set
    return {0, 0, 0};
  } else {
    table_row table[] = {
      // colors and indicies from gradient section
      {28.0*0.1, {0x00, 0x07, 0x64}},
      {92.0*0.1, {0x20, 0x6B, 0xCB}},
      {196.0*0.1, {0xED, 0xFF, 0xFF}},
      {285.0*0.1, {0xFF, 0xAA, 0x00}},
      {371.0*0.1, {0x31, 0x02, 0x30}},
      // interpolate towards black as we approach points that are in the set
      {real(iterations), {0, 0, 0}}
    };
    // it should be smooth, but it's not
    const real smooth = res.n + real{1.0} - std::log(std::log2(magnitude(res.zn)));
    // I know what a for-loop is, I promise
    if (smooth < table[1].index) {
      return lerp(table[0], table[1], smooth);
    } else if (table[1].index <= smooth && smooth < table[2].index) {
      return lerp(table[1], table[2], smooth);
    } else if (table[2].index <= smooth && smooth < table[3].index) {
      return lerp(table[2], table[3], smooth);
    } else if (table[3].index <= smooth && smooth < table[4].index) {
      return lerp(table[3], table[4], smooth);
    } else {
      return lerp(table[4], table[5], smooth);
    }
  }
}

The colors from the gradient section are in a table in mandelbrot_color. The indices from the gradient section are also in the table but I multiplied them by 0.1. The colors look completely off if I don't multiply by 0.1.

The formula section has maxiter=50000 and p_bailout=10000. These are iterations and bailout in the code. I don't know what p_start=0/0 p_power=2/0 means. I don't know why a different bailout is mentioned in the outside section and I don't know what density=0.42, transfer=none, transfer=log means. The gradient section also mentions rotation=29 but I don't understand how a gradient could be rotated.

The reason I am asking this question is that I don't like the white bands around my image (I'd prefer a smooth while glow like in the Wikipedia image). I also don't like the dark purple skin caused by interpolating towards black (the last row in the table in mandelbrot_color). If we remove that row, we end up with a deep blue skin.

I suspect that there is some kind of mapping from the indices in the gradient section to iteration counts. Maybe * 0.1 is an approximation of that mapping that works some of the time. Might have something to do with transfer, density or rotation. Leave a comment if you would like me to post the whole program. It depends on stb_image_write (a single header image writing library).

As a side note, I've I clean up this code and chuck it into a fragment shader, will it likely be faster (generally speaking) than running multithreaded on the CPU?

Indiana Kernick
  • 5,041
  • 2
  • 20
  • 50
  • It looks like maybe you're just doing your rounding in the wrong place. Try to delay it until the point you're generating RGB colors. – Mark Ransom Jan 30 '19 at 05:04
  • @MarkRansom I'm rounding float colors to int colors in `real_to_grey`. All the math is done with float colors and then I convert to ints at the end. – Indiana Kernick Jan 30 '19 at 05:25
  • I'm suggesting that `real_to_grey` is too early based on the banding I see in the background. – Mark Ransom Jan 30 '19 at 05:31
  • @MarkRansom I think that is not caused by rounding but too low iterations ... – Spektre Jan 30 '19 at 06:56
  • @Kerndog73 take a look at [Mandelbrot Set - Color Spectrum Suggestions?](https://stackoverflow.com/a/53666890/2521214) – Spektre Jan 30 '19 at 06:58
  • @Spektre Interesting. Perhaps the Wikipedia image is using a histrogram. That might explain `method=multipass`. Maybe `density=0.42` is the density of the histogram or something. I'll have a closer look at that post. – Indiana Kernick Jan 30 '19 at 07:12
  • The eye is sensitive to tiny colour differences so you would have to filter the image to smooth out the contours like the linked example. One way would be to blend nearby colours to obtain an apparently smooth gradient, because the eye is less good at picking out individual pixels of a slightly different colour. – Weather Vane Feb 04 '19 at 18:47
  • @Kerndog73 Take a look at this [Can't find a way to color the Mandelbrot-set the way i'm aiming for](https://stackoverflow.com/a/56197067/2521214) I think that is the way ... and its even faster as now the max iteration count can be much lower with much better results – Spektre May 19 '19 at 14:07

0 Answers0