Faster mountains

Added spatial partitioning (binning) to Reinder's excellent Polygon Clipping utility to improve clipping efficiency.
Compare Faster mountains (variation) to Basic mountains (variation) to experience the difference.

Log in to post a comment.

// Forked from "Basic mountains" by llemarie
// https://turtletoy.net/turtle/cfe3e80c0c

// LL 2021

const seed = 0; // min=0 max=100 step=1

const subdiv_x = 400; // min=1 max=800 step=1
const subdiv_y = 200; // min=1 max=400 step=1

const scale = 150; // min=1 max=300 step=1

const height = 1; // min=0 max=2 step=0.01

const altitude = 0.7; // min=0 max=1 step=0.1

const perspective = 0.2; // min=0 max=1 step=0.01
const isometry = 0.2; // min=-1 max=1 step=0.01

const terrace = 0.0001; // min=0.0001 max=0.3 step=0.0001

const style = 1; // min=0 max=1 step=1 (Fast,Slow)
const show_grid = 0; // min=0 max=1 step=1 (No,Yes)

const offset_y = 30;

Canvas.setpenopacity(1);

const turtle = new Turtle();

var polygons = null;

console.clear();

var mix_t = 1;

class Mountain {
    contructor() {
        this.grid = [];
        this.next_index = 0;
    }
    
    init() {
        const size_x = subdiv_x + 1;
        const size_y = subdiv_y + 1;
        const multx=rng.nextFloat() * 2.5 + 0.5;
        const multy=rng.nextFloat() * 2.5 + 0.5;
        this.grid = Array.from({length: size_x*size_y}, (_,id) => {
            const x = (id % size_x)  / subdiv_x;
            const y = Math.floor(id / size_x) / subdiv_y;
            
            const ax = x * Math.PI * 2;
            const ay = y * Math.PI * 2;
            var peak = 0;
            
            for (var i=1; i<=32; i*=2) {
                peak += Math.cos(ax*i*multx) * Math.cos(ay*i*multy) / i;
            }
            peak *= 0.4;
            peak = smin(peak, 0.3 + rng.nextFloat()*.005, 0.1);
            peak = smax(peak, 0, 0.1);
            const bell_spread = 0.25 * mix_t;
            peak *= bell(x-0.5, 1, 0, bell_spread) * bell(y-0.5, 1, 0, bell_spread);

            const wave_frequency = 6, wave_height = 0.01;
            peak = smax(peak, Math.sin(ax * wave_frequency) * Math.sin(ay * wave_frequency) * wave_height, 0.1);

            peak = Math.round(peak / terrace) * terrace;

            const z = peak * height * mix_t;
            
            return [ x - 0.5, -y, z ];
        });
        this.next_index = 0;
    }
    
    draw() {
        if ((this.next_index % (subdiv_x+1)) == subdiv_x) this.next_index++;

        const cell_count = 1;
        if ((this.next_index + cell_count + subdiv_x + 1) > this.grid.length) return false;

        const points = [];
        for (var j = 0; j <= cell_count; j++) {
            const k = j + this.next_index;

            points.push([ this.grid[k][0], this.grid[k][1], this.grid[k][2] ]);
            points.unshift([ this.grid[k + (subdiv_x+1)][0], this.grid[k + (subdiv_x+1)][1], this.grid[k + (subdiv_x+1)][2] ]);
        }
        this.next_index += cell_count;
        
        points.forEach(p => {
            p[0] *= 1 + perspective * p[1];
            p[0] += isometry * p[1] + isometry * 0.5;
            p[0] *= scale;
            p[1] += 0.5;
            p[1] *= altitude;
            p[1] -= p[2];
            p[1] *= scale;
            p[1] += offset_y;
        });

        if (style == 0) {
            turtle.jump(points[points.length-1]);
            points.forEach(p=>turtle.goto(p));
        } else {
            const p1 = polygons.create();
            p1.addPoints(...points);
            //p1.addHatching(-Math.PI/4, 1);
            if (show_grid) p1.addOutline();
            else  p1.addSegments(p1.cp[0], p1.cp[1]);
            polygons.draw(turtle, p1, true);
        }
        
        return true;
    }
}

function clamp(x, min, max) { return Math.min(max, Math.max(min, x)); }
function mix(x, y, a) { return x * (1-a) + y * a; }
function smin(a, b , s) { var h = clamp( 0.5 + 0.5*(b-a)/s, 0. , 1.); return mix(b, a, h) - h*(1.0-h)*s; }
function smax(a, b , s) { return -smin(-a, -b, s); }

// A: amplitude, B: phase, C: spread
function bell(x, a, b, c) {
    if (c < 0.0001) return 0;
    return a * Math.exp(((x-b)**2) / -(2*(c**2)));
}

const mountain = new Mountain();

function walk(i, t) {
    if (i==0) {
        mix_t = Math.cos(Math.pow(t,0.3)*Math.PI*2) * 0.5 + 0.5;
        polygons = new Polygons();
        mountain.init();
    }

    return mountain.draw();
}

////

function sleep(milliseconds) {
  const date = Date.now();
  let currentDate = null;
  do {
    currentDate = Date.now();
  } while (currentDate - date < milliseconds);
}

//// Random with seed

function RNG(_seed) {
  // LCG using GCC's constants
  this.m = 0x80000000; // 2**31;
  this.a = 1103515245;
  this.c = 12345;

  this.state = _seed ? _seed : Math.floor(Math.random() * (this.m - 1));
}
RNG.prototype.nextFloat = function() {
  // returns in range [0,1]
  this.state = (this.a * this.state + this.c) % this.m;
  return this.state / (this.m - 1);
}

var rng = new RNG(seed);

////////////////////////////////////////////////////////////////
// Polygon Clipping utility code - Created by Reinder Nijhoff 2019
// (Polygon binning by Lionel Lemarie 2021)
// https://turtletoy.net/turtle/a5befa1f8d
////////////////////////////////////////////////////////////////

function Polygons() {
	const fullPolygonList = [];
	const bin_size = 50;
	const bins = Array.from({length: bin_size**2}, (_) => []);
	const Polygon = class {
		constructor() {
			this.cp = [];       // clip path: array of [x,y] pairs
			this.dp = [];       // 2d lines [x0,y0],[x1,y1] to draw
			this.aabb = [];     // AABB bounding box
		}
		addPoints(...points) {
		    // add point to clip path and update bounding box
		    let xmin = 1e5, xmax = -1e5, ymin = 1e5, ymax = -1e5;
			(this.cp = [...this.cp, ...points]).forEach( p => {
				xmin = Math.min(xmin, p[0]), xmax = Math.max(xmax, p[0]);
				ymin = Math.min(ymin, p[1]), ymax = Math.max(ymax, p[1]);
			});
		    this.aabb = [xmin, ymin, xmax, ymax];
		}
		addSegments(...points) {
		    // add segments (each a pair of points)
		    points.forEach(p => this.dp.push(p));
		}
		addOutline() {
			for (let i = 0, l = this.cp.length; i < l; i++) {
				this.dp.push(this.cp[i], this.cp[(i + 1) % l]);
			}
		}
		draw(t) {
			for (let i = 0, l = this.dp.length; i < l; i+=2) {
				t.jump(this.dp[i]), t.goto(this.dp[i + 1]);
			}
		}
		addHatching(a, d) {
			const tp = new Polygon();
			tp.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);
			const dx = Math.sin(a) * d,   dy = Math.cos(a) * d;
			const cx = Math.sin(a) * 200, cy = Math.cos(a) * 200;
			for (let i = 0.5; i < 150 / d; i++) {
				tp.dp.push([dx * i + cy,   dy * i - cx], [dx * i - cy,   dy * i + cx]);
				tp.dp.push([-dx * i + cy, -dy * i - cx], [-dx * i - cy, -dy * i + cx]);
			}
			tp.boolean(this, false);
			this.dp = [...this.dp, ...tp.dp];
		}
		inside(p) {
			let int = 0; // find number of i ntersection points from p to far away
			for (let i = 0, l = this.cp.length; i < l; i++) {
				if (this.segment_intersect(p, [0.1, -1000], this.cp[i], this.cp[(i + 1) % l])) {
					int++;
				}
			}
			return int & 1; // if even your outside
		}
		boolean(p, diff = true) {
			// polygon diff algorithm (narrow phase)
			const ndp = [];
			for (let i = 0, l = this.dp.length; i < l; i+=2) {
				const ls0 = this.dp[i];
				const ls1 = this.dp[i + 1];
				// find all intersections with clip path
				const int = [];
				for (let j = 0, cl = p.cp.length; j < cl; j++) {
					const pint = this.segment_intersect(ls0, ls1, p.cp[j], p.cp[(j + 1) % cl]);
					if (pint !== false) {
						int.push(pint);
					}
				}
				if (int.length === 0) {
					// 0 intersections, inside or outside?
					if (diff === !p.inside(ls0)) {
						ndp.push(ls0, ls1);
					}
				} else {
					int.push(ls0, ls1);
					// order intersection points on line ls.p1 to ls.p2
					const cmpx = ls1[0] - ls0[0];
					const cmpy = ls1[1] - ls0[1];
					int.sort( (a,b) =>  (a[0] - ls0[0]) * cmpx + (a[1] - ls0[1]) * cmpy - 
					                    (b[0] - ls0[0]) * cmpx - (b[1] - ls0[1]) * cmpy);
					 
					for (let j = 0; j < int.length - 1; j++) {
						if ((int[j][0] - int[j+1][0])**2 + (int[j][1] - int[j+1][1])**2 >= 0.001) {
							if (diff === !p.inside([(int[j][0]+int[j+1][0])/2,(int[j][1]+int[j+1][1])/2])) {
								ndp.push(int[j], int[j+1]);
							}
						}
					}
				}
			}
			return (this.dp = ndp).length > 0;
		}
		//port of http://paulbourke.net/geometry/pointlineplane/Helpers.cs
		segment_intersect(l1p1, l1p2, l2p1, l2p2) {
			const d   = (l2p2[1] - l2p1[1]) * (l1p2[0] - l1p1[0]) - (l2p2[0] - l2p1[0]) * (l1p2[1] - l1p1[1]);
			if (d === 0) return false;
			const n_a = (l2p2[0] - l2p1[0]) * (l1p1[1] - l2p1[1]) - (l2p2[1] - l2p1[1]) * (l1p1[0] - l2p1[0]);
			const n_b = (l1p2[0] - l1p1[0]) * (l1p1[1] - l2p1[1]) - (l1p2[1] - l1p1[1]) * (l1p1[0] - l2p1[0]);
			const ua = n_a / d;
			const ub = n_b / d;
			if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
				return [l1p1[0] + ua * (l1p2[0] - l1p1[0]), l1p1[1] + ua * (l1p2[1] - l1p1[1])];
			}
			return false;
		}
	};
	function getBinnedPolygonList(aabb) {
	    const indexList = {};
	    const width = 200 / bin_size;
	    for (var bi=0; bi<bin_size; bi++) {
	        const y = bi * width - 100;
	        const b_aabb = [0, y, 200, y + width];
            if (aabb[3]<b_aabb[1] || aabb[1]>b_aabb[3]) continue;
	        for (var bj=0; bj<bin_size; bj++) {
    	        const x = bj * width - 100;
    	        b_aabb[0] = x, b_aabb[2] = x + width;
                if (aabb[0]>b_aabb[2] || aabb[2]<b_aabb[0]) continue;
	            bins[bj + bi * bin_size].forEach(id => {
        	        const p = fullPolygonList[id];
                    if (!(aabb[3]<p.aabb[1] || aabb[1]>p.aabb[3] || aabb[0]>p.aabb[2] || aabb[2]<p.aabb[0])) {
                        indexList[id] = 1;
                    }
        	    });
    	    }
	    }
	    const reducedPolygonList = Array.from(Object.keys(indexList), id => fullPolygonList[id]);
	    return reducedPolygonList;
	};
	function addToBins(p) {
	    fullPolygonList.push(p);
	    const id = fullPolygonList.length - 1, width = 200 / bin_size;
	    bins.forEach((b,i) => {
	        const x = (i % bin_size) * width - 100;
	        const y = (i / bin_size | 0)  * width - 100;
	        const aabb = [x, y, x + width, y + width];
            if (!(aabb[3]<p.aabb[1] || aabb[1]>p.aabb[3] || aabb[0]>p.aabb[2] || aabb[2]<p.aabb[0])) {
    	        b.push(id);
            }
	    });
	};
	return {
		list: () => fullPolygonList,
		create: () => new Polygon(),
		draw: (turtle, p, addToVisList=true) => {
		    reducedPolygonList = getBinnedPolygonList(p.aabb);
			for (let j = 0; j < reducedPolygonList.length && p.boolean(reducedPolygonList[j]); j++);
			p.draw(turtle);
			if (addToVisList) addToBins(p);
		}
	};
}