This commit is contained in:
Rainer Leit
2026-09-25 17:02:24 +03:00
parent cc43ed8dc8
commit 9597629951
2149 changed files with 460234 additions and 1770 deletions
+362
View File
@@ -0,0 +1,362 @@
// Plate generation — round-robin weighted fill with directional bias.
// Each plate gets a random growth rate and preferred direction.
import { makeRng, makeRandInt } from './rng.js';
import {
PLATE_LOW_PLATE_T_HIGH, PLATE_LOW_PLATE_T_RANGE,
PLATE_RATE_MIN_BASE, PLATE_RATE_MIN_LOW_T,
PLATE_RATE_RANGE_BASE, PLATE_RATE_RANGE_LOW_T,
PLATE_DIR_BASE_BASE, PLATE_DIR_BASE_LOW_T,
PLATE_DIR_SCALE_BASE, PLATE_DIR_SCALE_LOW_T,
PLATE_DIR_STRENGTH_CAP,
PLATE_COMPACT_BASE, PLATE_COMPACT_LOW_T,
PLATE_AREA_GOVERNOR_BASE, PLATE_AREA_GOVERNOR_LOW_T,
PLATE_COMPACT_THRESHOLD_MULT, PLATE_COMPACT_PENALTY_MULT,
PLATE_OMEGA_MIN, PLATE_OMEGA_RANGE,
PLATE_SMOOTH_BASE, PLATE_SMOOTH_LOW_T,
PLATE_SMOOTH_FIRST_THRESH, PLATE_SMOOTH_LATER_THRESH,
} from './terrain-config.js';
export function generatePlates(mesh, r_xyz, numPlates, seed) {
const { numRegions } = mesh;
const r_plate = new Int32Array(numRegions).fill(-1);
const rng = makeRng(seed + 0.5);
const randInt = makeRandInt(seed);
// Farthest-point seed distribution with top-3 jitter
const plateSeeds = new Set();
const isSeed = new Uint8Array(numRegions);
const minDistToSeed = new Float32Array(numRegions).fill(Infinity);
const firstSeed = randInt(numRegions);
plateSeeds.add(firstSeed);
isSeed[firstSeed] = 1;
const fsx = r_xyz[3*firstSeed], fsy = r_xyz[3*firstSeed+1], fsz = r_xyz[3*firstSeed+2];
for (let r = 0; r < numRegions; r++) {
minDistToSeed[r] = 1 - (r_xyz[3*r]*fsx + r_xyz[3*r+1]*fsy + r_xyz[3*r+2]*fsz);
}
minDistToSeed[firstSeed] = 0;
while (plateSeeds.size < numPlates && plateSeeds.size < numRegions) {
// Find top-3 farthest regions (flat vars, no object allocation)
let t0r = -1, t0d = -1, t1r = -1, t1d = -1, t2r = -1, t2d = -1;
for (let r = 0; r < numRegions; r++) {
if (isSeed[r]) continue;
const d = minDistToSeed[r];
if (d > t2d) {
if (d > t0d) {
t2r = t1r; t2d = t1d; t1r = t0r; t1d = t0d; t0r = r; t0d = d;
} else if (d > t1d) {
t2r = t1r; t2d = t1d; t1r = r; t1d = d;
} else {
t2r = r; t2d = d;
}
}
}
let validCount = (t0r !== -1) + (t1r !== -1) + (t2r !== -1);
if (!validCount) break;
const pick = randInt(validCount);
const newSeed = pick === 0 ? t0r : pick === 1 ? t1r : t2r;
plateSeeds.add(newSeed);
isSeed[newSeed] = 1;
const nsx = r_xyz[3*newSeed], nsy = r_xyz[3*newSeed+1], nsz = r_xyz[3*newSeed+2];
// Fused pass: update minDistToSeed from new seed AND find top-3 for next iteration
if (plateSeeds.size < numPlates) {
t0r = -1; t0d = -1; t1r = -1; t1d = -1; t2r = -1; t2d = -1;
for (let r = 0; r < numRegions; r++) {
const d = 1 - (r_xyz[3*r]*nsx + r_xyz[3*r+1]*nsy + r_xyz[3*r+2]*nsz);
if (d < minDistToSeed[r]) minDistToSeed[r] = d;
if (isSeed[r]) continue;
const md = minDistToSeed[r];
if (md > t2d) {
if (md > t0d) {
t2r = t1r; t2d = t1d; t1r = t0r; t1d = t0d; t0r = r; t0d = md;
} else if (md > t1d) {
t2r = t1r; t2d = t1d; t1r = r; t1d = md;
} else {
t2r = r; t2d = md;
}
}
}
// Next iteration can skip the search pass — top-3 is already computed
validCount = (t0r !== -1) + (t1r !== -1) + (t2r !== -1);
if (!validCount) break;
const pick2 = randInt(validCount);
const newSeed2 = pick2 === 0 ? t0r : pick2 === 1 ? t1r : t2r;
plateSeeds.add(newSeed2);
isSeed[newSeed2] = 1;
const ns2x = r_xyz[3*newSeed2], ns2y = r_xyz[3*newSeed2+1], ns2z = r_xyz[3*newSeed2+2];
for (let r = 0; r < numRegions; r++) {
const d = 1 - (r_xyz[3*r]*ns2x + r_xyz[3*r+1]*ns2y + r_xyz[3*r+2]*ns2z);
if (d < minDistToSeed[r]) minDistToSeed[r] = d;
}
} else {
// Last seed — just update distances (needed for distance field, but loop will exit)
for (let r = 0; r < numRegions; r++) {
const d = 1 - (r_xyz[3*r]*nsx + r_xyz[3*r+1]*nsy + r_xyz[3*r+2]*nsz);
if (d < minDistToSeed[r]) minDistToSeed[r] = d;
}
}
}
// Interpolation factor: more cragginess at low plate counts
const lowPlateT = Math.max(0, Math.min(1, (PLATE_LOW_PLATE_T_HIGH - numPlates) / PLATE_LOW_PLATE_T_RANGE));
// Per-plate growth properties
const plateGrowthRate = {};
const plateGrowthDir = {};
const plateDirStrength = {};
const rateMin = PLATE_RATE_MIN_BASE - PLATE_RATE_MIN_LOW_T * lowPlateT; // 0.7 → 0.3
const rateRange = PLATE_RATE_RANGE_BASE + PLATE_RATE_RANGE_LOW_T * lowPlateT; // 2.3 → 4.7
const dirBase = PLATE_DIR_BASE_BASE + PLATE_DIR_BASE_LOW_T * lowPlateT; // 0.15 → 0.4
const dirScale = PLATE_DIR_SCALE_BASE + PLATE_DIR_SCALE_LOW_T * lowPlateT; // 0.25 → 0.5
for (const center of plateSeeds) {
plateGrowthRate[center] = rateMin + rng() * rng() * rateRange;
const px = r_xyz[3*center], py = r_xyz[3*center+1], pz = r_xyz[3*center+2];
const pLen = Math.sqrt(px*px + py*py + pz*pz) || 1;
const nx = px/pLen, ny = py/pLen, nz = pz/pLen;
const rx = rng()-0.5, ry = rng()-0.5, rz = rng()-0.5;
const d = rx*nx + ry*ny + rz*nz;
let tx = rx - d*nx, ty = ry - d*ny, tz = rz - d*nz;
const tLen = Math.sqrt(tx*tx + ty*ty + tz*tz) || 1;
plateGrowthDir[center] = [tx/tLen, ty/tLen, tz/tLen];
plateDirStrength[center] = Math.min(PLATE_DIR_STRENGTH_CAP, rng() * (dirBase + dirScale / plateGrowthRate[center]));
}
// Per-plate frontiers — round-robin ensures every plate advances
const plateIds = Array.from(plateSeeds);
const frontiers = new Map();
const plateAreaCount = {};
for (const pid of plateIds) {
r_plate[pid] = pid;
frontiers.set(pid, [pid]);
plateAreaCount[pid] = 1;
}
const { adjOffset, adjList } = mesh;
let remaining = numRegions - plateIds.length;
const COMPACT_WEIGHT = PLATE_COMPACT_BASE - PLATE_COMPACT_LOW_T * lowPlateT; // 0.3 → 0.08
const expectedArea = Math.max(1, (numRegions - plateIds.length) / numPlates);
const areaGovernorMult = PLATE_AREA_GOVERNOR_BASE + PLATE_AREA_GOVERNOR_LOW_T * lowPlateT; // 2.0 → 4.0
const invNumRegions = 1 / numRegions;
while (remaining > 0) {
let anyProgress = false;
for (const pid of plateIds) {
const frontier = frontiers.get(pid);
if (frontier.length === 0) continue;
const rate = plateGrowthRate[pid];
const dir = plateGrowthDir[pid];
const d0 = dir[0], d1 = dir[1], d2 = dir[2];
const dirStr = plateDirStrength[pid];
const dirStrHalf = dirStr * 0.5;
let steps = Math.max(1, Math.ceil(rate * (0.5 + rng())));
// Governor: halve steps for plates exceeding threshold
if (plateAreaCount[pid] > expectedArea * areaGovernorMult) {
steps = Math.max(1, Math.ceil(steps * 0.5));
}
// Compactness: expected chord distance for a circular plate of current area
const expectedChordDist = Math.sqrt((plateAreaCount[pid] || 1) * invNumRegions / Math.PI) * 2;
const compactThreshold = expectedChordDist * PLATE_COMPACT_THRESHOLD_MULT;
// Precompute seed coordinates
const sx = r_xyz[3*pid], sy = r_xyz[3*pid+1], sz = r_xyz[3*pid+2];
for (let s = 0; s < steps && frontier.length > 0; s++) {
let bestIdx = 0, bestScore = -Infinity;
const samples = Math.min(frontier.length, 3 + Math.floor(dirStr * 5));
for (let i = 0; i < samples; i++) {
const idx = randInt(frontier.length);
const cell = frontier[idx];
const ci = 3*cell;
const dx = r_xyz[ci] - sx, dy = r_xyz[ci+1] - sy, dz = r_xyz[ci+2] - sz;
const dLenSq = dx*dx + dy*dy + dz*dz;
const dLen = Math.sqrt(dLenSq) || 1;
const alignment = (dx*d0 + dy*d1 + dz*d2) / dLen;
// Compactness: seedDist = dLenSq/2 for unit-sphere points
const excess = Math.max(0, dLenSq * 0.5 - compactThreshold);
const compactPenalty = excess * (COMPACT_WEIGHT * PLATE_COMPACT_PENALTY_MULT);
const score = alignment * dirStr + rng() * (1 - dirStrHalf) - compactPenalty;
if (score > bestScore) { bestScore = score; bestIdx = idx; }
}
const current = frontier[bestIdx];
frontier[bestIdx] = frontier[frontier.length - 1];
frontier.pop();
for (let j = adjOffset[current], jEnd = adjOffset[current + 1]; j < jEnd; j++) {
const nb = adjList[j];
if (r_plate[nb] === -1) {
r_plate[nb] = pid;
frontier.push(nb);
plateAreaCount[pid]++;
remaining--;
anyProgress = true;
}
}
}
}
if (!anyProgress) break;
}
// Cleanup: assign orphaned regions to nearest claimed neighbor
let orphans = true;
while (orphans) {
orphans = false;
for (let r = 0; r < numRegions; r++) {
if (r_plate[r] === -1) {
for (let j = adjOffset[r], jEnd = adjOffset[r + 1]; j < jEnd; j++) {
const nb = adjList[j];
if (r_plate[nb] !== -1) {
r_plate[r] = r_plate[nb];
orphans = true;
break;
}
}
}
}
}
smoothAndReconnectPlates(mesh, r_plate, plateSeeds, Math.round(PLATE_SMOOTH_BASE - PLATE_SMOOTH_LOW_T * lowPlateT));
// Assign an Euler pole + angular velocity per plate
const plateVec = {};
for (const center of plateSeeds) {
// Random Euler pole uniformly distributed on the sphere
const theta = rng() * 2 * Math.PI;
const cosP = 2 * rng() - 1;
const sinP = Math.sqrt(1 - cosP * cosP);
const pole = [sinP * Math.cos(theta), sinP * Math.sin(theta), cosP];
// Angular velocity: magnitude 0.5–2.0, random sign
const omega = (PLATE_OMEGA_MIN + rng() * PLATE_OMEGA_RANGE) * (rng() < 0.5 ? -1 : 1);
plateVec[center] = { pole, omega };
}
return { r_plate, plateSeeds, plateVec };
}
/**
* Smooth plate boundaries via majority-vote, then reconnect severed plates.
* @param {SphereMesh} mesh
* @param {Int32Array} r_plate — mutated in place
* @param {Set|Array} plateSeeds — seed region IDs (used for connectivity roots & protection)
* @param {number} numPasses — number of majority-vote smoothing passes
*/
export function smoothAndReconnectPlates(mesh, r_plate, plateSeeds, numPasses) {
const { numRegions, adjOffset, adjList } = mesh;
const plateIds = Array.from(plateSeeds);
// Build seed lookup for protection during smoothing.
// Protects plate seed regions from being reassigned by majority-vote.
// After coarse→hi-res projection the seed IDs are coarse-mesh indices
// that won't satisfy r_plate[pid] === pid on the hi-res mesh, so the
// array stays all-zeros and protection is effectively skipped — this is
// intentional since projected boundaries don't need seed anchoring.
const isSeed = new Uint8Array(numRegions);
for (const pid of plateIds) {
if (pid < numRegions && r_plate[pid] === pid) isSeed[pid] = 1;
}
// Smooth boundaries: majority-vote removes thin tendrils
let maxDeg = 0;
for (let r = 0; r < numRegions; r++) {
const deg = adjOffset[r + 1] - adjOffset[r];
if (deg > maxDeg) maxDeg = deg;
}
const cntPlates = new Int32Array(maxDeg);
const cntValues = new Uint8Array(maxDeg);
for (let pass = 0; pass < numPasses; pass++) {
const threshold = pass === 0 ? PLATE_SMOOTH_FIRST_THRESH : PLATE_SMOOTH_LATER_THRESH;
for (let r = 0; r < numRegions; r++) {
const rStart = adjOffset[r], rEnd = adjOffset[r + 1];
const deg = rEnd - rStart;
let nDistinct = 0;
for (let j = rStart; j < rEnd; j++) {
const p = r_plate[adjList[j]];
let found = false;
for (let k = 0; k < nDistinct; k++) {
if (cntPlates[k] === p) { cntValues[k]++; found = true; break; }
}
if (!found) { cntPlates[nDistinct] = p; cntValues[nDistinct] = 1; nDistinct++; }
}
let bestPlate = r_plate[r], bestCount = 0;
for (let k = 0; k < nDistinct; k++) {
if (cntValues[k] > bestCount) { bestCount = cntValues[k]; bestPlate = cntPlates[k]; }
}
if (bestCount > deg * threshold && !isSeed[r]) {
r_plate[r] = bestPlate;
}
}
}
// Reconnect: smoothing or projection may create disconnected plate fragments.
// For each plate, keep the LARGEST connected component and mark the rest
// for reassignment. This is stable across resolutions (unlike first-found).
{
const visited = new Uint8Array(numRegions);
// Per-plate: track the largest component's BFS list
const bestComponent = {}; // pid → [region indices]
for (let r = 0; r < numRegions; r++) {
if (visited[r]) continue;
const pid = r_plate[r];
const bfs = [r];
visited[r] = 1;
for (let qi = 0; qi < bfs.length; qi++) {
for (let ni = adjOffset[bfs[qi]], niEnd = adjOffset[bfs[qi] + 1]; ni < niEnd; ni++) {
const nb = adjList[ni];
if (!visited[nb] && r_plate[nb] === pid) {
visited[nb] = 1;
bfs.push(nb);
}
}
}
if (!bestComponent[pid] || bfs.length > bestComponent[pid].length) {
bestComponent[pid] = bfs;
}
}
// Mark regions in the largest component per plate
const inMain = new Uint8Array(numRegions);
for (const pid of Object.keys(bestComponent)) {
for (const r of bestComponent[pid]) inMain[r] = 1;
}
// Reassign orphaned regions (not in their plate's largest component)
// via BFS from the main-component boundary
const queue = [];
for (let r = 0; r < numRegions; r++) {
if (!inMain[r]) {
for (let ni = adjOffset[r], niEnd = adjOffset[r + 1]; ni < niEnd; ni++) {
if (inMain[adjList[ni]]) {
r_plate[r] = r_plate[adjList[ni]];
inMain[r] = 1;
queue.push(r);
break;
}
}
}
}
for (let qi = 0; qi < queue.length; qi++) {
const r = queue[qi];
for (let ni = adjOffset[r], niEnd = adjOffset[r + 1]; ni < niEnd; ni++) {
const nb = adjList[ni];
if (!inMain[nb]) {
r_plate[nb] = r_plate[r];
inMain[nb] = 1;
queue.push(nb);
}
}
}
}
}