diff --git a/src/index.html b/src/index.html index 9e59676..59c8494 100644 --- a/src/index.html +++ b/src/index.html @@ -6,6 +6,48 @@ Map Generator + + diff --git a/src/libs/dual-mesh-create.mjs b/src/libs/dual-mesh-create.mjs new file mode 100644 index 0000000..d1b933a --- /dev/null +++ b/src/libs/dual-mesh-create.mjs @@ -0,0 +1,241 @@ +/* + * From https://github.com/redblobgames/dual-mesh + * Copyright 2017 Red Blob Games + * License: Apache v2.0 + * + * Generate a random triangle mesh for the area 0 <= x <= 1000, 0 <= y <= 1000 + * + * This program runs on the command line (node) + */ + +'use strict'; + +import Delaunator from 'https://cdn.skypack.dev/delaunator@5.0.0'; // ISC licensed +import TriangleMesh from './dual-mesh.mjs' + +function s_next_s(s) { return (s % 3 == 2) ? s-2 : s+1; } + + +function checkPointInequality({_r_vertex, _triangles, _halfedges}) { + // TODO: check for collinear vertices. Around each red point P if + // there's a point Q and R both connected to it, and the angle P→Q and + // the angle P→R are 180° apart, then there's collinearity. This would + // indicate an issue with point selection. +} + + +function checkTriangleInequality({_r_vertex, _triangles, _halfedges}) { + // check for skinny triangles + const badAngleLimit = 30; + let summary = new Array(badAngleLimit).fill(0); + let count = 0; + for (let s = 0; s < _triangles.length; s++) { + let r0 = _triangles[s], + r1 = _triangles[s_next_s(s)], + r2 = _triangles[s_next_s(s_next_s(s))]; + let p0 = _r_vertex[r0], + p1 = _r_vertex[r1], + p2 = _r_vertex[r2]; + let d0 = [p0[0]-p1[0], p0[1]-p1[1]]; + let d2 = [p2[0]-p1[0], p2[1]-p1[1]]; + let dotProduct = d0[0] * d2[0] + d0[1] + d2[1]; + let angleDegrees = 180 / Math.PI * Math.acos(dotProduct); + if (angleDegrees < badAngleLimit) { + summary[angleDegrees|0]++; + count++; + } + } + // NOTE: a much faster test would be the ratio of the inradius to + // the circumradius, but as I'm generating these offline, I'm not + // worried about speed right now + + // TODO: consider adding circumcenters of skinny triangles to the point set + if (count > 0) { + console.warn(' bad angles:', summary.join(" ")); + } +} + + +function checkMeshConnectivity({_r_vertex, _triangles, _halfedges}) { + // 1. make sure each side's opposite is back to itself + // 2. make sure region-circulating starting from each side works + let ghost_r = _r_vertex.length - 1, out_s = []; + for (let s0 = 0; s0 < _triangles.length; s0++) { + if (_halfedges[_halfedges[s0]] !== s0) { + console.warn(`FAIL _halfedges[_halfedges[${s0}]] !== ${s0}`); + } + let s = s0, count = 0; + out_s.length = 0; + do { + count++; out_s.push(s); + s = s_next_s(_halfedges[s]); + if (count > 100 && _triangles[s0] !== ghost_r) { + console.warn(`FAIL to circulate around region with start side=${s0} from region ${_triangles[s0]} to ${_triangles[s_next_s(s0)]}, out_s=${out_s}`); + break; + } + } while (s !== s0); + } +} + + +/* + * Add vertices evenly along the boundary of the mesh; + * use a slight curve so that the Delaunay triangulation + * doesn't make long thing triangles along the boundary. + * These points also prevent the Poisson disc generator + * from making uneven points near the boundary. + */ +function addBoundaryPoints(width, height) { + const points = [[0, 0], [0, height - 1], [width - 1, 0], [width - 1, height - 1]]; + + for(let i = 0; i < width - 1; i++) + points.push([i, 0], [i, width - 1]); + for(let i = 0; i < height - 1; i++) + points.push([0, i], [height - 1, i]); + + return points; +} + + +function addGhostStructure({_r_vertex, _triangles, _halfedges}, width, height) { + const numSolidSides = _triangles.length; + const ghost_r = _r_vertex.length; + + let numUnpairedSides = 0, firstUnpairedEdge = -1; + let r_unpaired_s = []; // seed to side + for (let s = 0; s < numSolidSides; s++) { + if (_halfedges[s] === -1) { + numUnpairedSides++; + r_unpaired_s[_triangles[s]] = s; + firstUnpairedEdge = s; + } + } + + let r_newvertex = _r_vertex.concat([[width / 2, height / 2]]); + let s_newstart_r = new Int32Array(numSolidSides + 3 * numUnpairedSides); + s_newstart_r.set(_triangles); + let s_newopposite_s = new Int32Array(numSolidSides + 3 * numUnpairedSides); + s_newopposite_s.set(_halfedges); + + for (let i = 0, s = firstUnpairedEdge; + i < numUnpairedSides; + i++, s = r_unpaired_s[s_newstart_r[s_next_s(s)]]) { + + // Construct a ghost side for s + let ghost_s = numSolidSides + 3 * i; + s_newopposite_s[s] = ghost_s; + s_newopposite_s[ghost_s] = s; + s_newstart_r[ghost_s] = s_newstart_r[s_next_s(s)]; + + // Construct the rest of the ghost triangle + s_newstart_r[ghost_s + 1] = s_newstart_r[s]; + s_newstart_r[ghost_s + 2] = ghost_r; + let k = numSolidSides + (3 * i + 4) % (3 * numUnpairedSides); + s_newopposite_s[ghost_s + 2] = k; + s_newopposite_s[k] = ghost_s + 2; + } + + return { + numSolidSides, + _r_vertex: r_newvertex, + _triangles: s_newstart_r, + _halfedges: s_newopposite_s + }; +} + + + +/** + * Build a dual mesh from points, with ghost triangles around the exterior. + * + * The builder assumes 0 ≤ x < 1000, 0 ≤ y < 1000 + * + * Options: + * - To have equally spaced points added around the 1000x1000 boundary, + * pass in boundarySpacing > 0 with the spacing value. If using Poisson + * disc points, I recommend 1.5 times the spacing used for Poisson disc. + * + * Phases: + * - Your own set of points + * - Poisson disc points + * + * The mesh generator runs some sanity checks but does not correct the + * generated points. + * + * Examples: + * + * Build a mesh with poisson disc points and a boundary: + * + * new MeshBuilder({boundarySpacing: 150}) + * .addPoisson(Poisson, 100) + * .create() + */ +class MeshBuilder { + /** If boundarySpacing > 0 there will be a boundary added around the 1000x1000 area */ + constructor (width, height) { + let boundaryPoints = addBoundaryPoints(width, height); + this.width = width; + this.height = height; + this.points = boundaryPoints; + this.numBoundaryRegions = boundaryPoints.length; + } + + /** Points should be [x, y] */ + addPoints(newPoints) { + for (let p of newPoints) { + this.points.push(p); + } + return this; + } + + /** Points will be [x, y] */ + getNonBoundaryPoints() { + return this.points.slice(this.numBoundaryRegions); + } + + /** (used for more advanced mixing of different mesh types) */ + clearNonBoundaryPoints() { + this.points.splice(this.numBoundaryRegions, this.points.length); + return this; + } + + /** Pass in the constructor from the poisson-disk-sampling module */ + addPoisson(Poisson, random=Math.random) { + let generator = new Poisson({ + shape: [this.width - 1, this.height - 1], + minDistance: 1, + maxDistance: 1, + }, random); + this.points.forEach(p => generator.addPoint(p)); + this.points = generator.fill(); + return this; + } + + /** Build and return a TriangleMesh */ + create(runChecks=false) { + // TODO: use Float32Array instead of this, so that we can + // construct directly from points read in from a file + let delaunator = Delaunator.from(this.points); + let graph = { + _r_vertex: this.points, + _triangles: delaunator.triangles, + _halfedges: delaunator.halfedges + }; + + if (runChecks) { + checkPointInequality(graph); + checkTriangleInequality(graph); + } + + graph = addGhostStructure(graph); + graph.numBoundaryRegions = this.numBoundaryRegions; + if (runChecks) { + checkMeshConnectivity(graph); + } + + return new TriangleMesh(graph); + } +} + + +export default MeshBuilder; \ No newline at end of file diff --git a/src/libs/dual-mesh.mjs b/src/libs/dual-mesh.mjs new file mode 100644 index 0000000..c680ab6 --- /dev/null +++ b/src/libs/dual-mesh.mjs @@ -0,0 +1,204 @@ +/* + * From https://github.com/redblobgames/dual-mesh + * Copyright 2017 Red Blob Games + * License: Apache v2.0 + */ + +'use strict'; + +/** + * Represent a triangle-polygon dual mesh with: + * - Regions (r) + * - Sides (s) + * - Triangles (t) + * + * Each element has an id: + * - 0 <= r < numRegions + * - 0 <= s < numSides + * - 0 <= t < numTriangles + * + * Naming convention: x_name_y takes x (r, s, t) as input and produces + * y (r, s, t) as output. If the output isn't a mesh index (r, s, t) + * then the _y suffix is omitted. + * + * A side is directed. If two triangles t0, t1 are adjacent, there will + * be two sides representing the boundary, one for t0 and one for t1. These + * can be accessed with s_inner_t and s_outer_t. + * + * A side also represents the boundary between two regions. If two regions + * r0, r1 are adjacent, there will be two sides representing the boundary, + * s_begin_r and s_end_r. + * + * Each side will have a pair, accessed with s_opposite_s. + * + * If created using the functions in create.js, the mesh has no + * boundaries; it wraps around the "back" using a "ghost" region. Some + * regions are marked as the boundary; these are connected to the + * ghost region. Ghost triangles and ghost sides connect these + * boundary regions to the ghost region. Elements that aren't "ghost" + * are called "solid". + */ +class TriangleMesh { + static s_to_t(s) { return (s/3) | 0; } + static s_prev_s(s) { return (s % 3 === 0) ? s+2 : s-1; } + static s_next_s(s) { return (s % 3 === 2) ? s-2 : s+1; } + + /** + * Constructor takes partial mesh information and fills in the rest; the + * partial information is generated in create.js or in fromDelaunator. + */ + constructor ({numBoundaryRegions, numSolidSides, _r_vertex, _triangles, _halfedges}) { + Object.assign(this, {numBoundaryRegions, numSolidSides, + _r_vertex, _triangles, _halfedges}); + this._t_vertex = []; + this._update(); + } + + /** + * Update internal data structures from Delaunator + */ + update(points, delaunator) { + this._r_vertex = points; + this._triangles = delaunator.triangles; + this._halfedges = delaunator.halfedges; + this._update(); + } + + /** + * Update internal data structures to match the input mesh. + * + * Use if you have updated the triangles/halfedges with Delaunator + * and want the dual mesh to match the updated data. Note that + * this DOES not update boundary regions or ghost elements. + */ + _update() { + let {_triangles, _halfedges, _r_vertex, _t_vertex} = this; + + this.numSides = _triangles.length; + this.numRegions = _r_vertex.length; + this.numSolidRegions = this.numRegions - 1; // TODO: only if there are ghosts + this.numTriangles = this.numSides / 3; + this.numSolidTriangles = this.numSolidSides / 3; + + if (this._t_vertex.length < this.numTriangles) { + // Extend this array to be big enough + const numOldTriangles = _t_vertex.length; + const numNewTriangles = this.numTriangles - numOldTriangles; + _t_vertex = _t_vertex.concat(new Array(numNewTriangles)); + for (let t = numOldTriangles; t < this.numTriangles; t++) { + _t_vertex[t] = [0, 0]; + } + this._t_vertex = _t_vertex; + } + + // Construct an index for finding sides connected to a region + this._r_in_s = new Int32Array(this.numRegions); + for (let s = 0; s < _triangles.length; s++) { + let endpoint = _triangles[TriangleMesh.s_next_s(s)]; + if (this._r_in_s[endpoint] === 0 || _halfedges[s] === -1) { + this._r_in_s[endpoint] = s; + } + } + + // Construct triangle coordinates + for (let s = 0; s < _triangles.length; s += 3) { + let t = s/3, + a = _r_vertex[_triangles[s]], + b = _r_vertex[_triangles[s+1]], + c = _r_vertex[_triangles[s+2]]; + if (this.s_ghost(s)) { + // ghost triangle center is just outside the unpaired side + let dx = b[0]-a[0], dy = b[1]-a[1]; + let scale = 10 / Math.sqrt(dx*dx + dy*dy); // go 10units away from side + _t_vertex[t][0] = 0.5 * (a[0] + b[0]) + dy*scale; + _t_vertex[t][1] = 0.5 * (a[1] + b[1]) - dx*scale; + } else { + // solid triangle center is at the centroid + _t_vertex[t][0] = (a[0] + b[0] + c[0])/3; + _t_vertex[t][1] = (a[1] + b[1] + c[1])/3; + } + } + } + + /** + * Construct a DualMesh from a Delaunator object, without any + * additional boundary regions. + */ + static fromDelaunator(points, delaunator) { + return new TriangleMesh({ + numBoundaryRegions: 0, + numSolidSides: delaunator.triangles.length, + _r_vertex: points, + _triangles: delaunator.triangles, + _halfedges: delaunator.halfedges, + }); + } + + + r_x(r) { return this._r_vertex[r][0]; } + r_y(r) { return this._r_vertex[r][1]; } + t_x(r) { return this._t_vertex[r][0]; } + t_y(r) { return this._t_vertex[r][1]; } + r_pos(out, r) { out.length = 2; out[0] = this.r_x(r); out[1] = this.r_y(r); return out; } + t_pos(out, t) { out.length = 2; out[0] = this.t_x(t); out[1] = this.t_y(t); return out; } + + s_begin_r(s) { return this._triangles[s]; } + s_end_r(s) { return this._triangles[TriangleMesh.s_next_s(s)]; } + + s_inner_t(s) { return TriangleMesh.s_to_t(s); } + s_outer_t(s) { return TriangleMesh.s_to_t(this._halfedges[s]); } + + s_next_s(s) { return TriangleMesh.s_next_s(s); } + s_prev_s(s) { return TriangleMesh.s_prev_s(s); } + + s_opposite_s(s) { return this._halfedges[s]; } + + t_circulate_s(out_s, t) { out_s.length = 3; for (let i = 0; i < 3; i++) { out_s[i] = 3*t + i; } return out_s; } + t_circulate_r(out_r, t) { out_r.length = 3; for (let i = 0; i < 3; i++) { out_r[i] = this._triangles[3*t+i]; } return out_r; } + t_circulate_t(out_t, t) { out_t.length = 3; for (let i = 0; i < 3; i++) { out_t[i] = this.s_outer_t(3*t+i); } return out_t; } + + r_circulate_s(out_s, r) { + const s0 = this._r_in_s[r]; + let incoming = s0; + out_s.length = 0; + do { + out_s.push(this._halfedges[incoming]); + let outgoing = TriangleMesh.s_next_s(incoming); + incoming = this._halfedges[outgoing]; + } while (incoming !== -1 && incoming !== s0); + return out_s; + } + + r_circulate_r(out_r, r) { + const s0 = this._r_in_s[r]; + let incoming = s0; + out_r.length = 0; + do { + out_r.push(this.s_begin_r(incoming)); + let outgoing = TriangleMesh.s_next_s(incoming); + incoming = this._halfedges[outgoing]; + } while (incoming !== -1 && incoming !== s0); + return out_r; + } + + r_circulate_t(out_t, r) { + const s0 = this._r_in_s[r]; + let incoming = s0; + out_t.length = 0; + do { + out_t.push(TriangleMesh.s_to_t(incoming)); + let outgoing = TriangleMesh.s_next_s(incoming); + incoming = this._halfedges[outgoing]; + } while (incoming !== -1 && incoming !== s0); + return out_t; + } + + ghost_r() { return this.numRegions - 1; } + s_ghost(s) { return s >= this.numSolidSides; } + r_ghost(r) { return r === this.numRegions - 1; } + t_ghost(t) { return this.s_ghost(3 * t); } + s_boundary(s) { return this.s_ghost(s) && (s % 3 === 0); } + r_boundary(r) { return r < this.numBoundaryRegions; } +} + +export default TriangleMesh; \ No newline at end of file diff --git a/src/main.mjs b/src/main.mjs index db23d9e..9ed5d03 100644 --- a/src/main.mjs +++ b/src/main.mjs @@ -2,6 +2,7 @@ import { Thread, supportThreads } from "./utils/workerUtils.mjs"; import Renderer from "./modules/renderer/renderer.mjs"; import Noise from '../libs/alea.mjs' import Delaunator from 'https://cdn.skypack.dev/delaunator@5.0.0'; +import {Delaunay, Voronoi} from "https://cdn.skypack.dev/d3-delaunay@6"; (async function() { @@ -10,20 +11,45 @@ import Delaunator from 'https://cdn.skypack.dev/delaunator@5.0.0'; globalThis.TIMINGS = DEBUG && localStorage.getItem("timings"); globalThis.RELEASE = !DEBUG; + async function applySettings(settings) + { + await thread.global.updateSettings(settings); + + await thread.model.generateTriangles(); + + await render(); + window.debug = await thread.debug.getAll(); + } + async function render() + { + const view = await thread.view.getRenderValues(window.innerWidth, window.innerHeight); + Renderer.resize(); + Renderer.initGrid(view.position, view.vertices, view.centers, view.lines, view.centroids); + } + + if(globalThis.DEBUG) + { + window.applySettings = applySettings; + window.Delaunator = Delaunator; + window.Delaunay = Delaunay; + window.Voronoi = Voronoi; + window.settings = { model: { x: 10, y: 10, seed: 0, relax: 0, jitter: 0.3, }, }; + } + else + { + const settings = { model: { x: 3, y: 3, seed: 0, relax: 0, jitter: 0.25, }, }; + } + const thread = new Thread("./workers/base.mjs", "model", true); await thread.setup(); - //await new Promise(r => setTimeout(r, 3000)); - - const settings = { model: { x: 30, y: 30, seed: 0, jitter: 0.1, }, view: { width: window.innerWidth, height: window.innerHeight, }, }; - await thread.global.updateSettings(settings); - - await thread.model.generateTriangles(); - - const view = await thread.view.lerpToViewport(); - - window.debug = await thread.debug.getAll(); - + window.addEventListener("resize", render); Renderer.init(); - Renderer.initGrid(undefined, view.position, view.vertices); -})(); + + do + { + //window.settings.model.jitter = 0.5 + (1 + Math.sin(Renderer._clock.getElapsedTime()))/2; + //window.settings.model.relax = Math.round(1 + Math.sin(Renderer._clock.getElapsedTime())); + await applySettings(settings); + } while(false); +})(); \ No newline at end of file diff --git a/src/modules/renderer/renderer.mjs b/src/modules/renderer/renderer.mjs index 228ca00..98c6bc0 100644 --- a/src/modules/renderer/renderer.mjs +++ b/src/modules/renderer/renderer.mjs @@ -20,18 +20,33 @@ class Renderer this._renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(this._renderer.domElement); + console.log(this._renderer.capabilities.isWebGL2); + this._width = window.innerWidth; this._height = window.innerHeight; this._clock = new Three.Clock(); this._scene = new Three.Scene(); this._camera = new Three.OrthographicCamera(0, this._width, 0, this._height, 0.1, 1000); - this._camera.zoom = 0.98; + this._camera.zoom = 0.8; this._camera.updateProjectionMatrix(); + this._vertex = document.getElementById("vertexShader").textContent; + this._fragment = document.getElementById("fragmentShader").textContent; + Renderer.render(); } } + static resize() + { + this._renderer.setSize(window.innerWidth, window.innerHeight); + + this._width = window.innerWidth; + this._height = window.innerHeight; + this._camera.right = this._width; + this._camera.bottom = this._height; + this._camera.updateProjectionMatrix(); + } static render() { if(this._threaded) @@ -44,7 +59,7 @@ class Renderer this._renderer.render(this._scene, this._camera); } - static async initGrid(centers, positions, vertices) //Share the list of cell and the resolution in the model with the view + static async initGrid(positions, vertices, centers, lines, centroids) //Share the list of cell and the resolution in the model with the view { if(this._threaded) { @@ -53,22 +68,57 @@ class Renderer } else { - /*const centerGeometry = new Three.BufferGeometry(); - centerGeometry.setAttribute('position', new Three.Float32BufferAttribute( centers, 3)); -*/ - const geometry = new Three.BufferGeometry(); + this._scene.clear(); + + /*const geometry = new Three.BufferGeometry(); geometry.setIndex( new Three.Uint32BufferAttribute( vertices, 1 ) ); - geometry.setAttribute('position', new Three.Float32BufferAttribute( positions, 3)); + geometry.setAttribute('position', new Three.Float32BufferAttribute( positions, 3 )); - const material = new Three.MeshBasicMaterial( { color: 0xff0000, wireframe: true, } ); + const material = new Three.MeshBasicMaterial( { color: 0x00ff00, wireframe: true, } ); const mesh = new Three.Mesh( geometry, material ); - this._scene.add( mesh ); + this._scene.add( mesh );*/ - const points = new Three.Points( geometry, new Three.PointsMaterial( { size: 3.0 } ) ); - this._scene.add( points ); + /*const points = new Three.Points( geometry, new Three.PointsMaterial( { size: 3.0 } ) ); + this._scene.add( points );*/ - /*const center = new Three.Points( centerGeometry, new Three.PointsMaterial( { color: 0xffbb00, size: 2.0 } ) ); - this._scene.add( center );*/ + /*if(lines !== undefined) + { + const lineGeometry = new Three.BufferGeometry(); + lineGeometry.setAttribute('position', new Three.Float32BufferAttribute( lines, 3 )); + + const line = new Three.LineSegments( lineGeometry ); + this._scene.add( line ); + }*/ + + /*if(centers !== undefined) + { + const centerGeometry = new Three.BufferGeometry(); + centerGeometry.setAttribute('position', new Three.Float32BufferAttribute( centers, 3 )); + + const center = new Three.Points( centerGeometry, new Three.PointsMaterial( { color: 0x0000ff, size: 4.0 } ) ); + this._scene.add( center ); + } + + if(centroids !== undefined) + { + const centroidsGeometry = new Three.BufferGeometry(); + centroidsGeometry.setAttribute('position', new Three.Float32BufferAttribute( centroids, 3 )); + + const centroid = new Three.Points( centroidsGeometry, new Three.PointsMaterial( { color: 0xff0000, size: 5.0 } ) ); + this._scene.add( centroid ); + }*/ + + this._quad = new Three.PlaneGeometry(1, 1); + this._shader = new Three.RawShaderMaterial({ + attributes: { + + }, vertexShader: this._vertex, + fragmentShader: this._fragment, + }); + + this._mesh = new Three.InstancedMesh(this._quad, this._shader); + + this._scene.add(this._mesh); } } } diff --git a/src/workers/base.mjs b/src/workers/base.mjs index 78a9b63..e9148c7 100644 --- a/src/workers/base.mjs +++ b/src/workers/base.mjs @@ -1,7 +1,11 @@ import { Process } from "../utils/workerUtils.mjs"; import Noise from '../libs/alea.mjs'; import Delaunator from 'https://cdn.skypack.dev/delaunator@5.0.0'; +import {Delaunay, Voronoi} from "https://cdn.skypack.dev/d3-delaunay@6"; +import MeshBuilder from '../libs/dual-mesh-create.mjs'; +import Poisson from 'https://cdn.skypack.dev/poisson-disk-sampling'; +const OFFSET = 1; let settings = {}; const model = {}; @@ -15,51 +19,181 @@ function inverse_lerp(a, b, x) { return (x - a) / (b - a); } +function circumcenter(i) +{ + const ta = model.delaunay.triangles[3 * i ]; + const tb = model.delaunay.triangles[3 * i + 1]; + const tc = model.delaunay.triangles[3 * i + 2]; -//Settings model contains x, y, seed, jitter + const a = [model.delaunay.coords[2 * ta ], model.delaunay.coords[2 * ta + 1]]; + const b = [model.delaunay.coords[2 * tb ], model.delaunay.coords[2 * tb + 1]]; + const c = [model.delaunay.coords[2 * tc ], model.delaunay.coords[2 * tc + 1]]; + + const ad = a[0] * a[0] + a[1] * a[1]; + const bd = b[0] * b[0] + b[1] * b[1]; + const cd = c[0] * c[0] + c[1] * c[1]; + const D = 2 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1])); + return [ + 1 / D * (ad * (b[1] - c[1]) + bd * (c[1] - a[1]) + cd * (a[1] - b[1])), + 1 / D * (ad * (c[0] - b[0]) + bd * (a[0] - c[0]) + cd * (b[0] - a[0])), + ]; +} +function edgesAroundPoint(i) +{ + const result = []; + let incoming = i; + do + { + result.push(Math.floor(incoming / 3)); + const outgoing = (incoming % 3 === 2) ? incoming - 2 : incoming + 1; + incoming = model.delaunay.halfedges[outgoing]; + } while (incoming !== -1 && incoming !== i); + return result; +} + +//Settings model contains x, y, seed, jitter and relax function generateTriangles() { const rand = Noise(settings.model.seed); const grid = new Float32Array(settings.model.x * settings.model.y * 2); - for(let i = grid.length * 2 - 2; i > 0; i -= 2) + for(let i = grid.length - 2; i >= 0; i -= 2) { const _x = i / 2 % settings.model.x; const _y = (i / 2 - _x) / settings.model.x; const angle = lerp(0, Math.PI * 2, rand()); - const magnitude = lerp(0, settings.model.jitter, 1); + const magnitude = lerp(0, settings.model.jitter, rand()); grid[i ] = _x + Math.sin(angle) * magnitude; grid[i + 1] = _y + Math.cos(angle) * magnitude; } - model.delaunay = new Delaunator(grid); + model.test = { + poisson_grid: new MeshBuilder(10, settings.model.x, settings.model.y).addPoisson(Poisson, rand).create() + }; + /*model.d3 = {}; + model.d3.delaunay = new Delaunay(grid); + model.d3.voronoi = model.d3.delaunay.voronoi([-OFFSET, -OFFSET, settings.model.x - 1 + OFFSET, settings.model.y - 1 + OFFSET]); - //model.voronoi = model.delaunay.voronoi([-1, -1, settings.model.x + 1, settings.model.y + 1]); - //model.cells = []; - - /*console.group("Cells"); - let cell = model.voronoi.cellPolygons(); - for(let i = 0, n = (model.delaunay.points.length / 2); i < n; i++) + for(let i = 0; i < settings.model.relax; i++) { - const cells = model.voronoi._cell(i); - const val = cell.next().value.flat(); - console.log(val, cells); - model.cells.push(...val); + model.d3.delaunay = new Delaunay(new_relax(model.d3.delaunay)); + model.d3.voronoi = model.d3.delaunay.voronoi([-OFFSET, -OFFSET, settings.model.x - 1 + OFFSET, settings.model.y - 1 + OFFSET]); } - console.groupEnd("Cells");*/ + + model.d3.lines = model.d3.voronoi.render().split(/M/).slice(1).flatMap(e => e.split(/L/)).flatMap(e => e.split(/,/)).map(parseFloat); + + view.triangulation = new Delaunay(model.d3.voronoi.circumcenters);*/ +} + +function buildVoronoi(delaunay) +{ + const circumcenters = new Float32Array(delaunay.trianglesLen / 3 * 2); + const lines = []; + const voronoi = []; + const triangles = delaunay.triangles; + const coords = delaunay.coords; + + //For each voronoi cells, get the circumcenter + for(let i = 0; i < triangles.length; i++) + { + if(i < triangles.length / 3) + { + const center = circumcenter(i); + + circumcenters[2 * i ] = center[0]; + circumcenters[2 * i + 1] = center[1]; + } + + if(i < delaunay.halfedges[i]) + { + lines.push(Math.floor(i / 3), Math.floor(delaunay.halfedges[i] / 3)); + } + } + + return { circumcenters: circumcenters, cells: voronoi, lines: lines, centroids: undefined }; +} + +function centroid(arr) +{ + const size = arr.length / 2; + + let sum = 0; + let x = 0; + let y = 0; + + for(let i = 0, j = size - 1; i < size; j = i++) + { + const temp = arr[2 * i + 0] * arr[2 * j + 1] - arr[2 * j + 0] * arr[2 * i + 1]; + sum += temp; + x += (arr[2 * i + 0] + arr[2 * j + 0]) * temp; + y += (arr[2 * i + 1] + arr[2 * j + 1]) * temp; + } + + if(Math.abs(sum) < 1e-7) + return [0, 0]; + + sum *= 3; + return [x / sum, y / sum]; +} + +function new_relax(delaunay) +{ + const centroids = new Float32Array(delaunay.points.length); + const voronoi = model.d3.voronoi; + + for(let i = 0, n = delaunay.points.length / 2; i < n; ++i) + { + const cell = voronoi._clip(i); + + const c = centroid(cell); + + centroids[2 * i ] = c[0]; + centroids[2 * i + 1] = c[1]; + } + + voronoi.delaunay.points = centroids; + voronoi.update(); + + return centroids; } function relax(delaunay) { - delaunay.coords = new Float32Array(delaunay.coords.length); - //For each voronoi cells, get the centroid - for(let i = 0; i < delaunay.trianglesLen; i++) + const centroids = new Float32Array(delaunay.coords.length); + const circumcenters = new Float32Array(delaunay.trianglesLen / 3 * 2); + //For each voronoi cells, get the circumcenter + for(let i = 0; i < delaunay.trianglesLen / 3; i++) { + const center = circumcenter(i); + circumcenters[2 * i ] = center[0]; + circumcenters[2 * i + 1] = center[1]; } - delaunay.coords = + + //For each point, get the centroid + for(let i = 0; i < delaunay.coords.length / 2; i++) + { + const triangles = edgesAroundPoint(i); + + const c = centroid(triangles.flatMap(e => [circumcenters[2 * e + 0], circumcenters[2 * e + 1]])); + + if(c[0] === 0 && c[1] === 0) + { + centroids[2 * i ] = delaunay.coords[2 * i ]; + centroids[2 * i + 1] = delaunay.coords[2 * i + 1]; + } + else + { + centroids[2 * i ] = c[0]; + centroids[2 * i + 1] = c[1]; + } + } + /*delaunay.coords = centroids; + delaunay.update();*/ + + return centroids; } function getDelaunay() @@ -72,34 +206,52 @@ function getAll() } //Settings view contains width & height -function lerpToViewport() +function lerpToViewport(arr, z, width, height) { - /*const centers = model.voronoi.circumcenters; - const centerSize = centers.length / 2; - view.centers = new Float32Array(centerSize * 3); - for(let i = centerSize - 1; i >= 0; --i) - { - view.centers[i * 3 ] = lerp(0, settings.view.width , inverse_lerp(0, settings.model.x - 1, centers[i * 2 ])); - view.centers[i * 3 + 1] = lerp(0, settings.view.height, inverse_lerp(0, settings.model.y - 1, centers[i * 2 + 1])); - view.centers[i * 3 + 2] = -2; - }*/ - - const grid = model.delaunay.coords; - const size = grid.length / 2; - view.grid = new Float32Array(size * 3); + if(arr === undefined) + return undefined; + const size = arr.length / 2; + const newArr = new Float32Array(size * 3); for(let i = size - 1; i >= 0; --i) { - view.grid[i * 3 ] = lerp(0, settings.view.width , inverse_lerp(0, settings.model.x - 1, grid[i * 2 ])); - view.grid[i * 3 + 1] = lerp(0, settings.view.height, inverse_lerp(0, settings.model.y - 1, grid[i * 2 + 1])); - view.grid[i * 3 + 2] = -3; + newArr[i * 3 ] = lerp(0, width , inverse_lerp(0, settings.model.x - 1, arr[i * 2 ])); + newArr[i * 3 + 1] = lerp(0, height, inverse_lerp(0, settings.model.y - 1, arr[i * 2 + 1])); + newArr[i * 3 + 2] = z; + } + return newArr; +} +function getRenderValues(width, height) +{ + /*view.grid = lerpToViewport(model.delaunay.coords, -3, width, height); + view.centers = lerpToViewport(model.voronoi.circumcenters, -3, width, height); + view.centroids = lerpToViewport(model.voronoi.centroids, -3, width, height);*/ + + /*view.d3 = {}; + view.d3.grid = lerpToViewport(model.d3.delaunay.points, -3, width, height); + view.d3.centers = lerpToViewport(model.d3.voronoi.circumcenters, -3, width, height); + view.d3.centroids = lerpToViewport(model.d3.voronoi.centroids, -3, width, height);*/ + + /*view.d3 = {}; + view.d3.grid = lerpToViewport(view.triangulation.points, -3, width, height); + view.d3.centers = lerpToViewport(model.d3.delaunay.points, -3, width, height); + view.d3.centroids = lerpToViewport(model.d3.voronoi.centroids, -3, width, height); + view.d3.lines = lerpToViewport(model.d3.lines, -3, width, height);*/ + + view.test = { + poisson_grid: { + grid: lerpToViewport(model.test.poisson_grid._r_vertex.flat(), -3, width, height) + } } - return { position: view.grid/*, centers: view.centers*/, vertices: model.delaunay.triangles }; + return { position: view.test.poisson_grid.grid, vertices: model.test.poisson_grid._triangles }; + //return { position: view.d3.grid, centers: view.d3.centers, vertices: view.triangulation.triangles, lines: view.d3.lines, centroids: view.d3.centroids }; + //return { position: view.grid, centers: view.centers, vertices: model.delaunay.triangles, lines: model.voronoi.lines, centroids: view.centroids }; + //return { position: view.centers, centers: undefined, vertices: model.voronoi.cells }; } //Process.register is the equivalent of export Process.register("model", [generateTriangles]); -Process.register("view", [lerpToViewport]); +Process.register("view", [getRenderValues]); function updateSettings(s) {