More tests
This commit is contained in:
parent
cbe0f0a5a8
commit
86a2e34401
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
44
src/main.mjs
44
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;
|
||||
|
||||
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, }, };
|
||||
async function applySettings(settings)
|
||||
{
|
||||
await thread.global.updateSettings(settings);
|
||||
|
||||
await thread.model.generateTriangles();
|
||||
|
||||
const view = await thread.view.lerpToViewport();
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
})();
|
||||
|
|
@ -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 ));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue