Files
UnrealPrototyping/Tools/Orogen/js/sphere-mesh.js
T
2026-09-25 17:02:24 +03:00

220 lines
8.0 KiB
JavaScript

// Sphere mesh construction: Fibonacci sphere → Delaunay → close pole → SphereMesh.
// Adapted from Red Blob Games sphere-mesh.js.
let _Delaunator = null;
export function setDelaunator(D) { _Delaunator = D; }
// Fibonacci sphere with jitter — evenly-distributed points using the
// Fibonacci spiral. Jitter randomises positions for more organic Voronoi cells.
export function generateFibonacciSphere(N, jitter, rng) {
const r_xyz = new Float32Array(3 * N);
const s = 3.6 / Math.sqrt(N);
const dlong = Math.PI * (3 - Math.sqrt(5));
const dz = 2.0 / N;
for (let k = 0, lng = 0, z = 1 - dz / 2; k < N; k++, z -= dz) {
const r = Math.sqrt(1 - z * z);
let latDeg = Math.asin(z) * 180 / Math.PI;
let lonDeg = lng * 180 / Math.PI;
if (jitter > 0) {
const jLat = (rng() - rng());
const jLon = (rng() - rng());
const nextZ = Math.max(-1, z - dz * 2 * Math.PI * r / s);
latDeg += jitter * jLat * (latDeg - Math.asin(nextZ) * 180 / Math.PI);
lonDeg += jitter * jLon * (s / r * 180 / Math.PI);
}
const latR = latDeg * Math.PI / 180;
const lonR = lonDeg * Math.PI / 180;
r_xyz[3*k] = Math.cos(latR) * Math.cos(lonR);
r_xyz[3*k+1] = Math.cos(latR) * Math.sin(lonR);
r_xyz[3*k+2] = Math.sin(latR);
lng += dlong;
}
return r_xyz;
}
// Stereographic projection (for Delaunay on a sphere).
// Projects every point from the "north pole" (0,0,1) onto a plane.
export function stereographicProjection(r_xyz, N) {
const flat = new Float64Array(2 * N);
for (let i = 0; i < N; i++) {
const z = r_xyz[3*i+2];
// Clamp denominator to prevent Infinity when a jittered point lands
// on or near the projection pole (z ≈ 1). The exact projected position
// doesn't matter for near-pole points — addPoleToMesh corrects connectivity.
const denom = Math.max(1e-12, 1 - z);
flat[2*i] = r_xyz[3*i] / denom;
flat[2*i+1] = r_xyz[3*i+1] / denom;
}
return flat;
}
// Add pole back into mesh — close the mesh by connecting hull edges to the pole.
export function addPoleToMesh(poleId, triangles, halfedges) {
const numSides = triangles.length;
const next = s => (s % 3 === 2) ? s - 2 : s + 1;
let numUnpaired = 0, firstUnpaired = -1;
const pointToSide = [];
for (let s = 0; s < numSides; s++) {
if (halfedges[s] === -1) {
numUnpaired++;
pointToSide[triangles[s]] = s;
firstUnpaired = s;
}
}
const nt = new Int32Array(numSides + 3 * numUnpaired);
const nh = new Int32Array(numSides + 3 * numUnpaired);
nt.set(triangles);
nh.set(halfedges);
for (let i = 0, s = firstUnpaired;
i < numUnpaired;
i++, s = pointToSide[nt[next(s)]]) {
const ns = numSides + 3 * i;
nh[s] = ns;
nh[ns] = s;
nt[ns] = nt[next(s)];
nt[ns + 1] = nt[s];
nt[ns + 2] = poleId;
const k = numSides + (3 * i + 4) % (3 * numUnpaired);
nh[ns + 2] = k;
nh[k] = ns + 2;
}
return { triangles: nt, halfedges: nh };
}
// Lightweight dual-mesh helper wrapping Delaunator output.
// Regions = Voronoi cells, Triangles = Delaunay triangles, Sides = half-edges.
export class SphereMesh {
constructor(triangles, halfedges, numRegions) {
this.triangles = triangles;
this.halfedges = halfedges;
this.numRegions = numRegions;
this.numSides = triangles.length;
this.numTriangles = (triangles.length / 3) | 0;
this._r_s = new Int32Array(numRegions).fill(-1);
for (let s = 0; s < this.numSides; s++) {
const r = triangles[s];
if (this._r_s[r] === -1) this._r_s[r] = s;
}
// Pre-compute flat adjacency lists for r_circulate_r and r_circulate_t.
// Replaces per-call half-edge traversal with cache-friendly array reads.
const adjCount = new Int32Array(numRegions);
for (let r = 0; r < numRegions; r++) {
const s0 = this._r_s[r];
if (s0 === -1) continue;
let s = s0;
do {
adjCount[r]++;
s = this._next(this.halfedges[s]);
} while (s !== s0);
}
this._adjOffset = new Int32Array(numRegions + 1);
for (let r = 0; r < numRegions; r++) {
this._adjOffset[r + 1] = this._adjOffset[r] + adjCount[r];
}
const totalAdj = this._adjOffset[numRegions];
this._adjList = new Int32Array(totalAdj); // neighbor regions
this._adjTriList = new Int32Array(totalAdj); // neighbor triangles
for (let r = 0; r < numRegions; r++) {
const s0 = this._r_s[r];
if (s0 === -1) continue;
let s = s0;
let idx = this._adjOffset[r];
do {
this._adjList[idx] = this.s_end_r(s);
this._adjTriList[idx] = this.s_inner_t(s);
idx++;
s = this._next(this.halfedges[s]);
} while (s !== s0);
}
// Public aliases for direct adjacency iteration (avoids r_circulate_r copy overhead)
this.adjOffset = this._adjOffset;
this.adjList = this._adjList;
}
_next(s) { return (s % 3 === 2) ? s - 2 : s + 1; }
s_begin_r(s){ return this.triangles[s]; }
s_end_r(s) { return this.triangles[this._next(s)]; }
s_inner_t(s){ return (s / 3) | 0; }
s_outer_t(s){ return (this.halfedges[s] / 3) | 0; }
r_circulate_r(out, r) {
const start = this._adjOffset[r];
const end = this._adjOffset[r + 1];
const len = end - start;
out.length = len;
for (let i = 0; i < len; i++) out[i] = this._adjList[start + i];
return out;
}
r_circulate_t(out, r) {
const start = this._adjOffset[r];
const end = this._adjOffset[r + 1];
const len = end - start;
out.length = len;
for (let i = 0; i < len; i++) out[i] = this._adjTriList[start + i];
return out;
}
}
// Build sphere — Fibonacci points → Delaunay → close pole.
export function buildSphere(N, jitter, rng) {
const r_xyz = generateFibonacciSphere(N, jitter, rng);
const flat = stereographicProjection(r_xyz, N);
const delaunay = new _Delaunator(flat);
const poleXYZ = new Float32Array(3 * (N + 1));
poleXYZ.set(r_xyz);
poleXYZ[3*N] = 0; poleXYZ[3*N+1] = 0; poleXYZ[3*N+2] = 1;
const closed = addPoleToMesh(N, delaunay.triangles, delaunay.halfedges);
const mesh = new SphereMesh(closed.triangles, closed.halfedges, N + 1);
return { mesh, r_xyz: poleXYZ };
}
// Pre-compute Euclidean distance between each region and its neighbors.
// Indexed by the same adjacency slot as adjList: neighborDist[i] is the
// distance from region r to adjList[i] where adjOffset[r] <= i < adjOffset[r+1].
export function computeNeighborDist(mesh, r_xyz) {
const { adjOffset, adjList } = mesh;
const neighborDist = new Float32Array(adjList.length);
for (let r = 0; r < mesh.numRegions; r++) {
const x = r_xyz[3*r], y = r_xyz[3*r+1], z = r_xyz[3*r+2];
for (let i = adjOffset[r]; i < adjOffset[r+1]; i++) {
const nb = adjList[i];
const dx = x - r_xyz[3*nb], dy = y - r_xyz[3*nb+1], dz = z - r_xyz[3*nb+2];
neighborDist[i] = Math.sqrt(dx*dx + dy*dy + dz*dz);
}
}
return neighborDist;
}
// Triangle centres (= Voronoi vertices on the sphere).
export function generateTriangleCenters(mesh, r_xyz) {
const { numTriangles } = mesh;
const t_xyz = new Float32Array(3 * numTriangles);
for (let t = 0; t < numTriangles; t++) {
const s0 = 3 * t;
const a = mesh.s_begin_r(s0),
b = mesh.s_begin_r(s0 + 1),
c = mesh.s_begin_r(s0 + 2);
t_xyz[3*t] = (r_xyz[3*a] + r_xyz[3*b] + r_xyz[3*c]) / 3;
t_xyz[3*t+1] = (r_xyz[3*a+1]+r_xyz[3*b+1]+r_xyz[3*c+1]) / 3;
t_xyz[3*t+2] = (r_xyz[3*a+2]+r_xyz[3*b+2]+r_xyz[3*c+2]) / 3;
}
return t_xyz;
}