More tests

This commit is contained in:
Clément Pons 2022-12-20 17:11:31 +01:00
parent cbe0f0a5a8
commit 86a2e34401
6 changed files with 779 additions and 64 deletions

View File

@ -6,6 +6,48 @@
<title>Map Generator</title>
<link rel="stylesheet" type="text/css" href="resources/style.css">
<script type="module" src="main.mjs"></script>
<script id="vertexShader" type="x-shader/x-vertex">
#version 300 es
precision mediump float;
precision mediump int;
#define attribute in
#define varying out
attribute vec2 in_Quad;
attribute vec2 in_Centroid;
varying vec2 ex_Quad;
varying vec3 ex_Color;
varying vec2 ex_Centroid;
vec3 color(int i)
{
float r = ((i >> 0) & 0xff)/255.0;
float g = ((i >> 8) & 0xff)/255.0;
float b = ((i >> 16) & 0xff)/255.0;
return vec3(r,g,b);
}
void main()
{
ex_Centroid = in_Centroid; //Pass Centroid Along
ex_Color = color(gl_InstanceID); //Compute Color
ex_Quad = in_Quad + in_Centroid; //Shift and Scale
gl_Position = vec4(ex_Quad, 0.0, 1.0);
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
varying vec2 ex_Quad;
varying vec2 ex_Centroid;
varying vec3 ex_Color;
void main()
{
gl_FragDepth = length(ex_Quad - ex_Centroid); //Depth Buffer Write
gl_FragColor = vec4(ex_Color, 1.0);
}
</script>
</head>
<body></body>
</html>

View File

@ -0,0 +1,241 @@
/*
* From https://github.com/redblobgames/dual-mesh
* Copyright 2017 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*
* 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;

204
src/libs/dual-mesh.mjs Normal file
View File

@ -0,0 +1,204 @@
/*
* From https://github.com/redblobgames/dual-mesh
* Copyright 2017 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*/
'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;

View File

@ -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);
})();

View File

@ -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);
}
}
}

View File

@ -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)
{