terrain update. broken but ok
This commit is contained in:
parent
3f2be96395
commit
1be3d86da0
7 changed files with 2208 additions and 254 deletions
547
mapTerrain.js
547
mapTerrain.js
|
|
@ -9,31 +9,39 @@ import {
|
|||
} from "./mapGeneratorHelpers.js";
|
||||
|
||||
export function buildTerrainTemplate(seed) {
|
||||
const deposition = 0.18 + rand(seed, 41) * 0.72;
|
||||
const erosion = 0.24 + rand(seed, 42) * 0.68;
|
||||
const roughness = 0.34 + rand(seed, 43) * 0.62;
|
||||
const deposition = 0.32 + rand(seed, 41) * 0.58;
|
||||
const erosion = 0.42 + rand(seed, 42) * 0.48;
|
||||
const roughness = 0.28 + rand(seed, 43) * 0.48;
|
||||
const coastAxisPick = Math.floor(rand(seed, 10) * 3);
|
||||
const coastAngle = coastAxisPick === 0
|
||||
? Math.PI / 2
|
||||
: coastAxisPick === 1
|
||||
? 0
|
||||
: (rand(seed, 11) > 0.5 ? Math.PI / 4 : -Math.PI / 4) + (rand(seed, 14) - 0.5) * 0.28;
|
||||
const ridgeJaggedness = 0.20 + rand(seed, 44) * 0.70;
|
||||
const spineCount = rand(seed, 45) > 0.64 ? 2 : 1;
|
||||
const ridgeJaggedness = 0.18 + rand(seed, 44) * 0.48;
|
||||
// Japan-like regional relief: discontinuous mountain belts made of massifs.
|
||||
// Avoid a centered, ruler-like spine. Two separated belts are common; a single
|
||||
// dominant belt or three belts appear occasionally.
|
||||
const spineRoll = rand(seed, 45);
|
||||
const spineCount = spineRoll < 0.18 ? 1 : spineRoll < 0.86 ? 2 : 3;
|
||||
const spineSpacing = 0.215 + rand(seed, 62) * 0.165;
|
||||
const sideAPlain = 0.035 + rand(seed, 56) * 0.115 + deposition * 0.085;
|
||||
const sideBPlain = 0.035 + rand(seed, 57) * 0.115 + deposition * 0.085;
|
||||
|
||||
return {
|
||||
seed,
|
||||
spineCount,
|
||||
spineAngle: coastAngle + Math.PI * (0.28 + rand(seed, 46) * 0.44),
|
||||
spineCurve: (rand(seed, 47) - 0.5) * 0.28,
|
||||
spinePosition: (rand(seed, 48) - 0.5) * 0.56,
|
||||
spineStrength: 0.66 + rand(seed, 49) * 0.44,
|
||||
spineWidth: 0.060 + rand(seed, 50) * 0.050,
|
||||
secondaryMountainCount: 3 + Math.floor(rand(seed, 51) * 5),
|
||||
secondaryMountainSize: 0.060 + rand(seed, 52) * 0.085,
|
||||
secondaryMountainStrength: 0.55 + rand(seed, 53) * 0.55,
|
||||
spineSpacing,
|
||||
// The ranges track the long island/coastal axis with modest local wobble.
|
||||
spineAngle: coastAngle + Math.PI / 2 + (rand(seed, 46) - 0.5) * 0.18,
|
||||
spineCurve: (rand(seed, 47) - 0.5) * 0.20,
|
||||
spinePosition: (rand(seed, 48) - 0.5) * 0.62,
|
||||
spineStrength: 1.05 + rand(seed, 49) * 0.44,
|
||||
spineWidth: 0.030 + rand(seed, 50) * 0.024,
|
||||
secondaryMountainCount: 10 + Math.floor(rand(seed, 51) * 10),
|
||||
secondaryMountainSize: 0.040 + rand(seed, 52) * 0.095,
|
||||
secondaryMountainStrength: 0.40 + rand(seed, 53) * 0.52,
|
||||
auxiliaryRangeCount: 6 + Math.floor(rand(seed, 63) * 8),
|
||||
coastAxis: coastAxisPick === 0 ? "east-west" : coastAxisPick === 1 ? "north-south" : "diagonal",
|
||||
coastAngle,
|
||||
coastBias: 0.18 + rand(seed, 12) * 0.24,
|
||||
|
|
@ -54,7 +62,7 @@ export function buildTerrainTemplate(seed) {
|
|||
erosion,
|
||||
roughness,
|
||||
ridgeJaggedness,
|
||||
ridgeBranchiness: 0.28 + rand(seed, 55) * 0.62,
|
||||
ridgeBranchiness: 0.26 + rand(seed, 55) * 0.58,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -69,82 +77,172 @@ function jaggedRidgeContribution(x, y, ridge, seed) {
|
|||
const lengthFade = smoothstep(1 - Math.abs(nAlong));
|
||||
if (lengthFade <= 0) return 0;
|
||||
|
||||
// Bend the centerline itself with coherent long/mid waves, then apply ridge falloff.
|
||||
const low = (valueNoise(along * 0.85 + ridge.seedOffset, ridge.seedOffset * 0.37, seed + 6100, 28) - 0.5) * 2;
|
||||
const mid = (valueNoise(along * 1.7 - ridge.seedOffset, ridge.seedOffset * 0.23, seed + 6200, 13) - 0.5) * 2;
|
||||
const sine = Math.sin(along * ridge.kinkFrequency + ridge.kinkPhase);
|
||||
const curve = (ridge.curve || 0) * along * along * (along >= 0 ? 1 : -1);
|
||||
const axisOffset = low * ridge.axisWobble + mid * ridge.axisWobble * 0.55 + sine * ridge.axisWobble * 0.25 + curve;
|
||||
const widthNoise = 0.78 + valueNoise(along * 1.2 + ridge.seedOffset, ridge.seedOffset * 0.19, seed + 6300, 21) * ridge.widthVariation;
|
||||
// Bend the centerline with long waves and coherent noise. This keeps ranges
|
||||
// arcuate and wandering instead of a ruler-straight belt through the map.
|
||||
const low = (valueNoise(along * 0.46 + ridge.seedOffset, ridge.seedOffset * 0.37, seed + 6100, 34) - 0.5) * 2;
|
||||
const mid = (valueNoise(along * 0.95 - ridge.seedOffset, ridge.seedOffset * 0.23, seed + 6200, 18) - 0.5) * 2;
|
||||
const detail = (valueNoise(along * 1.85 + ridge.seedOffset * 0.11, ridge.seedOffset * 0.31, seed + 6217, 9) - 0.5) * 2;
|
||||
const curve = (ridge.curve || 0) * along * along * 0.46 * (along >= 0 ? 1 : -1);
|
||||
const axisOffset = low * ridge.axisWobble * 0.70 + mid * ridge.axisWobble * 0.42 + detail * ridge.axisWobble * 0.18 + curve;
|
||||
|
||||
// Real mountain belts are made of linked massifs, not sinusoidal ribbons.
|
||||
// Use coherent along-strike noise for strengthening/gaps; avoid periodic waves.
|
||||
let continuity = 1;
|
||||
if (ridge.segmentFrequency) {
|
||||
const segA = valueNoise(along * ridge.segmentFrequency * 0.42 + ridge.seedOffset * 0.19, ridge.seedOffset * 0.41, seed + ridge.seedOffset + 101, 1.35);
|
||||
const segB = valueNoise(along * ridge.segmentFrequency * 0.78 - ridge.seedOffset * 0.27, ridge.seedOffset * 0.33, seed + ridge.seedOffset + 271, 0.78);
|
||||
const seg = segA * 0.68 + segB * 0.32;
|
||||
const broken = smoothstep((seg - 0.24) / 0.46);
|
||||
continuity = lerp(1, broken * 0.90 + 0.10, ridge.gapStrength || 0);
|
||||
}
|
||||
if (continuity <= 0.018) return 0;
|
||||
|
||||
const widthNoise = 0.82 + (valueNoise(along * 0.88 + ridge.seedOffset, ridge.seedOffset * 0.19, seed + 6300, 21) - 0.5) * ridge.widthVariation;
|
||||
const localWidth = Math.max(0.006, ridge.width * widthNoise);
|
||||
const jaggedPerp = perp - axisOffset;
|
||||
const serration = 0.76 + valueNoise(x * 1.1 + along * 0.18, y * 1.1 + perp * 0.18, seed + ridge.seedOffset, 7) * 0.48;
|
||||
return Math.exp(-(jaggedPerp * jaggedPerp) / (localWidth * localWidth)) * lengthFade * ridge.h * serration;
|
||||
|
||||
// A rounded Gaussian-like section gives ridges and uplands, while local
|
||||
// summit noise and later erosion prevent broad, flat-looking mountaintops.
|
||||
const d = Math.abs(jaggedPerp) / localWidth;
|
||||
const core = Math.exp(-Math.pow(d, ridge.crestPower || 1.85));
|
||||
const massifNoise = 0.72 + valueNoise(along * 1.10 + ridge.seedOffset, ridge.seedOffset * 0.53, seed + ridge.seedOffset + 411, 6.5) * 0.56;
|
||||
const serration = 0.78 + (valueNoise(x * 2.3 + along * 0.18, y * 2.3 + perp * 0.18, seed + ridge.seedOffset, 5.2) - 0.5) * 0.42;
|
||||
const summitNoise = 0.82 + (valueNoise(x * 4.6 + ridge.seedOffset, y * 4.6 - ridge.seedOffset, seed + ridge.seedOffset + 333, 2.6) - 0.5) * 0.36;
|
||||
return core * lengthFade * continuity * ridge.h * massifNoise * serration * summitNoise;
|
||||
}
|
||||
|
||||
function primarySpineCrossOffset(seed, template, i) {
|
||||
const shift = template.spinePosition * 0.24;
|
||||
if (template.spineCount === 1) {
|
||||
const side = rand(seed, 680) > 0.5 ? 1 : -1;
|
||||
return shift + side * (0.105 + rand(seed, 681) * 0.230);
|
||||
}
|
||||
if (template.spineCount === 2) {
|
||||
const side = i === 0 ? -1 : 1;
|
||||
return shift + side * (0.225 + rand(seed, 681 + i) * 0.155) + (rand(seed, 705 + i) - 0.5) * 0.035;
|
||||
}
|
||||
const side = i === 0 ? -1 : i === 1 ? 1 : (rand(seed, 706) > 0.5 ? -1 : 1);
|
||||
const base = i === 2 ? 0.055 + rand(seed, 707) * 0.110 : 0.235 + rand(seed, 708 + i) * 0.125;
|
||||
return shift + side * base + (rand(seed, 705 + i) - 0.5) * 0.045;
|
||||
}
|
||||
|
||||
function makePrimarySpine(seed, template, spineIndex) {
|
||||
const crossOffset = primarySpineCrossOffset(seed, template, spineIndex);
|
||||
const angle = template.spineAngle + (rand(seed, 700 + spineIndex) - 0.5) * 0.24;
|
||||
const x = 0.5 + Math.cos(angle + Math.PI / 2) * crossOffset + Math.cos(angle) * (rand(seed, 690 + spineIndex) - 0.5) * 0.08;
|
||||
const y = 0.5 + Math.sin(angle + Math.PI / 2) * crossOffset + Math.sin(angle) * (rand(seed, 691 + spineIndex) - 0.5) * 0.08;
|
||||
return {
|
||||
x, y, angle,
|
||||
width: template.spineWidth * (0.92 + rand(seed, 710 + spineIndex) * 0.44),
|
||||
length: 0.46 + rand(seed, 720 + spineIndex) * 0.34,
|
||||
h: template.spineStrength * (0.225 + rand(seed, 730 + spineIndex) * 0.120),
|
||||
curve: template.spineCurve + (rand(seed, 735 + spineIndex) - 0.5) * 0.18,
|
||||
axisWobble: template.spineWidth * (0.52 + template.ridgeJaggedness * 0.90),
|
||||
kinkFrequency: 4 + rand(seed, 740 + spineIndex) * 8,
|
||||
kinkPhase: rand(seed, 750 + spineIndex) * Math.PI * 2,
|
||||
seedOffset: 7600 + spineIndex * 211,
|
||||
widthVariation: 0.20 + template.ridgeJaggedness * 0.30,
|
||||
segmentFrequency: 2.0 + rand(seed, 755 + spineIndex) * 2.0,
|
||||
segmentPhase: rand(seed, 756 + spineIndex),
|
||||
gapStrength: 0.26 + rand(seed, 757 + spineIndex) * 0.30,
|
||||
crestPower: 1.72 + rand(seed, 758 + spineIndex) * 0.36,
|
||||
};
|
||||
}
|
||||
|
||||
function spineFieldAt(x, y, template, spineIndex) {
|
||||
const seed = template.seed || 0;
|
||||
const spacing = spineIndex === 0 ? 0 : (spineIndex % 2 ? 0.18 : -0.18);
|
||||
const angle = template.spineAngle + (spineIndex - 0.5) * 0.17 + (rand(seed, 700 + spineIndex) - 0.5) * 0.18;
|
||||
const ridge = {
|
||||
x: 0.5 + Math.cos(angle + Math.PI / 2) * (template.spinePosition + spacing) * 0.45,
|
||||
y: 0.5 + Math.sin(angle + Math.PI / 2) * (template.spinePosition + spacing) * 0.45,
|
||||
angle,
|
||||
width: template.spineWidth * (0.82 + rand(seed, 710 + spineIndex) * 0.38),
|
||||
length: 0.78 + rand(seed, 720 + spineIndex) * 0.28,
|
||||
h: template.spineStrength * (0.18 + rand(seed, 730 + spineIndex) * 0.08),
|
||||
curve: template.spineCurve,
|
||||
axisWobble: template.spineWidth * (0.45 + template.ridgeJaggedness * 1.15),
|
||||
kinkFrequency: 10 + rand(seed, 740 + spineIndex) * 18,
|
||||
kinkPhase: rand(seed, 750 + spineIndex) * Math.PI * 2,
|
||||
seedOffset: 7600 + spineIndex * 211,
|
||||
widthVariation: 0.18 + template.ridgeJaggedness * 0.34,
|
||||
};
|
||||
return jaggedRidgeContribution(x, y, ridge, seed);
|
||||
return jaggedRidgeContribution(x, y, makePrimarySpine(seed, template, spineIndex), seed);
|
||||
}
|
||||
|
||||
function broadRidgeContribution(x, y, ridge, seed, widthScale = 4.2, heightScale = 0.14) {
|
||||
return jaggedRidgeContribution(x, y, {
|
||||
...ridge,
|
||||
width: ridge.width * widthScale,
|
||||
h: ridge.h * heightScale,
|
||||
axisWobble: ridge.axisWobble * 0.55,
|
||||
widthVariation: Math.max(0.06, ridge.widthVariation * 0.42),
|
||||
gapStrength: Math.max(0.14, (ridge.gapStrength || 0) * 0.55),
|
||||
crestPower: 1.65,
|
||||
}, seed);
|
||||
}
|
||||
|
||||
function recalcSlope(elevation, sea, slope) {
|
||||
slope.fill(0);
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const gx = elevation[indexOf(x + 1, y)] - elevation[indexOf(x - 1, y)];
|
||||
const gy = elevation[indexOf(x, y + 1)] - elevation[indexOf(x, y - 1)];
|
||||
slope[i] = clamp(Math.sqrt(gx * gx + gy * gy) * 10.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildSpineRidges(seed, template) {
|
||||
const spines = [];
|
||||
const branches = [];
|
||||
const auxRanges = [];
|
||||
for (let i = 0; i < template.spineCount; i++) {
|
||||
const angle = template.spineAngle + (i - 0.5) * 0.17 + (rand(seed, 700 + i) - 0.5) * 0.18;
|
||||
const spacing = i === 0 ? 0 : (i % 2 ? 0.18 : -0.18);
|
||||
const x = 0.5 + Math.cos(angle + Math.PI / 2) * (template.spinePosition + spacing) * 0.45;
|
||||
const y = 0.5 + Math.sin(angle + Math.PI / 2) * (template.spinePosition + spacing) * 0.45;
|
||||
spines.push({
|
||||
x, y, angle,
|
||||
width: template.spineWidth * (0.82 + rand(seed, 710 + i) * 0.38),
|
||||
length: 0.78 + rand(seed, 720 + i) * 0.28,
|
||||
h: template.spineStrength * (0.18 + rand(seed, 730 + i) * 0.08),
|
||||
curve: template.spineCurve,
|
||||
axisWobble: template.spineWidth * (0.45 + template.ridgeJaggedness * 1.15),
|
||||
kinkFrequency: 10 + rand(seed, 740 + i) * 18,
|
||||
kinkPhase: rand(seed, 750 + i) * Math.PI * 2,
|
||||
seedOffset: 7600 + i * 211,
|
||||
widthVariation: 0.18 + template.ridgeJaggedness * 0.34,
|
||||
});
|
||||
const branchCount = 3 + Math.floor(template.ridgeBranchiness * 5);
|
||||
const spine = makePrimarySpine(seed, template, i);
|
||||
spines.push(spine);
|
||||
|
||||
const branchCount = 2 + Math.floor(template.ridgeBranchiness * 4);
|
||||
for (let b = 0; b < branchCount; b++) {
|
||||
const along = (rand(seed, 810 + i * 31 + b) - 0.5) * 0.62;
|
||||
const along = (rand(seed, 810 + i * 31 + b) - 0.5) * spine.length * 0.74;
|
||||
const side = rand(seed, 820 + i * 31 + b) > 0.5 ? 1 : -1;
|
||||
const branchAngle = angle + side * (0.55 + rand(seed, 830 + i * 31 + b) * 0.72);
|
||||
const branchAngle = spine.angle + side * (0.46 + rand(seed, 830 + i * 31 + b) * 0.88);
|
||||
branches.push({
|
||||
x: x + Math.cos(angle) * along,
|
||||
y: y + Math.sin(angle) * along,
|
||||
x: spine.x + Math.cos(spine.angle) * along,
|
||||
y: spine.y + Math.sin(spine.angle) * along,
|
||||
angle: branchAngle,
|
||||
width: template.spineWidth * (0.42 + rand(seed, 840 + i * 31 + b) * 0.36),
|
||||
length: 0.16 + rand(seed, 850 + i * 31 + b) * 0.28,
|
||||
h: template.spineStrength * (0.055 + template.ridgeBranchiness * 0.085 + rand(seed, 860 + i * 31 + b) * 0.055),
|
||||
curve: template.spineCurve * 0.45,
|
||||
axisWobble: template.spineWidth * (0.32 + template.ridgeJaggedness * 0.72),
|
||||
kinkFrequency: 14 + rand(seed, 870 + i * 31 + b) * 20,
|
||||
width: template.spineWidth * (0.48 + rand(seed, 840 + i * 31 + b) * 0.62),
|
||||
length: 0.10 + rand(seed, 850 + i * 31 + b) * 0.22,
|
||||
h: template.spineStrength * (0.055 + template.ridgeBranchiness * 0.062 + rand(seed, 860 + i * 31 + b) * 0.060),
|
||||
curve: template.spineCurve * 0.42 + (rand(seed, 865 + i * 31 + b) - 0.5) * 0.18,
|
||||
axisWobble: template.spineWidth * (0.45 + template.ridgeJaggedness * 0.80),
|
||||
kinkFrequency: 4 + rand(seed, 870 + i * 31 + b) * 9,
|
||||
kinkPhase: rand(seed, 880 + i * 31 + b) * Math.PI * 2,
|
||||
seedOffset: 8800 + i * 311 + b * 37,
|
||||
widthVariation: 0.22 + template.ridgeJaggedness * 0.30,
|
||||
widthVariation: 0.24 + template.ridgeJaggedness * 0.34,
|
||||
segmentFrequency: 2.4 + rand(seed, 882 + i * 31 + b) * 3.6,
|
||||
segmentPhase: rand(seed, 883 + i * 31 + b),
|
||||
gapStrength: 0.22 + rand(seed, 884 + i * 31 + b) * 0.35,
|
||||
crestPower: 2.30,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { spines, branches };
|
||||
|
||||
// Subsidiary uplands/ranges around the main mountain systems: examples in
|
||||
// the target style are Atsumi-like peninsular uplands and Kitakami-like
|
||||
// parallel outer highlands. They are not dominant spines, but they prevent
|
||||
// the terrain from reading as only two artificial stripes.
|
||||
for (let a = 0; a < template.auxiliaryRangeCount; a++) {
|
||||
const base = spines[Math.floor(rand(seed, 940 + a) * spines.length) % spines.length];
|
||||
const along = (rand(seed, 941 + a) - 0.5) * base.length * 0.95;
|
||||
const side = rand(seed, 942 + a) > 0.5 ? 1 : -1;
|
||||
const offset = side * (0.105 + rand(seed, 943 + a) * 0.255);
|
||||
const angle = base.angle + (rand(seed, 944 + a) - 0.5) * 0.48 + (rand(seed, 945 + a) > 0.72 ? side * (0.35 + rand(seed, 946 + a) * 0.35) : 0);
|
||||
auxRanges.push({
|
||||
x: base.x + Math.cos(base.angle) * along + Math.cos(base.angle + Math.PI / 2) * offset,
|
||||
y: base.y + Math.sin(base.angle) * along + Math.sin(base.angle + Math.PI / 2) * offset,
|
||||
angle,
|
||||
width: template.spineWidth * (1.05 + rand(seed, 947 + a) * 1.30),
|
||||
length: 0.16 + rand(seed, 948 + a) * 0.34,
|
||||
h: template.spineStrength * (0.075 + rand(seed, 949 + a) * 0.125),
|
||||
curve: (rand(seed, 950 + a) - 0.5) * 0.22,
|
||||
axisWobble: template.spineWidth * (0.48 + template.ridgeJaggedness * 0.85),
|
||||
kinkFrequency: 3 + rand(seed, 951 + a) * 8,
|
||||
kinkPhase: rand(seed, 952 + a) * Math.PI * 2,
|
||||
seedOffset: 9400 + a * 173,
|
||||
widthVariation: 0.22 + template.ridgeJaggedness * 0.36,
|
||||
segmentFrequency: 1.8 + rand(seed, 953 + a) * 3.0,
|
||||
segmentPhase: rand(seed, 954 + a),
|
||||
gapStrength: 0.16 + rand(seed, 955 + a) * 0.28,
|
||||
crestPower: 1.60,
|
||||
});
|
||||
}
|
||||
return { spines, branches, auxRanges };
|
||||
}
|
||||
|
||||
export function generateTerrainAndRivers(seed) {
|
||||
|
|
@ -187,7 +285,7 @@ export function generateTerrainAndRivers(seed) {
|
|||
const coastY = Math.sin(coastAngle);
|
||||
const coastThreshold = terrainTemplate.coastBias;
|
||||
const coastStrength = 0.13 + (1 - terrainTemplate.deposition) * 0.16 + rand(seed, 13) * 0.13;
|
||||
const { spines, branches } = buildSpineRidges(seed, terrainTemplate);
|
||||
const { spines, branches, auxRanges } = buildSpineRidges(seed, terrainTemplate);
|
||||
|
||||
function coastPressureAt(x, y, wx = x, wy = y) {
|
||||
const nx = x / (MAP_W - 1) - 0.5;
|
||||
|
|
@ -230,74 +328,130 @@ export function generateTerrainAndRivers(seed) {
|
|||
x: clamp(x) * MAP_W,
|
||||
y: clamp(y) * MAP_H,
|
||||
r: (terrainTemplate.secondaryMountainSize * (0.72 + rand(seed, 300 + i) * 0.72)) * Math.min(MAP_W, MAP_H),
|
||||
h: terrainTemplate.secondaryMountainStrength * (0.08 + rand(seed, 400 + i) * 0.17),
|
||||
h: terrainTemplate.secondaryMountainStrength * (0.13 + rand(seed, 400 + i) * 0.24),
|
||||
};
|
||||
});
|
||||
|
||||
// Stage 1-3: start from a submerged surface, uplift several roughly
|
||||
// parallel spine ranges, preserve near-sea-level paleo-platforms, then add
|
||||
// terrain noise. This replaces the previous "high central plateau" bias.
|
||||
const axisX = Math.cos(terrainTemplate.spineAngle);
|
||||
const axisY = Math.sin(terrainTemplate.spineAngle);
|
||||
const crossX = Math.cos(terrainTemplate.spineAngle + Math.PI / 2);
|
||||
const crossY = Math.sin(terrainTemplate.spineAngle + Math.PI / 2);
|
||||
const glacialFlatLevel = seaLevel + 0.012 + (rand(seed, 66) - 0.5) * 0.020;
|
||||
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const nx = x / (MAP_W - 1) - 0.5;
|
||||
const ny = y / (MAP_H - 1) - 0.5;
|
||||
const i = indexOf(x, y);
|
||||
|
||||
const warpX = (fbm(x * 0.62 + 180, y * 0.62 - 90, seed + 3101) - 0.5) * 13;
|
||||
const warpY = (fbm(x * 0.62 - 70, y * 0.62 + 210, seed + 3201) - 0.5) * 13;
|
||||
const warpX = (fbm(x * 0.46 + 180, y * 0.46 - 90, seed + 3101) - 0.5) * 7.5;
|
||||
const warpY = (fbm(x * 0.46 - 70, y * 0.46 + 210, seed + 3201) - 0.5) * 7.5;
|
||||
const wx = x + warpX;
|
||||
const wy = y + warpY;
|
||||
const px = wx / (MAP_W - 1);
|
||||
const py = wy / (MAP_H - 1);
|
||||
const rx = px - 0.5;
|
||||
const ry = py - 0.5;
|
||||
const along = rx * axisX + ry * axisY;
|
||||
const cross = rx * crossX + ry * crossY;
|
||||
|
||||
let mountains = 0;
|
||||
for (const blob of mountainBlobs) {
|
||||
const d = Math.hypot(wx - blob.x, wy - blob.y) / blob.r;
|
||||
mountains += Math.exp(-d * d * 2.35) * blob.h;
|
||||
mountains += Math.exp(-d * d * 2.70) * blob.h;
|
||||
}
|
||||
|
||||
const px = wx / (MAP_W - 1);
|
||||
const py = wy / (MAP_H - 1);
|
||||
let spineRidges = 0;
|
||||
for (let si = 0; si < spines.length; si++) spineRidges += jaggedRidgeContribution(px, py, spines[si], seed);
|
||||
let broadSpineUplift = 0;
|
||||
for (const spine of spines) {
|
||||
spineRidges += jaggedRidgeContribution(px, py, spine, seed);
|
||||
broadSpineUplift += broadRidgeContribution(px, py, spine, seed, 4.6, 0.135);
|
||||
}
|
||||
let branchRidges = 0;
|
||||
for (const ridge of branches) branchRidges += jaggedRidgeContribution(px, py, ridge, seed);
|
||||
const ridges = spineRidges + branchRidges;
|
||||
for (const ridge of auxRanges) branchRidges += jaggedRidgeContribution(px, py, ridge, seed);
|
||||
|
||||
const coast = coastPressureAt(x, y, wx, wy);
|
||||
const coastLower = coast.pressure;
|
||||
// Four terrain-noise bands from continental structure to fine surface roughness.
|
||||
const terrainLarge = fbm(wx * 0.36 + 40, wy * 0.36 - 60, seed + 710);
|
||||
const terrainRegional = fbm(wx * 0.95 + 80, wy * 0.95 - 20, seed + 777);
|
||||
const terrainLocal = fbm(wx * 2.05 + 17, wy * 2.05 - 31, seed + 1777);
|
||||
const terrainFine = valueNoise(wx * 2.9 + 11, wy * 2.9 - 19, seed + 2444, 4.5);
|
||||
const fineDissection = (Math.abs(terrainLocal - 0.5) * 0.08 + Math.abs(terrainFine - 0.5) * 0.035) * (0.68 + terrainTemplate.roughness * 0.74);
|
||||
const basin = 0.1 * Math.sin((nx * 3.1 + ny * 1.7 + rand(seed, 15)) * Math.PI) - 0.045 * Math.cos((nx * 5.2 - ny * 3.6 + rand(seed, 16)) * Math.PI);
|
||||
const rawElevation =
|
||||
0.30 * terrainLarge +
|
||||
0.235 * terrainRegional +
|
||||
0.105 * terrainLocal +
|
||||
0.055 * terrainFine +
|
||||
mountains * 0.54 +
|
||||
spineRidges * 0.78 +
|
||||
branchRidges * 0.92 +
|
||||
basin +
|
||||
fineDissection -
|
||||
coastLower * (coastStrength + 0.10 + terrainTemplate.deposition * 0.10) +
|
||||
0.055;
|
||||
// Long-island basement. It keeps the map from becoming a square continent,
|
||||
// but does not itself create a high plateau.
|
||||
const coastWave = (fbm(wx * 0.26 + 901, wy * 0.26 - 307, seed + 4210) - 0.5) * (0.060 + terrainTemplate.coastRoughness * 0.075)
|
||||
+ (valueNoise(wx + 109, wy - 53, seed + 4211, 30) - 0.5) * 0.050;
|
||||
const longFade = smoothstep((0.82 - Math.abs(along)) / 0.20);
|
||||
const halfWidth = 0.305 + terrainTemplate.deposition * 0.040 + (valueNoise(wx - 141, wy + 70, seed + 4212, 44) - 0.5) * 0.105;
|
||||
const islandCore = smoothstep((halfWidth - Math.abs(cross + coastWave)) / 0.115) * longFade;
|
||||
const offshorePlatform = smoothstep((halfWidth + 0.120 - Math.abs(cross + coastWave)) / 0.145) * longFade;
|
||||
|
||||
elevation[i] = clamp(0.5 + (rawElevation - 0.5) * 1.26);
|
||||
arcSpineField[i] = clamp(spineRidges * 3.7);
|
||||
branchRidgeField[i] = clamp(branchRidges * 3.9);
|
||||
ridgeField[i] = clamp(arcSpineField[i] * 0.86 + branchRidgeField[i] * 0.72 + Math.max(0, mountains - 0.10) * 0.95 + fineDissection * 2.0);
|
||||
basinField[i] = clamp(Math.max(0, -basin) * 3.0 + (1 - coastLower) * Math.max(0, 0.42 - elevation[i]) * (0.48 + terrainTemplate.deposition * 0.42));
|
||||
moisture[i] = clamp(0.44 * fbm(wx + 400, wy - 200, seed + 333) + 0.18 * valueNoise(wx, wy, seed + 343, 11) + 0.22 * (1 - Math.abs(ny * 1.7)) + 0.28 * coastLower - Math.max(0, elevation[i] - 0.62) * 0.22);
|
||||
// Intermontane troughs between parallel ridges: a low background around
|
||||
// ridges prevents the ridges from blending into one beige plateau.
|
||||
const nearestSpine = clamp(spineRidges * 3.3);
|
||||
const broadHighland = clamp(broadSpineUplift * 2.2);
|
||||
const betweenRanges = clamp(broadHighland * (1 - nearestSpine * 0.82));
|
||||
const trough = betweenRanges * (0.028 + terrainTemplate.deposition * 0.020);
|
||||
|
||||
const terrainLarge = fbm(wx * 0.24 + 40, wy * 0.24 - 60, seed + 710) - 0.5;
|
||||
const terrainRegional = fbm(wx * 0.72 + 80, wy * 0.72 - 20, seed + 777) - 0.5;
|
||||
const terrainLocal = fbm(wx * 1.65 + 17, wy * 1.65 - 31, seed + 1777) - 0.5;
|
||||
const terrainFine = valueNoise(wx * 2.55 + 11, wy * 2.55 - 19, seed + 2444, 4.5) - 0.5;
|
||||
const ridgeNoiseGate = clamp(nearestSpine * 0.55 + branchRidges * 2.4 + mountains * 1.55 + broadSpineUplift * 1.15);
|
||||
const surfaceNoise =
|
||||
terrainLarge * 0.105 +
|
||||
terrainRegional * 0.068 +
|
||||
terrainLocal * (0.028 + terrainTemplate.roughness * 0.024) +
|
||||
terrainFine * (0.010 + terrainTemplate.roughness * 0.016);
|
||||
const mountainTexture = (terrainLocal * 0.064 + terrainFine * 0.036 + terrainRegional * 0.025) * ridgeNoiseGate;
|
||||
const ravineCut = Math.pow(clamp(0.58 - terrainLocal), 1.45) * (0.030 + terrainTemplate.erosion * 0.032) * ridgeNoiseGate;
|
||||
|
||||
// All cells start below sea. Land exists where the island basement and
|
||||
// mountain belts uplift it above the current sea level. Primary ranges
|
||||
// are massifs on a broad base, not flat, full-width bars.
|
||||
let rawElevation =
|
||||
seaLevel - 0.090 +
|
||||
islandCore * (0.175 + terrainTemplate.deposition * 0.045) +
|
||||
offshorePlatform * 0.026 +
|
||||
broadSpineUplift * 0.43 +
|
||||
spineRidges * 1.34 +
|
||||
branchRidges * 0.82 +
|
||||
mountains * 0.66 +
|
||||
surfaceNoise +
|
||||
mountainTexture -
|
||||
ravineCut -
|
||||
trough;
|
||||
|
||||
// Randomly preserve flat shelves around the glacial sea-level band. These
|
||||
// later become coastal terraces, valley floors, and broad alluvial plains.
|
||||
const seaBand = clamp(1 - Math.abs(rawElevation - glacialFlatLevel) / (0.070 + terrainTemplate.deposition * 0.045));
|
||||
const platformMask = clamp(offshorePlatform * (1 - nearestSpine * 0.82) * (0.55 + valueNoise(wx + 314, wy - 271, seed + 4300, 22) * 0.55));
|
||||
const paleoFlat = seaBand * platformMask;
|
||||
if (paleoFlat > 0.02) {
|
||||
const terraceStep = 0.010 + terrainTemplate.deposition * 0.008;
|
||||
const terraced = glacialFlatLevel + Math.round((rawElevation - glacialFlatLevel) / terraceStep) * terraceStep;
|
||||
rawElevation = lerp(rawElevation, terraced, paleoFlat * 0.72);
|
||||
}
|
||||
|
||||
elevation[i] = clamp(rawElevation, 0, 1);
|
||||
arcSpineField[i] = clamp(spineRidges * 3.20 + broadSpineUplift * 0.85);
|
||||
branchRidgeField[i] = clamp(branchRidges * 2.80 + mountains * 0.42);
|
||||
ridgeField[i] = clamp(arcSpineField[i] * 0.72 + branchRidgeField[i] * 0.66 + Math.max(0, mountains - 0.06) * 0.90 + ridgeNoiseGate * 0.18);
|
||||
basinField[i] = clamp(paleoFlat * 0.42 + betweenRanges * 0.14 + (1 - islandCore) * offshorePlatform * 0.08);
|
||||
coastalLowland[i] = clamp((elevation[i] < seaLevel + 0.105 ? platformMask * 0.48 + offshorePlatform * 0.18 : 0) * (1 - ridgeField[i] * 0.55));
|
||||
moisture[i] = clamp(0.46 * fbm(wx + 400, wy - 200, seed + 333) + 0.18 * valueNoise(wx, wy, seed + 343, 11) + 0.24 * offshorePlatform + 0.16 * islandCore - Math.max(0, elevation[i] - 0.68) * 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const i = indexOf(x, y);
|
||||
const coast = coastPressureAt(x, y);
|
||||
const mountainToSea = ridgeField[i] * (1 - terrainTemplate.deposition) * 0.035;
|
||||
const oceanSide = coast.pressure + mountainToSea > 0.56 + terrainTemplate.deposition * 0.035;
|
||||
if (elevation[i] < seaLevel || oceanSide) sea[i] = 1;
|
||||
if (sea[i]) elevation[i] = Math.min(elevation[i], seaLevel - 0.018 + hash2(x, y, seed + 2311) * 0.012);
|
||||
const edgeBleed = Math.max(
|
||||
smoothstep((5 - x) / 5),
|
||||
smoothstep((x - (MAP_W - 6)) / 5),
|
||||
smoothstep((5 - y) / 5),
|
||||
smoothstep((y - (MAP_H - 6)) / 5)
|
||||
);
|
||||
const coastalNoise = (hash2(x, y, seed + 2311) - 0.5) * 0.010;
|
||||
if (elevation[i] + coastalNoise < seaLevel || (edgeBleed > 0.65 && elevation[i] < seaLevel + 0.050 && ridgeField[i] < 0.28)) sea[i] = 1;
|
||||
if (sea[i]) elevation[i] = Math.min(elevation[i], seaLevel - 0.020 + hash2(x, y, seed + 2311) * 0.010);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -423,6 +577,41 @@ export function generateTerrainAndRivers(seed) {
|
|||
}
|
||||
}
|
||||
|
||||
// Sea-level platform smoothing from the glacial-stage surface. Only low,
|
||||
// weakly dissected terrain is affected; mountain belts remain sharp.
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
const nextElevation = new Float32Array(elevation);
|
||||
for (let y = 2; y < MAP_H - 2; y++) {
|
||||
for (let x = 2; x < MAP_W - 2; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const nearSeaLevel = clamp(1 - Math.abs(elevation[i] - (seaLevel + 0.050)) / 0.105);
|
||||
const flatPotential = clamp(nearSeaLevel * (coastalLowland[i] * 0.75 + basinField[i] * 0.42 + (1 - ridgeField[i]) * 0.22));
|
||||
if (flatPotential <= 0.10) continue;
|
||||
let sum = 0;
|
||||
let wsum = 0;
|
||||
for (let dy = -2; dy <= 2; dy++) {
|
||||
for (let dx = -2; dx <= 2; dx++) {
|
||||
const ni = indexOf(x + dx, y + dy);
|
||||
if (sea[ni]) continue;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > 2.3) continue;
|
||||
const compatible = clamp(1 - Math.abs(elevation[ni] - elevation[i]) / 0.12);
|
||||
const w = compatible / (1 + d);
|
||||
sum += elevation[ni] * w;
|
||||
wsum += w;
|
||||
}
|
||||
}
|
||||
if (wsum > 0) {
|
||||
nextElevation[i] = clamp(lerp(elevation[i], sum / wsum, flatPotential * 0.34), seaLevel + 0.006, 1);
|
||||
depositionalLowland[i] = clamp(depositionalLowland[i] + flatPotential * 0.12);
|
||||
basinField[i] = clamp(basinField[i] + flatPotential * 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
elevation.set(nextElevation);
|
||||
}
|
||||
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const gx = elevation[indexOf(x + 1, y)] - elevation[indexOf(x - 1, y)];
|
||||
|
|
@ -479,6 +668,34 @@ export function generateTerrainAndRivers(seed) {
|
|||
if (!sea[i]) valleyField[i] = clamp(valleyField[i] * 0.68 + Math.pow(flowAccum[i], 0.55) * 0.48);
|
||||
}
|
||||
|
||||
// Stage 4: coarse fluvial simulation on the elevation field before drawing
|
||||
// explicit rivers. Steep, high-flow cells are incised; low-gradient cells
|
||||
// near sea level, basins, and coasts receive sediment and are smoothed.
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
recalcSlope(elevation, sea, slope);
|
||||
const nextElevation = new Float32Array(elevation);
|
||||
for (let y = 2; y < MAP_H - 2; y++) {
|
||||
for (let x = 2; x < MAP_W - 2; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const flow = Math.pow(flowAccum[i], 0.50);
|
||||
const steep = slope[i];
|
||||
const high = clamp((elevation[i] - seaLevel) / 0.46);
|
||||
const incise = clamp(flow * steep * (0.021 + terrainTemplate.erosion * 0.040) * (0.66 + high * 0.82) * (0.60 + ridgeField[i] * 0.44));
|
||||
const deposit = clamp(flow * (1 - steep) * (coastalLowland[i] * 0.42 + basinField[i] * 0.34 + (elevation[i] < seaLevel + 0.16 ? 0.18 : 0)) * (0.012 + terrainTemplate.deposition * 0.035) * (1 - ridgeField[i] * 0.60));
|
||||
if (incise > 0.002 || deposit > 0.002) {
|
||||
nextElevation[i] = clamp(elevation[i] - incise + deposit * 0.56, seaLevel + 0.005, 1);
|
||||
erosionField[i] = clamp(erosionField[i] + incise * 2.7);
|
||||
depositionField[i] = clamp(depositionField[i] + deposit * 2.1);
|
||||
valleyField[i] = clamp(valleyField[i] + incise * 5.4 + flow * 0.10);
|
||||
depositionalLowland[i] = clamp(depositionalLowland[i] + deposit * 8.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
elevation.set(nextElevation);
|
||||
}
|
||||
recalcSlope(elevation, sea, slope);
|
||||
|
||||
// First-order fluvial shaping: cut valley floors on steep/high-flow cells and
|
||||
// deposit gently in coastal lowlands and basin floors. This gives visible
|
||||
// river valleys without destroying the macro terrain structure.
|
||||
|
|
@ -489,8 +706,8 @@ export function generateTerrainAndRivers(seed) {
|
|||
if (sea[i]) continue;
|
||||
const flow = Math.pow(flowAccum[i], 0.46);
|
||||
const incisionNoise = 0.82 + hash2(x, y, seed + 8120) * 0.36;
|
||||
const steepValley = clamp(flow * (0.036 + terrainTemplate.erosion * 0.050 + slope[i] * (0.14 + terrainTemplate.erosion * 0.13) + ridgeField[i] * (0.022 + terrainTemplate.erosion * 0.044)) * incisionNoise);
|
||||
const lateralCut = clamp(Math.pow(flowAccum[i], 0.66) * valleyField[i] * (0.044 + terrainTemplate.erosion * 0.064));
|
||||
const steepValley = clamp(flow * (0.026 + terrainTemplate.erosion * 0.038 + slope[i] * (0.105 + terrainTemplate.erosion * 0.095) + ridgeField[i] * (0.016 + terrainTemplate.erosion * 0.032)) * incisionNoise);
|
||||
const lateralCut = clamp(Math.pow(flowAccum[i], 0.66) * valleyField[i] * (0.032 + terrainTemplate.erosion * 0.046));
|
||||
const lowSettling = clamp(flow * (coastalLowland[i] * (0.018 + terrainTemplate.deposition * 0.040) + basinField[i] * (0.010 + terrainTemplate.deposition * 0.028) + (elevation[i] < 0.40 ? 0.006 + terrainTemplate.deposition * 0.018 : 0)) * (1 - slope[i] * 0.82) * (1 - ridgeField[i] * 0.45));
|
||||
erosionField[i] = steepValley + lateralCut;
|
||||
depositionField[i] = lowSettling;
|
||||
|
|
@ -802,7 +1019,7 @@ export function generateTerrainAndRivers(seed) {
|
|||
}
|
||||
}
|
||||
|
||||
function sanitizeDownhillRiverPath(path, tolerance = 0.040) {
|
||||
function sanitizeDownhillRiverPath(path, tolerance = 0.075) {
|
||||
if (!path || path.length < 2) return path || [];
|
||||
const out = [path[0]];
|
||||
for (let k = 1; k < path.length; k++) {
|
||||
|
|
@ -810,7 +1027,7 @@ export function generateTerrainAndRivers(seed) {
|
|||
const [x, y] = path[k];
|
||||
const pi = indexOf(px, py);
|
||||
const i = indexOf(x, y);
|
||||
if (!sea[i] && elevation[i] > elevation[pi] + tolerance) break;
|
||||
if (!sea[i] && elevation[i] > elevation[pi] + tolerance && flowAccum[i] < flowAccum[pi] + 0.025) break;
|
||||
out.push(path[k]);
|
||||
if (sea[i]) break;
|
||||
}
|
||||
|
|
@ -823,14 +1040,14 @@ export function generateTerrainAndRivers(seed) {
|
|||
const [x, y] = path[start];
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) break;
|
||||
if (elevation[i] <= 0.72 && (valleyField[i] >= 0.18 || flowAccum[i] >= 0.05)) break;
|
||||
if (elevation[i] <= 0.84 && (valleyField[i] >= 0.10 || flowAccum[i] >= 0.022 || river[i] > 0.12)) break;
|
||||
start++;
|
||||
}
|
||||
return path.slice(start);
|
||||
}
|
||||
for (let r = 0; r < riverPaths.length; r++) riverPaths[r] = sanitizeDownhillRiverPath(trimMountainHeadwaters(riverPaths[r]), 0.032);
|
||||
for (let r = 0; r < riverPaths.length; r++) riverPaths[r] = forceRiverToWater(sanitizeDownhillRiverPath(trimMountainHeadwaters(riverPaths[r]), 0.075));
|
||||
for (let r = riverPaths.length - 1; r >= 0; r--) if (riverPaths[r].length < 2) riverPaths.splice(r, 1);
|
||||
for (let r = 0; r < streamPaths.length; r++) streamPaths[r] = sanitizeDownhillRiverPath(trimMountainHeadwaters(streamPaths[r]), 0.026);
|
||||
for (let r = 0; r < streamPaths.length; r++) streamPaths[r] = sanitizeDownhillRiverPath(trimMountainHeadwaters(streamPaths[r]), 0.060);
|
||||
for (let r = streamPaths.length - 1; r >= 0; r--) if (streamPaths[r].length < 2) streamPaths.splice(r, 1);
|
||||
river.fill(0);
|
||||
for (const path of riverPaths) {
|
||||
|
|
@ -905,6 +1122,62 @@ export function generateTerrainAndRivers(seed) {
|
|||
}
|
||||
}
|
||||
|
||||
// Large downstream alluvial plains: expand lowland around the lower reaches of
|
||||
// the strongest rivers before the generic deposition pass. This creates Kanto-
|
||||
// or Nobi-like broad plains while still rejecting ridge/high-slope cells.
|
||||
const protoMainRivers = riverPaths
|
||||
.map((path, i) => ({ path, score: riverScores[i] ?? path.length }))
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, Math.min(4, riverPaths.length))
|
||||
.map((entry) => entry.path);
|
||||
|
||||
for (const path of protoMainRivers) {
|
||||
const start = Math.floor(path.length * 0.45);
|
||||
for (let k = start; k < path.length; k += 2) {
|
||||
const [rx, ry] = path[k];
|
||||
const lowerReach = k / Math.max(1, path.length - 1);
|
||||
const radius = 3.5 + lowerReach * 6.5 + terrainTemplate.deposition * 4.0;
|
||||
const radiusCells = Math.ceil(radius);
|
||||
|
||||
for (let dy = -radiusCells; dy <= radiusCells; dy++) {
|
||||
for (let dx = -radiusCells; dx <= radiusCells; dx++) {
|
||||
const nx = rx + dx;
|
||||
const ny = ry + dy;
|
||||
if (!inside(nx, ny)) continue;
|
||||
const ni = indexOf(nx, ny);
|
||||
if (sea[ni]) continue;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > radius) continue;
|
||||
|
||||
const radial = smoothstep(1 - d / radius);
|
||||
const lowEnergy = clamp(
|
||||
coastalLowland[ni] * 0.55 +
|
||||
basinField[ni] * 0.38 +
|
||||
Math.pow(flowAccum[ni], 0.45) * 0.34 +
|
||||
(1 - slope[ni]) * 0.18 -
|
||||
ridgeField[ni] * 0.58 -
|
||||
Math.max(0, fluvialElevation[ni] - 0.50) * 1.35
|
||||
);
|
||||
const w = radial * lowEnergy * (0.25 + terrainTemplate.deposition * 0.75);
|
||||
if (w <= 0.015) continue;
|
||||
|
||||
depositionalLowland[ni] = clamp(depositionalLowland[ni] + w * 0.65);
|
||||
deltaField[ni] = clamp(deltaField[ni] + w * coastalLowland[ni] * 0.55);
|
||||
floodplain[ni] = clamp(floodplain[ni] + w * 0.45);
|
||||
valleyField[ni] = clamp(valleyField[ni] + w * 0.22);
|
||||
basinField[ni] = clamp(basinField[ni] + w * 0.18);
|
||||
|
||||
const floor = seaLevel + 0.018 + coastalLowland[ni] * 0.010 + basinField[ni] * 0.020 + d * 0.0015;
|
||||
fluvialElevation[ni] = clamp(
|
||||
lerp(fluvialElevation[ni], Math.max(floor, fluvialElevation[ni] - 0.035), w * 0.26),
|
||||
seaLevel + 0.006,
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Template-driven deposition is limited to plausible low-energy places:
|
||||
// river mouths, basin floors, coastal plains, and slope breaks below ridges.
|
||||
const depositionElevation = new Float32Array(fluvialElevation);
|
||||
|
|
@ -962,10 +1235,10 @@ export function generateTerrainAndRivers(seed) {
|
|||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const high = clamp((fluvialElevation[i] - 0.62) / 0.26);
|
||||
const summit = high * clamp(ridgeField[i] * 1.4 - flowAccum[i] * 0.8);
|
||||
const rugged = (valueNoise(x * 2.1 + 19, y * 2.1 - 23, seed + 9661, 3.2) - 0.5) * 0.035;
|
||||
const uplift = summit * (0.018 + Math.max(0, rugged));
|
||||
const high = clamp((fluvialElevation[i] - 0.54) / 0.30);
|
||||
const summit = high * clamp(ridgeField[i] * 1.25 + arcSpineField[i] * 0.55 + branchRidgeField[i] * 0.30 - flowAccum[i] * 0.65);
|
||||
const rugged = (valueNoise(x * 2.1 + 19, y * 2.1 - 23, seed + 9661, 3.2) - 0.5) * 0.040;
|
||||
const uplift = summit * (0.038 + Math.max(0, rugged));
|
||||
if (uplift > 0) {
|
||||
fluvialElevation[i] = clamp(fluvialElevation[i] + uplift, seaLevel + 0.005, 1);
|
||||
erosionField[i] = Math.max(0, erosionField[i] - uplift * 0.6);
|
||||
|
|
@ -1028,6 +1301,34 @@ export function generateTerrainAndRivers(seed) {
|
|||
elevation.set(nextElevation);
|
||||
}
|
||||
|
||||
// Alpine summit reinforcement. The geomorphic pipeline can otherwise erode
|
||||
// the whole mountain system into mid-altitude upland, especially in high-
|
||||
// deposition seeds. Add rugged peaks only where existing ridge fields agree,
|
||||
// not as a continuous stripe.
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const alpinePotential = clamp(
|
||||
arcSpineField[i] * 0.72 +
|
||||
branchRidgeField[i] * 0.54 +
|
||||
ridgeField[i] * 0.30 -
|
||||
flowAccum[i] * 0.34 -
|
||||
coastalLowland[i] * 0.24 -
|
||||
basinField[i] * 0.16
|
||||
);
|
||||
if (alpinePotential <= 0.38) continue;
|
||||
const summitNoise = 0.72 + valueNoise(x * 1.55 + 103, y * 1.55 - 89, seed + 9731, 4.7) * 0.58;
|
||||
const cragNoise = 0.82 + (valueNoise(x * 3.3 - 71, y * 3.3 + 47, seed + 9732, 2.2) - 0.5) * 0.42;
|
||||
const lift = Math.pow(alpinePotential, 1.58) * (0.060 + terrainTemplate.roughness * 0.050) * summitNoise * cragNoise;
|
||||
elevation[i] = clamp(elevation[i] + lift, seaLevel + 0.006, 1);
|
||||
if (alpinePotential > 0.54) {
|
||||
const target = 0.675 + Math.pow(alpinePotential, 1.28) * 0.170 + (summitNoise - 1) * 0.035;
|
||||
elevation[i] = clamp(lerp(elevation[i], Math.max(elevation[i], target), (alpinePotential - 0.54) * 0.92), seaLevel + 0.006, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const i = indexOf(x, y);
|
||||
|
|
@ -1039,9 +1340,9 @@ export function generateTerrainAndRivers(seed) {
|
|||
}
|
||||
|
||||
// Re-trim visible river paths after fluvial reshaping changes local elevation.
|
||||
for (let r = 0; r < riverPaths.length; r++) riverPaths[r] = sanitizeDownhillRiverPath(trimMountainHeadwaters(riverPaths[r]), 0.028);
|
||||
for (let r = 0; r < riverPaths.length; r++) riverPaths[r] = forceRiverToWater(sanitizeDownhillRiverPath(trimMountainHeadwaters(riverPaths[r]), 0.075));
|
||||
for (let r = riverPaths.length - 1; r >= 0; r--) if (riverPaths[r].length < 2) riverPaths.splice(r, 1);
|
||||
for (let r = 0; r < streamPaths.length; r++) streamPaths[r] = sanitizeDownhillRiverPath(trimMountainHeadwaters(streamPaths[r]), 0.022);
|
||||
for (let r = 0; r < streamPaths.length; r++) streamPaths[r] = sanitizeDownhillRiverPath(trimMountainHeadwaters(streamPaths[r]), 0.055);
|
||||
for (let r = streamPaths.length - 1; r >= 0; r--) if (streamPaths[r].length < 2) streamPaths.splice(r, 1);
|
||||
|
||||
const mainRivers = riverPaths
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue