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

363 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}
}
}
}
}