Peace on Gliese 876d

Playing around with hands and planets.

Log in to post a comment.

// LL 2021

//const turtle = new Turtle();
const turtle = new Slowpoke();

const detail = 0.0;        /// min=0 max=1 step=0.1
const perspective = 1.6;   /// min=1 max=3 step=0.1
const camera_theta = 0.;   // min=-1 max=1 step=0.05
const camera_phi = 0.;     // min=-0.99 max=1 step=0.05
const camera_r = 22;       // min=0.1 max=40 step=0.1
const look_at_z = 0;       /// min=-10 max=10 step=0.1
const inside_lines = 0.3;  /// min=0 max=1 step=0.01
const style = 2;           // min=0 max=3 step=1 (Preview,All outlines,Silhouette,Hatched)
const render = 1;          /// min=0 max=1 step=1 (Voxels,Marching cubes)
const seed = 0;            // min=0 max=100 step=1

const fold = 0.6    /// min=-2 max=2 step=0.05
const spread = 0.2  /// min=-2 max=2 step=0.05
const thumb1 = 0.75 /// min=-2 max=2 step=0.05
const thumb2 = 0.75 /// min=-2 max=2 step=0.05

const detail_max_length = Math.max(0.9, 0.9 + 5 * (1 - detail));

var silhouette = new Silhouette();

const noise = SimplexNoise(seed ? seed : Math.floor(Math.random() * 1000));

function walk(i, t) {
	if (i==0) {
		if (t == 0 || t == 1) initOnce();
		initFrame(t);
	}

	if (silhouette.faceCount() < 1) {
	    //console.log(`Slowpoke draw: ${slowpoke_draw} skip: ${slowpoke_skip}`)
		return false;
   	}
	
	silhouette.nextFace().draw();

	return true;
}

function initFrame(t) {
	const cameraOffset = [
		 camera_r * perspective ** 3 * Math.cos((camera_theta+t*2) * Math.PI) * Math.sin((camera_phi/2+0.5) * Math.PI),
		 camera_r * perspective ** 3 * Math.sin((camera_theta+t*2) * Math.PI) * Math.sin((camera_phi/2+0.5) * Math.PI),
		-camera_r * perspective ** 3 * Math.cos((camera_phi/2+0.5) * Math.PI)
		];
	const cameraLookAt = [0, 0, look_at_z];
	viewProjectionMatrix = setupCamera(add3(cameraOffset, cameraLookAt), cameraLookAt);

	polygons = new Polygons();
	
	silhouette.processFrameModels();
	silhouette.sortFaces();

	console.log(`Models: ${silhouette.modelCount()}. Faces: ${silhouette.faceCount()}.`);
}

function initOnce(t) {
	seed_t = (t < 1 && seed == 0) ? (Math.random() * 100 | 0) : seed;
	rng = undefined;

	initScene();

	silhouette.processOnceModels();

}

function initScene() {
	{
		const angle = fold;
		const mdl = makePlanet(25);
		const matrix = new Matrix();
		mdl.transform(matrix);
		silhouette.addModel(mdl);
	}

	{
		const angle = fold;
		const mdl = makeHand2(angle, spread);
		const matrix = new Matrix();
		matrix.scale(6);
		matrix.rotateZ(Math.PI/1.5);
		matrix.rotateX(Math.PI/10);
		matrix.rotateY(-Math.PI/4);
		matrix.translate(-12, -25, -28);
		mdl.transform(matrix);
		silhouette.addModel(mdl);
	}

	{
		const angle = fold * 0.7;
		const mdl = makeHand2(angle, spread);
		const matrix = new Matrix();
		matrix.scale(6);
		matrix.rotateZ(Math.PI/1.5);
		matrix.rotateX(Math.PI/10);
		matrix.rotateY(-Math.PI/4);
		matrix.translate(-15, -22, -25);
		matrix.scale(1, 1, -1);
		mdl.transform(matrix);
		silhouette.addModel(mdl);

	}
}

/////////////////////////////////////////////////////////////////////
// Prototype Silhouette utility code - Created by Lionel Lemarie 2021
// https://turtletoy.net/turtle/334500a2c5

function Silhouette() {
	const models = [];
	const all_faces = [];

	class Model {
		constructor() {
			this.faces = [];
			this.edges = [];
			this.hide_overlap = true;
			this.inside_lines = inside_lines;
		}

		transform(matrix) {
			this.faces.forEach(face => {
				face.transform(matrix);
			});
		}

		processOnce() {
			if (detail && style) this.subdivideDetail(detail_max_length);
			this.updateEdgeList();
		}

		processFrame() {
			this.faces.forEach(face => { face.projectAndAdd(); });
			this.findStaticOutlines();
			this.findProjectedOutlines();
			this.updateOutlineMasks();
		}
		
		updateOutlineMasks() {
			this.edges.forEach(edge => {
				edge.faces.forEach(face => {
					const pc = face.points.length;
					if (pc > 1) {
						for (var i=0; i<pc; i++) {
							const hash0 = getPointHash(face.points[i]);
							const hash1 = getPointHash(face.points[(i+1)%pc]);
							if (edge.hash == hash0 + hash1 || edge.hash == hash1 + hash0) {
								if (edge.state) {
									face.outline_mask |= 1 << i;
								} else {
									face.outline_mask &= ~(1 << i);
								}
							}
						}
					}	
				});
			});
		}

		addFace(points) {
			const face = new Face(points);
			this.faces.push(face);
		}

		merge(model) {
			model.faces.forEach(face => { this.faces.push(face); })
		}

		subdivideDetail(max_length) {
			var nfaces = this.faces.length;
			var loop = true;
			while (loop) {
				const old_faces = this.faces;
				this.faces = [];
				old_faces.forEach(face => {
					const new_faces = face.getSubdivided(max_length);
					new_faces.forEach(nface => { this.faces.push(nface); });
				});
				loop = this.faces.length != nfaces;
				nfaces = this.faces.length;
			}
		}

		subdivideCount(count) {
			while (count-- > 0) {
				const old_faces = this.faces;
				this.faces = [];
				old_faces.forEach(face => {
					const new_faces = face.getSubdivided(0);
					new_faces.forEach(nface => { this.faces.push(nface); });
				});
			}
		}

		updateEdgeList() {
			this.edges = [];
			const edge_lookup = {};
			this.faces.forEach(face => {
				const pc = face.points.length;
				if (pc > 1) {
					for (var i=0; i<pc; i++) {
						const hash0 = getPointHash(face.points[i]);
						const hash1 = getPointHash(face.points[(i+1)%pc]);
						if ((hash0 + hash1) in edge_lookup) {
							const edge_id = edge_lookup[hash0 + hash1];
							this.addFaceToEdge(edge_id, face);
						} else if ((hash1 + hash0) in edge_lookup) {
							const edge_id = edge_lookup[hash1 + hash0];
							this.addFaceToEdge(edge_id, face);
						} else {
							const edge_id = this.edges.length;
							edge_lookup[hash0 + hash1] = edge_id;
							this.edges.push({ faces: [face], hash: hash0 + hash1, state: 1 });
						}
					}
				}
			});
		}

		addFaceToEdge(edge_id, new_face) {
			var good = true;
			this.edges[edge_id].faces.forEach(face => {
				if (face.matches(new_face)) {
					good = false;
					if (this.hide_overlap) face.overlap = true;
					new_face.overlap = true;
				}
			});
			if (good) this.edges[edge_id].faces.push(new_face);
		}

		findStaticOutlines() {
			this.edges.forEach(edge => {
				edge.state = 1;
				for (var i=0, fl=edge.faces.length; i<fl && edge.state; i++) {
					if (edge.faces[i].overlap) continue;
					const nfi = edge.faces[i].getStaticNormal();
					for (var j=i+1; j<fl && edge.state; j++) {
						if (edge.faces[j].overlap) continue;
						const nfj = edge.faces[j].getStaticNormal();
						const d = len3(sub3(nfi, nfj));
						if (d < this.inside_lines * 2) { edge.state = 0; }
					}
				}
			});
		}

		findProjectedOutlines() {
			this.edges.forEach(edge => {
				var found_pos = false, found_neg = false;
				edge.faces.forEach(face => {
						if (!face.overlap) {
						const nf = face.getProjectedNormal();
						const EPS = 0.001;
						if (nf[2] <   EPS) found_neg = true;
						if (nf[2] >= -EPS) found_pos = true;
					}
				});
				if (found_pos && found_neg) edge.state = 1;
			});
		}
	}

	function getPointHash(point) {
		const mult = 100;
		const x0 = Math.round(point[0] * mult), y0 = Math.round(point[1] * mult), z0 = Math.round(point[2] * mult);
		return `${x0},${y0},${z0}`;
	}

	class Face {
		constructor(points) {
			this.points = [...points];
			this.z = 0;
			this.projected_points = [];
			this.outline_mask = -1;
			this.overlap = false;
		}

		draw() {
			if (this.projected_points.length < 2 || this.overlap) return;

			var good = true;
			this.projected_points.forEach(p => good &= Math.min(Math.abs(p[0]), Math.abs(p[1])) < 100 );
			if (!good) return;

			if (style == 0) {
				// turtle.jump(this.projected_points[this.projected_points.length-1]);
				// this.projected_points.forEach(p=>turtle.goto(p));
				for (var i=0; i<this.projected_points.length; i++) {
					if (this.outline_mask & (1 << i)) {
						turtle.jump(this.projected_points[i]);
						turtle.goto(this.projected_points[(i+1) % this.projected_points.length]);
					}
				}
			} else {
				const p1 = polygons.create();
				p1.addPoints(...this.projected_points);
				if (style == 1) {
					p1.addOutline();
				} else if (style > 1) {
					for (var i=0; i<this.projected_points.length; i++) {
						if (this.outline_mask & (1 << i)) {
							p1.addSegments(p1.cp[i], p1.cp[(i+1) % this.projected_points.length]);
						}
					}
					if (style > 2) {
						const hmin = 0.15, hmax = 0.9;
						const hatching = hmin + (hmax - hmin) * this.getLight();
						p1.addHatching(-Math.PI/4, hatching);
					}
				}
				polygons.draw(turtle, p1, true);
			}
		}

		getStaticNormal() {
			if (this.cached_static_normal === undefined) {
				if (this.points.length < 3) this.cached_static_normal = [0, 1, 0];
				else this.cached_static_normal = normalize3(cross3(sub3(this.points[1], this.points[0]), sub3(this.points[2], this.points[0])));
			}
			return this.cached_static_normal;
		}

		getProjectedNormal() {
			if (this.projected_points.length < 3) return [0, 1, 0];
			return normalize3(cross3(sub3(this.projected_points[1], this.projected_points[0]), sub3(this.projected_points[2], this.projected_points[0])));
		}

		getLight() {
			const n = this.getProjectedNormal();
			return n[0] * 0.5 + 0.5;
		}

		transform(matrix) {
			for (var i=0, c=this.points.length; i<c; i++) {
				this.points[i] = matrix.transform(this.points[i]);
			}
		}

		projectAndAdd() {
			if (this.overlap) return;

			this.projected_points = [];
			this.z = 0;
			this.points.forEach(point => {
				const pp = project(point);
				if (pp === undefined) return;
				this.projected_points.push(pp);
				this.z += pp[2];
			})
			if (this.projected_points.length > 0) this.z /= this.projected_points.length;

			silhouette.addFace(this);
		}

		getSubdivided(max_length) {
			var long_index = -1;
			const pc = this.points.length;

			for (var i=0; i<pc; i++) {
				const len = len3(sub3(this.points[(i+1)%pc],this.points[i]));
				if (len > max_length) {
					max_length = len;
					long_index = i;
				}
			}

			if (long_index >= 0) {
				if (pc == 4) {
					const new_faces = [];
					new_faces.push(new Face([this.points[0], this.points[1], this.points[2]]));
					new_faces.push(new Face([this.points[2], this.points[3], this.points[0]]));
					return new_faces;
				} else {
					const new_faces = [];
					const point = mulf(add3(this.points[(long_index+1)%pc],this.points[long_index]), 0.5);
					for (var i=0; i<pc; i++) {
						if (i != long_index) {
							new_faces.push(new Face([this.points[i], this.points[(i+1)%pc], point]));
						}
					}
					return new_faces;
				}
			}
			return [ this ];
		}

		matches(face) {
			const fl = face.points.length;
			if (fl != this.points.length) return false;
			if (fl < 1) return true;

			var first_id = undefined;
			{
				const hash0 = getPointHash(this.points[0]);
				for (var i=0; i<fl; i++) {
					const hash1 = getPointHash(face.points[i]);
					if (hash0 == hash1) { first_id = i; break; }
				}
			}
			if (first_id === undefined) return false;

			for (var i=1; i<fl; i++) {
				const j = (first_id - i + fl) % fl;
				const hash0 = getPointHash(this.points[i]);
				const hash1 = getPointHash(face.points[j]);
				if (hash0 != hash1) {
					for (var i2=1; i2<fl; i2++) {
						const j2 = (first_id + i2) % fl;
						const hash0 = getPointHash(this.points[i2]);
						const hash1 = getPointHash(face.points[j2]);
						if (hash0 != hash1) return false;
					}
					return true;
				}
			}
			return true;
		}
	}

	return {
		addModel: (model) => { models.push(model); },
		newModel: () => { return new Model(); },
		processFrameModels: () => { models.forEach(model => { model.processFrame(); }); },
		processOnceModels: () => { models.forEach(model => { model.processOnce(); }); },
		modelCount: () => { return models.length; },
		sortFaces: () => { all_faces.sort(function(a, b) { return a.z - b.z; }); },
		nextFace: () => { return all_faces.shift(); },
		faceCount: () => { return all_faces.length; },
		addFace: (face) => { all_faces.push(face); },
	};
}

class Matrix {
	constructor() {
		this.identity();
		this.stack = [];
	}

	identity() {
		this.matrix = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ];
		return this;
	}

	translate(tx, ty, tz) {
		const m2 = new Matrix();
		m2.matrix[12] = tx;
		m2.matrix[13] = ty;
		m2.matrix[14] = tz;
		return this.multiply(m2);
	}

	scale(sx, sy, sz) {
		if (sy === undefined) sy = sx;
		if (sz === undefined) sz = sy;
		const m2 = new Matrix();
		m2.matrix[0]  *= sx;
		m2.matrix[5]  *= sy;
		m2.matrix[10] *= sz;
		return this.multiply(m2);
	}

	rotateX(a) {
		const m2 = new Matrix();
		m2.matrix[5]  =  Math.cos(a);
		m2.matrix[9]  =  Math.sin(a);
		m2.matrix[6]  = -Math.sin(a);
		m2.matrix[10] =  Math.cos(a);
		return this.multiply(m2);
	}

	rotateY(a) {
		const m2 = new Matrix();
		m2.matrix[0]  =  Math.cos(a);
		m2.matrix[8]  = -Math.sin(a);
		m2.matrix[2]  =  Math.sin(a);
		m2.matrix[10] =  Math.cos(a);
		return this.multiply(m2);
	}

	rotateZ(a) {
		const m2 = new Matrix();
		m2.matrix[0]  =  Math.cos(a);
		m2.matrix[1]  = -Math.sin(a);
		m2.matrix[4]  =  Math.sin(a);
		m2.matrix[5]  =  Math.cos(a);
		return this.multiply(m2);
	}

	multiply(rhs) {
		const m1 = [...this.matrix];
		const m2 = [...rhs.matrix];
		this.matrix = [];
		for(let n=0; 16>n; n+=4) {
			for(let o=0; 4>o; o++) {
				this.matrix[n+o] = m1[n+0] * m2[0+o] + m1[n+1] * m2[4+o] + m1[n+2] * m2[8+o] + m1[n+3] * m2[12+o];
			}
		}
		return this;
	}

	transform(point) {
		const p = [...point];
		for (let i=0; i<3; i++) {
			p[i] = this.matrix[i] * point[0] + this.matrix[i+4] * point[1] + this.matrix[i+8] * point[2] + this.matrix[i+12];
		}
		return p;
	}

	push() {
		this.stack.push([...this.matrix]);
	}

	pop() {
		this.matrix = this.stack.pop();
	}
}

const cube_points = [];
for (var z=-1; z<=1; z+=2) { for (var y=-1; y<=1; y+=2) { for (var x=-1; x<=1; x+=2) { cube_points.push([x, y, z]); } } }
const cube_faces = [ [0, 1, 2, 3], [7, 3, 5, 1], [6, 2, 7, 3], [0, 4, 1, 5], [4, 0, 6, 2], [6, 7, 4, 5] ];

function makeBox(w, h, d) {
	const model = silhouette.newModel();

	cube_faces.forEach(f => {
		const quad = [ [...cube_points[f[0]]], [...cube_points[f[1]]], [...cube_points[f[3]]], [...cube_points[f[2]]] ];
		quad.forEach(point => { point[0] *= w/2; point[1] *= h/2; point[2] *= d/2; });
		model.addFace(quad);
	});
	
	return model;
}

function makeSphere(radius, subdiv=0) {
	const model = makeBox(1, 1, 1);
	model.subdivideCount(subdiv);
	model.faces.forEach(face => { for (var i=0; i<face.points.length; i++) face.points[i] = mulf(normalize3(face.points[i]), radius); });

	return model;
}

// Input: 3D point in -1,1 range
function heightField3D(xyz) {
	const factor = 1.8;
	let x = xyz[0] * factor, y = xyz[1] * factor, z = xyz[2] * factor;

	let height = noise.noise3D([x,y,z]);
	height += .55 * noise.noise3D([x*2+3.4, y*2-56.1, z*2-4.5]);
	height += .35 * Math.abs(1-noise.noise3D([x*4+21.2, y*4+.5, z*4-8.7]));
	height += .25 * Math.abs(1-noise.noise3D([x*8+421.12, y*8+21.3, z*8-45.3]));
 	//height += .15 * Math.abs(1-noise.noise3D([x*32+150.34, y*32+150.34, z*32-150.34]));
	//height += 1 - 2.5 * ((xyz[0]/100)**2 + (xyz[1]/100)**2 + (xyz[2]/100)**2);
	height -= 0.5;
	height *= -0.4;
	height = Math.max(height, 0);
	const max = 0.5;
	height = Math.min(height, max);
	height *= 0.15 / max;
	height++;
	return height;
}

function makeVoxelCube(count) {
	const grid = {};

	for (var z=1; z<count-1; z++) {
		for (var y=1; y<count-1; y++) {
			for (var x=1; x<count-1; x++) {
				if ((x^y^z) % 9 == 0) continue;
				for (var dz=-1; dz<=0; dz++) for (var dy=-1; dy<=0; dy++) for (var dx=-1; dx<=0; dx++) {
					const key = (x+dx) + (y+dy) * count + (z+dz) * count * count;
					if (!(key in grid)) grid[key] = 0;
					grid[key] |= !(dx || dy || dz);
				}
			}
		}
	}
	
	const model = modelFromGrid(grid, count);
	return model;
}

function getTriangles3(voxel_grid, countx, county, countz, x, y, z, cxyz, r) {
	var cube_index = 0;
	for (var i=0; i<8; i++) {
		const dx = v_xyz[i][0] * .5 + .5, dy = v_xyz[i][1] * .5 + .5, dz = v_xyz[i][2] * .5 + .5;
		const idx = (x+dx) + (y+dy) * countx + (z+dz) * county * countx;
		if (idx in voxel_grid && voxel_grid[idx] == 1) {
			cube_index |= 1 << i;
		}
	}
	const polys = [];
	var points = [];
	tri_table[cube_index].forEach( (t, i) => {
		const p0 = add3(mulf(v_xyz[edges[t][0]], r), cxyz);
		const p1 = add3(mulf(v_xyz[edges[t][1]], r), cxyz);
		const point = add3(p0, mulf(sub3(p1, p0), .5));
		points.push(point);
		if (((i+1)%3) == 0) {
			polys.push(points);
			points = [];
		}
	});
	return polys;
}

function modelFromGrid3(grid, countx, county, countz) {
	const model = silhouette.newModel();

	var total = 0;
	Object.keys(grid).forEach(key => { if (grid[key] == 1) total++; });

	Object.keys(grid).forEach(key => {
		const x = key % countx;
		const y = (key / countx | 0) % county;
		const z = key / county / countx| 0;
		const cx = (x - countx / 2);
		const cy = (y - county / 2);
		const cz = (z - countz / 2);
		if (render) {
			const polys = getTriangles3(grid, countx, county, countz, x, y, z, [cx, cy, cz], 1/2);
			polys.forEach(poly => { model.addFace(poly); });
   		} else {
			if (grid[key]) {
				const mdl = makeBox(1, 1, 1);
				mdl.transform(new Matrix().translate(cx, cy, cz));
				model.merge(mdl);
			}
		}
	});

	return model;
}

function makeVoxelCube3(countx, county, countz) {
	const factor = 10, thxz = 1, thy = 2;
	countx *= factor; county *= factor; countz *= factor;
	countx += 2; county += 2; countz += 2;
	countx = Math.round(countx); county = Math.round(county); countz = Math.round(countz);

	const grid = {};

	for (var z=1; z<countz-1; z++) {
		for (var y=1; y<county-1; y++) {
			for (var x=1; x<countx-1; x++) {
				for (var dz=-1; dz<=0; dz++) for (var dy=-1; dy<=0; dy++) for (var dx=-1; dx<=0; dx++) {
					if (y > thy && y < county-thy-1)
						if (x <= thxz || x >= countx-thxz-1 || z <= thxz || z >= countz-thxz-1)
							continue;
					const key = (x+dx) + (y+dy) * countx + (z+dz) * county * countx;
					if (!(key in grid)) grid[key] = 0;
					grid[key] |= !(dx || dy || dz);
				}
			}
		}
	}
	
	const model = modelFromGrid3(grid, countx, county, countz);
	if (render) model.transform(new Matrix().translate(0.5, 0.5, 0.5));
	model.transform(new Matrix().scale(1/factor));
	return model;
}

function makeFinger(x, y, z) {
	if (true) return makeVoxelCube3(x, y, z);
	else if (true) {
		const model = makeSphere(1, 7);
		model.transform(new Matrix().scale(x, y, z).scale(0.7));
		return model;
	}
	return makeBox(x, y, z);
}

function makeHand2(angle_fold, angle_spread) {
	const gap = 0.9;

	const pose = {};

	const palm_w = 4;
	const palm_d = 1;
	const palm1_h = 3;
	const palm2_h = 1;

	const root_node = new Node();
	var node = root_node;
	const thumb_base_node = node;

	node.addModel(makeFinger(palm_w, palm1_h, palm_d));

	node = node.newNode();
	node.transform(new Matrix().translate(0, palm2_h/2 * gap, 0));
	node.transform(new Matrix().rotateX(-Math.min(1, angle_fold)));
	node.transform(new Matrix().translate(0, palm1_h/2 * gap, 0));
	node.addModel(makeFinger(palm_w, palm2_h, palm_d));

	node = node.newNode();
	node.transform(new Matrix().translate(0, palm2_h/2 * gap, 0));

	const fgap = 1.1;
	const finger_w = palm_w / (3.9 * fgap); 
	const finger1_h = [ 1.5, 1.8, 2.0, 1.8 ];
	const finger2_h = [ 1.0, 1.3, 1.5, 1.3 ];
	const finger3_h = [ 0.8, 0.8, 1.0, 1.2 ];

	const finger_base_node = node;
	var finger1_ox = palm_w / 2 - finger_w / 2;

	for (var i=0; i<4; i++) {
		const sa = angle_spread * (i-2) * .2;
		const fa = Math.min(1, angle_fold / (i+1));

		node = finger_base_node.newNode();
		node.transform(new Matrix().translate(0, finger1_h[i]/2 * gap, 0));
		node.transform(new Matrix().rotateZ(-sa));
		node.transform(new Matrix().translate(finger1_ox, 0, 0));
		node.transform(new Matrix().rotateX(-fa));
		node.addModel(makeFinger(finger_w, finger1_h[i], finger_w));

		node = node.newNode();
		node.transform(new Matrix().translate(0, finger2_h[i]/2 * gap, 0));
		node.transform(new Matrix().rotateX(-fa * 1.5));
		node.transform(new Matrix().translate(0, finger1_h[i]/2 * gap, 0));
		node.addModel(makeFinger(finger_w, finger2_h[i], finger_w));

		node = node.newNode();
		node.transform(new Matrix().translate(0, finger3_h[i]/2 * gap, 0));
		node.transform(new Matrix().rotateX(-fa * 2));
		node.transform(new Matrix().translate(0, finger2_h[i]/2 * gap, 0));
		node.addModel(makeFinger(finger_w, finger3_h[i], finger_w));

		finger1_ox -= finger_w * fgap;
	}

	const thumb1_h = 2;
	const thumb2_h = 1;

	node = thumb_base_node.newNode();
	node.transform(new Matrix().translate(-finger_w/2, thumb1_h/2, 0));
	node.transform(new Matrix().rotateZ(thumb1 - Math.PI / 2));
	node.transform(new Matrix().translate(finger_w/2, 0, 0));
	node.transform(new Matrix().rotateY(-thumb2));
	node.transform(new Matrix().translate(-finger_w/2, 0, 0));
	node.transform(new Matrix().translate(-palm_w/2 * gap, -palm1_h/3 + thumb1_h/2, 0));
	node.addModel(makeFinger(finger_w, thumb1_h, finger_w));

	node = node.newNode();
	node.transform(new Matrix().translate(0, thumb2_h/2 * gap, 0));
	node.transform(new Matrix().rotateX(-thumb2 * .5));
	node.transform(new Matrix().translate(0, thumb1_h/2 * gap, 0));
	node.addModel(makeFinger(finger_w, thumb2_h, finger_w));

	console.log(`const pose = ${pose}`);

	const model = silhouette.newModel();
	const models = root_node.getModels();
	models.forEach(mdl => { model.merge(mdl); });
	return model;
}

class Node {
	constructor() {
		this.models = [];
		this.nodes = [];
		this.matrix = new Matrix();
		this.parent = undefined;
	}
	
	addModel(model) {
		this.models.push(model);
	}

	getModels() {
		const models = [];
		this.models.forEach(model => {
			const matrices = this.getTransformChain();
			matrices.forEach(matrix => {
				model.transform(matrix);
			});
			models.push(model);
		});
		this.nodes.forEach(node => { 
			models.push(...node.getModels());
		});
		return models;
	}

	getTransformChain() {
		if (this.parent === undefined) return [ this.matrix ];
		return [ this.matrix, ...this.parent.getTransformChain() ];
	}

	newNode() {
		const node = new Node();
		node.parent = this;
		this.nodes.push(node);
		return node;
	}

	transform(matrix) {
		this.matrix.multiply(matrix);
	}
}

// http://paulbourke.net/geometry/polygonise/
const tri_table = [ [], [0, 8, 3], [0, 1, 9], [1, 8, 3, 9, 8, 1], [1, 2, 10], [0, 8, 3, 1, 2, 10], [9, 2, 10, 0, 2, 9], [2, 8, 3, 2, 10, 8, 10, 9, 8], [3, 11, 2], [0, 11, 2, 8, 11, 0], [1, 9, 0, 2, 3, 11], [1, 11, 2, 1, 9, 11, 9, 8, 11], [3, 10, 1, 11, 10, 3], [0, 10, 1, 0, 8, 10, 8, 11, 10], [3, 9, 0, 3, 11, 9, 11, 10, 9], [9, 8, 10, 10, 8, 11], [4, 7, 8], [4, 3, 0, 7, 3, 4], [0, 1, 9, 8, 4, 7], [4, 1, 9, 4, 7, 1, 7, 3, 1], [1, 2, 10, 8, 4, 7], [3, 4, 7, 3, 0, 4, 1, 2, 10], [9, 2, 10, 9, 0, 2, 8, 4, 7], [2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4], [8, 4, 7, 3, 11, 2], [11, 4, 7, 11, 2, 4, 2, 0, 4], [9, 0, 1, 8, 4, 7, 2, 3, 11], [4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1], [3, 10, 1, 3, 11, 10, 7, 8, 4], [1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4], [4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3], [4, 7, 11, 4, 11, 9, 9, 11, 10], [9, 5, 4], [9, 5, 4, 0, 8, 3], [0, 5, 4, 1, 5, 0], [8, 5, 4, 8, 3, 5, 3, 1, 5], [1, 2, 10, 9, 5, 4], [3, 0, 8, 1, 2, 10, 4, 9, 5], [5, 2, 10, 5, 4, 2, 4, 0, 2], [2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8], [9, 5, 4, 2, 3, 11], [0, 11, 2, 0, 8, 11, 4, 9, 5], [0, 5, 4, 0, 1, 5, 2, 3, 11], [2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5], [10, 3, 11, 10, 1, 3, 9, 5, 4], [4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10], [5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3], [5, 4, 8, 5, 8, 10, 10, 8, 11], [9, 7, 8, 5, 7, 9], [9, 3, 0, 9, 5, 3, 5, 7, 3], [0, 7, 8, 0, 1, 7, 1, 5, 7], [1, 5, 3, 3, 5, 7], [9, 7, 8, 9, 5, 7, 10, 1, 2], [10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3], [8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2], [2, 10, 5, 2, 5, 3, 3, 5, 7], [7, 9, 5, 7, 8, 9, 3, 11, 2], [9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11], [2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7], [11, 2, 1, 11, 1, 7, 7, 1, 5], [9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11], [5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0], [11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0], [11, 10, 5, 7, 11, 5], [10, 6, 5], [0, 8, 3, 5, 10, 6], [9, 0, 1, 5, 10, 6], [1, 8, 3, 1, 9, 8, 5, 10, 6], [1, 6, 5, 2, 6, 1], [1, 6, 5, 1, 2, 6, 3, 0, 8], [9, 6, 5, 9, 0, 6, 0, 2, 6], [5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8], [2, 3, 11, 10, 6, 5], [11, 0, 8, 11, 2, 0, 10, 6, 5], [0, 1, 9, 2, 3, 11, 5, 10, 6], [5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11], [6, 3, 11, 6, 5, 3, 5, 1, 3], [0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6], [3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9], [6, 5, 9, 6, 9, 11, 11, 9, 8], [5, 10, 6, 4, 7, 8], [4, 3, 0, 4, 7, 3, 6, 5, 10], [1, 9, 0, 5, 10, 6, 8, 4, 7], [10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4], [6, 1, 2, 6, 5, 1, 4, 7, 8], [1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7], [8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6], [7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9], [3, 11, 2, 7, 8, 4, 10, 6, 5], [5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11], [0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6], [9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6], [8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6], [5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11], [0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7], [6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9], [10, 4, 9, 6, 4, 10], [4, 10, 6, 4, 9, 10, 0, 8, 3], [10, 0, 1, 10, 6, 0, 6, 4, 0], [8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10], [1, 4, 9, 1, 2, 4, 2, 6, 4], [3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4], [0, 2, 4, 4, 2, 6], [8, 3, 2, 8, 2, 4, 4, 2, 6], [10, 4, 9, 10, 6, 4, 11, 2, 3], [0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6], [3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10], [6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1], [9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3], [8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1], [3, 11, 6, 3, 6, 0, 0, 6, 4], [6, 4, 8, 11, 6, 8], [7, 10, 6, 7, 8, 10, 8, 9, 10], [0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10], [10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0], [10, 6, 7, 10, 7, 1, 1, 7, 3], [1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7], [2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9], [7, 8, 0, 7, 0, 6, 6, 0, 2], [7, 3, 2, 6, 7, 2], [2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7], [2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7], [1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11], [11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1], [8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6], [0, 9, 1, 11, 6, 7], [7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0], [7, 11, 6], [7, 6, 11], [3, 0, 8, 11, 7, 6], [0, 1, 9, 11, 7, 6], [8, 1, 9, 8, 3, 1, 11, 7, 6], [10, 1, 2, 6, 11, 7], [1, 2, 10, 3, 0, 8, 6, 11, 7], [2, 9, 0, 2, 10, 9, 6, 11, 7], [6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8], [7, 2, 3, 6, 2, 7], [7, 0, 8, 7, 6, 0, 6, 2, 0], [2, 7, 6, 2, 3, 7, 0, 1, 9], [1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6], [10, 7, 6, 10, 1, 7, 1, 3, 7], [10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8], [0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7], [7, 6, 10, 7, 10, 8, 8, 10, 9], [6, 8, 4, 11, 8, 6], [3, 6, 11, 3, 0, 6, 0, 4, 6], [8, 6, 11, 8, 4, 6, 9, 0, 1], [9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6], [6, 8, 4, 6, 11, 8, 2, 10, 1], [1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6], [4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9], [10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3], [8, 2, 3, 8, 4, 2, 4, 6, 2], [0, 4, 2, 4, 6, 2], [1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8], [1, 9, 4, 1, 4, 2, 2, 4, 6], [8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1], [10, 1, 0, 10, 0, 6, 6, 0, 4], [4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3], [10, 9, 4, 6, 10, 4], [4, 9, 5, 7, 6, 11], [0, 8, 3, 4, 9, 5, 11, 7, 6], [5, 0, 1, 5, 4, 0, 7, 6, 11], [11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5], [9, 5, 4, 10, 1, 2, 7, 6, 11], [6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5], [7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2], [3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6], [7, 2, 3, 7, 6, 2, 5, 4, 9], [9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7], [3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0], [6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8], [9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7], [1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4], [4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10], [7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10], [6, 9, 5, 6, 11, 9, 11, 8, 9], [3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5], [0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11], [6, 11, 3, 6, 3, 5, 5, 3, 1], [1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6], [0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10], [11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5], [6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3], [5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2], [9, 5, 6, 9, 6, 0, 0, 6, 2], [1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8], [1, 5, 6, 2, 1, 6], [1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6], [10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0], [0, 3, 8, 5, 6, 10], [10, 5, 6], [11, 5, 10, 7, 5, 11], [11, 5, 10, 11, 7, 5, 8, 3, 0], [5, 11, 7, 5, 10, 11, 1, 9, 0], [10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1], [11, 1, 2, 11, 7, 1, 7, 5, 1], [0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11], [9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7], [7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2], [2, 5, 10, 2, 3, 5, 3, 7, 5], [8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5], [9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2], [9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2], [1, 3, 5, 3, 7, 5], [0, 8, 7, 0, 7, 1, 1, 7, 5], [9, 0, 3, 9, 3, 5, 5, 3, 7], [9, 8, 7, 5, 9, 7], [5, 8, 4, 5, 10, 8, 10, 11, 8], [5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0], [0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5], [10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4], [2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8], [0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11], [0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5], [9, 4, 5, 2, 11, 3], [2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4], [5, 10, 2, 5, 2, 4, 4, 2, 0], [3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9], [5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2], [8, 4, 5, 8, 5, 3, 3, 5, 1], [0, 4, 5, 1, 0, 5], [8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5], [9, 4, 5], [4, 11, 7, 4, 9, 11, 9, 10, 11], [0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11], [1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11], [3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4], [4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2], [9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3], [11, 7, 4, 11, 4, 2, 2, 4, 0], [11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4], [2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9], [9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7], [3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10], [1, 10, 2, 8, 7, 4], [4, 9, 1, 4, 1, 7, 7, 1, 3], [4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1], [4, 0, 3, 7, 4, 3], [4, 8, 7], [9, 10, 8, 10, 11, 8], [3, 0, 9, 3, 9, 11, 11, 9, 10], [0, 1, 10, 0, 10, 8, 8, 10, 11], [3, 1, 10, 11, 3, 10], [1, 2, 11, 1, 11, 9, 9, 11, 8], [3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9], [0, 2, 11, 8, 0, 11], [3, 2, 11], [2, 3, 8, 2, 8, 10, 10, 8, 9], [9, 10, 2, 0, 9, 2], [2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8], [1, 10, 2], [1, 3, 8, 9, 1, 8], [0, 9, 1], [0, 3, 8], [] ];
const edges = [ [0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [4, 0], [5, 1], [6, 2], [7, 3] ];
const v_xyz = [ [-1, -1, -1], [1, -1, -1], [1, -1, 1], [-1, -1, 1], [-1, 1, -1], [1, 1, -1], [1, 1, 1], [-1, 1, 1] ];

function getTriangles(voxel_grid, grid_size, x, y, z, cxyz, r) {
	var cube_index = 0;
	for (var i=0; i<8; i++) {
		const dx = v_xyz[i][0] * .5 + .5, dy = v_xyz[i][1] * .5 + .5, dz = v_xyz[i][2] * .5 + .5;
		const idx = (x+dx) + (y+dy) * grid_size + (z+dz) * grid_size * grid_size;
		if (idx in voxel_grid && voxel_grid[idx] == 1) {
			cube_index |= 1 << i;
		}
	}
	const polys = [];
	var points = [];
	tri_table[cube_index].forEach( (t, i) => {
		const p0 = add3(mulf(v_xyz[edges[t][0]], r), cxyz);
		const p1 = add3(mulf(v_xyz[edges[t][1]], r), cxyz);
		const point = add3(p0, mulf(sub3(p1, p0), .5));
		points.push(point);
		if (((i+1)%3) == 0) {
			polys.push(points);
			points = [];
		}
	});
	return polys;
}

function makePlanet(radius) {
	const model = makeBox(1, 1, 1);
	model.inside_lines = 0.15;
	model.subdivideCount(15);
	model.faces.forEach(face => {
		for (var i=0; i<face.points.length; i++) {
			face.points[i] = normalize3(face.points[i]);
			const h = heightField3D(face.points[i]);
			face.points[i] = mulf(face.points[i], radius * h);
		}
	});
	return model;
}

function project(op) {
	const p = transform4([op[0], op[2], op[1], 1], viewProjectionMatrix);
	const s = 5 * perspective **3;
	if (p[2] < 0) return undefined;
	return [ p[0]/p[3]*s, -p[1]/p[3]*s, p[2] ];
}

////////////////////////////////////////////////////////////////
// Polygon Clipping utility code - Created by Reinder Nijhoff 2019
// (Polygon binning by Lionel Lemarie 2021)
// https://turtletoy.net/turtle/a5befa1f8d
////////////////////////////////////////////////////////////////
function Polygons(){const t=[],s=25,e=Array.from({length:s**2},t=>[]),n=class{constructor(){this.cp=[],this.dp=[],this.aabb=[]}addPoints(...t){let s=1e5,e=-1e5,n=1e5,h=-1e5;(this.cp=[...this.cp,...t]).forEach(t=>{s=Math.min(s,t[0]),e=Math.max(e,t[0]),n=Math.min(n,t[1]),h=Math.max(h,t[1])}),this.aabb=[s,n,e,h]}addSegments(...t){t.forEach(t=>this.dp.push(t))}addOutline(){for(let t=0,s=this.cp.length;t<s;t++)this.dp.push(this.cp[t],this.cp[(t+1)%s])}draw(t){for(let s=0,e=this.dp.length;s<e;s+=2)t.jump(this.dp[s]),t.goto(this.dp[s+1])}addHatching(t,s){const e=new n;e.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);const h=Math.sin(t)*s,o=Math.cos(t)*s,a=200*Math.sin(t),i=200*Math.cos(t);for(let t=.5;t<150/s;t++)e.dp.push([h*t+i,o*t-a],[h*t-i,o*t+a]),e.dp.push([-h*t+i,-o*t-a],[-h*t-i,-o*t+a]);e.boolean(this,!1),this.dp=[...this.dp,...e.dp]}inside(t){let s=0;for(let e=0,n=this.cp.length;e<n;e++)this.segment_intersect(t,[.1,-1e3],this.cp[e],this.cp[(e+1)%n])&&s++;return 1&s}boolean(t,s=!0){const e=[];for(let n=0,h=this.dp.length;n<h;n+=2){const h=this.dp[n],o=this.dp[n+1],a=[];for(let s=0,e=t.cp.length;s<e;s++){const n=this.segment_intersect(h,o,t.cp[s],t.cp[(s+1)%e]);!1!==n&&a.push(n)}if(0===a.length)s===!t.inside(h)&&e.push(h,o);else{a.push(h,o);const n=o[0]-h[0],i=o[1]-h[1];a.sort((t,s)=>(t[0]-h[0])*n+(t[1]-h[1])*i-(s[0]-h[0])*n-(s[1]-h[1])*i);for(let n=0;n<a.length-1;n++)(a[n][0]-a[n+1][0])**2+(a[n][1]-a[n+1][1])**2>=.001&&s===!t.inside([(a[n][0]+a[n+1][0])/2,(a[n][1]+a[n+1][1])/2])&&e.push(a[n],a[n+1])}}return(this.dp=e).length>0}segment_intersect(t,s,e,n){const h=(n[1]-e[1])*(s[0]-t[0])-(n[0]-e[0])*(s[1]-t[1]);if(0===h)return!1;const o=((n[0]-e[0])*(t[1]-e[1])-(n[1]-e[1])*(t[0]-e[0]))/h,a=((s[0]-t[0])*(t[1]-e[1])-(s[1]-t[1])*(t[0]-e[0]))/h;return o>=0&&o<=1&&a>=0&&a<=1&&[t[0]+o*(s[0]-t[0]),t[1]+o*(s[1]-t[1])]}};return{list:()=>t,create:()=>new n,draw:(n,h,o=!0)=>{reducedPolygonList=function(n){const h={},o=200/s;for(var a=0;a<s;a++){const c=a*o-100,r=[0,c,200,c+o];if(!(n[3]<r[1]||n[1]>r[3]))for(var i=0;i<s;i++){const c=i*o-100;r[0]=c,r[2]=c+o,n[0]>r[2]||n[2]<r[0]||e[i+a*s].forEach(s=>{const e=t[s];n[3]<e.aabb[1]||n[1]>e.aabb[3]||n[0]>e.aabb[2]||n[2]<e.aabb[0]||(h[s]=1)})}}return Array.from(Object.keys(h),s=>t[s])}(h.aabb);for(let t=0;t<reducedPolygonList.length&&h.boolean(reducedPolygonList[t]);t++);h.draw(n),o&&function(n){t.push(n);const h=t.length-1,o=200/s;e.forEach((t,e)=>{const a=e%s*o-100,i=(e/s|0)*o-100,c=[a,i,a+o,i+o];c[3]<n.aabb[1]||c[1]>n.aabb[3]||c[0]>n.aabb[2]||c[2]<n.aabb[0]||t.push(h)})}(h)}}}

////////////////////////////////////////////////////////////////
// Simplex Noise utility code. Created by Reinder Nijhoff 2020
// https://turtletoy.net/turtle/6e4e06d42e
// Based on: http://webstaff.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf
////////////////////////////////////////////////////////////////
function SimplexNoise(t=1){const a=[[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]],o=new Uint8Array(512),r=(Math.sqrt(3)-1)/2,h=1/3,n=(3-Math.sqrt(3))/6,s=1/6,e=(t,a)=>t[0]*a[0]+t[1]*a[1],M=(t,a)=>[t[0]-a[0],t[1]-a[1]],l=(t,a)=>t[0]*a[0]+t[1]*a[1]+t[2]*a[2],c=(t,a)=>[t[0]-a[0],t[1]-a[1],t[2]-a[2]];return new class{constructor(t=1){for(let t=0;t<512;t++)o[t]=255&t;for(let a=0;a<255;a++){const r=(t=this.hash(a+t))%(256-a)+a,h=o[a];o[a+256]=o[a]=o[r],o[r+256]=o[r]=h}}noise2D(t){const h=e(t,[r,r]),s=[Math.floor(t[0]+h),Math.floor(t[1]+h)],l=255&s[0],c=255&s[1],f=e(s,[n,n]),m=M(t,M(s,[f,f])),x=m[0]>m[1]?[1,0]:[0,1],i=M(M(m,x),[-n,-n]),u=M(m,[1-2*n,1-2*n]);let q=Math.max(0,.5-e(m,m))**4*e(a[o[l+o[c]]%12],m);return q+=Math.max(0,.5-e(i,i))**4*e(a[o[l+x[0]+o[c+x[1]]]%12],i),70*(q+=Math.max(0,.5-e(u,u))**4*e(a[o[l+1+o[c+1]]%12],u))}noise3D(t){const r=l(t,[h,h,h]),n=[Math.floor(t[0]+r),Math.floor(t[1]+r),Math.floor(t[2]+r)],e=255&n[0],M=255&n[1],f=255&n[2],m=l(n,[s,s,s]),x=c(t,c(n,[m,m,m])),[i,u]=x[0]>=x[1]?x[1]>=x[2]?[[1,0,0],[1,1,0]]:x[0]>=x[2]?[[1,0,0],[1,0,1]]:[[0,0,1],[1,0,1]]:x[1]<x[2]?[[0,0,1],[0,1,1]]:x[0]<x[2]?[[0,1,0],[0,1,1]]:[[0,1,0],[1,1,0]],q=c(c(x,i),[-s,-s,-s]),w=c(c(x,u),[-2*s,-2*s,-2*s]),D=c(x,[1-3*s,1-3*s,1-3*s]);let p=Math.max(0,.6-l(x,x))**4*l(a[o[e+o[M+o[f]]]%12],x);return p+=Math.max(0,.6-l(q,q))**4*l(a[o[e+i[0]+o[M+i[1]+o[f+i[2]]]]%12],q),p+=Math.max(0,.6-l(w,w))**4*l(a[o[e+u[0]+o[M+u[1]+o[f+u[2]]]]%12],w),32*(p+=Math.max(0,.6-l(D,D))**4*l(a[o[e+1+o[M+1+o[f+1]]]%12],D))}hash(t){const a=1103515245*((t=1103515245*(t>>1^t))^t>>3);return a^a>>16}}(t)}


// Random with seed
var rng;
function random() { if (rng === undefined) rng = new RNG(seed_t); return rng.nextFloat(); }
function RNG(t){return new class{constructor(t){this.m=2147483648,this.a=1103515245,this.c=12345,this.state=t||Math.floor(Math.random()*(this.m-1))}nextFloat(){return this.state=(this.a*this.state+this.c)%this.m,this.state/(this.m-1)}}(t)}

////////////////////////////////////////////////////////////////
// Projection from reinder's https://turtletoy.net/turtle/b3acf08303
let viewProjectionMatrix;
function setupCamera(t,e){const m=lookAt4m(t,e,[0,0,1]),n=perspective4m(.25,1);return multiply4m(n,m)}
function lookAt4m(o,n,r){const s=new Float32Array(16);n=normalize3(sub3(o,n)),r=normalize3(cross3(r,n));const t=normalize3(cross3(n,r));return s[0]=r[0],s[1]=t[0],s[2]=n[0],s[3]=0,s[4]=r[1],s[5]=t[1],s[6]=n[1],s[7]=0,s[8]=r[2],s[9]=t[2],s[10]=n[2],s[11]=0,s[12]=-(r[0]*o[0]+r[1]*o[1]+r[2]*o[2]),s[13]=-(t[0]*o[0]+t[1]*o[1]+t[2]*o[2]),s[14]=-(n[0]*o[0]+n[1]*o[1]+n[2]*o[2]),s[15]=1,s}
function perspective4m(t,n){const e=new Float32Array(16).fill(0,0);return e[5]=1/Math.tan(t/2),e[0]=e[5]/n,e[10]=e[11]=-1,e}
function multiply4m(t,r){const l=new Float32Array(16);for(let n=0;16>n;n+=4)for(let o=0;4>o;o++)l[n+o]=r[n+0]*t[0+o]+r[n+1]*t[4+o]+r[n+2]*t[8+o]+r[n+3]*t[12+o];return l}
function transform4(r,n){const t=new Float32Array(4);for(let o=0;4>o;o++)t[o]=n[o]*r[0]+n[o+4]*r[1]+n[o+8]*r[2]+n[o+12];return t}

function normalize3(a) { const len = len3(a); return scale3(a,len3<0.0001?1:1/len); }
function scale3(a,b) { return [a[0]*b,a[1]*b,a[2]*b]; }
function len3(a) { return Math.sqrt(dot3(a,a)); }
function add3(a,b) { return [a[0]+b[0],a[1]+b[1],a[2]+b[2]]; }
function sub3(a,b) { return [a[0]-b[0],a[1]-b[1],a[2]-b[2]]; }
function dot3(a,b) { return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; }
function cross3(a,b) { return [a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]]; }
function mulf(v, f) { return [v[0]*f,v[1]*f,v[2]*f]; }

////////////////////////////////////////////////////////////////
// Slowpoke utility code. Created by Reinder Nijhoff 2019
// https://turtletoy.net/turtle/cfe9091ad8
////////////////////////////////////////////////////////////////

var slowpoke_draw = 0, slowpoke_skip = 0;

function Slowpoke(x, y) {
    const linesDrawn = {};
    class Slowpoke extends Turtle {
        goto(x, y) {
            const p = Array.isArray(x) ? [...x] : [x, y];
            if (this.isdown()) {
                const o = [this.x(), this.y()];
                const h1 = o[0].toFixed(2)+'_'+p[0].toFixed(2)+o[1].toFixed(2)+'_'+p[1].toFixed(2);
                const h2 = p[0].toFixed(2)+'_'+o[0].toFixed(2)+p[1].toFixed(2)+'_'+o[1].toFixed(2);
                if (linesDrawn[h1] || linesDrawn[h2]) {
                    super.up();
                    super.goto(p);
                    super.down();
                    slowpoke_skip++;
                    return;
                }
                slowpoke_draw++;
                linesDrawn[h1] = linesDrawn[h2] = true;
            } 
            super.goto(p);
        }
    }
    return new Slowpoke(x,y);
}