Compare commits

..

7 Commits

9 changed files with 1038 additions and 135 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

@@ -1,17 +1,55 @@
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()
{
globalThis.DEBUG = localStorage.getItem("debug");
globalThis.DEBUG = !!localStorage.getItem("debug");
globalThis.VERBOSE = DEBUG && localStorage.getItem("verbose");
globalThis.TIMINGS = DEBUG && localStorage.getItem("timings");
globalThis.RELEASE = !DEBUG;
const renderer = new Renderer();
renderer.render();
async function applySettings(settings)
{
await thread.global.updateSettings(settings);
const thread = new Thread("./workers/base.mjs", "backend");
await thread.setup().complete();
const grid = await thread.test_fillArr({x: 20, y: 20, depth: 2, seed: 0, jitter: 3 });
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();
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);
})();

View File

@@ -1,13 +1,125 @@
import * as Three from "../../libs/three.mjs";
import Thread from "../../utils/workerUtils.mjs";
import { Thread } from "../../utils/workerUtils.mjs";
class Renderer
{
static async init()
static async init(threaded)
{
const renderer_thread = new Thread("../../workers/renderer.mjs", "renderer");
await renderer_thread.setup().complete();
renderer_thread.init(document.createElement("canvas").transferControlToOffscreen(), window.innerWidth, window.innerHeight);
this._threaded = threaded === undefined ? false : threaded;
if(threaded)
{
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);
}
}
}

View File

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

View File

@@ -9,6 +9,7 @@ const Constants = {
WAITING: 1,
OK: 2,
ERROR: 3,
LOG: 4,
},
REQUEST: {
WAKEUP: 1,
@@ -16,6 +17,17 @@ const Constants = {
CALL: 3,
TERMINATE: 4
},
TRANSFERABLE: [
ArrayBuffer,
MessagePort,
ReadableStream,
WritableStream,
TransformStream,
AudioData,
ImageBitmap,
VideoFrame,
OffscreenCanvas,
]
};
function cleanUp(thread)
@@ -23,21 +35,44 @@ function cleanUp(thread)
thread._waiting = false;
thread._resolver = undefined;
thread._rejecter = undefined;
thread._calledMod = "";
thread._calledFn = "";
}
function request(thread, req)
function request(thread, req, shared)
{
thread._promise = new Promise(function(res, rej) {
thread._waiting = true;
thread._resolver = res;
thread._rejecter = rej;
thread._worker && thread._worker.postMessage(req);
thread._worker && thread._worker.postMessage(req, shared);
});
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)
{
if(data.length != 2)
@@ -50,22 +85,39 @@ function parseReturns(data)
}
return data[1];
}
function wrapper(thread, fn)
function wrapper(thread, mod, fn)
{
return function()
return function(...args)
{
globalThis.TIMINGS && console.time("Measuring " + fn);
return request(thread, [Constants.REQUEST.CALL, fn, [...arguments]]);
if(globalThis.TIMINGS)
{
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)
{
return function(e) {
const ret = parseReturns(e.data);
globalThis.VERBOSE && console.debug(`Received message from ${thread._name} containing ${ret}`);
if(e.data[0] === Constants.STATE.LOG)
{
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}`);
thread._ret = ret;
@@ -77,7 +129,8 @@ function onmessage(thread)
function onerror(thread)
{
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)
{
@@ -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.
* 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._worker.onmessage = onmessage(this);
@@ -107,27 +160,25 @@ const Thread = Object.freeze(class
*/
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[custom] = wrapper(this, custom);
this[mod] = {};
for(let i = 0; i < result[mod].length; ++i)
{
const fn = result[mod][i];
this[mod][fn] = wrapper(this, mod, fn);
}
}
}
complete()
{
if(this._waiting)
{
return this._promise;
}
else
async terminate()
{
return Promise.resolve(this._ret);
}
await request(this, [Constants.REQUEST.TERMINATE]);
this._worker.terminate();
}
});
@@ -135,8 +186,18 @@ const Thread = Object.freeze(class
function send(ret)
{
globalThis.VERBOSE && console.debug(`Sending ${ret} to the main thread`);
postMessage([Constants.STATE.OK, ret]);
globalThis.VERBOSE && console.debug(`Sending `, ret, ` back to the main thread`);
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
@@ -148,20 +209,38 @@ class Process
Process._isSetup = true;
Process._customFn = {};
globalThis.onmessage = Process.onmessage;
globalThis.onerror = Process.onerror;
}
static onmessage(e)
{
globalThis.VERBOSE && console.debug(`Received ${e.data}`);
globalThis.VERBOSE && log(`Received `, e.data);
if(e && e.data)
{
switch(e.data[0])
{
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;
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;
case Constants.REQUEST.TERMINATE:
@@ -186,21 +265,28 @@ class Process
if(!Array.isArray(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)
{
if(typeof arr[i] != 'function')
throw new Error("You can only register functions");
const name = mod + "_" arr[i].name;
if(name in Process._customFn)
const name = arr[i].name;
if(name in modObj)
throw new Error("This function name is already registered in the process. Please use a different one.");
else
Process._customFn[name] = arr[i];
modObj[name] = arr[i];
}
Process._customFn[mod] = modObj;
}
static cleanUp()
{
Process._customFn = {};
}
}

View File

@@ -1,18 +1,266 @@
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
function fillArr(settings)
const OFFSET = 1;
let settings = {};
const model = {};
const view = {};
function lerp(a, b, x)
{
const noise = Noise(settings.seed);
const arr = new Float32Array(settings.x * settings.y * settings.depth);
for(let i = arr.length - 1; i >= 0; --i)
return a + (b - a) * x;
}
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
{
const _x = i % settings.x;
const _y = (i - _x) / settings.x;
arr[i] =
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 / 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, 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("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
View 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]);