Mandelbrot 平滑着色问题

Issue with Mandelbrot smooth coloring

我实现了常规算法来显示 Mandelbrot 集并为其着色,并且 现在我正在使用 255 色图处理平滑着色功能。

这部分已经在网上(甚至在这个论坛上)有很好的记录,我选择最流行的转义重规范化方式:

function setup() {
  createCanvas(500, 500);
  background(0);
  pixelDensity(1);
  noLoop();
}

function draw() {
  // Mandelbrot
  const A = [
    [-2, 1],
    [-1.5, 1.5]
  ];

  mandelbrot(A, 18, colormap);
}

/* ################## */
/* ### Mandelbrot ### */
/* ################## */

function mandelbrot(A, K, colormap) {
  loadPixels();

  // Loop through all area point
  for (let x = 0; x < width; x++) {
    for (let y = 0; y < height; y++) {
      // Get point coordinates
      const c = new Complex(
        map(x, 0, width, A[0][0], A[0][1]),
        map(y, 0, height, A[1][0], A[1][1])
      );

      let R = max(c.abs(), 2);
      let z = new Complex(0, 0);
      let ic = color(0);

      for (let i = 0; i < K; i++) {
        // Next sequence term
        z.pow2();
        z.add(c);

        if (z.abs() > R) {
          // Anti-aliasing (smooth) color index from the map
          let mu = i - log(log(z.abs())) / log(R);
          let mapIndex = int(colormap.length * mu / K);

          // Retieve color from map
          ic = colormap[mapIndex];
          break;
        }
      }

      // Color pixel
      const index = (x + y * width) * 4;
      pixels[index] = red(ic);
      pixels[index + 1] = green(ic);
      pixels[index + 2] = blue(ic);
      pixels[index + 3] = 255;
    }
  }

  updatePixels();
}

/* ############### */
/* ### Complex ### */
/* ############### */

class Complex {
  constructor(r, im) {
    this.r = r;
    this.im = im;
  }

  add(c) {
    this.r += c.r;
    this.im += c.im;
  }

  pow2() {
    let r = this.r;
    let im = this.im;

    this.r = r ** 2 - im ** 2;
    this.im = 2 * r * im;
  }

  abs() {
    return sqrt(this.r ** 2 + this.im ** 2)
  }
}

/* ################ */
/* ### ColorMap ### */
/* ################ */

const colormap = [
  [0, 0, 0],
  [96, 148, 140],
  [92, 140, 136],
  [92, 132, 128],
  [88, 124, 124],
  [88, 116, 116],
  [84, 112, 112],
  [84, 104, 104],
  [80, 96, 100],
  [80, 88, 92],
  [76, 80, 88],
  [76, 72, 80],
  [72, 64, 76],
  [68, 56, 68],
  [88, 60, 80],
  [112, 64, 96],
  [136, 68, 108],
  [160, 72, 124],
  [184, 76, 136],
  [208, 80, 152],
  [232, 88, 168],
  [216, 104, 176],
  [196, 124, 188],
  [180, 140, 196],
  [160, 160, 208],
  [140, 176, 220],
  [124, 196, 228],
  [104, 212, 240],
  [84, 232, 252],
  [88, 232, 248],
  [96, 232, 240],
  [104, 232, 232],
  [112, 232, 224],
  [120, 232, 216],
  [128, 232, 208],
  [136, 232, 200],
  [140, 232, 192],
  [148, 232, 184],
  [156, 232, 176],
  [164, 232, 168],
  [172, 232, 160],
  [180, 232, 152],
  [188, 232, 144],
  [176, 232, 152],
  [160, 232, 160],
  [144, 232, 168],
  [128, 232, 180],
  [124, 228, 176],
  [120, 220, 168],
  [112, 212, 160],
  [108, 204, 152],
  [104, 196, 144],
  [92, 168, 124],
  [76, 140, 104],
  [60, 112, 84],
  [48, 84, 64],
  [32, 56, 44],
  [16, 28, 24],
  [0, 0, 0],
  [0, 0, 0],
  [4, 4, 4],
  [4, 4, 4],
  [8, 8, 8],
  [8, 8, 8],
  [12, 12, 12],
  [12, 12, 12],
  [16, 16, 16],
  [16, 16, 16],
  [20, 20, 20],
  [20, 20, 20],
  [24, 24, 24],
  [24, 24, 24],
  [28, 28, 28],
  [28, 28, 28],
  [32, 32, 32],
  [32, 32, 32],
  [36, 36, 36],
  [36, 36, 36],
  [40, 40, 40],
  [40, 40, 40],
  [44, 44, 44],
  [44, 44, 44],
  [48, 48, 48],
  [48, 48, 48],
  [52, 52, 52],
  [52, 52, 52],
  [56, 56, 56],
  [56, 56, 56],
  [60, 60, 60],
  [60, 60, 60],
  [64, 64, 64],
  [64, 64, 64],
  [68, 68, 68],
  [68, 68, 68],
  [72, 72, 72],
  [72, 72, 72],
  [76, 76, 76],
  [76, 76, 76],
  [80, 80, 80],
  [84, 84, 84],
  [84, 84, 84],
  [88, 88, 88],
  [88, 88, 88],
  [92, 92, 92],
  [92, 92, 92],
  [96, 96, 96],
  [96, 96, 96],
  [100, 100, 100],
  [100, 100, 100],
  [104, 104, 104],
  [104, 104, 104],
  [108, 108, 108],
  [108, 108, 108],
  [112, 112, 112],
  [112, 112, 112],
  [116, 116, 116],
  [116, 116, 116],
  [120, 120, 120],
  [120, 120, 120],
  [124, 124, 124],
  [124, 124, 124],
  [128, 128, 128],
  [128, 128, 128],
  [132, 132, 132],
  [132, 132, 132],
  [136, 136, 136],
  [136, 136, 136],
  [140, 140, 140],
  [140, 140, 140],
  [144, 144, 144],
  [144, 144, 144],
  [148, 148, 148],
  [148, 148, 148],
  [152, 152, 152],
  [152, 152, 152],
  [156, 156, 156],
  [156, 156, 156],
  [160, 160, 160],
  [160, 160, 160],
  [164, 164, 164],
  [168, 168, 168],
  [168, 168, 168],
  [172, 172, 172],
  [172, 172, 172],
  [176, 176, 176],
  [176, 176, 176],
  [180, 180, 180],
  [180, 180, 180],
  [184, 184, 184],
  [184, 184, 184],
  [188, 188, 188],
  [188, 188, 188],
  [192, 192, 192],
  [192, 192, 192],
  [196, 196, 196],
  [196, 196, 196],
  [200, 200, 200],
  [200, 200, 200],
  [204, 204, 204],
  [204, 204, 204],
  [208, 208, 208],
  [208, 208, 208],
  [212, 212, 212],
  [212, 212, 212],
  [216, 216, 216],
  [216, 216, 216],
  [220, 220, 220],
  [220, 220, 220],
  [224, 224, 224],
  [224, 224, 224],
  [228, 228, 228],
  [228, 228, 228],
  [232, 232, 232],
  [232, 232, 232],
  [236, 236, 236],
  [236, 236, 236],
  [240, 240, 240],
  [240, 240, 240],
  [244, 244, 244],
  [244, 244, 244],
  [248, 248, 248],
  [252, 252, 252],
  [252, 252, 228],
  [252, 252, 204],
  [252, 252, 180],
  [252, 252, 152],
  [252, 252, 128],
  [252, 252, 104],
  [252, 252, 80],
  [252, 252, 52],
  [244, 240, 56],
  [236, 228, 60],
  [228, 212, 64],
  [220, 200, 72],
  [212, 184, 76],
  [204, 172, 80],
  [196, 160, 88],
  [188, 144, 92],
  [176, 132, 96],
  [168, 116, 100],
  [160, 104, 108],
  [152, 92, 112],
  [144, 76, 116],
  [136, 64, 124],
  [128, 48, 128],
  [120, 36, 132],
  [108, 20, 140],
  [196, 184, 220],
  [188, 176, 212],
  [180, 168, 204],
  [172, 160, 192],
  [160, 148, 184],
  [152, 140, 172],
  [144, 132, 164],
  [132, 120, 152],
  [124, 112, 144],
  [116, 104, 136],
  [104, 92, 124],
  [96, 84, 116],
  [88, 76, 104],
  [76, 64, 96],
  [68, 56, 84],
  [60, 48, 76],
  [48, 36, 64],
  [52, 36, 72],
  [60, 32, 84],
  [68, 32, 92],
  [76, 28, 104],
  [80, 28, 112],
  [88, 24, 124],
  [96, 20, 136],
  [104, 20, 144],
  [108, 16, 156],
  [116, 16, 164],
  [124, 12, 176],
  [132, 8, 188],
  [136, 8, 168],
  [140, 8, 144],
  [144, 8, 120],
  [148, 8, 96],
  [152, 8, 72],
  [156, 8, 48],
  [160, 4, 24],
  [156, 16, 36],
  [152, 32, 48],
  [144, 48, 60],
  [140, 60, 72],
  [136, 76, 84],
  [128, 92, 96],
  [124, 108, 108],
  [120, 120, 120],
  [112, 136, 132],
  [108, 152, 144],
  [100, 168, 156],
  [100, 164, 152],
  [96, 156, 148]
];
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>

尽管有这个公式,我仍然可以在可视化中看到 bands/gap 颜色,但无法弄清楚我的问题出在哪里。

P.S: 我去掉了对数公式的“+1”,避免在colourmap中找颜色时越界

链接算法有两个偏差导致了问题:

  1. 在某些情况下,您将逃逸半径设置为较低的值 (~2)
  2. 你除以 log(R) 是不正确的。你应该除以 log(2)

此外,一些条带是因为您仍在使用离散颜色贴图。您可以使用值 -> 色调的连续映射,但如果您想要特定的颜色循环,您可以使用 lerpColor() 根据 continuous/smooth 之间的差异从一种颜色平滑过渡到另一种颜色值和当前索引。

function setup() {
  createCanvas(500, 500);
  background(0);
  pixelDensity(1);
  noLoop();
}

function draw() {
  // Mandelbrot
  const A = [
    [-2, 1],
    [-1.5, 1.5]
  ];

  mandelbrot(A, 18, colormap);
}

/* ################## */
/* ### Mandelbrot ### */
/* ################## */

function mandelbrot(A, K, colormap) {
  loadPixels();
  // Allowing the escape radius to be too small (i.e. 2) leads to bad results
  const R = 4;

  // Loop through all area point
  for (let x = 0; x < width; x++) {
    for (let y = 0; y < height; y++) {
      // Get point coordinates
      const c = new Complex(
        map(x, 0, width, A[0][0], A[0][1]),
        map(y, 0, height, A[1][0], A[1][1])
      );

      // let R = max(c.abs(), 2);
      let z = new Complex(0, 0);
      let ic = color(0);

      for (let i = 0; i < K; i++) {
        // Next sequence term
        z.pow2();
        z.add(c);

        if (z.abs() > R) {
          // Anti-aliasing (smooth) color index from the map
          // / log(R) is incorrect
          let mu = i - log(log(z.abs())) / log(2);
          let mapIndex = floor((colormap.length - 1) * mu / K);
          let interval = (colormap.length - 1) * mu / K - mapIndex;
          
          if (mapIndex < colormap.length - 1) {
            // lerp between the current color and the next color by interval
            ic = lerpColor(color(...colormap[mapIndex]), color(...colormap[mapIndex + 1]), interval);
          } else {
            // Retieve color from map
            ic = colormap[mapIndex];
          }

          break;
        }
      }

      // Color pixel
      const index = (x + y * width) * 4;
      pixels[index] = red(ic);
      pixels[index + 1] = green(ic);
      pixels[index + 2] = blue(ic);
      pixels[index + 3] = 255;
    }
  }

  updatePixels();
}

/* ############### */
/* ### Complex ### */
/* ############### */

class Complex {
  constructor(r, im) {
    this.r = r;
    this.im = im;
  }

  add(c) {
    this.r += c.r;
    this.im += c.im;
  }

  pow2() {
    let r = this.r;
    let im = this.im;

    this.r = r ** 2 - im ** 2;
    this.im = 2 * r * im;
  }

  abs() {
    return sqrt(this.r ** 2 + this.im ** 2)
  }
}

/* ################ */
/* ### ColorMap ### */
/* ################ */

const colormap = [
  [96, 148, 140],
  [68, 56, 68],
  [232, 88, 168],
  [84, 232, 252],
  [0, 0, 0],
  [252, 252, 252],
  [252, 252, 52],
  [196, 184, 220],
  [48, 36, 64],
  [160, 4, 24],
  [96, 156, 148]
];
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>