You've already forked Generator
Compare commits
7 Commits
8be7991b1d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86a2e34401 | ||
| cbe0f0a5a8 | |||
|
|
0410e3d709 | ||
| e4217d3f4d | |||
| 37d1051838 | |||
| f1a3ca1f2e | |||
| 5ae0479e02 |
@@ -6,6 +6,48 @@
|
|||||||
<title>Map Generator</title>
|
<title>Map Generator</title>
|
||||||
<link rel="stylesheet" type="text/css" href="resources/style.css">
|
<link rel="stylesheet" type="text/css" href="resources/style.css">
|
||||||
<script type="module" src="main.mjs"></script>
|
<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>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
241
src/libs/dual-mesh-create.mjs
Normal file
241
src/libs/dual-mesh-create.mjs
Normal 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
204
src/libs/dual-mesh.mjs
Normal 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;
|
||||||
50
src/main.mjs
50
src/main.mjs
@@ -1,17 +1,55 @@
|
|||||||
import { Thread, supportThreads } from "./utils/workerUtils.mjs";
|
import { Thread, supportThreads } from "./utils/workerUtils.mjs";
|
||||||
import Renderer from "./modules/renderer/renderer.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()
|
(async function()
|
||||||
{
|
{
|
||||||
globalThis.DEBUG = localStorage.getItem("debug");
|
globalThis.DEBUG = !!localStorage.getItem("debug");
|
||||||
globalThis.VERBOSE = DEBUG && localStorage.getItem("verbose");
|
globalThis.VERBOSE = DEBUG && localStorage.getItem("verbose");
|
||||||
globalThis.TIMINGS = DEBUG && localStorage.getItem("timings");
|
globalThis.TIMINGS = DEBUG && localStorage.getItem("timings");
|
||||||
globalThis.RELEASE = !DEBUG;
|
globalThis.RELEASE = !DEBUG;
|
||||||
|
|
||||||
const renderer = new Renderer();
|
async function applySettings(settings)
|
||||||
renderer.render();
|
{
|
||||||
|
await thread.global.updateSettings(settings);
|
||||||
|
|
||||||
const thread = new Thread("./workers/base.mjs", "backend");
|
await thread.model.generateTriangles();
|
||||||
await thread.setup().complete();
|
|
||||||
const grid = await thread.test_fillArr({x: 20, y: 20, depth: 2, seed: 0, jitter: 3 });
|
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();
|
||||||
|
|
||||||
|
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);
|
||||||
})();
|
})();
|
||||||
@@ -1,13 +1,125 @@
|
|||||||
import * as Three from "../../libs/three.mjs";
|
import * as Three from "../../libs/three.mjs";
|
||||||
import Thread from "../../utils/workerUtils.mjs";
|
import { Thread } from "../../utils/workerUtils.mjs";
|
||||||
|
|
||||||
class Renderer
|
class Renderer
|
||||||
{
|
{
|
||||||
static async init()
|
static async init(threaded)
|
||||||
{
|
{
|
||||||
const renderer_thread = new Thread("../../workers/renderer.mjs", "renderer");
|
this._threaded = threaded === undefined ? false : threaded;
|
||||||
await renderer_thread.setup().complete();
|
if(threaded)
|
||||||
renderer_thread.init(document.createElement("canvas").transferControlToOffscreen(), window.innerWidth, window.innerHeight);
|
{
|
||||||
|
this._renderer_thread = new Thread("../../workers/renderer.mjs", "view", true);
|
||||||
|
await this._renderer_thread.setup();
|
||||||
|
this._canvas = document.createElement("canvas");
|
||||||
|
const offcanvas = this._canvas.transferControlToOffscreen();
|
||||||
|
await this._renderer_thread.render.init(offcanvas, window.innerWidth, window.innerHeight);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this._renderer = new Three.WebGLRenderer();
|
||||||
|
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.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)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._rAF = requestAnimationFrame(Renderer.render.bind(Renderer));
|
||||||
|
|
||||||
|
/*this._camera.zoom = 1 + (0.5 + Math.sin(this._clock.getElapsedTime()) / 2) * 5;
|
||||||
|
this._camera.updateProjectionMatrix();*/
|
||||||
|
|
||||||
|
this._renderer.render(this._scene, this._camera);
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
//We can share the cells with the rendering thread because it will only be used as read-only to create the point data
|
||||||
|
await this._renderer_thread.view.lerpToView(position, settings);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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: 0x00ff00, wireframe: true, } );
|
||||||
|
const mesh = new Three.Mesh( geometry, material );
|
||||||
|
this._scene.add( mesh );*/
|
||||||
|
|
||||||
|
/*const points = new Three.Points( geometry, new Three.PointsMaterial( { size: 3.0 } ) );
|
||||||
|
this._scene.add( points );*/
|
||||||
|
|
||||||
|
/*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,78 +0,0 @@
|
|||||||
// WORKER SIDE //
|
|
||||||
|
|
||||||
const Constants = {
|
|
||||||
STATE: {
|
|
||||||
WAITING: 1,
|
|
||||||
OK: 2,
|
|
||||||
ERROR: 3,
|
|
||||||
},
|
|
||||||
REQUEST: {
|
|
||||||
SETUP: 1,
|
|
||||||
CALL: 2,
|
|
||||||
TERMINATE: 3
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function send(ret)
|
|
||||||
{
|
|
||||||
globalThis.VERBOSE && console.debug(`Sending ${ret} to the main thread`);
|
|
||||||
postMessage([Constants.STATE.OK, ret]);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Process
|
|
||||||
{
|
|
||||||
static _isSetup = false;
|
|
||||||
static setup()
|
|
||||||
{
|
|
||||||
globalThis.DEBUG && console.log(`Setting up process`);
|
|
||||||
Process._isSetup = true;
|
|
||||||
Process._customFn = {};
|
|
||||||
onmessage = Process.onmessage;
|
|
||||||
}
|
|
||||||
static onmessage(e)
|
|
||||||
{
|
|
||||||
globalThis.VERBOSE && console.debug(`Received ${e.data}`);
|
|
||||||
if(e && e.data)
|
|
||||||
{
|
|
||||||
switch(e.data[0])
|
|
||||||
{
|
|
||||||
case Constants.REQUEST.SETUP:
|
|
||||||
send(Object.keys(Process._customFn));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Constants.REQUEST.CALL:
|
|
||||||
send(Process._customFn[e.data[1]](...e.data[2]));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Constants.REQUEST.TERMINATE:
|
|
||||||
Process.cleanUp();
|
|
||||||
send();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error("Invalid message received in the Process");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new Error("Can't find any data");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static register(name, fn)
|
|
||||||
{
|
|
||||||
if(!Process._isSetup)
|
|
||||||
Process.setup();
|
|
||||||
|
|
||||||
if(name in Process._customFn)
|
|
||||||
throw new Error("This function name is already registered in the process. Please use a different one.");
|
|
||||||
else
|
|
||||||
Process._customFn[name] = fn;
|
|
||||||
}
|
|
||||||
static cleanUp()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Process, Constants };
|
|
||||||
@@ -9,6 +9,7 @@ const Constants = {
|
|||||||
WAITING: 1,
|
WAITING: 1,
|
||||||
OK: 2,
|
OK: 2,
|
||||||
ERROR: 3,
|
ERROR: 3,
|
||||||
|
LOG: 4,
|
||||||
},
|
},
|
||||||
REQUEST: {
|
REQUEST: {
|
||||||
WAKEUP: 1,
|
WAKEUP: 1,
|
||||||
@@ -16,6 +17,17 @@ const Constants = {
|
|||||||
CALL: 3,
|
CALL: 3,
|
||||||
TERMINATE: 4
|
TERMINATE: 4
|
||||||
},
|
},
|
||||||
|
TRANSFERABLE: [
|
||||||
|
ArrayBuffer,
|
||||||
|
MessagePort,
|
||||||
|
ReadableStream,
|
||||||
|
WritableStream,
|
||||||
|
TransformStream,
|
||||||
|
AudioData,
|
||||||
|
ImageBitmap,
|
||||||
|
VideoFrame,
|
||||||
|
OffscreenCanvas,
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
function cleanUp(thread)
|
function cleanUp(thread)
|
||||||
@@ -23,21 +35,44 @@ function cleanUp(thread)
|
|||||||
thread._waiting = false;
|
thread._waiting = false;
|
||||||
thread._resolver = undefined;
|
thread._resolver = undefined;
|
||||||
thread._rejecter = undefined;
|
thread._rejecter = undefined;
|
||||||
|
thread._calledMod = "";
|
||||||
thread._calledFn = "";
|
thread._calledFn = "";
|
||||||
}
|
}
|
||||||
function request(thread, req)
|
function request(thread, req, shared)
|
||||||
{
|
{
|
||||||
thread._promise = new Promise(function(res, rej) {
|
thread._promise = new Promise(function(res, rej) {
|
||||||
thread._waiting = true;
|
thread._waiting = true;
|
||||||
thread._resolver = res;
|
thread._resolver = res;
|
||||||
thread._rejecter = rej;
|
thread._rejecter = rej;
|
||||||
|
|
||||||
thread._worker && thread._worker.postMessage(req);
|
thread._worker && thread._worker.postMessage(req, shared);
|
||||||
});
|
});
|
||||||
|
|
||||||
return thread._promise;
|
return thread._promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTransferables(args)
|
||||||
|
{
|
||||||
|
const arr = [];
|
||||||
|
|
||||||
|
if(!Array.isArray(args))
|
||||||
|
args = [args];
|
||||||
|
|
||||||
|
for(let i = 0; i < args.length; ++i)
|
||||||
|
{
|
||||||
|
for(let j = 0; j < Constants.TRANSFERABLE.length; ++j)
|
||||||
|
{
|
||||||
|
if(args[i] instanceof Constants.TRANSFERABLE[j])
|
||||||
|
{
|
||||||
|
arr.push(args[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr.length === 0 ? undefined : arr;
|
||||||
|
}
|
||||||
|
|
||||||
function parseReturns(data)
|
function parseReturns(data)
|
||||||
{
|
{
|
||||||
if(data.length != 2)
|
if(data.length != 2)
|
||||||
@@ -50,22 +85,39 @@ function parseReturns(data)
|
|||||||
}
|
}
|
||||||
return data[1];
|
return data[1];
|
||||||
}
|
}
|
||||||
function wrapper(thread, fn)
|
function wrapper(thread, mod, fn)
|
||||||
{
|
{
|
||||||
return function()
|
return function(...args)
|
||||||
{
|
{
|
||||||
globalThis.TIMINGS && console.time("Measuring " + fn);
|
if(globalThis.TIMINGS)
|
||||||
return request(thread, [Constants.REQUEST.CALL, fn, [...arguments]]);
|
{
|
||||||
|
console.time("Measuring " + mod + "." + fn);
|
||||||
|
thread._calledMod = mod;
|
||||||
|
thread._calledFn = fn;
|
||||||
|
}
|
||||||
|
return request(thread, [Constants.REQUEST.CALL, mod, fn, [...args]], getTransferables([...args]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onmessage(thread)
|
function onmessage(thread)
|
||||||
{
|
{
|
||||||
return function(e) {
|
return function(e) {
|
||||||
const ret = parseReturns(e.data);
|
if(e.data[0] === Constants.STATE.LOG)
|
||||||
globalThis.VERBOSE && console.debug(`Received message from ${thread._name} containing ${ret}`);
|
{
|
||||||
|
globalThis.VERBOSE && console.debug(ret);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(e.data[0] === Constants.STATE.ERROR)
|
||||||
|
{
|
||||||
|
console.error(e.data[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(thread._waiting && ret)
|
const ret = parseReturns(e.data);
|
||||||
|
globalThis.VERBOSE && console.debug(`Received message from ${thread._name} containing `, ret);
|
||||||
|
globalThis.TIMINGS && thread._calledFn !== "" && console.timeEnd("Measuring " + thread._calledMod + "." + thread._calledFn);
|
||||||
|
|
||||||
|
if(thread._waiting)
|
||||||
{
|
{
|
||||||
globalThis.DEBUG && console.log(`Resolving ${thread._name}`);
|
globalThis.DEBUG && console.log(`Resolving ${thread._name}`);
|
||||||
thread._ret = ret;
|
thread._ret = ret;
|
||||||
@@ -77,7 +129,8 @@ function onmessage(thread)
|
|||||||
function onerror(thread)
|
function onerror(thread)
|
||||||
{
|
{
|
||||||
return function(e) {
|
return function(e) {
|
||||||
console.error(`Received error from ${thread._name} containing ${e}`);
|
globalThis.DEBUG && console.error(`Received error from ${thread._name} containing `, e);
|
||||||
|
globalThis.TIMINGS && thread._calledFn !== "" && console.timeEnd("Measuring " + thread._calledMod + "." + thread._calledFn);
|
||||||
|
|
||||||
if(thread._waiting)
|
if(thread._waiting)
|
||||||
{
|
{
|
||||||
@@ -93,9 +146,9 @@ const Thread = Object.freeze(class
|
|||||||
* Creates a new Thread loading the linked file. The linked file must follow the Thread expectations.
|
* Creates a new Thread loading the linked file. The linked file must follow the Thread expectations.
|
||||||
* You can give it a optionnal name to make the debug easier.
|
* You can give it a optionnal name to make the debug easier.
|
||||||
*/
|
*/
|
||||||
constructor(url, name)
|
constructor(url, name, mod)
|
||||||
{
|
{
|
||||||
this._worker = new Worker(url, {type: "module"});
|
this._worker = new Worker(url, mod ? {type: "module"} : undefined);
|
||||||
this._name = name || url;
|
this._name = name || url;
|
||||||
|
|
||||||
this._worker.onmessage = onmessage(this);
|
this._worker.onmessage = onmessage(this);
|
||||||
@@ -107,27 +160,25 @@ const Thread = Object.freeze(class
|
|||||||
*/
|
*/
|
||||||
async setup()
|
async setup()
|
||||||
{
|
{
|
||||||
const result = await request(this, [Constants.REQUEST.SETUP]);
|
const result = await request(this, [Constants.REQUEST.SETUP], undefined);
|
||||||
|
|
||||||
if(result && result.length)
|
if(result)
|
||||||
{
|
{
|
||||||
for(let i = 0; i < result.length; i++)
|
for(let mod of Object.getOwnPropertyNames(result))
|
||||||
{
|
{
|
||||||
const custom = result[i];
|
this[mod] = {};
|
||||||
this[custom] = wrapper(this, custom);
|
for(let i = 0; i < result[mod].length; ++i)
|
||||||
|
{
|
||||||
|
const fn = result[mod][i];
|
||||||
|
this[mod][fn] = wrapper(this, mod, fn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
complete()
|
async terminate()
|
||||||
{
|
{
|
||||||
if(this._waiting)
|
await request(this, [Constants.REQUEST.TERMINATE]);
|
||||||
{
|
this._worker.terminate();
|
||||||
return this._promise;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Promise.resolve(this._ret);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,8 +186,18 @@ const Thread = Object.freeze(class
|
|||||||
|
|
||||||
function send(ret)
|
function send(ret)
|
||||||
{
|
{
|
||||||
globalThis.VERBOSE && console.debug(`Sending ${ret} to the main thread`);
|
globalThis.VERBOSE && console.debug(`Sending `, ret, ` back to the main thread`);
|
||||||
postMessage([Constants.STATE.OK, ret]);
|
postMessage([Constants.STATE.OK, ret], getTransferables(ret));
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(err)
|
||||||
|
{
|
||||||
|
postMessage([Constants.STATE.ERROR, err]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(msg)
|
||||||
|
{
|
||||||
|
postMessage([Constants.STATE.LOG, msg]);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Process
|
class Process
|
||||||
@@ -148,20 +209,38 @@ class Process
|
|||||||
Process._isSetup = true;
|
Process._isSetup = true;
|
||||||
Process._customFn = {};
|
Process._customFn = {};
|
||||||
globalThis.onmessage = Process.onmessage;
|
globalThis.onmessage = Process.onmessage;
|
||||||
|
globalThis.onerror = Process.onerror;
|
||||||
}
|
}
|
||||||
static onmessage(e)
|
static onmessage(e)
|
||||||
{
|
{
|
||||||
globalThis.VERBOSE && console.debug(`Received ${e.data}`);
|
globalThis.VERBOSE && log(`Received `, e.data);
|
||||||
if(e && e.data)
|
if(e && e.data)
|
||||||
{
|
{
|
||||||
switch(e.data[0])
|
switch(e.data[0])
|
||||||
{
|
{
|
||||||
case Constants.REQUEST.SETUP:
|
case Constants.REQUEST.SETUP:
|
||||||
send(Object.keys(Process._customFn));
|
const obj = {};
|
||||||
|
for(let mod of Object.getOwnPropertyNames(Process._customFn))
|
||||||
|
{
|
||||||
|
obj[mod] = [];
|
||||||
|
for(let fn of Object.getOwnPropertyNames(Process._customFn[mod]))
|
||||||
|
{
|
||||||
|
obj[mod].push(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
send(obj);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Constants.REQUEST.CALL:
|
case Constants.REQUEST.CALL:
|
||||||
send(Process._customFn[e.data[1]](...e.data[2]));
|
const args = e.data[3];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
send(Process._customFn[e.data[1]][e.data[2]](...args));
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Constants.REQUEST.TERMINATE:
|
case Constants.REQUEST.TERMINATE:
|
||||||
@@ -186,21 +265,28 @@ class Process
|
|||||||
|
|
||||||
if(!Array.isArray(arr))
|
if(!Array.isArray(arr))
|
||||||
arr = [arr];
|
arr = [arr];
|
||||||
|
|
||||||
|
if(mod in Process._customFn)
|
||||||
|
throw new Error("This module has already been registered in the process.");
|
||||||
|
|
||||||
|
const modObj = {};
|
||||||
for(let i = 0; i < arr.length; ++i)
|
for(let i = 0; i < arr.length; ++i)
|
||||||
{
|
{
|
||||||
if(typeof arr[i] != 'function')
|
if(typeof arr[i] != 'function')
|
||||||
throw new Error("You can only register functions");
|
throw new Error("You can only register functions");
|
||||||
|
|
||||||
const name = mod + "_" arr[i].name;
|
const name = arr[i].name;
|
||||||
if(name in Process._customFn)
|
if(name in modObj)
|
||||||
throw new Error("This function name is already registered in the process. Please use a different one.");
|
throw new Error("This function name is already registered in the process. Please use a different one.");
|
||||||
else
|
else
|
||||||
Process._customFn[name] = arr[i];
|
modObj[name] = arr[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process._customFn[mod] = modObj;
|
||||||
}
|
}
|
||||||
static cleanUp()
|
static cleanUp()
|
||||||
{
|
{
|
||||||
|
Process._customFn = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,266 @@
|
|||||||
import { Process } from "../utils/workerUtils.mjs";
|
import { Process } from "../utils/workerUtils.mjs";
|
||||||
import Noise from '../libs/alea.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';
|
||||||
|
|
||||||
//Settings contains x, y depth, seed, jitter
|
const OFFSET = 1;
|
||||||
function fillArr(settings)
|
|
||||||
|
let settings = {};
|
||||||
|
const model = {};
|
||||||
|
const view = {};
|
||||||
|
|
||||||
|
function lerp(a, b, x)
|
||||||
{
|
{
|
||||||
const noise = Noise(settings.seed);
|
return a + (b - a) * x;
|
||||||
const arr = new Float32Array(settings.x * settings.y * settings.depth);
|
}
|
||||||
for(let i = arr.length - 1; i >= 0; --i)
|
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];
|
||||||
|
|
||||||
|
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; i >= 0; i -= 2)
|
||||||
{
|
{
|
||||||
const _x = i % settings.x;
|
const _x = i / 2 % settings.model.x;
|
||||||
const _y = (i - _x) / settings.x;
|
const _y = (i / 2 - _x) / settings.model.x;
|
||||||
arr[i] =
|
|
||||||
|
const angle = lerp(0, Math.PI * 2, rand());
|
||||||
|
const magnitude = lerp(0, settings.model.jitter, rand());
|
||||||
|
|
||||||
|
grid[i ] = _x + Math.sin(angle) * magnitude;
|
||||||
|
grid[i + 1] = _y + Math.cos(angle) * magnitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
for(let i = 0; i < settings.model.relax; i++)
|
||||||
|
{
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
//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()
|
||||||
|
{
|
||||||
|
return model.delaunay;
|
||||||
|
}
|
||||||
|
function getAll()
|
||||||
|
{
|
||||||
|
return { model: model, view: view };
|
||||||
|
}
|
||||||
|
|
||||||
|
//Settings view contains width & height
|
||||||
|
function lerpToViewport(arr, z, width, height)
|
||||||
|
{
|
||||||
|
if(arr === undefined)
|
||||||
|
return undefined;
|
||||||
|
const size = arr.length / 2;
|
||||||
|
const newArr = new Float32Array(size * 3);
|
||||||
|
for(let i = size - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
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.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 is the equivalent of export
|
||||||
Process.register("test", [fillArr]);
|
Process.register("model", [generateTriangles]);
|
||||||
|
Process.register("view", [getRenderValues]);
|
||||||
|
|
||||||
|
function updateSettings(s)
|
||||||
|
{
|
||||||
|
const old_settings = settings;
|
||||||
|
settings = s;
|
||||||
|
|
||||||
|
//Check which property has change and regenerate the appropriate objects.
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Process.register("global", [updateSettings]);
|
||||||
|
Process.register("debug", [getDelaunay, getAll]);
|
||||||
|
|||||||
10
src/workers/renderer.mjs
Normal file
10
src/workers/renderer.mjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Process } from "../utils/workerUtils.mjs";
|
||||||
|
import * as Three from '../libs/three.mjs'
|
||||||
|
|
||||||
|
class Renderer
|
||||||
|
{
|
||||||
|
static init(canvas, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Process.register is the equivalent of export
|
||||||
|
Process.register("render", [Renderer.init]);
|
||||||
Reference in New Issue
Block a user