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

239 lines
8.8 KiB
JavaScript

// Ocean / land assignment.
// Targets ~30% land by surface area. numContinents controls how many
// separate landmasses to create. Small trapped interior seas are absorbed.
import { makeRng } from './rng.js';
export function assignOceanLand(mesh, r_plate, plateSeeds, r_xyz, seed, numContinents, continentSizeVariety = 0, landCoverage = 0.3) {
const rng = makeRng(seed + 42);
const numRegions = mesh.numRegions;
const plateIds = Array.from(plateSeeds);
const numPlates = plateIds.length;
const { adjOffset, adjList } = mesh;
// 1. Plate areas and centroids
const plateArea = {};
const plateCentroid = {};
for (const pid of plateIds) {
plateArea[pid] = 0;
plateCentroid[pid] = [0, 0, 0];
}
for (let r = 0; r < numRegions; r++) {
const p = r_plate[r];
if (!plateCentroid[p]) { plateArea[p] = 0; plateCentroid[p] = [0, 0, 0]; }
plateArea[p]++;
plateCentroid[p][0] += r_xyz[3*r];
plateCentroid[p][1] += r_xyz[3*r+1];
plateCentroid[p][2] += r_xyz[3*r+2];
}
for (const pid of plateIds) {
const a = plateArea[pid] || 1;
plateCentroid[pid][0] /= a;
plateCentroid[pid][1] /= a;
plateCentroid[pid][2] /= a;
}
// 2. Plate adjacency graph + perimeter
const plateAdj = {};
const platePerim = {};
for (const pid of plateIds) { plateAdj[pid] = new Set(); platePerim[pid] = 0; }
for (let r = 0; r < numRegions; r++) {
const myPlate = r_plate[r];
let isBoundary = false;
for (let ni = adjOffset[r], niEnd = adjOffset[r + 1]; ni < niEnd; ni++) {
const nbPlate = r_plate[adjList[ni]];
if (myPlate !== nbPlate) {
plateAdj[myPlate].add(nbPlate);
isBoundary = true;
}
}
if (isBoundary) platePerim[myPlate]++;
}
// Plate compactness
const plateCompact = {};
let maxCompact = 0;
for (const pid of plateIds) {
const c = Math.sqrt(plateArea[pid] || 1) / (platePerim[pid] || 1);
plateCompact[pid] = c;
if (c > maxCompact) maxCompact = c;
}
if (maxCompact > 0) {
for (const pid of plateIds) plateCompact[pid] /= maxCompact;
}
const targetLandArea = landCoverage * numRegions;
// 3. Pick continent seeds via farthest-point sampling
const effectiveNum = Math.min(numContinents, numPlates);
const continentSeeds = [];
const chosen = new Set();
const first = plateIds[Math.floor(rng() * numPlates)];
continentSeeds.push(first);
chosen.add(first);
for (let s = 1; s < effectiveNum; s++) {
const candidates = [];
for (const pid of plateIds) {
if (chosen.has(pid)) continue;
const cx = plateCentroid[pid];
let minDist = Infinity;
for (const existing of continentSeeds) {
const ex = plateCentroid[existing];
const dx = cx[0]-ex[0], dy = cx[1]-ex[1], dz = cx[2]-ex[2];
const d = dx*dx + dy*dy + dz*dz;
if (d < minDist) minDist = d;
}
const rawAreaFactor = Math.sqrt(numRegions / numPlates) / Math.sqrt(plateArea[pid] || 1);
const areaFactor = 1 + (rawAreaFactor - 1) * (1 - continentSizeVariety * 0.5);
const compact = 0.3 + 0.7 * plateCompact[pid];
candidates.push({ pid, score: minDist * areaFactor * compact });
}
if (candidates.length === 0) break;
candidates.sort((a, b) => b.score - a.score);
const topK = Math.min(candidates.length, 3);
const pick = candidates[Math.floor(rng() * topK)];
continentSeeds.push(pick.pid);
chosen.add(pick.pid);
}
// If seeds alone exceed the land budget, trim the largest seeds
let seedArea = 0;
for (const pid of continentSeeds) seedArea += plateArea[pid];
while (continentSeeds.length > 1 && seedArea > targetLandArea) {
let maxIdx = 0;
for (let i = 1; i < continentSeeds.length; i++) {
if (plateArea[continentSeeds[i]] > plateArea[continentSeeds[maxIdx]]) maxIdx = i;
}
seedArea -= plateArea[continentSeeds[maxIdx]];
chosen.delete(continentSeeds[maxIdx]);
continentSeeds.splice(maxIdx, 1);
}
// 4. Initialize continent assignment
const plateContinent = {};
for (let c = 0; c < continentSeeds.length; c++) {
plateContinent[continentSeeds[c]] = c;
}
let landArea = seedArea;
// 5. Round-robin growth with per-continent targets
const growTarget = targetLandArea * 0.9;
const numC = continentSeeds.length;
// Per-continent growth targets: at variety=0 all equal, at variety=1 highly skewed
const continentTarget = new Float64Array(numC);
const continentArea = new Float64Array(numC);
for (let c = 0; c < numC; c++) {
continentArea[c] = plateArea[continentSeeds[c]];
}
if (continentSizeVariety > 0 && numC > 1) {
const weights = [];
for (let c = 0; c < numC; c++) {
// Log-normal-ish: at variety=1, weights span ~0.3x to ~3.5x (12:1 ratio)
const logWeight = (rng() - 0.5) * continentSizeVariety * 2.5;
weights.push(Math.exp(logWeight));
}
const totalWeight = weights.reduce((a, b) => a + b, 0);
for (let c = 0; c < numC; c++) {
continentTarget[c] = growTarget * weights[c] / totalWeight;
}
} else {
const equal = growTarget / Math.max(numC, 1);
for (let c = 0; c < numC; c++) continentTarget[c] = equal;
}
let progress = true;
while (progress && landArea < growTarget) {
progress = false;
for (let c = 0; c < numC && landArea < growTarget; c++) {
// Skip continents that have reached their individual target
if (continentArea[c] >= continentTarget[c]) continue;
const candidates = [];
for (const pid of plateIds) {
if (plateContinent[pid] !== undefined) continue;
let touchesSelf = false, touchesOther = false;
let sameCount = 0;
for (const adj of plateAdj[pid]) {
const ac = plateContinent[adj];
if (ac === c) { touchesSelf = true; sameCount++; }
else if (ac !== undefined) { touchesOther = true; break; }
}
if (touchesSelf && !touchesOther) {
candidates.push({ pid, score: sameCount + plateCompact[pid] * 3 + rng() * 0.5 });
}
}
if (candidates.length === 0) continue;
candidates.sort((a, b) => b.score - a.score);
const topK = Math.min(candidates.length, 3);
const pick = candidates[Math.floor(rng() * topK)];
plateContinent[pick.pid] = c;
continentArea[c] += plateArea[pick.pid];
landArea += plateArea[pick.pid];
progress = true;
}
}
// 6. Absorb trapped interior seas
const oceanComponents = [];
const visited = new Set();
for (const pid of plateIds) {
if (plateContinent[pid] !== undefined || visited.has(pid)) continue;
const component = [pid];
visited.add(pid);
for (let qi = 0; qi < component.length; qi++) {
for (const adj of plateAdj[component[qi]]) {
if (plateContinent[adj] === undefined && !visited.has(adj)) {
visited.add(adj);
component.push(adj);
}
}
}
oceanComponents.push(component);
}
let mainIdx = 0;
for (let i = 1; i < oceanComponents.length; i++) {
let areaI = 0, areaM = 0;
for (const p of oceanComponents[i]) areaI += plateArea[p];
for (const p of oceanComponents[mainIdx]) areaM += plateArea[p];
if (areaI > areaM) mainIdx = i;
}
const absorbCap = targetLandArea * 1.1;
for (let i = 0; i < oceanComponents.length; i++) {
if (i === mainIdx) continue;
const component = oceanComponents[i];
const bordering = new Set();
for (const op of component) {
for (const adj of plateAdj[op]) {
if (plateContinent[adj] !== undefined) bordering.add(plateContinent[adj]);
}
if (bordering.size > 1) break;
}
if (bordering.size === 1) {
let compArea = 0;
for (const op of component) compArea += plateArea[op];
if (landArea + compArea <= absorbCap) {
const c = bordering.values().next().value;
for (const op of component) plateContinent[op] = c;
landArea += compArea;
}
}
}
// 7. Build plateIsOcean set
const plateIsOcean = new Set();
for (const pid of plateIds) {
if (plateContinent[pid] === undefined) plateIsOcean.add(pid);
}
return plateIsOcean;
}