xxxxxxxxxx
190
/**
* Mandelbrot 3D
* created by Quark 2025
*
* This sketch uses p5.geometry to create 3D views of the Mandelbrot set. Each
* represents a square region of the complex plane which is split into
* 1000 x 1000 slices, that means there are 2 million triangles to render. The
* sketch uses p5js orbitControl so the view can be rotated by pressing the
* left button and dragging the mouse, pressing the right button pans the view.
*
* On my computer it to0k ~1.6 seconds to calulate the full Mandelbrot set and
* create the 3D model so there is a delay before the view is visible.
*/
// These numbers define the scale and maximum height and were chosen to suit
// the canvas size and that we are using 'orbitControl, to view the Mandelbrot.
const DOMAIN_SIZE = 640, MAX_HEIGHT = 25;
// The maximum number of iterations to perform before deciding the point is
// in the Mandelbrot set.
const MAX_ITERS = 256;
// The maximum number of colours to use in shading the set. It should be less
// than or equal to MAX_ITERS
const COL_MAP_SIZE = 256;
// The number of slices along the real and imaginary axis. The bigger the
// number the greater detail but the longer it takes to compute the set.
// A 1000 x 1000 set has 1 million vetrices and 2 million triangles to
// compute.
// If NBR_SLICES is too large or running on a slow computer the web page
// might freeze in which case reduce the number of slices. Any value over
// 600 will provide a pleasing result.
const NBR_SLICES = 1000;
let tile, colmap, brot;
function setup() {
createCanvas(640, 640, WEBGL);
colmap = getColorMap(COL_MAP_SIZE, 240, 60, -1);
// Tiles that you might be interested in looking at. Simply comment out
// all but the one your interested in.
// This first tile shows the entire Mandelbrot set
// tile = { x0: -2, y0: -1.25, x1: 0.5, y1: 1.25 }
// tile = { x0: -0.7501, y0: 0.1967, w: 0.0256, h: 0.0256 };
// tile = { x0: -1.12691, y0: 0.2615, w: 0.01128, h: 0.01128 };
tile = { x0: 0.35473, y0: 0.06759, w: 0.005, h: 0.005 };
// tile = { x0: 0.38221, y0: 0.09950, w: 3.24E-4, h: 3.24E-4 };
// Add common tile attributes (REQUIRED)
tile.nsx = NBR_SLICES; tile.nsy = NBR_SLICES; tile.maxiters = MAX_ITERS;
brot = makeMandelbrot(tile);
}
/**
* To get a smooth transition of colours this function uses the HSL colour
* sapce and iterates between a start and end hue. Since the hue value is
* cyclic the user must also specify whether the hue iteraes in the positive
* or negative direction.
* Each element in the colour map is another array for the equivalent colour
* in the RGBA colour space i.e. [R, G, B, A] where each channel value is in
* the range 0.0 to 1.0 inclusive.
*
* @param nbr the size of the colour map
* @param sHue the start hue
* @param eHue the end hue
* @param dir iteration direction (+1 or -1)
* @returns the colour map array
*/
function getColorMap(nbr, sHue, eHue, dir = 1) {
let sat = 80; // sautration
let lgt = 70; // lightness
let eGTs = (eHue > sHue), range = abs(eHue - sHue);
if ((dir == -1 && eGTs) || (dir == 1 && !eGTs)) range = 360 - range;
let deltaHue = dir * range / nbr, cm = [];
for (let i = 0; i < nbr; i++) {
let rgb = hsl_rgb((sHue + i * deltaHue) % 360, sat, lgt);
rgb.forEach(function (v, i, a) { a[i] = v / 255 });
rgb.push(1);
cm.push(rgb);
}
return cm;
}
function makeMandelbrot(tile) {
let time = millis();
let brot_data = calcHeights(tile);
let t0 = millis() - time;
time = millis();
let model = createBrotModel(brot_data);
let t1 = millis() - time;
console.log(`Took ${t0} ms to calculate heights and ${t1} ms to create the model`);
console.log(`Height ranges from ${model.info.low} to ${model.info.high}`)
return model;
}
function createBrotModel(brot_info) {
let { x0, y0, x1, y1, nsx, nsy, maxiters } = brot_info.tile;
let heights = brot_info.heights;
let cx = Math.round(nsx / 2), cy = Math.round(nsx / 2);
let scale = DOMAIN_SIZE / nsx;
let verts = [], vcols = []; nitIdx = 0;
for (let y = 0; y <= nsy; y++) {
let vy = (y - cy) * scale;
for (let x = 0; x <= nsx; x++) {
let norm_height = heights[nitIdx] / maxiters;
let vx = (x - cx) * scale, vz = MAX_HEIGHT * norm_height;
let col_idx = Math.floor(norm_height * COL_MAP_SIZE);
verts.push(new p5.Vector(vx, vy, vz));
vcols.push(colmap[col_idx])
nitIdx++;
}
}
return new p5.Geometry(
// Number of horizontal and vertical slices
nsx, nsy,
// The callback must not be an arrow function otherwise "this"
// will not be bound correctly.
function createGeometry() {
this.info = brot_info.tile;
this.vertices = verts;
this.computeFaces();
this.computeNormals();
this.vertexColors = vcols;
this.gid = `MB[${x0},${y0}]>>[${x1},${y1}]:${nsx}x${nsy}:${millis()}`;
}
);
}
function calcHeights(tile) {
// Get tile data and calulate any missing parts
let { x0, y0, x1, y1, w, h, nsx, nsy, maxiters } = tile;
if (w) x1 = x0 + w; else w = x1 - x0;
if (h) y1 = y0 + h; else h = y1 - y0;
// Create array for heights
let nbr = (nsx + 1) * (nsy + 1);
let heights = new Uint16Array(nbr);
let dx = w / nsx, dy = h / nsy, idx = 0, n = 0;
let low = maxiters, high = 0;
for (let yi = 0; yi <= nsy; yi++) {
let py = y0 + yi * dy;
for (let xi = 0; xi <= nsx; xi++) {
let px = x0 + xi * dx, r = px, i = py;
for (n = 0; n < maxiters; n++) {
heights[idx] = n;
let rr = r * r, ii = i * i;
if (rr + ii > 4) break;
i = 2 * r * i + py;
r = rr - ii + px;
}
heights[idx++] = n = n < maxiters ? n : maxiters - 1;
low = Math.min(low, n), high = Math.max(high, n);
}
}
let updated_tile = {
x0: x0, y0, y0, x1: x1, y1: y1, w: w, h: h,
nsx: nsx, nsy: nsy, maxiters: maxiters, low: low, high: high
};
return { action: 'done', tile: updated_tile, heights: heights };
}
function draw() {
background(0);
lights();
orbitControl(1, 1, 0.2);
if (brot) model(brot);
}
/**
* HSL > RGB
* @param hue - Hue as degrees 0..360
* @param sat - Saturation in reference range [0,100]
* @param light - Lightness in reference range [0,100]
* @return Array of RGB components 0..255
*/
const hsl_rgb = function (hue, sat, light) {
function f(n) {
let k = (n + hue / 30) % 12;
let a = sat * Math.min(light, 1 - light);
return light - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
}
hue = hue % 360;
if (hue < 0) { hue += 360; }
sat /= 100; light /= 100;
let rgb = [f(0), f(8), f(4)];
rgb.forEach(function (v, i, a) { a[i] = v == 1 ? 255 : Math.floor(v * 256); });
return rgb;
}