mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
dwa
This commit is contained in:
24 files changed
+2208
-809
No files matched your search
@@ -111,10 +111,10 @@ final class PackRiverValidator {
|
||||
}
|
||||
|
||||
if (topology != null && terrain != null) {
|
||||
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
|
||||
WormEnvelope wormEnvelope = wormEnvelope(terrain);
|
||||
int cellSize = integerValue(topology, "cellSize", 512);
|
||||
if (Double.isFinite(meanderStrength) && meanderStrength > cellSize) {
|
||||
warnings.add(path + ".terrain.meanderStrength exceeds topology.cellSize; reaches may require large cache halos.");
|
||||
if (wormEnvelope.maximumOffset() > cellSize) {
|
||||
warnings.add(path + ".terrain.worms maxOffset exceeds topology.cellSize; reaches may require large cache halos.");
|
||||
}
|
||||
validateTopologyComplexity(packFolder, path, topology, terrain, errors);
|
||||
}
|
||||
@@ -144,8 +144,6 @@ final class PackRiverValidator {
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingNoiseWeight", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "flowAlignmentWeight", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "confluenceWeight", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "branchSoftCap", 1, 8, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "branchChildShrinkFactor", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainHeightWeight", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainSlopeWeight", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "oceanAttraction", 0D, 16D, errors);
|
||||
@@ -180,8 +178,6 @@ final class PackRiverValidator {
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelMouthBlend", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelFloorVariation", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelRoofVariation", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "meanderStrength", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "meanderSubdivisions", 1, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bedRoughness", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, terrain, "terminalMode", TERMINAL_MODES, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "terminalTaper", 8, 1024, errors);
|
||||
@@ -189,8 +185,8 @@ final class PackRiverValidator {
|
||||
validateNoiseChance(packFolder, terrain, "incision", path, errors);
|
||||
validateStyle(packFolder, terrain, "tunnelFloorStyle", path, errors);
|
||||
validateStyle(packFolder, terrain, "tunnelRoofStyle", path, errors);
|
||||
validateStyle(packFolder, terrain, "meanderStyle", path, errors);
|
||||
validateStyle(packFolder, terrain, "bedRoughnessStyle", path, errors);
|
||||
validateWorms(path, terrain, errors);
|
||||
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
|
||||
double maximumTunnelWidthMultiplier = styledRangeMaximum(
|
||||
packFolder,
|
||||
@@ -240,8 +236,7 @@ final class PackRiverValidator {
|
||||
int tileCells = integerValue(topology, "tileCells", 4);
|
||||
double siteJitter = doubleValue(topology, "siteJitter", 0.35D);
|
||||
int maxRouteReaches = integerValue(topology, "maxRouteReaches", 16);
|
||||
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
|
||||
int meanderSubdivisions = integerValue(terrain, "meanderSubdivisions", 8);
|
||||
WormEnvelope wormEnvelope = wormEnvelope(terrain);
|
||||
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
|
||||
double maximumBankWidth = doubleValue(terrain, "maxBankWidth", 4D);
|
||||
double maximumTunnelWidthMultiplier = styledRangeMaximum(
|
||||
@@ -256,8 +251,8 @@ final class PackRiverValidator {
|
||||
|| tileCells < 1 || tileCells > 64
|
||||
|| !Double.isFinite(siteJitter) || siteJitter < 0D || siteJitter > 0.49D
|
||||
|| maxRouteReaches < 1 || maxRouteReaches > 256
|
||||
|| !Double.isFinite(meanderStrength) || meanderStrength < 0D || meanderStrength > 1024D
|
||||
|| meanderSubdivisions < 1 || meanderSubdivisions > 64
|
||||
|| wormEnvelope.maximumOffset() < 0D || wormEnvelope.maximumOffset() > 1024D
|
||||
|| wormEnvelope.maximumSegments() < 1 || wormEnvelope.maximumSegments() > 64
|
||||
|| !Double.isFinite(maximumChannelWidth) || maximumChannelWidth < 1D || maximumChannelWidth > 2048D
|
||||
|| !Double.isFinite(maximumBankWidth) || maximumBankWidth < 0D || maximumBankWidth > 2048D
|
||||
|| !Double.isFinite(maximumTunnelWidthMultiplier)
|
||||
@@ -275,14 +270,187 @@ final class PackRiverValidator {
|
||||
siteJitter,
|
||||
maxRouteReaches,
|
||||
maximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
wormEnvelope.maximumOffset(),
|
||||
wormEnvelope.maximumSegments()
|
||||
);
|
||||
for (String violation : estimate.violations()) {
|
||||
errors.add(path + " exceeds the safe derived complexity budget. " + violation);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateWorms(String path, JSONObject terrain, List<String> errors) {
|
||||
Object rawWorms = terrain.opt("worms");
|
||||
if (!(rawWorms instanceof JSONArray worms)) {
|
||||
errors.add(path + ".worms must be an array with at least one Perlin-worm profile.");
|
||||
return;
|
||||
}
|
||||
if (worms.length() < 1) {
|
||||
errors.add(path + ".worms must contain at least one Perlin-worm profile.");
|
||||
return;
|
||||
}
|
||||
if (worms.length() > 16) {
|
||||
errors.add(path + ".worms must contain at most 16 root profiles.");
|
||||
}
|
||||
Set<String> ids = new HashSet<String>();
|
||||
Set<Long> seeds = new HashSet<Long>();
|
||||
int profileCount = validateWormArray(path + ".worms", worms, 1, ids, seeds, errors);
|
||||
if (profileCount > 128) {
|
||||
errors.add(path + ".worms hierarchy must contain at most 128 profiles.");
|
||||
}
|
||||
}
|
||||
|
||||
private static int validateWormArray(
|
||||
String path,
|
||||
JSONArray worms,
|
||||
int depth,
|
||||
Set<String> ids,
|
||||
Set<Long> seeds,
|
||||
List<String> errors
|
||||
) {
|
||||
if (worms.length() > 16) {
|
||||
errors.add(path + " must contain at most 16 profiles.");
|
||||
}
|
||||
double totalWeight = 0D;
|
||||
int profileCount = 0;
|
||||
for (int index = 0; index < worms.length(); index++) {
|
||||
JSONObject worm = worms.optJSONObject(index);
|
||||
String wormPath = path + "[" + index + "]";
|
||||
if (worm == null) {
|
||||
errors.add(wormPath + " must be an object.");
|
||||
continue;
|
||||
}
|
||||
profileCount++;
|
||||
validateWormId(wormPath, worm, ids, errors);
|
||||
long seed = validateWormSeed(wormPath, worm, errors);
|
||||
if (!seeds.add(seed)) {
|
||||
errors.add(wormPath + ".seed must be unique inside the worm hierarchy.");
|
||||
}
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "weight", 0.000001D, 1000000D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "wavelength", 8D, 16384D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "detailWavelength", 8D, 16384D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "tortuosity", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "detailTortuosity", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "maxOffset", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(wormPath, worm, "segments", 1, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "widthMultiplier", 0.125D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "bankMultiplier", 0.125D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "depthMultiplier", 0.125D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "bodyWavelength", 32D, 16384D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "bodyDetailWavelength", 32D, 16384D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "widthVariation", 0D, 0.875D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "bankVariation", 0D, 0.875D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "depthVariation", 0D, 0.875D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "roofVariation", 0D, 0.875D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(wormPath, worm, "branchCap", 1, 8, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "branchDecay", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "confluenceMultiplier", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "childChance", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||
wormPath, worm, "branchChildChance", 0D, 1D, errors);
|
||||
totalWeight += doubleValue(worm, "weight", 1D);
|
||||
Object rawChildren = worm.opt("children");
|
||||
if (rawChildren == null || rawChildren == JSONObject.NULL) {
|
||||
continue;
|
||||
}
|
||||
if (!(rawChildren instanceof JSONArray children)) {
|
||||
errors.add(wormPath + ".children must be an array.");
|
||||
continue;
|
||||
}
|
||||
if (children.length() > 0 && depth >= 4) {
|
||||
errors.add(wormPath + ".children exceeds the maximum hierarchy depth of 4.");
|
||||
continue;
|
||||
}
|
||||
profileCount += validateWormArray(
|
||||
wormPath + ".children",
|
||||
children,
|
||||
depth + 1,
|
||||
ids,
|
||||
seeds,
|
||||
errors
|
||||
);
|
||||
}
|
||||
if (worms.length() > 0 && (!Double.isFinite(totalWeight) || totalWeight <= 0D)) {
|
||||
errors.add(path + " total weight must be finite and positive.");
|
||||
}
|
||||
return profileCount;
|
||||
}
|
||||
|
||||
private static void validateWormId(String path, JSONObject worm, Set<String> ids, List<String> errors) {
|
||||
Object rawId = worm.opt("id");
|
||||
if (!(rawId instanceof String id) || !id.matches("[a-z0-9][a-z0-9_-]{0,63}")) {
|
||||
errors.add(path + ".id must use 1 to 64 lowercase letters, digits, underscores, or hyphens.");
|
||||
return;
|
||||
}
|
||||
if (!ids.add(id)) {
|
||||
errors.add(path + ".id must be unique inside the worm hierarchy.");
|
||||
}
|
||||
}
|
||||
|
||||
private static long validateWormSeed(String path, JSONObject worm, List<String> errors) {
|
||||
Object rawSeed = worm.opt("seed");
|
||||
if (rawSeed == null || rawSeed == JSONObject.NULL) {
|
||||
return 1L;
|
||||
}
|
||||
if (!(rawSeed instanceof Number number)
|
||||
|| !Double.isFinite(number.doubleValue())
|
||||
|| number.doubleValue() != StrictMath.rint(number.doubleValue())) {
|
||||
errors.add(path + ".seed must be an integer.");
|
||||
return 1L;
|
||||
}
|
||||
return number.longValue();
|
||||
}
|
||||
|
||||
private static WormEnvelope wormEnvelope(JSONObject terrain) {
|
||||
JSONArray worms = terrain.optJSONArray("worms");
|
||||
if (worms == null || worms.length() == 0) {
|
||||
return new WormEnvelope(320D, 48);
|
||||
}
|
||||
double maximumOffset = 0D;
|
||||
int maximumSegments = 1;
|
||||
WormEnvelope envelope = wormEnvelope(worms, maximumOffset, maximumSegments);
|
||||
maximumOffset = envelope.maximumOffset();
|
||||
maximumSegments = envelope.maximumSegments();
|
||||
return new WormEnvelope(maximumOffset, maximumSegments);
|
||||
}
|
||||
|
||||
private static WormEnvelope wormEnvelope(JSONArray worms, double maximumOffset, int maximumSegments) {
|
||||
double resolvedOffset = maximumOffset;
|
||||
int resolvedSegments = maximumSegments;
|
||||
for (int index = 0; index < worms.length(); index++) {
|
||||
JSONObject worm = worms.optJSONObject(index);
|
||||
if (worm == null) {
|
||||
continue;
|
||||
}
|
||||
resolvedOffset = Math.max(resolvedOffset, doubleValue(worm, "maxOffset", 320D));
|
||||
resolvedSegments = Math.max(resolvedSegments, integerValue(worm, "segments", 48));
|
||||
JSONArray children = worm.optJSONArray("children");
|
||||
if (children != null) {
|
||||
WormEnvelope childEnvelope = wormEnvelope(children, resolvedOffset, resolvedSegments);
|
||||
resolvedOffset = Math.max(resolvedOffset, childEnvelope.maximumOffset());
|
||||
resolvedSegments = Math.max(resolvedSegments, childEnvelope.maximumSegments());
|
||||
}
|
||||
}
|
||||
return new WormEnvelope(resolvedOffset, resolvedSegments);
|
||||
}
|
||||
|
||||
private static void validateCaves(File packFolder, String path, JSONObject caves,
|
||||
boolean forceGeneratedGrotto,
|
||||
List<String> errors, List<String> warnings) {
|
||||
@@ -961,6 +1129,9 @@ final class PackRiverValidator {
|
||||
return chance == null ? defaultValue : doubleValue(chance, "chance", defaultValue);
|
||||
}
|
||||
|
||||
private record WormEnvelope(double maximumOffset, int maximumSegments) {
|
||||
}
|
||||
|
||||
record Validation(List<String> errors, List<String> warnings) {
|
||||
Validation {
|
||||
errors = List.copyOf(errors);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
@@ -83,18 +86,10 @@ public class IrisRiverTerrain {
|
||||
@Desc("The maximum vertical roof variation in river tunnels.")
|
||||
private double tunnelRoofVariation = 3D;
|
||||
|
||||
@Desc("Warps the spacing and amplitude of varied local-normal sweeps, hooks, curls, and wandering bends while preserving graph endpoints.")
|
||||
private IrisGeneratorStyle meanderStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(512D);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1024)
|
||||
@Desc("The total endpoint-spline and shape-personality displacement envelope in blocks.")
|
||||
private double meanderStrength = 72D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("The number of straight segments used to flatten and resolve each meandering graph reach.")
|
||||
private int meanderSubdivisions = 8;
|
||||
@Required
|
||||
@ArrayType(min = 1, type = IrisRiverWorm.class)
|
||||
@Desc("Weighted root Perlin-worm families with inherited child styles for trunks and tributaries.")
|
||||
private KList<IrisRiverWorm> worms = new KList<IrisRiverWorm>();
|
||||
|
||||
@Desc("Modulates small river-bed height variation after the connected channel shape is solved.")
|
||||
private IrisGeneratorStyle bedRoughnessStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(96D);
|
||||
|
||||
@@ -92,16 +92,6 @@ public class IrisRiverTopology {
|
||||
@Desc("The deterministic attraction toward shared downstream nodes. Larger values form stronger tributary trees and confluences.")
|
||||
private double confluenceWeight = 0D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(8)
|
||||
@Desc("The number of upstream children a graph node accepts before additional branches begin shrinking probabilistically.")
|
||||
private int branchSoftCap = 4;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The multiplicative survival factor for each child beyond branchSoftCap. Recursive generations remain unbounded by depth.")
|
||||
private double branchChildShrinkFactor = 0.35D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("The contribution of natural terrain height to downstream routing cost.")
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("One weighted Perlin-worm river shape and its channel proportions.")
|
||||
@Data
|
||||
public class IrisRiverWorm {
|
||||
@Required
|
||||
@Desc("Unique lowercase identifier for this root or child style.")
|
||||
private String id = "river";
|
||||
|
||||
@Desc("Stable salt for this Perlin field pair.")
|
||||
private long seed = 1L;
|
||||
|
||||
@MinNumber(0.000001)
|
||||
@MaxNumber(1000000)
|
||||
@Desc("Relative probability when selecting this root family or one child transition.")
|
||||
private double weight = 1D;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(16384)
|
||||
@Desc("Primary gradient-Perlin wavelength in blocks.")
|
||||
private double wavelength = 1024D;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(16384)
|
||||
@Desc("Secondary gradient-Perlin wavelength in blocks.")
|
||||
private double detailWavelength = 256D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Primary heading deviation as a fraction of 180 degrees.")
|
||||
private double tortuosity = 0.5D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Secondary heading deviation as a fraction of 180 degrees.")
|
||||
private double detailTortuosity = 0.15D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1024)
|
||||
@Desc("Maximum endpoint-bridged displacement from the reach chord in blocks.")
|
||||
private double maxOffset = 320D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("Number of deterministic Perlin-worm steps used to resolve the reach.")
|
||||
private int segments = 48;
|
||||
|
||||
@MinNumber(0.125)
|
||||
@MaxNumber(8)
|
||||
@Desc("Channel-width multiplier for reaches selecting this worm.")
|
||||
private double widthMultiplier = 1D;
|
||||
|
||||
@MinNumber(0.125)
|
||||
@MaxNumber(8)
|
||||
@Desc("Bank-width multiplier for reaches selecting this worm.")
|
||||
private double bankMultiplier = 1D;
|
||||
|
||||
@MinNumber(0.125)
|
||||
@MaxNumber(8)
|
||||
@Desc("Depth multiplier for reaches selecting this worm.")
|
||||
private double depthMultiplier = 1D;
|
||||
|
||||
@MinNumber(32)
|
||||
@MaxNumber(16384)
|
||||
@Desc("Primary world-space wavelength controlling longitudinal body swelling and pinching.")
|
||||
private double bodyWavelength = 512D;
|
||||
|
||||
@MinNumber(32)
|
||||
@MaxNumber(16384)
|
||||
@Desc("Detail wavelength adding smaller changes to the longitudinal body profile.")
|
||||
private double bodyDetailWavelength = 128D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(0.875)
|
||||
@Desc("Maximum proportional channel-width variation along this style's body.")
|
||||
private double widthVariation = 0D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(0.875)
|
||||
@Desc("Maximum proportional bank or basin-width variation along this style's body.")
|
||||
private double bankVariation = 0D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(0.875)
|
||||
@Desc("Maximum proportional bed-depth variation along this style's body.")
|
||||
private double depthVariation = 0D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(0.875)
|
||||
@Desc("Maximum downward variation of tunnel roof clearance without exceeding the authored cave headroom.")
|
||||
private double roofVariation = 0D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(8)
|
||||
@Desc("Number of upstream children admitted before additional siblings decay probabilistically.")
|
||||
private int branchCap = 4;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Multiplicative survival probability for every sibling beyond branchCap.")
|
||||
private double branchDecay = 0.35D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8)
|
||||
@Desc("Multiplier applied to the dimension confluence attraction for this style.")
|
||||
private double confluenceMultiplier = 1D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Chance that an upstream continuation mutates from this style into one weighted child.")
|
||||
private double childChance = 0D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Additional child-mutation chance for each sibling slot beyond the primary branch.")
|
||||
private double branchChildChance = 0D;
|
||||
|
||||
@ArrayType(type = IrisRiverWorm.class)
|
||||
@Desc("Weighted descendant styles inherited by the complete upstream lineage after mutation.")
|
||||
private KList<IrisRiverWorm> children = new KList<IrisRiverWorm>();
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class RiverBodyProfile {
|
||||
private final double[] positions;
|
||||
private final double[] widths;
|
||||
private final double[] bankWidths;
|
||||
private final double[] depths;
|
||||
private final double[] roofScales;
|
||||
private final double maximumWidth;
|
||||
private final double maximumBankWidth;
|
||||
private final double maximumDepth;
|
||||
|
||||
public RiverBodyProfile(
|
||||
double[] positions,
|
||||
double[] widths,
|
||||
double[] bankWidths,
|
||||
double[] depths,
|
||||
double[] roofScales
|
||||
) {
|
||||
if (positions == null || widths == null || bankWidths == null || depths == null || roofScales == null
|
||||
|| positions.length < 2
|
||||
|| positions.length != widths.length
|
||||
|| positions.length != bankWidths.length
|
||||
|| positions.length != depths.length
|
||||
|| positions.length != roofScales.length) {
|
||||
throw new IllegalArgumentException("River body profiles require matching dimension samples");
|
||||
}
|
||||
this.positions = positions.clone();
|
||||
this.widths = widths.clone();
|
||||
this.bankWidths = bankWidths.clone();
|
||||
this.depths = depths.clone();
|
||||
this.roofScales = roofScales.clone();
|
||||
double resolvedMaximumWidth = 0D;
|
||||
double resolvedMaximumBankWidth = 0D;
|
||||
double resolvedMaximumDepth = 0D;
|
||||
for (int index = 0; index < this.positions.length; index++) {
|
||||
double position = this.positions[index];
|
||||
if (!Double.isFinite(position) || position < 0D || position > 1D
|
||||
|| index > 0 && position <= this.positions[index - 1]) {
|
||||
throw new IllegalArgumentException("River body profile positions must increase from zero to one");
|
||||
}
|
||||
requirePositive(this.widths[index], "width");
|
||||
requireNonNegative(this.bankWidths[index], "bank width");
|
||||
requirePositive(this.depths[index], "depth");
|
||||
requireUnitScale(this.roofScales[index], "roof scale");
|
||||
resolvedMaximumWidth = StrictMath.max(resolvedMaximumWidth, this.widths[index]);
|
||||
resolvedMaximumBankWidth = StrictMath.max(resolvedMaximumBankWidth, this.bankWidths[index]);
|
||||
resolvedMaximumDepth = StrictMath.max(resolvedMaximumDepth, this.depths[index]);
|
||||
}
|
||||
if (this.positions[0] != 0D || this.positions[this.positions.length - 1] != 1D) {
|
||||
throw new IllegalArgumentException("River body profile positions must include zero and one");
|
||||
}
|
||||
maximumWidth = resolvedMaximumWidth;
|
||||
maximumBankWidth = resolvedMaximumBankWidth;
|
||||
maximumDepth = resolvedMaximumDepth;
|
||||
}
|
||||
|
||||
public static RiverBodyProfile constant(double width, double bankWidth, double depth) {
|
||||
return new RiverBodyProfile(
|
||||
new double[]{0D, 1D},
|
||||
new double[]{width, width},
|
||||
new double[]{bankWidth, bankWidth},
|
||||
new double[]{depth, depth},
|
||||
new double[]{1D, 1D}
|
||||
);
|
||||
}
|
||||
|
||||
public double width(double alongReach) {
|
||||
return sample(widths, alongReach);
|
||||
}
|
||||
|
||||
public double bankWidth(double alongReach) {
|
||||
return sample(bankWidths, alongReach);
|
||||
}
|
||||
|
||||
public double depth(double alongReach) {
|
||||
return sample(depths, alongReach);
|
||||
}
|
||||
|
||||
public double roofScale(double alongReach) {
|
||||
return sample(roofScales, alongReach);
|
||||
}
|
||||
|
||||
public double maximumWidth() {
|
||||
return maximumWidth;
|
||||
}
|
||||
|
||||
public double maximumBankWidth() {
|
||||
return maximumBankWidth;
|
||||
}
|
||||
|
||||
public double maximumDepth() {
|
||||
return maximumDepth;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return positions.length;
|
||||
}
|
||||
|
||||
public double position(int index) {
|
||||
return positions[index];
|
||||
}
|
||||
|
||||
public double widthAtIndex(int index) {
|
||||
return widths[index];
|
||||
}
|
||||
|
||||
public double bankWidthAtIndex(int index) {
|
||||
return bankWidths[index];
|
||||
}
|
||||
|
||||
public double depthAtIndex(int index) {
|
||||
return depths[index];
|
||||
}
|
||||
|
||||
public double roofScaleAtIndex(int index) {
|
||||
return roofScales[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
if (!(object instanceof RiverBodyProfile profile)) {
|
||||
return false;
|
||||
}
|
||||
return Arrays.equals(positions, profile.positions)
|
||||
&& Arrays.equals(widths, profile.widths)
|
||||
&& Arrays.equals(bankWidths, profile.bankWidths)
|
||||
&& Arrays.equals(depths, profile.depths)
|
||||
&& Arrays.equals(roofScales, profile.roofScales);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = Arrays.hashCode(positions);
|
||||
hash = 31 * hash + Arrays.hashCode(widths);
|
||||
hash = 31 * hash + Arrays.hashCode(bankWidths);
|
||||
hash = 31 * hash + Arrays.hashCode(depths);
|
||||
return 31 * hash + Arrays.hashCode(roofScales);
|
||||
}
|
||||
|
||||
private double sample(double[] values, double alongReach) {
|
||||
double position = StrictMath.max(0D, StrictMath.min(1D, alongReach));
|
||||
int index = Arrays.binarySearch(positions, position);
|
||||
if (index >= 0) {
|
||||
return values[index];
|
||||
}
|
||||
int upper = -index - 1;
|
||||
if (upper <= 0) {
|
||||
return values[0];
|
||||
}
|
||||
if (upper >= positions.length) {
|
||||
return values[values.length - 1];
|
||||
}
|
||||
int lower = upper - 1;
|
||||
double range = positions[upper] - positions[lower];
|
||||
double interpolation = range <= 0D ? 0D : (position - positions[lower]) / range;
|
||||
return values[lower] + (values[upper] - values[lower]) * interpolation;
|
||||
}
|
||||
|
||||
private static void requirePositive(double value, String name) {
|
||||
if (!Double.isFinite(value) || value <= 0D) {
|
||||
throw new IllegalArgumentException("River body profile " + name + " must be finite and positive");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireNonNegative(double value, String name) {
|
||||
if (!Double.isFinite(value) || value < 0D) {
|
||||
throw new IllegalArgumentException("River body profile " + name + " must be finite and non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireUnitScale(double value, String name) {
|
||||
if (!Double.isFinite(value) || value <= 0D || value > 1D) {
|
||||
throw new IllegalArgumentException("River body profile " + name + " must be greater than zero and at most one");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record RiverMeanderContext(
|
||||
RiverEdgeId reachId,
|
||||
double normalizedPosition,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
public RiverMeanderContext {
|
||||
Objects.requireNonNull(reachId);
|
||||
if (!Double.isFinite(normalizedPosition) || normalizedPosition < 0.0 || normalizedPosition > 1.0
|
||||
|| !Double.isFinite(x) || !Double.isFinite(z)) {
|
||||
throw new IllegalArgumentException("River meander context must be finite and normalized");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,21 +21,25 @@ public final class RiverNetwork {
|
||||
private static final long SOURCE_FLOOR_SALT = 0xD6E8FEB86659FD93L;
|
||||
private static final long REACH_SALT = 0x9B05688C2B3E6C1FL;
|
||||
private static final long DRY_SALT = 0x1F83D9ABFB41BD6BL;
|
||||
private static final long MEANDER_SALT = 0x5BE0CD19137E2179L;
|
||||
private static final long MEANDER_PERSONALITY_SALT = 0x243F6A8885A308D3L;
|
||||
private static final long MEANDER_AMPLITUDE_SALT = 0x13198A2E03707344L;
|
||||
private static final long MEANDER_PHASE_SALT = 0xD1310BA698DFB5ACL;
|
||||
private static final long MEANDER_SKEW_SALT = 0x2FFD72DBD01ADFB7L;
|
||||
private static final long MEANDER_CYCLE_SALT = 0x452821E638D01377L;
|
||||
private static final long MEANDER_FEATURE_A_SALT = 0xBE5466CF34E90C6CL;
|
||||
private static final long MEANDER_FEATURE_B_SALT = 0xC0AC29B7C97C50DDL;
|
||||
private static final long WORM_FAMILY_SALT = 0x5BE0CD19137E2179L;
|
||||
private static final long WORM_CHILD_GATE_SALT = 0x452821E638D01377L;
|
||||
private static final long WORM_CHILD_SELECTION_SALT = 0xBE5466CF34E90C6CL;
|
||||
private static final long WORM_PRIMARY_SALT = 0x243F6A8885A308D3L;
|
||||
private static final long WORM_DETAIL_SALT = 0x13198A2E03707344L;
|
||||
private static final long BODY_WIDTH_PRIMARY_SALT = 0xA4093822299F31D0L;
|
||||
private static final long BODY_WIDTH_DETAIL_SALT = 0x082EFA98EC4E6C89L;
|
||||
private static final long BODY_BANK_PRIMARY_SALT = 0x452821E638D01377L;
|
||||
private static final long BODY_BANK_DETAIL_SALT = 0xBE5466CF34E90C6CL;
|
||||
private static final long BODY_DEPTH_PRIMARY_SALT = 0xC0AC29B7C97C50DDL;
|
||||
private static final long BODY_DEPTH_DETAIL_SALT = 0x3F84D5B5B5470917L;
|
||||
private static final long BODY_ROOF_PRIMARY_SALT = 0xD1310BA698DFB5ACL;
|
||||
private static final long BODY_ROOF_DETAIL_SALT = 0x2FFD72DBD01ADFB7L;
|
||||
private static final long CONFLUENCE_SALT = 0x9E3779B97F4A7C15L;
|
||||
private static final long BRANCH_SLOT_SALT = 0x94D049BB133111EBL;
|
||||
private static final long BRANCH_GATE_SALT = 0x2545F4914F6CDD1DL;
|
||||
private static final int MEANDER_NOISE_SAMPLES = 12;
|
||||
private static final int WIDTH_PROFILE_SAMPLES = 12;
|
||||
private static final double TWO_PI = StrictMath.PI * 2D;
|
||||
private static final MeanderPersonality[] MEANDER_PERSONALITIES = MeanderPersonality.values();
|
||||
private static final int MINIMUM_BODY_PROFILE_SAMPLES = 12;
|
||||
private static final int MAXIMUM_BODY_PROFILE_SAMPLES = 32;
|
||||
private static final double PERLIN_NORMALIZATION = 1.4142135623730951D;
|
||||
|
||||
private final RiverNetworkOptions options;
|
||||
|
||||
@@ -208,7 +212,7 @@ public final class RiverNetwork {
|
||||
double routingScore = naturalHeight * options.terrainHeightWeight()
|
||||
+ routingNoise * options.routingNoiseWeight()
|
||||
+ finiteOrZero(terrainSample.routingCost());
|
||||
double drainageDistance = drainageDistance(id);
|
||||
double drainageDistance = drainageBasin(id).distance();
|
||||
double hydraulicHeight = ocean
|
||||
? options.hydraulicBaseHeight()
|
||||
: options.hydraulicBaseHeight()
|
||||
@@ -228,6 +232,10 @@ public final class RiverNetwork {
|
||||
}
|
||||
|
||||
private double drainageDistance(RiverNodeId id) {
|
||||
return drainageBasin(id).distance();
|
||||
}
|
||||
|
||||
private DrainageBasin drainageBasin(RiverNodeId id) {
|
||||
int basinCells = options.routingBasinCells();
|
||||
double nodeX = id.cellX() + 0.5D;
|
||||
double nodeZ = id.cellZ() + 0.5D;
|
||||
@@ -245,19 +253,21 @@ public final class RiverNetwork {
|
||||
long basinZ = (long) StrictMath.floor(nodeZ / basinCells);
|
||||
double jitterRadius = basinCells * 0.45D;
|
||||
double nearestDistance = Double.MAX_VALUE;
|
||||
DrainageBasinId nearestId = null;
|
||||
for (long candidateX = basinX - 1L; candidateX <= basinX + 1L; candidateX++) {
|
||||
for (long candidateZ = basinZ - 1L; candidateZ <= basinZ + 1L; candidateZ++) {
|
||||
double siteX = (candidateX + 0.5D) * basinCells
|
||||
+ centered(hash(candidateX, candidateZ, BASIN_X_SALT)) * jitterRadius;
|
||||
double siteZ = (candidateZ + 0.5D) * basinCells
|
||||
+ centered(hash(candidateX, candidateZ, BASIN_Z_SALT)) * jitterRadius;
|
||||
nearestDistance = StrictMath.min(
|
||||
nearestDistance,
|
||||
StrictMath.hypot(nodeX - siteX, nodeZ - siteZ)
|
||||
);
|
||||
double distance = StrictMath.hypot(nodeX - siteX, nodeZ - siteZ);
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
nearestId = new DrainageBasinId(candidateX, candidateZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
return nearestDistance;
|
||||
return new DrainageBasin(nearestId, nearestDistance);
|
||||
}
|
||||
|
||||
private double smoothCellNoise(double x, double z, int scale, long salt) {
|
||||
@@ -305,7 +315,8 @@ public final class RiverNetwork {
|
||||
if (compareRank(neighbor, node) >= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!branchPermitted(node, neighbor, resolver)) {
|
||||
RiverWorm worm = resolver.worm(node, neighbor);
|
||||
if (!branchPermitted(node, neighbor, resolver, worm)) {
|
||||
continue;
|
||||
}
|
||||
RiverRoutingContext context = resolver.routingContext(node, neighbor);
|
||||
@@ -313,7 +324,8 @@ public final class RiverNetwork {
|
||||
double oceanAttraction = neighbor.ocean() ? options.oceanAttraction() : 0.0;
|
||||
double flowAlignmentCost = flowAlignmentCost(node, neighbor, resolver);
|
||||
double confluenceAttraction = unit(hash(neighbor.id(), CONFLUENCE_SALT))
|
||||
* options.confluenceWeight();
|
||||
* options.confluenceWeight()
|
||||
* worm.confluenceMultiplier();
|
||||
ranked.add(new RankedCandidate(
|
||||
neighbor,
|
||||
neighbor.routingScore() + routingCost + flowAlignmentCost
|
||||
@@ -331,15 +343,20 @@ public final class RiverNetwork {
|
||||
return List.copyOf(candidates);
|
||||
}
|
||||
|
||||
private boolean branchPermitted(RiverNode child, RiverNode parent, NodeResolver resolver) {
|
||||
private boolean branchPermitted(
|
||||
RiverNode child,
|
||||
RiverNode parent,
|
||||
NodeResolver resolver,
|
||||
RiverWorm worm
|
||||
) {
|
||||
RiverEdgeId childEdge = RiverEdgeId.of(child.id(), parent.id());
|
||||
int childSlot = resolver.branchSlot(parent, child);
|
||||
if (childSlot < options.branchSoftCap()) {
|
||||
if (childSlot < worm.branchCap()) {
|
||||
return true;
|
||||
}
|
||||
double survivalChance = 1D;
|
||||
for (int overflow = options.branchSoftCap(); overflow <= childSlot; overflow++) {
|
||||
survivalChance *= options.branchChildShrinkFactor();
|
||||
for (int overflow = worm.branchCap(); overflow <= childSlot; overflow++) {
|
||||
survivalChance *= worm.branchDecay();
|
||||
}
|
||||
return gate(hash(childEdge, BRANCH_GATE_SALT), survivalChance);
|
||||
}
|
||||
@@ -427,6 +444,7 @@ public final class RiverNetwork {
|
||||
from,
|
||||
to,
|
||||
resolver.routingContext(from, to),
|
||||
resolver.worm(from, to),
|
||||
resolver.terrain
|
||||
);
|
||||
accumulators.put(edgeId, accumulator);
|
||||
@@ -451,61 +469,65 @@ public final class RiverNetwork {
|
||||
}
|
||||
|
||||
private RiverPolyline createPolyline(
|
||||
RiverEdgeId id,
|
||||
RiverNode from,
|
||||
RiverNode to,
|
||||
NodeResolver resolver
|
||||
RiverWorm worm
|
||||
) {
|
||||
int pointCount = options.meanderSubdivisions() + 1;
|
||||
double[] baseX = new double[pointCount];
|
||||
double[] baseZ = new double[pointCount];
|
||||
int pointCount = worm.segments() + 1;
|
||||
double[] rawX = new double[pointCount];
|
||||
double[] rawZ = new double[pointCount];
|
||||
double deltaX = to.x() - from.x();
|
||||
double deltaZ = to.z() - from.z();
|
||||
double length = StrictMath.hypot(deltaX, deltaZ);
|
||||
double maximumOffset = StrictMath.min(options.meanderStrength(), length * 0.35);
|
||||
double directionX = length == 0D ? 1D : deltaX / length;
|
||||
double directionZ = length == 0D ? 0D : deltaZ / length;
|
||||
FlowTangent fromTangent = flowTangent(from, directionX, directionZ, resolver);
|
||||
FlowTangent toTangent = flowTangent(to, directionX, directionZ, resolver);
|
||||
for (int point = 0; point < pointCount; point++) {
|
||||
double t = (double) point / (pointCount - 1);
|
||||
double tSquared = t * t;
|
||||
double tCubed = tSquared * t;
|
||||
double fromWeight = 2D * tCubed - 3D * tSquared + 1D;
|
||||
double fromTangentWeight = tCubed - 2D * tSquared + t;
|
||||
double toWeight = -2D * tCubed + 3D * tSquared;
|
||||
double toTangentWeight = tCubed - tSquared;
|
||||
double curvedX = fromWeight * from.x()
|
||||
+ fromTangentWeight * fromTangent.x() * maximumOffset
|
||||
+ toWeight * to.x()
|
||||
+ toTangentWeight * toTangent.x() * maximumOffset;
|
||||
double curvedZ = fromWeight * from.z()
|
||||
+ fromTangentWeight * fromTangent.z() * maximumOffset
|
||||
+ toWeight * to.z()
|
||||
+ toTangentWeight * toTangent.z() * maximumOffset;
|
||||
baseX[point] = curvedX;
|
||||
baseZ[point] = curvedZ;
|
||||
rawX[0] = from.x();
|
||||
rawZ[0] = from.z();
|
||||
if (length <= 0D) {
|
||||
return new RiverPolyline(rawX, rawZ);
|
||||
}
|
||||
if (maximumOffset <= 0D) {
|
||||
return new RiverPolyline(baseX, baseZ);
|
||||
double baseHeading = StrictMath.atan2(deltaZ, deltaX);
|
||||
double stepLength = length / worm.segments();
|
||||
for (int point = 1; point < pointCount; point++) {
|
||||
double x = rawX[point - 1];
|
||||
double z = rawZ[point - 1];
|
||||
double primary = perlin(x, z, worm.wavelength(), worm.seed() ^ WORM_PRIMARY_SALT);
|
||||
double detail = perlin(x, z, worm.detailWavelength(), worm.seed() ^ WORM_DETAIL_SALT);
|
||||
double heading = baseHeading + StrictMath.PI * (
|
||||
primary * worm.tortuosity()
|
||||
+ detail * worm.detailTortuosity()
|
||||
);
|
||||
rawX[point] = x + StrictMath.cos(heading) * stepLength;
|
||||
rawZ[point] = z + StrictMath.sin(heading) * stepLength;
|
||||
}
|
||||
MeanderProfile profile = meanderProfile(id);
|
||||
double[] meanderNoise = meanderNoise(id, baseX, baseZ, resolver);
|
||||
double[] meanderSignal = meanderSignal(id, profile, meanderNoise);
|
||||
double[] x = new double[pointCount];
|
||||
double[] z = new double[pointCount];
|
||||
double rawDeltaX = rawX[pointCount - 1] - from.x();
|
||||
double rawDeltaZ = rawZ[pointCount - 1] - from.z();
|
||||
double maximumDisplacement = 0D;
|
||||
for (int point = 0; point < pointCount; point++) {
|
||||
double t = (double) point / (pointCount - 1);
|
||||
double tSquared = t * t;
|
||||
double envelope = 16D * tSquared * (1D - t) * (1D - t);
|
||||
double straightX = from.x() + deltaX * t;
|
||||
double straightZ = from.z() + deltaZ * t;
|
||||
double baseDisplacement = StrictMath.hypot(baseX[point] - straightX, baseZ[point] - straightZ);
|
||||
double availableOffset = StrictMath.max(0D, maximumOffset - baseDisplacement);
|
||||
double offset = availableOffset * 0.92D * envelope * meanderSignal[point];
|
||||
FlowTangent normal = localNormal(baseX, baseZ, point);
|
||||
x[point] = baseX[point] + normal.x() * offset;
|
||||
z[point] = baseZ[point] + normal.z() * offset;
|
||||
double rawBridgeX = rawX[point] - (from.x() + rawDeltaX * t);
|
||||
double rawBridgeZ = rawZ[point] - (from.z() + rawDeltaZ * t);
|
||||
x[point] = straightX + rawBridgeX * envelope;
|
||||
z[point] = straightZ + rawBridgeZ * envelope;
|
||||
maximumDisplacement = StrictMath.max(
|
||||
maximumDisplacement,
|
||||
StrictMath.hypot(x[point] - straightX, z[point] - straightZ)
|
||||
);
|
||||
}
|
||||
double maximumOffset = StrictMath.min(worm.maxOffset(), length * 0.35D);
|
||||
if (maximumDisplacement > maximumOffset && maximumDisplacement > 0D) {
|
||||
double scale = maximumOffset / maximumDisplacement;
|
||||
for (int point = 1; point < pointCount - 1; point++) {
|
||||
double t = (double) point / (pointCount - 1);
|
||||
double straightX = from.x() + deltaX * t;
|
||||
double straightZ = from.z() + deltaZ * t;
|
||||
x[point] = straightX + (x[point] - straightX) * scale;
|
||||
z[point] = straightZ + (z[point] - straightZ) * scale;
|
||||
}
|
||||
}
|
||||
x[0] = from.x();
|
||||
z[0] = from.z();
|
||||
@@ -514,222 +536,141 @@ public final class RiverNetwork {
|
||||
return new RiverPolyline(x, z);
|
||||
}
|
||||
|
||||
private double[] meanderNoise(
|
||||
RiverEdgeId id,
|
||||
double[] baseX,
|
||||
double[] baseZ,
|
||||
NodeResolver resolver
|
||||
private RiverWorm rootWormFor(RiverNodeId rootId) {
|
||||
return selectWeighted(options.worms(), hash(rootId, WORM_FAMILY_SALT));
|
||||
}
|
||||
|
||||
private RiverWorm childWormFor(
|
||||
RiverWorm parent,
|
||||
RiverNodeId parentId,
|
||||
RiverNodeId childId,
|
||||
int branchSlot
|
||||
) {
|
||||
int sampleCount = StrictMath.min(MEANDER_NOISE_SAMPLES, baseX.length);
|
||||
double[] knots = new double[sampleCount];
|
||||
for (int sample = 0; sample < sampleCount; sample++) {
|
||||
double t = (double) sample / (sampleCount - 1);
|
||||
double position = t * (baseX.length - 1);
|
||||
int lower = (int) StrictMath.floor(position);
|
||||
int upper = StrictMath.min(baseX.length - 1, lower + 1);
|
||||
double interpolation = position - lower;
|
||||
double curvedX = baseX[lower] + (baseX[upper] - baseX[lower]) * interpolation;
|
||||
double curvedZ = baseZ[lower] + (baseZ[upper] - baseZ[lower]) * interpolation;
|
||||
RiverMeanderContext context = new RiverMeanderContext(id, t, curvedX, curvedZ);
|
||||
double configuredNoise = resolver.terrain.meanderNoise(context);
|
||||
knots[sample] = Double.isFinite(configuredNoise)
|
||||
? StrictMath.max(-1D, StrictMath.min(1D, configuredNoise))
|
||||
: smoothEdgeNoise(id, t * 3D);
|
||||
if (parent.children().isEmpty()) {
|
||||
return parent;
|
||||
}
|
||||
double[] noise = new double[baseX.length];
|
||||
for (int point = 0; point < noise.length; point++) {
|
||||
double t = (double) point / (noise.length - 1);
|
||||
double position = t * (sampleCount - 1);
|
||||
int lower = (int) StrictMath.floor(position);
|
||||
int upper = StrictMath.min(sampleCount - 1, lower + 1);
|
||||
double interpolation = smoothStep(position - lower);
|
||||
noise[point] = knots[lower] + (knots[upper] - knots[lower]) * interpolation;
|
||||
double chance = StrictMath.min(
|
||||
1D,
|
||||
parent.childChance()
|
||||
+ StrictMath.min(7, branchSlot) * parent.branchChildChance()
|
||||
);
|
||||
RiverEdgeId edgeId = RiverEdgeId.of(childId, parentId);
|
||||
if (!gate(hash(edgeId, WORM_CHILD_GATE_SALT), chance)) {
|
||||
return parent;
|
||||
}
|
||||
return noise;
|
||||
return selectWeighted(parent.children(), hash(edgeId, WORM_CHILD_SELECTION_SALT));
|
||||
}
|
||||
|
||||
private MeanderProfile meanderProfile(RiverEdgeId id) {
|
||||
int personalityIndex = (int) StrictMath.floor(
|
||||
unit(hash(id, MEANDER_PERSONALITY_SALT)) * MEANDER_PERSONALITIES.length
|
||||
);
|
||||
MeanderPersonality personality = MEANDER_PERSONALITIES[
|
||||
StrictMath.min(MEANDER_PERSONALITIES.length - 1, personalityIndex)
|
||||
];
|
||||
double amplitudeRoll = unit(hash(id, MEANDER_AMPLITUDE_SALT));
|
||||
double amplitude = personality == MeanderPersonality.QUIET
|
||||
? 0.16D + amplitudeRoll * 0.24D
|
||||
: 0.48D + amplitudeRoll * 0.52D;
|
||||
double maximumCycles = StrictMath.max(1D, StrictMath.min(8D, options.meanderSubdivisions() / 5D));
|
||||
double cycleRoll = unit(hash(id, MEANDER_CYCLE_SALT));
|
||||
double cycles = switch (personality) {
|
||||
case SWEEP, HOOK, QUIET -> 0.45D + cycleRoll * 0.65D;
|
||||
case S_CURVE -> 0.75D + cycleRoll * StrictMath.min(1.25D, maximumCycles);
|
||||
case OXBOW -> 1D + cycleRoll * StrictMath.min(1.5D, maximumCycles);
|
||||
case COIL -> StrictMath.min(maximumCycles, 2.25D + cycleRoll * 4.75D);
|
||||
case WANDER -> 0.65D + cycleRoll * StrictMath.min(2.35D, maximumCycles);
|
||||
case CHIRP -> StrictMath.min(maximumCycles, 1.5D + cycleRoll * 5.5D);
|
||||
};
|
||||
double handedness = (hash(id, MEANDER_SALT) & 1L) == 0L ? -1D : 1D;
|
||||
return new MeanderProfile(
|
||||
personality,
|
||||
amplitude,
|
||||
handedness,
|
||||
unit(hash(id, MEANDER_PHASE_SALT)) * TWO_PI,
|
||||
centered(hash(id, MEANDER_SKEW_SALT)) * 0.72D,
|
||||
cycles,
|
||||
unit(hash(id, MEANDER_FEATURE_A_SALT)),
|
||||
unit(hash(id, MEANDER_FEATURE_B_SALT))
|
||||
);
|
||||
}
|
||||
|
||||
private double[] meanderSignal(RiverEdgeId id, MeanderProfile profile, double[] noise) {
|
||||
int subdivisions = noise.length - 1;
|
||||
for (int point = 0; point < noise.length; point++) {
|
||||
double t = (double) point / subdivisions;
|
||||
noise[point] = noise[point] * 0.78D + smoothEdgeNoise(id, t * 3.5D) * 0.22D;
|
||||
private RiverWorm selectWeighted(List<RiverWorm> worms, long selectionHash) {
|
||||
double totalWeight = 0D;
|
||||
for (RiverWorm worm : worms) {
|
||||
totalWeight += worm.weight();
|
||||
}
|
||||
smoothSignal(noise);
|
||||
double detailCycles = StrictMath.max(0.5D, profile.cycles() + profile.featureB() * 1.5D);
|
||||
double baseStep = TWO_PI * detailCycles / subdivisions;
|
||||
double phase = profile.phase() + profile.featureA() * StrictMath.PI;
|
||||
double[] signal = new double[noise.length];
|
||||
for (int point = 0; point < noise.length; point++) {
|
||||
double t = (double) point / subdivisions;
|
||||
if (point > 0) {
|
||||
double frequencyNoise = (noise[point - 1] + noise[point]) * 0.25D + 0.5D;
|
||||
phase += baseStep * (0.45D + frequencyNoise * 1.1D);
|
||||
double selection = unit(selectionHash) * totalWeight;
|
||||
double cumulative = 0D;
|
||||
for (RiverWorm worm : worms) {
|
||||
cumulative += worm.weight();
|
||||
if (selection < cumulative) {
|
||||
return worm;
|
||||
}
|
||||
double warpedT = warpMeanderPosition(t, profile.skew());
|
||||
double macro = personalitySignal(profile, warpedT, noise[point]);
|
||||
double detailWeight = detailWeight(profile.personality());
|
||||
double detail = detailWeight <= 0D
|
||||
? 0D
|
||||
: StrictMath.sin(phase) * (0.65D + StrictMath.abs(noise[point]) * 0.35D);
|
||||
double noiseWeight = noiseWeight(profile.personality());
|
||||
double macroWeight = 1D - detailWeight - noiseWeight;
|
||||
signal[point] = clampSigned(profile.amplitude()
|
||||
* (macro * macroWeight + detail * detailWeight + noise[point] * noiseWeight));
|
||||
}
|
||||
smoothSignal(signal);
|
||||
return signal;
|
||||
return worms.get(worms.size() - 1);
|
||||
}
|
||||
|
||||
private double personalitySignal(MeanderProfile profile, double t, double noise) {
|
||||
double handedness = profile.handedness();
|
||||
return switch (profile.personality()) {
|
||||
case SWEEP -> handedness * (0.68D + StrictMath.sin(StrictMath.PI * t) * 0.32D);
|
||||
case HOOK -> handedness * (0.12D + smoothStep(
|
||||
profile.featureA() < 0.5D ? t : 1D - t
|
||||
) * 0.88D);
|
||||
case S_CURVE -> handedness * StrictMath.sin(
|
||||
TWO_PI * profile.cycles() * t + profile.phase()
|
||||
);
|
||||
case COIL -> StrictMath.sin(TWO_PI * profile.cycles() * t + profile.phase())
|
||||
* (0.62D + StrictMath.sin(StrictMath.PI * t) * 0.38D);
|
||||
case WANDER -> clampSigned(
|
||||
noise * 0.82D
|
||||
+ StrictMath.sin(TWO_PI * profile.cycles() * t + profile.phase()) * 0.38D
|
||||
+ handedness * 0.12D
|
||||
);
|
||||
case OXBOW -> handedness * clampSigned(
|
||||
compactLobe(t, 0.18D + profile.featureA() * 0.2D, 0.24D + profile.featureB() * 0.12D)
|
||||
- compactLobe(t, 0.62D + profile.featureB() * 0.2D, 0.22D + profile.featureA() * 0.14D)
|
||||
* (0.55D + profile.featureA() * 0.45D)
|
||||
);
|
||||
case CHIRP -> StrictMath.sin(
|
||||
profile.phase() + TWO_PI * (0.45D * t + profile.cycles() * t * t)
|
||||
);
|
||||
case QUIET -> handedness * (0.72D + noise * 0.28D);
|
||||
private double perlin(double x, double z, double wavelength, long salt) {
|
||||
double scaledX = x / wavelength;
|
||||
double scaledZ = z / wavelength;
|
||||
long minimumX = (long) StrictMath.floor(scaledX);
|
||||
long minimumZ = (long) StrictMath.floor(scaledZ);
|
||||
double fractionX = scaledX - minimumX;
|
||||
double fractionZ = scaledZ - minimumZ;
|
||||
double fadeX = perlinFade(fractionX);
|
||||
double fadeZ = perlinFade(fractionZ);
|
||||
double northwest = perlinGradient(minimumX, minimumZ, fractionX, fractionZ, salt);
|
||||
double northeast = perlinGradient(minimumX + 1L, minimumZ, fractionX - 1D, fractionZ, salt);
|
||||
double southwest = perlinGradient(minimumX, minimumZ + 1L, fractionX, fractionZ - 1D, salt);
|
||||
double southeast = perlinGradient(
|
||||
minimumX + 1L,
|
||||
minimumZ + 1L,
|
||||
fractionX - 1D,
|
||||
fractionZ - 1D,
|
||||
salt
|
||||
);
|
||||
double north = northwest + (northeast - northwest) * fadeX;
|
||||
double south = southwest + (southeast - southwest) * fadeX;
|
||||
return StrictMath.max(
|
||||
-1D,
|
||||
StrictMath.min(1D, (north + (south - north) * fadeZ) * PERLIN_NORMALIZATION)
|
||||
);
|
||||
}
|
||||
|
||||
private double perlinGradient(long latticeX, long latticeZ, double x, double z, long salt) {
|
||||
return switch ((int) (hash(latticeX, latticeZ, salt) & 7L)) {
|
||||
case 0 -> x;
|
||||
case 1 -> -x;
|
||||
case 2 -> z;
|
||||
case 3 -> -z;
|
||||
case 4 -> (x + z) / PERLIN_NORMALIZATION;
|
||||
case 5 -> (-x + z) / PERLIN_NORMALIZATION;
|
||||
case 6 -> (x - z) / PERLIN_NORMALIZATION;
|
||||
default -> (-x - z) / PERLIN_NORMALIZATION;
|
||||
};
|
||||
}
|
||||
|
||||
private double detailWeight(MeanderPersonality personality) {
|
||||
return switch (personality) {
|
||||
case SWEEP, HOOK, OXBOW, QUIET -> 0D;
|
||||
case S_CURVE -> 0.12D;
|
||||
case COIL, WANDER -> 0.08D;
|
||||
case CHIRP -> 0.1D;
|
||||
};
|
||||
private double perlinFade(double value) {
|
||||
double squared = value * value;
|
||||
double cubed = squared * value;
|
||||
return cubed * (value * (value * 6D - 15D) + 10D);
|
||||
}
|
||||
|
||||
private double noiseWeight(MeanderPersonality personality) {
|
||||
return switch (personality) {
|
||||
case SWEEP, S_CURVE, COIL, OXBOW, CHIRP -> 0.08D;
|
||||
case HOOK -> 0.1D;
|
||||
case WANDER -> 0.34D;
|
||||
case QUIET -> 0.14D;
|
||||
};
|
||||
}
|
||||
|
||||
private double warpMeanderPosition(double t, double skew) {
|
||||
return t + skew * t * (1D - t);
|
||||
}
|
||||
|
||||
private double compactLobe(double t, double center, double radius) {
|
||||
double distance = StrictMath.abs(t - center) / radius;
|
||||
if (distance >= 1D) {
|
||||
return 0D;
|
||||
}
|
||||
return smoothStep(1D - distance);
|
||||
}
|
||||
|
||||
private double smoothStep(double value) {
|
||||
double clamped = StrictMath.max(0D, StrictMath.min(1D, value));
|
||||
return clamped * clamped * (3D - 2D * clamped);
|
||||
}
|
||||
|
||||
private void smoothSignal(double[] signal) {
|
||||
if (signal.length < 3) {
|
||||
return;
|
||||
}
|
||||
double previousRaw = signal[0];
|
||||
double currentRaw = signal[1];
|
||||
for (int point = 1; point < signal.length - 1; point++) {
|
||||
double nextRaw = signal[point + 1];
|
||||
signal[point] = (previousRaw + currentRaw * 2D + nextRaw) * 0.25D;
|
||||
previousRaw = currentRaw;
|
||||
currentRaw = nextRaw;
|
||||
}
|
||||
}
|
||||
|
||||
private double clampSigned(double value) {
|
||||
return StrictMath.max(-1D, StrictMath.min(1D, value));
|
||||
}
|
||||
|
||||
private FlowTangent localNormal(double[] x, double[] z, int point) {
|
||||
int previous = StrictMath.max(0, point - 1);
|
||||
int next = StrictMath.min(x.length - 1, point + 1);
|
||||
double tangentX = x[next] - x[previous];
|
||||
double tangentZ = z[next] - z[previous];
|
||||
double tangentLength = StrictMath.hypot(tangentX, tangentZ);
|
||||
if (tangentLength <= 0.0000001D) {
|
||||
return new FlowTangent(0D, 0D);
|
||||
}
|
||||
return new FlowTangent(-tangentZ / tangentLength, tangentX / tangentLength);
|
||||
}
|
||||
|
||||
private FlowTangent flowTangent(
|
||||
RiverNode node,
|
||||
double fallbackX,
|
||||
double fallbackZ,
|
||||
NodeResolver resolver
|
||||
private double bodyMultiplier(
|
||||
ReachPosition position,
|
||||
RiverWorm worm,
|
||||
long primarySalt,
|
||||
long detailSalt,
|
||||
double variation
|
||||
) {
|
||||
FlowTangent preferred = resolver.flowTangent(node);
|
||||
double tangentX = preferred.x();
|
||||
double tangentZ = preferred.z();
|
||||
if (tangentX == 0D && tangentZ == 0D) {
|
||||
return new FlowTangent(fallbackX, fallbackZ);
|
||||
if (variation <= 0D) {
|
||||
return 1D;
|
||||
}
|
||||
double alignment = tangentX * fallbackX + tangentZ * fallbackZ;
|
||||
if (alignment < 0D) {
|
||||
tangentX = -tangentX;
|
||||
tangentZ = -tangentZ;
|
||||
alignment = -alignment;
|
||||
return StrictMath.max(
|
||||
0.125D,
|
||||
1D + bodyField(position, worm, primarySalt, detailSalt) * variation
|
||||
);
|
||||
}
|
||||
|
||||
private double roofScale(ReachPosition position, RiverWorm worm) {
|
||||
if (worm.roofVariation() <= 0D) {
|
||||
return 1D;
|
||||
}
|
||||
if (alignment < 0.15D) {
|
||||
return new FlowTangent(fallbackX, fallbackZ);
|
||||
}
|
||||
return new FlowTangent(tangentX, tangentZ);
|
||||
double normalized = bodyField(
|
||||
position,
|
||||
worm,
|
||||
BODY_ROOF_PRIMARY_SALT,
|
||||
BODY_ROOF_DETAIL_SALT
|
||||
) * 0.5D + 0.5D;
|
||||
return StrictMath.max(0.125D, 1D - normalized * worm.roofVariation());
|
||||
}
|
||||
|
||||
private double bodyField(
|
||||
ReachPosition position,
|
||||
RiverWorm worm,
|
||||
long primarySalt,
|
||||
long detailSalt
|
||||
) {
|
||||
double primary = perlin(
|
||||
position.x(),
|
||||
position.z(),
|
||||
worm.bodyWavelength(),
|
||||
worm.seed() ^ primarySalt
|
||||
);
|
||||
double detail = perlin(
|
||||
position.x(),
|
||||
position.z(),
|
||||
worm.bodyDetailWavelength(),
|
||||
worm.seed() ^ detailSalt
|
||||
);
|
||||
return primary * 0.7D + detail * 0.3D;
|
||||
}
|
||||
|
||||
private FlowTangent resolveFlowTangent(RiverNode node, RiverTerrainSampler terrain) {
|
||||
@@ -771,16 +712,6 @@ public final class RiverNetwork {
|
||||
return options.flowAlignmentWeight() * (1D - StrictMath.min(1D, alignment));
|
||||
}
|
||||
|
||||
private double smoothEdgeNoise(RiverEdgeId id, double position) {
|
||||
int lower = (int) StrictMath.floor(position);
|
||||
int upper = lower + 1;
|
||||
double fraction = position - lower;
|
||||
double fade = fraction * fraction * (3.0 - 2.0 * fraction);
|
||||
double a = centered(mix(options.seed() ^ id.stableId() ^ MEANDER_SALT ^ lower * 0x9E3779B97F4A7C15L));
|
||||
double b = centered(mix(options.seed() ^ id.stableId() ^ MEANDER_SALT ^ upper * 0x9E3779B97F4A7C15L));
|
||||
return a + (b - a) * fade;
|
||||
}
|
||||
|
||||
private boolean intersects(
|
||||
RiverReach reach,
|
||||
long minimumX,
|
||||
@@ -812,8 +743,8 @@ public final class RiverNetwork {
|
||||
long maximumZ
|
||||
) {
|
||||
double length = StrictMath.hypot(to.x() - from.x(), to.z() - from.z());
|
||||
double maximumMeander = StrictMath.min(options.meanderStrength(), length * 0.35D);
|
||||
double padding = options.maximumReachRadius() + maximumMeander;
|
||||
double maximumWormOffset = StrictMath.min(options.maximumWormOffset(), length * 0.35D);
|
||||
double padding = options.maximumReachRadius() + maximumWormOffset;
|
||||
double reachMinimumX = StrictMath.min(from.x(), to.x()) - padding;
|
||||
double reachMaximumX = StrictMath.max(from.x(), to.x()) + padding;
|
||||
double reachMinimumZ = StrictMath.min(from.z(), to.z()) - padding;
|
||||
@@ -825,8 +756,8 @@ public final class RiverNetwork {
|
||||
private int geometryPaddingCells() {
|
||||
double maximumEdgeAxisDelta = options.cellSize() * (1D + options.siteJitter());
|
||||
double maximumEdgeLength = StrictMath.sqrt(2D) * maximumEdgeAxisDelta;
|
||||
double maximumMeander = StrictMath.min(options.meanderStrength(), maximumEdgeLength * 0.35D);
|
||||
double displacement = options.maximumReachRadius() + maximumMeander;
|
||||
double maximumWormOffset = StrictMath.min(options.maximumWormOffset(), maximumEdgeLength * 0.35D);
|
||||
double displacement = options.maximumReachRadius() + maximumWormOffset;
|
||||
return 1 + (int) StrictMath.ceil(displacement / options.cellSize());
|
||||
}
|
||||
|
||||
@@ -917,7 +848,12 @@ public final class RiverNetwork {
|
||||
private final Map<RiverEdgeId, RiverRoutingContext> routingContexts;
|
||||
private final Map<RiverNodeId, FlowTangent> flowTangents;
|
||||
private final Map<RiverEdgeId, Integer> branchSlots;
|
||||
private final Map<RiverNodeId, Double> styleDistances;
|
||||
private final Map<RiverNodeId, StyleParent> styleParents;
|
||||
private final Map<RiverNodeId, RiverWorm> styleWorms;
|
||||
private final Map<RiverEdgeId, Integer> styleBranchSlots;
|
||||
private final Map<RiverNodeId, Boolean> resolvedBranchParents;
|
||||
private final Map<RiverNodeId, Boolean> resolvedStyleBranchParents;
|
||||
|
||||
private NodeResolver(RiverTerrainSampler terrain) {
|
||||
this.terrain = terrain;
|
||||
@@ -932,7 +868,12 @@ public final class RiverNetwork {
|
||||
routingContexts = new HashMap<>();
|
||||
flowTangents = new HashMap<>();
|
||||
branchSlots = new HashMap<>();
|
||||
styleDistances = new HashMap<>();
|
||||
styleParents = new HashMap<>();
|
||||
styleWorms = new HashMap<>();
|
||||
styleBranchSlots = new HashMap<>();
|
||||
resolvedBranchParents = new HashMap<>();
|
||||
resolvedStyleBranchParents = new HashMap<>();
|
||||
}
|
||||
|
||||
private RiverNode resolve(RiverNodeId id) {
|
||||
@@ -1099,11 +1040,88 @@ public final class RiverNetwork {
|
||||
|
||||
private RiverRoutingContext routingContext(RiverNode from, RiverNode to) {
|
||||
RiverEdgeId edgeId = RiverEdgeId.of(from.id(), to.id());
|
||||
RiverWorm worm = worm(from, to);
|
||||
return routingContexts.computeIfAbsent(edgeId, ignored -> RiverRoutingContext.lazy(
|
||||
edgeId,
|
||||
from,
|
||||
to,
|
||||
() -> createPolyline(edgeId, from, to, this)));
|
||||
() -> createPolyline(from, to, worm)));
|
||||
}
|
||||
|
||||
private RiverWorm worm(RiverNode first, RiverNode second) {
|
||||
RiverNode child = compareRank(first, second) > 0 ? first : second;
|
||||
return styleWorm(child.id());
|
||||
}
|
||||
|
||||
private RiverWorm styleWorm(RiverNodeId id) {
|
||||
RiverWorm cached = styleWorms.get(id);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
StyleParent parent = styleParent(id);
|
||||
RiverWorm selected;
|
||||
if (parent.id() == null) {
|
||||
selected = rootWormFor(id);
|
||||
} else {
|
||||
RiverWorm parentWorm = styleWorm(parent.id());
|
||||
selected = childWormFor(
|
||||
parentWorm,
|
||||
parent.id(),
|
||||
id,
|
||||
styleBranchSlot(parent.id(), id)
|
||||
);
|
||||
}
|
||||
styleWorms.put(id, selected);
|
||||
return selected;
|
||||
}
|
||||
|
||||
private StyleParent styleParent(RiverNodeId id) {
|
||||
return styleParents.computeIfAbsent(id, nodeId -> {
|
||||
double nodeDistance = styleDistance(nodeId);
|
||||
RiverNodeId selected = null;
|
||||
double selectedDistance = Double.POSITIVE_INFINITY;
|
||||
for (RiverNodeId candidate : neighbors(nodeId)) {
|
||||
double candidateDistance = styleDistance(candidate);
|
||||
if (candidateDistance >= nodeDistance - 0.000000001D) {
|
||||
continue;
|
||||
}
|
||||
if (candidateDistance < selectedDistance
|
||||
|| candidateDistance == selectedDistance
|
||||
&& (selected == null || candidate.compareTo(selected) < 0)) {
|
||||
selected = candidate;
|
||||
selectedDistance = candidateDistance;
|
||||
}
|
||||
}
|
||||
return new StyleParent(selected);
|
||||
});
|
||||
}
|
||||
|
||||
private double styleDistance(RiverNodeId id) {
|
||||
return styleDistances.computeIfAbsent(id, RiverNetwork.this::drainageDistance);
|
||||
}
|
||||
|
||||
private int styleBranchSlot(RiverNodeId parentId, RiverNodeId childId) {
|
||||
if (!resolvedStyleBranchParents.containsKey(parentId)) {
|
||||
ArrayList<RiverNodeId> children = new ArrayList<>(8);
|
||||
for (RiverNodeId candidate : neighbors(parentId)) {
|
||||
StyleParent candidateParent = styleParent(candidate);
|
||||
if (parentId.equals(candidateParent.id())) {
|
||||
children.add(candidate);
|
||||
}
|
||||
}
|
||||
children.sort((first, second) -> {
|
||||
long firstPriority = hash(RiverEdgeId.of(first, parentId), BRANCH_SLOT_SALT);
|
||||
long secondPriority = hash(RiverEdgeId.of(second, parentId), BRANCH_SLOT_SALT);
|
||||
int priorityComparison = Long.compareUnsigned(firstPriority, secondPriority);
|
||||
return priorityComparison != 0 ? priorityComparison : first.compareTo(second);
|
||||
});
|
||||
for (int slot = 0; slot < children.size(); slot++) {
|
||||
RiverNodeId child = children.get(slot);
|
||||
styleBranchSlots.put(RiverEdgeId.of(child, parentId), slot);
|
||||
}
|
||||
resolvedStyleBranchParents.put(parentId, true);
|
||||
}
|
||||
return styleBranchSlots.getOrDefault(RiverEdgeId.of(childId, parentId), Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
private FlowTangent flowTangent(RiverNode node) {
|
||||
@@ -1122,27 +1140,13 @@ public final class RiverNetwork {
|
||||
private record FlowTangent(double x, double z) {
|
||||
}
|
||||
|
||||
private record MeanderProfile(
|
||||
MeanderPersonality personality,
|
||||
double amplitude,
|
||||
double handedness,
|
||||
double phase,
|
||||
double skew,
|
||||
double cycles,
|
||||
double featureA,
|
||||
double featureB
|
||||
) {
|
||||
private record DrainageBasinId(long x, long z) {
|
||||
}
|
||||
|
||||
private enum MeanderPersonality {
|
||||
QUIET,
|
||||
SWEEP,
|
||||
HOOK,
|
||||
S_CURVE,
|
||||
COIL,
|
||||
WANDER,
|
||||
OXBOW,
|
||||
CHIRP
|
||||
private record DrainageBasin(DrainageBasinId id, double distance) {
|
||||
}
|
||||
|
||||
private record StyleParent(RiverNodeId id) {
|
||||
}
|
||||
|
||||
private record SourceTileId(long tileX, long tileZ) {
|
||||
@@ -1156,6 +1160,7 @@ public final class RiverNetwork {
|
||||
private final RiverNode from;
|
||||
private final RiverNode to;
|
||||
private final RiverRoutingContext context;
|
||||
private final RiverWorm worm;
|
||||
private final RiverTerrainSampler terrain;
|
||||
private int wetFlow;
|
||||
private int dryFlow;
|
||||
@@ -1167,12 +1172,14 @@ public final class RiverNetwork {
|
||||
RiverNode from,
|
||||
RiverNode to,
|
||||
RiverRoutingContext context,
|
||||
RiverWorm worm,
|
||||
RiverTerrainSampler terrain
|
||||
) {
|
||||
this.id = id;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.context = context;
|
||||
this.worm = worm;
|
||||
this.terrain = terrain;
|
||||
}
|
||||
|
||||
@@ -1193,21 +1200,7 @@ public final class RiverNetwork {
|
||||
private RiverReach build() {
|
||||
int flow = wetFlow + dryFlow;
|
||||
int order = 1 + (31 - Integer.numberOfLeadingZeros(flow));
|
||||
double bankWidth = nonNegativeOrFallback(
|
||||
terrain.bankWidth(context, options.bankWidth()),
|
||||
options.bankWidth()
|
||||
);
|
||||
double baseDepth = positiveOrFallback(
|
||||
terrain.depth(context, options.depth()),
|
||||
options.depth()
|
||||
);
|
||||
RiverWidthProfile widthProfile = widthProfile(order);
|
||||
double width = widthProfile.maximum();
|
||||
bankWidth = StrictMath.min(options.maxBankWidth(), bankWidth);
|
||||
double depth = StrictMath.min(
|
||||
options.maxDepth(),
|
||||
baseDepth * (1.0 + options.orderDepthFactor() * (order - 1))
|
||||
);
|
||||
RiverBodyProfile bodyProfile = bodyProfile(order, worm);
|
||||
RiverRouteState state = wetFlow > 0 ? RiverRouteState.WET : RiverRouteState.DRY;
|
||||
return new RiverReach(
|
||||
id,
|
||||
@@ -1216,10 +1209,10 @@ public final class RiverNetwork {
|
||||
state,
|
||||
flow,
|
||||
order,
|
||||
width,
|
||||
widthProfile,
|
||||
bankWidth,
|
||||
depth,
|
||||
bodyProfile.maximumWidth(),
|
||||
bodyProfile.maximumBankWidth(),
|
||||
bodyProfile.maximumDepth(),
|
||||
bodyProfile,
|
||||
state == RiverRouteState.WET && to.ocean(),
|
||||
state == RiverRouteState.WET
|
||||
? terminalWetFlow == wetFlow
|
||||
@@ -1228,12 +1221,21 @@ public final class RiverNetwork {
|
||||
);
|
||||
}
|
||||
|
||||
private RiverWidthProfile widthProfile(int order) {
|
||||
private RiverBodyProfile bodyProfile(int order, RiverWorm worm) {
|
||||
RiverPolyline polyline = context.polyline();
|
||||
int sampleCount = Math.min(WIDTH_PROFILE_SAMPLES, Math.max(2, polyline.size()));
|
||||
double minimumWavelength = StrictMath.min(worm.bodyWavelength(), worm.bodyDetailWavelength());
|
||||
int resolvedSamples = 1 + (int) StrictMath.ceil(polyline.length() * 2D / minimumWavelength);
|
||||
int sampleCount = StrictMath.max(
|
||||
MINIMUM_BODY_PROFILE_SAMPLES,
|
||||
StrictMath.min(MAXIMUM_BODY_PROFILE_SAMPLES, resolvedSamples)
|
||||
);
|
||||
double[] positions = new double[sampleCount];
|
||||
double[] widths = new double[sampleCount];
|
||||
double orderScale = 1D + options.orderWidthFactor() * (order - 1);
|
||||
double[] bankWidths = new double[sampleCount];
|
||||
double[] depths = new double[sampleCount];
|
||||
double[] roofScales = new double[sampleCount];
|
||||
double widthOrderScale = 1D + options.orderWidthFactor() * (order - 1);
|
||||
double depthOrderScale = 1D + options.orderDepthFactor() * (order - 1);
|
||||
for (int index = 0; index < sampleCount; index++) {
|
||||
double alongReach = (double) index / (sampleCount - 1);
|
||||
ReachPosition position = positionAt(polyline, alongReach);
|
||||
@@ -1249,10 +1251,59 @@ public final class RiverNetwork {
|
||||
positions[index] = alongReach;
|
||||
widths[index] = StrictMath.min(
|
||||
options.maxChannelWidth(),
|
||||
StrictMath.max(1D, baseWidth * orderScale)
|
||||
StrictMath.max(
|
||||
1D,
|
||||
baseWidth
|
||||
* widthOrderScale
|
||||
* worm.widthMultiplier()
|
||||
* bodyMultiplier(
|
||||
position,
|
||||
worm,
|
||||
BODY_WIDTH_PRIMARY_SALT,
|
||||
BODY_WIDTH_DETAIL_SALT,
|
||||
worm.widthVariation()
|
||||
)
|
||||
)
|
||||
);
|
||||
double baseBankWidth = nonNegativeOrFallback(
|
||||
terrain.bankWidth(context, position.x(), position.z(), options.bankWidth()),
|
||||
options.bankWidth()
|
||||
);
|
||||
bankWidths[index] = StrictMath.min(
|
||||
options.maxBankWidth(),
|
||||
baseBankWidth
|
||||
* worm.bankMultiplier()
|
||||
* bodyMultiplier(
|
||||
position,
|
||||
worm,
|
||||
BODY_BANK_PRIMARY_SALT,
|
||||
BODY_BANK_DETAIL_SALT,
|
||||
worm.bankVariation()
|
||||
)
|
||||
);
|
||||
double baseDepth = positiveOrFallback(
|
||||
terrain.depth(context, position.x(), position.z(), options.depth()),
|
||||
options.depth()
|
||||
);
|
||||
depths[index] = StrictMath.min(
|
||||
options.maxDepth(),
|
||||
StrictMath.max(
|
||||
1D,
|
||||
baseDepth
|
||||
* depthOrderScale
|
||||
* worm.depthMultiplier()
|
||||
* bodyMultiplier(
|
||||
position,
|
||||
worm,
|
||||
BODY_DEPTH_PRIMARY_SALT,
|
||||
BODY_DEPTH_DETAIL_SALT,
|
||||
worm.depthVariation()
|
||||
)
|
||||
)
|
||||
);
|
||||
roofScales[index] = roofScale(position, worm);
|
||||
}
|
||||
return new RiverWidthProfile(positions, widths);
|
||||
return new RiverBodyProfile(positions, widths, bankWidths, depths, roofScales);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public record RiverNetworkOptions(
|
||||
long seed,
|
||||
int cellSize,
|
||||
@@ -21,8 +25,6 @@ public record RiverNetworkOptions(
|
||||
double routingNoiseWeight,
|
||||
double flowAlignmentWeight,
|
||||
double confluenceWeight,
|
||||
int branchSoftCap,
|
||||
double branchChildShrinkFactor,
|
||||
double oceanAttraction,
|
||||
double channelWidth,
|
||||
double bankWidth,
|
||||
@@ -33,8 +35,7 @@ public record RiverNetworkOptions(
|
||||
double orderWidthFactor,
|
||||
double orderDepthFactor,
|
||||
double maximumReachRadius,
|
||||
double meanderStrength,
|
||||
int meanderSubdivisions
|
||||
List<RiverWorm> worms
|
||||
) {
|
||||
public RiverNetworkOptions {
|
||||
requireRange(cellSize, 8, 4096, "cellSize");
|
||||
@@ -47,7 +48,6 @@ public record RiverNetworkOptions(
|
||||
requireRange(routingDeviationStrengthCells, 0D, 32D, "routingDeviationStrengthCells");
|
||||
requirePositive(routingPlateauHeight, "routingPlateauHeight");
|
||||
requireFinite(hydraulicBaseHeight, "hydraulicBaseHeight");
|
||||
requireRange(meanderSubdivisions, 1, 64, "meanderSubdivisions");
|
||||
requireProbability(siteJitter, "siteJitter");
|
||||
requireProbability(sourceChance, "sourceChance");
|
||||
requireProbability(reachChance, "reachChance");
|
||||
@@ -56,8 +56,6 @@ public record RiverNetworkOptions(
|
||||
requireFiniteNonNegative(routingNoiseWeight, "routingNoiseWeight");
|
||||
requireFiniteNonNegative(flowAlignmentWeight, "flowAlignmentWeight");
|
||||
requireFiniteNonNegative(confluenceWeight, "confluenceWeight");
|
||||
requireRange(branchSoftCap, 1, 8, "branchSoftCap");
|
||||
requireProbability(branchChildShrinkFactor, "branchChildShrinkFactor");
|
||||
requireFiniteNonNegative(oceanAttraction, "oceanAttraction");
|
||||
requirePositive(channelWidth, "channelWidth");
|
||||
requireFiniteNonNegative(bankWidth, "bankWidth");
|
||||
@@ -68,15 +66,27 @@ public record RiverNetworkOptions(
|
||||
requireFiniteNonNegative(orderWidthFactor, "orderWidthFactor");
|
||||
requireFiniteNonNegative(orderDepthFactor, "orderDepthFactor");
|
||||
requireFiniteNonNegative(maximumReachRadius, "maximumReachRadius");
|
||||
requireFiniteNonNegative(meanderStrength, "meanderStrength");
|
||||
if (worms == null || worms.isEmpty()) {
|
||||
throw new IllegalArgumentException("worms must contain at least one profile");
|
||||
}
|
||||
if (worms.size() > 16) {
|
||||
throw new IllegalArgumentException("worms must contain at most 16 root profiles");
|
||||
}
|
||||
Set<String> ids = new HashSet<String>();
|
||||
Set<Long> seeds = new HashSet<Long>();
|
||||
int wormCount = validateWormTree(worms, 1, ids, seeds);
|
||||
if (wormCount > 128) {
|
||||
throw new IllegalArgumentException("worm hierarchy must contain at most 128 profiles");
|
||||
}
|
||||
worms = List.copyOf(worms);
|
||||
RiverTopologyComplexity.requireSafe(
|
||||
cellSize,
|
||||
tileCells,
|
||||
siteJitter,
|
||||
maxRouteReaches,
|
||||
maximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
maximumWormOffset(worms),
|
||||
maximumWormSegments(worms)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,6 +94,66 @@ public record RiverNetworkOptions(
|
||||
return new Builder(seed);
|
||||
}
|
||||
|
||||
public double maximumWormOffset() {
|
||||
return maximumWormOffset(worms);
|
||||
}
|
||||
|
||||
public int maximumWormSegments() {
|
||||
return maximumWormSegments(worms);
|
||||
}
|
||||
|
||||
private static double maximumWormOffset(List<RiverWorm> worms) {
|
||||
double maximum = 0D;
|
||||
for (RiverWorm worm : worms) {
|
||||
if (worm == null) {
|
||||
throw new IllegalArgumentException("worms must not contain null profiles");
|
||||
}
|
||||
maximum = StrictMath.max(maximum, worm.maxOffset());
|
||||
maximum = StrictMath.max(maximum, maximumWormOffset(worm.children()));
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
private static int maximumWormSegments(List<RiverWorm> worms) {
|
||||
int maximum = 1;
|
||||
for (RiverWorm worm : worms) {
|
||||
if (worm == null) {
|
||||
throw new IllegalArgumentException("worms must not contain null profiles");
|
||||
}
|
||||
maximum = StrictMath.max(maximum, worm.segments());
|
||||
maximum = StrictMath.max(maximum, maximumWormSegments(worm.children()));
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
private static int validateWormTree(
|
||||
List<RiverWorm> worms,
|
||||
int depth,
|
||||
Set<String> ids,
|
||||
Set<Long> seeds
|
||||
) {
|
||||
if (depth > 4) {
|
||||
throw new IllegalArgumentException("worm hierarchy must be at most 4 profiles deep");
|
||||
}
|
||||
int count = 0;
|
||||
for (RiverWorm worm : worms) {
|
||||
if (worm == null) {
|
||||
throw new IllegalArgumentException("worm hierarchy must not contain null profiles");
|
||||
}
|
||||
if (!ids.add(worm.id())) {
|
||||
throw new IllegalArgumentException("worm ids must be unique: " + worm.id());
|
||||
}
|
||||
if (!seeds.add(worm.seed())) {
|
||||
throw new IllegalArgumentException("worm seeds must be unique: " + worm.seed());
|
||||
}
|
||||
count++;
|
||||
if (!worm.children().isEmpty()) {
|
||||
count += validateWormTree(worm.children(), depth + 1, ids, seeds);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void requireRange(int value, int minimum, int maximum, String name) {
|
||||
if (value < minimum || value > maximum) {
|
||||
throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
|
||||
@@ -141,8 +211,6 @@ public record RiverNetworkOptions(
|
||||
private double routingNoiseWeight;
|
||||
private double flowAlignmentWeight;
|
||||
private double confluenceWeight;
|
||||
private int branchSoftCap;
|
||||
private double branchChildShrinkFactor;
|
||||
private double oceanAttraction;
|
||||
private double channelWidth;
|
||||
private double bankWidth;
|
||||
@@ -153,8 +221,7 @@ public record RiverNetworkOptions(
|
||||
private double orderWidthFactor;
|
||||
private double orderDepthFactor;
|
||||
private double maximumReachRadius;
|
||||
private double meanderStrength;
|
||||
private int meanderSubdivisions;
|
||||
private List<RiverWorm> worms;
|
||||
|
||||
private Builder(long seed) {
|
||||
this.seed = seed;
|
||||
@@ -177,8 +244,6 @@ public record RiverNetworkOptions(
|
||||
routingNoiseWeight = 24.0;
|
||||
flowAlignmentWeight = 0D;
|
||||
confluenceWeight = 0D;
|
||||
branchSoftCap = 4;
|
||||
branchChildShrinkFactor = 0.35D;
|
||||
oceanAttraction = 64.0;
|
||||
channelWidth = 10.0;
|
||||
bankWidth = 8.0;
|
||||
@@ -189,8 +254,32 @@ public record RiverNetworkOptions(
|
||||
orderWidthFactor = 0.35;
|
||||
orderDepthFactor = 0.2;
|
||||
maximumReachRadius = Double.NaN;
|
||||
meanderStrength = 40.0;
|
||||
meanderSubdivisions = 8;
|
||||
worms = List.of(new RiverWorm(
|
||||
"default",
|
||||
1L,
|
||||
1D,
|
||||
1024D,
|
||||
256D,
|
||||
0.5D,
|
||||
0.15D,
|
||||
40D,
|
||||
8,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
512D,
|
||||
128D,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
4,
|
||||
0.35D,
|
||||
1D,
|
||||
0D,
|
||||
0D,
|
||||
List.of()
|
||||
));
|
||||
}
|
||||
|
||||
public Builder cellSize(int value) {
|
||||
@@ -288,16 +377,6 @@ public record RiverNetworkOptions(
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder branchSoftCap(int value) {
|
||||
branchSoftCap = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder branchChildShrinkFactor(double value) {
|
||||
branchChildShrinkFactor = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder oceanAttraction(double value) {
|
||||
oceanAttraction = value;
|
||||
return this;
|
||||
@@ -348,13 +427,8 @@ public record RiverNetworkOptions(
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder meanderStrength(double value) {
|
||||
meanderStrength = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder meanderSubdivisions(int value) {
|
||||
meanderSubdivisions = value;
|
||||
public Builder worms(List<RiverWorm> value) {
|
||||
worms = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -383,8 +457,6 @@ public record RiverNetworkOptions(
|
||||
routingNoiseWeight,
|
||||
flowAlignmentWeight,
|
||||
confluenceWeight,
|
||||
branchSoftCap,
|
||||
branchChildShrinkFactor,
|
||||
oceanAttraction,
|
||||
channelWidth,
|
||||
bankWidth,
|
||||
@@ -395,8 +467,7 @@ public record RiverNetworkOptions(
|
||||
orderWidthFactor,
|
||||
orderDepthFactor,
|
||||
resolvedMaximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
worms
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ public record RiverReach(
|
||||
int flow,
|
||||
int order,
|
||||
double width,
|
||||
RiverWidthProfile widthProfile,
|
||||
double bankWidth,
|
||||
double depth,
|
||||
RiverBodyProfile bodyProfile,
|
||||
boolean mouth,
|
||||
boolean terminal,
|
||||
RiverPolyline polyline
|
||||
@@ -22,7 +22,7 @@ public record RiverReach(
|
||||
Objects.requireNonNull(from);
|
||||
Objects.requireNonNull(to);
|
||||
Objects.requireNonNull(state);
|
||||
Objects.requireNonNull(widthProfile);
|
||||
Objects.requireNonNull(bodyProfile);
|
||||
Objects.requireNonNull(polyline);
|
||||
if (state == RiverRouteState.SUPPRESSED) {
|
||||
throw new IllegalArgumentException("Suppressed routes cannot produce reaches");
|
||||
@@ -34,12 +34,26 @@ public record RiverReach(
|
||||
|| !Double.isFinite(depth) || depth <= 0.0) {
|
||||
throw new IllegalArgumentException("River reach dimensions must be finite and valid");
|
||||
}
|
||||
if (Double.compare(width, widthProfile.maximum()) != 0) {
|
||||
throw new IllegalArgumentException("River reach width must equal its profile maximum");
|
||||
if (Double.compare(width, bodyProfile.maximumWidth()) != 0
|
||||
|| Double.compare(bankWidth, bodyProfile.maximumBankWidth()) != 0
|
||||
|| Double.compare(depth, bodyProfile.maximumDepth()) != 0) {
|
||||
throw new IllegalArgumentException("River reach dimensions must equal their body-profile maxima");
|
||||
}
|
||||
}
|
||||
|
||||
public double widthAt(double alongReach) {
|
||||
return widthProfile.sample(alongReach);
|
||||
return bodyProfile.width(alongReach);
|
||||
}
|
||||
|
||||
public double bankWidthAt(double alongReach) {
|
||||
return bodyProfile.bankWidth(alongReach);
|
||||
}
|
||||
|
||||
public double depthAt(double alongReach) {
|
||||
return bodyProfile.depth(alongReach);
|
||||
}
|
||||
|
||||
public double roofScaleAt(double alongReach) {
|
||||
return bodyProfile.roofScale(alongReach);
|
||||
}
|
||||
}
|
||||
@@ -50,10 +50,6 @@ public interface RiverTerrainSampler {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
default double meanderNoise(RiverMeanderContext context) {
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
default double flowNoise(double x, double z) {
|
||||
return Double.NaN;
|
||||
}
|
||||
@@ -75,10 +71,28 @@ public interface RiverTerrainSampler {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
default double bankWidth(
|
||||
RiverRoutingContext context,
|
||||
double x,
|
||||
double z,
|
||||
double fallback
|
||||
) {
|
||||
return bankWidth(context, fallback);
|
||||
}
|
||||
|
||||
default double depth(RiverRoutingContext context, double fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
default double depth(
|
||||
RiverRoutingContext context,
|
||||
double x,
|
||||
double z,
|
||||
double fallback
|
||||
) {
|
||||
return depth(context, fallback);
|
||||
}
|
||||
|
||||
default RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
|
||||
return RiverTerminalPolicy.INHERIT;
|
||||
}
|
||||
|
||||
@@ -210,9 +210,11 @@ public final class RiverTile {
|
||||
|
||||
double distance = StrictMath.sqrt(nearestDistanceSquared);
|
||||
double localWidth = nearestReach.widthAt(nearestAlongReach);
|
||||
double localBankWidth = nearestReach.bankWidthAt(nearestAlongReach);
|
||||
double localDepth = nearestReach.depthAt(nearestAlongReach);
|
||||
double channelRadius = localWidth * 0.5;
|
||||
RiverSection section = section(nearestReach, distance, channelRadius);
|
||||
double carveWeight = carveWeight(distance, channelRadius, nearestReach.bankWidth());
|
||||
double carveWeight = carveWeight(distance, channelRadius, localBankWidth);
|
||||
return new RiverSample(
|
||||
true,
|
||||
nearestReach.state(),
|
||||
@@ -223,8 +225,8 @@ public final class RiverTile {
|
||||
nearestReach.flow(),
|
||||
nearestReach.order(),
|
||||
localWidth,
|
||||
nearestReach.bankWidth(),
|
||||
nearestReach.depth(),
|
||||
localBankWidth,
|
||||
localDepth,
|
||||
nearestReach.terminal(),
|
||||
nearestReach.id()
|
||||
);
|
||||
@@ -325,7 +327,7 @@ public final class RiverTile {
|
||||
RiverPolyline polyline = reach.polyline();
|
||||
if (polyline.length() == 0D) {
|
||||
double distanceSquared = squared(x - polyline.x(0)) + squared(z - polyline.z(0));
|
||||
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidth() + additionalRadius;
|
||||
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D) + additionalRadius;
|
||||
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
|
||||
}
|
||||
double nearest = Double.POSITIVE_INFINITY;
|
||||
@@ -339,9 +341,9 @@ public final class RiverTile {
|
||||
}
|
||||
double deltaX = polyline.x(point + 1) - polyline.x(point);
|
||||
double deltaZ = polyline.z(point + 1) - polyline.z(point);
|
||||
for (int profileIndex = 0; profileIndex < reach.widthProfile().size() - 1; profileIndex++) {
|
||||
double profileStart = reach.widthProfile().position(profileIndex);
|
||||
double profileEnd = reach.widthProfile().position(profileIndex + 1);
|
||||
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
|
||||
double profileStart = reach.bodyProfile().position(profileIndex);
|
||||
double profileEnd = reach.bodyProfile().position(profileIndex + 1);
|
||||
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
||||
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
||||
if (overlapStart > overlapEnd) {
|
||||
@@ -349,13 +351,16 @@ public final class RiverTile {
|
||||
}
|
||||
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
||||
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
||||
double widthSlope = (reach.widthProfile().width(profileIndex + 1)
|
||||
- reach.widthProfile().width(profileIndex)) / (profileEnd - profileStart);
|
||||
double radiusBase = (reach.widthProfile().width(profileIndex)
|
||||
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
|
||||
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
|
||||
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex)
|
||||
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
|
||||
+ reach.bankWidth()
|
||||
+ reach.bodyProfile().bankWidthAtIndex(profileIndex)
|
||||
+ bankSlope * (segmentStartAlong - profileStart)
|
||||
+ additionalRadius;
|
||||
double radiusSlope = widthSlope * segmentAlongSpan * 0.5D;
|
||||
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
|
||||
ClosestPoint candidate = coveringPoint(
|
||||
intervalStart,
|
||||
intervalEnd,
|
||||
@@ -394,7 +399,7 @@ public final class RiverTile {
|
||||
maximumX,
|
||||
maximumZ
|
||||
);
|
||||
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidth();
|
||||
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D);
|
||||
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
|
||||
}
|
||||
double nearest = Double.POSITIVE_INFINITY;
|
||||
@@ -410,9 +415,9 @@ public final class RiverTile {
|
||||
double startZ = polyline.z(point);
|
||||
double deltaX = polyline.x(point + 1) - startX;
|
||||
double deltaZ = polyline.z(point + 1) - startZ;
|
||||
for (int profileIndex = 0; profileIndex < reach.widthProfile().size() - 1; profileIndex++) {
|
||||
double profileStart = reach.widthProfile().position(profileIndex);
|
||||
double profileEnd = reach.widthProfile().position(profileIndex + 1);
|
||||
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
|
||||
double profileStart = reach.bodyProfile().position(profileIndex);
|
||||
double profileEnd = reach.bodyProfile().position(profileIndex + 1);
|
||||
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
||||
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
||||
if (overlapStart > overlapEnd) {
|
||||
@@ -420,11 +425,15 @@ public final class RiverTile {
|
||||
}
|
||||
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
||||
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
||||
double widthSlope = (reach.widthProfile().width(profileIndex + 1)
|
||||
- reach.widthProfile().width(profileIndex)) / (profileEnd - profileStart);
|
||||
double radiusBase = (reach.widthProfile().width(profileIndex)
|
||||
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D + reach.bankWidth();
|
||||
double radiusSlope = widthSlope * segmentAlongSpan * 0.5D;
|
||||
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
|
||||
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
|
||||
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
||||
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex)
|
||||
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
|
||||
+ reach.bodyProfile().bankWidthAtIndex(profileIndex)
|
||||
+ bankSlope * (segmentStartAlong - profileStart);
|
||||
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
|
||||
double cursor = intervalStart;
|
||||
do {
|
||||
double next = intervalEnd;
|
||||
|
||||
@@ -19,14 +19,12 @@ public final class RiverTopologyComplexity {
|
||||
double siteJitter,
|
||||
int maxRouteReaches,
|
||||
double maximumReachRadius,
|
||||
double meanderStrength,
|
||||
int meanderSubdivisions
|
||||
double maximumWormOffset,
|
||||
int maximumWormSegments
|
||||
) {
|
||||
double maximumEdgeAxisDelta = cellSize * (1D + siteJitter);
|
||||
double maximumEdgeLength = StrictMath.sqrt(2D) * maximumEdgeAxisDelta;
|
||||
double maximumMeander = StrictMath.min(meanderStrength, maximumEdgeLength * 0.35D);
|
||||
long geometryPaddingCells = 1L + ceilToLong(
|
||||
(maximumReachRadius + maximumMeander) / cellSize
|
||||
(maximumReachRadius + maximumWormOffset) / cellSize
|
||||
);
|
||||
long targetWindowAxis = saturatedAdd(tileCells, saturatedMultiply(2L, geometryPaddingCells));
|
||||
long sourceWindowAxis = saturatedAdd(
|
||||
@@ -36,7 +34,7 @@ public final class RiverTopologyComplexity {
|
||||
long sourceWindowCells = saturatedMultiply(sourceWindowAxis, sourceWindowAxis);
|
||||
long maximumRouteScanSteps = saturatedMultiply(sourceWindowCells, maxRouteReaches);
|
||||
double maximumSegmentSpan = maximumEdgeAxisDelta
|
||||
+ maximumMeander * 2D
|
||||
+ maximumWormOffset * 2D
|
||||
+ maximumReachRadius * 2D;
|
||||
long maximumSegmentBucketAxis = saturatedAdd(
|
||||
ceilToLong(maximumSegmentSpan / SPATIAL_BUCKET_SIZE),
|
||||
@@ -48,7 +46,7 @@ public final class RiverTopologyComplexity {
|
||||
);
|
||||
long maximumBucketWritesPerReach = saturatedMultiply(
|
||||
maximumSegmentBucketCount,
|
||||
meanderSubdivisions
|
||||
maximumWormSegments
|
||||
);
|
||||
return new Estimate(
|
||||
geometryPaddingCells,
|
||||
@@ -66,8 +64,8 @@ public final class RiverTopologyComplexity {
|
||||
double siteJitter,
|
||||
int maxRouteReaches,
|
||||
double maximumReachRadius,
|
||||
double meanderStrength,
|
||||
int meanderSubdivisions
|
||||
double maximumWormOffset,
|
||||
int maximumWormSegments
|
||||
) {
|
||||
Estimate estimate = estimate(
|
||||
cellSize,
|
||||
@@ -75,8 +73,8 @@ public final class RiverTopologyComplexity {
|
||||
siteJitter,
|
||||
maxRouteReaches,
|
||||
maximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
maximumWormOffset,
|
||||
maximumWormSegments
|
||||
);
|
||||
List<String> violations = estimate.violations();
|
||||
if (!violations.isEmpty()) {
|
||||
@@ -205,7 +203,7 @@ public final class RiverTopologyComplexity {
|
||||
+ " bucket writes for one reach (" + maximumSegmentBucketAxis
|
||||
+ " buckets per segment axis), above the safe limit of "
|
||||
+ MAXIMUM_BUCKET_WRITES_PER_REACH
|
||||
+ "; reduce channel width, bank width, orderWidthFactor, meanderStrength, or meanderSubdivisions.");
|
||||
+ "; reduce channel width, bank width, orderWidthFactor, worm maxOffset, or worm segments.");
|
||||
}
|
||||
return List.copyOf(violations);
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class RiverWidthProfile {
|
||||
private final double[] positions;
|
||||
private final double[] widths;
|
||||
private final double maximum;
|
||||
|
||||
public RiverWidthProfile(double[] positions, double[] widths) {
|
||||
if (positions == null || widths == null || positions.length < 2 || positions.length != widths.length) {
|
||||
throw new IllegalArgumentException("River width profiles require matching position and width samples");
|
||||
}
|
||||
this.positions = positions.clone();
|
||||
this.widths = widths.clone();
|
||||
double resolvedMaximum = 0D;
|
||||
for (int index = 0; index < this.positions.length; index++) {
|
||||
double position = this.positions[index];
|
||||
double width = this.widths[index];
|
||||
if (!Double.isFinite(position) || position < 0D || position > 1D
|
||||
|| (index > 0 && position <= this.positions[index - 1])) {
|
||||
throw new IllegalArgumentException("River width profile positions must increase from zero to one");
|
||||
}
|
||||
if (!Double.isFinite(width) || width <= 0D) {
|
||||
throw new IllegalArgumentException("River width profile widths must be finite and positive");
|
||||
}
|
||||
resolvedMaximum = Math.max(resolvedMaximum, width);
|
||||
}
|
||||
if (this.positions[0] != 0D || this.positions[this.positions.length - 1] != 1D) {
|
||||
throw new IllegalArgumentException("River width profile positions must include zero and one");
|
||||
}
|
||||
maximum = resolvedMaximum;
|
||||
}
|
||||
|
||||
public static RiverWidthProfile constant(double width) {
|
||||
return new RiverWidthProfile(new double[]{0D, 1D}, new double[]{width, width});
|
||||
}
|
||||
|
||||
public double sample(double alongReach) {
|
||||
double position = Math.max(0D, Math.min(1D, alongReach));
|
||||
int index = Arrays.binarySearch(positions, position);
|
||||
if (index >= 0) {
|
||||
return widths[index];
|
||||
}
|
||||
int upper = -index - 1;
|
||||
if (upper <= 0) {
|
||||
return widths[0];
|
||||
}
|
||||
if (upper >= positions.length) {
|
||||
return widths[widths.length - 1];
|
||||
}
|
||||
int lower = upper - 1;
|
||||
double range = positions[upper] - positions[lower];
|
||||
double interpolation = range <= 0D ? 0D : (position - positions[lower]) / range;
|
||||
return widths[lower] + (widths[upper] - widths[lower]) * interpolation;
|
||||
}
|
||||
|
||||
public double maximum() {
|
||||
return maximum;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return widths.length;
|
||||
}
|
||||
|
||||
public double position(int index) {
|
||||
return positions[index];
|
||||
}
|
||||
|
||||
public double width(int index) {
|
||||
return widths[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
if (!(object instanceof RiverWidthProfile profile)) {
|
||||
return false;
|
||||
}
|
||||
return Arrays.equals(positions, profile.positions) && Arrays.equals(widths, profile.widths);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * Arrays.hashCode(positions) + Arrays.hashCode(widths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record RiverWorm(
|
||||
String id,
|
||||
long seed,
|
||||
double weight,
|
||||
double wavelength,
|
||||
double detailWavelength,
|
||||
double tortuosity,
|
||||
double detailTortuosity,
|
||||
double maxOffset,
|
||||
int segments,
|
||||
double widthMultiplier,
|
||||
double bankMultiplier,
|
||||
double depthMultiplier,
|
||||
double bodyWavelength,
|
||||
double bodyDetailWavelength,
|
||||
double widthVariation,
|
||||
double bankVariation,
|
||||
double depthVariation,
|
||||
double roofVariation,
|
||||
int branchCap,
|
||||
double branchDecay,
|
||||
double confluenceMultiplier,
|
||||
double childChance,
|
||||
double branchChildChance,
|
||||
List<RiverWorm> children
|
||||
) {
|
||||
public RiverWorm {
|
||||
if (id == null || !id.matches("[a-z0-9][a-z0-9_-]{0,63}")) {
|
||||
throw new IllegalArgumentException("id must use 1 to 64 lowercase letters, digits, underscores, or hyphens");
|
||||
}
|
||||
requireRange(weight, 0.000001D, 1000000D, "weight");
|
||||
requireRange(wavelength, 8D, 16384D, "wavelength");
|
||||
requireRange(detailWavelength, 8D, 16384D, "detailWavelength");
|
||||
requireRange(tortuosity, 0D, 1D, "tortuosity");
|
||||
requireRange(detailTortuosity, 0D, 1D, "detailTortuosity");
|
||||
requireRange(maxOffset, 0D, 1024D, "maxOffset");
|
||||
if (segments < 1 || segments > 64) {
|
||||
throw new IllegalArgumentException("segments must be between 1 and 64");
|
||||
}
|
||||
requireRange(widthMultiplier, 0.125D, 8D, "widthMultiplier");
|
||||
requireRange(bankMultiplier, 0.125D, 8D, "bankMultiplier");
|
||||
requireRange(depthMultiplier, 0.125D, 8D, "depthMultiplier");
|
||||
requireRange(bodyWavelength, 32D, 16384D, "bodyWavelength");
|
||||
requireRange(bodyDetailWavelength, 32D, 16384D, "bodyDetailWavelength");
|
||||
requireRange(widthVariation, 0D, 0.875D, "widthVariation");
|
||||
requireRange(bankVariation, 0D, 0.875D, "bankVariation");
|
||||
requireRange(depthVariation, 0D, 0.875D, "depthVariation");
|
||||
requireRange(roofVariation, 0D, 0.875D, "roofVariation");
|
||||
if (branchCap < 1 || branchCap > 8) {
|
||||
throw new IllegalArgumentException("branchCap must be between 1 and 8");
|
||||
}
|
||||
requireRange(branchDecay, 0D, 1D, "branchDecay");
|
||||
requireRange(confluenceMultiplier, 0D, 8D, "confluenceMultiplier");
|
||||
requireRange(childChance, 0D, 1D, "childChance");
|
||||
requireRange(branchChildChance, 0D, 1D, "branchChildChance");
|
||||
if (children == null) {
|
||||
throw new IllegalArgumentException("children must not be null");
|
||||
}
|
||||
if (children.size() > 16) {
|
||||
throw new IllegalArgumentException("children must contain at most 16 profiles");
|
||||
}
|
||||
for (RiverWorm child : children) {
|
||||
if (child == null) {
|
||||
throw new IllegalArgumentException("children must not contain null profiles");
|
||||
}
|
||||
}
|
||||
children = List.copyOf(children);
|
||||
}
|
||||
|
||||
private static void requireRange(double value, double minimum, double maximum, String name) {
|
||||
if (!Double.isFinite(value) || value < minimum || value > maximum) {
|
||||
throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,11 @@ import art.arcane.iris.engine.object.IrisRiverTerrain;
|
||||
import art.arcane.iris.engine.object.IrisRiverTopology;
|
||||
import art.arcane.iris.engine.object.IrisRiverWater;
|
||||
import art.arcane.iris.engine.object.IrisRiverWaterMode;
|
||||
import art.arcane.iris.engine.object.IrisRiverWorm;
|
||||
import art.arcane.iris.engine.object.IrisStyledRange;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.river.RiverAnchor;
|
||||
import art.arcane.iris.engine.river.RiverEdgeId;
|
||||
import art.arcane.iris.engine.river.RiverMeanderContext;
|
||||
import art.arcane.iris.engine.river.RiverNetworkOptions;
|
||||
import art.arcane.iris.engine.river.RiverPolyline;
|
||||
import art.arcane.iris.engine.river.RiverReach;
|
||||
@@ -35,6 +35,7 @@ import art.arcane.iris.engine.river.RiverTerminalPolicy;
|
||||
import art.arcane.iris.engine.river.RiverTile;
|
||||
import art.arcane.iris.engine.river.RiverTileCache;
|
||||
import art.arcane.iris.engine.river.RiverTopologyComplexity;
|
||||
import art.arcane.iris.engine.river.RiverWorm;
|
||||
import art.arcane.iris.util.project.interpolation.NoiseBounds;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
@@ -61,7 +62,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
private static final long WIDTH_NOISE_SALT = 0xBE5466CF34E90C6CL;
|
||||
private static final long BANK_NOISE_SALT = 0xC0AC29B7C97C50DDL;
|
||||
private static final long DEPTH_NOISE_SALT = 0x3F84D5B5B5470917L;
|
||||
private static final long MEANDER_NOISE_SALT = 0x9216D5D98979FB1BL;
|
||||
private static final long BED_NOISE_SALT = 0xD1310BA698DFB5ACL;
|
||||
private static final long BIOME_NOISE_SALT = 0x2FFD72DBD01ADFB7L;
|
||||
private static final long CAVE_ENTRY_NOISE_SALT = 0xB8E1AFED6A267E96L;
|
||||
@@ -98,7 +98,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
private final CNG widthNoise;
|
||||
private final CNG bankNoise;
|
||||
private final CNG depthNoise;
|
||||
private final CNG meanderNoise;
|
||||
private final CNG bedNoise;
|
||||
private final CNG biomeNoise;
|
||||
private final CNG caveEntryNoise;
|
||||
@@ -144,7 +143,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
widthNoise = noise(terrain.getChannelWidth(), WIDTH_NOISE_SALT);
|
||||
bankNoise = noise(terrain.getBankWidth(), BANK_NOISE_SALT);
|
||||
depthNoise = noise(terrain.getDepth(), DEPTH_NOISE_SALT);
|
||||
meanderNoise = noise(terrain.getMeanderStyle(), MEANDER_NOISE_SALT);
|
||||
bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT);
|
||||
biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_NOISE_SALT);
|
||||
caveEntryNoise = noise(caves.getEntry(), CAVE_ENTRY_NOISE_SALT);
|
||||
@@ -256,7 +254,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
z
|
||||
);
|
||||
int ceilingY = shapedTunnelCeilingY(
|
||||
waterHeadY, caves.getDryHeadroom(), profile, roofOffset);
|
||||
waterHeadY,
|
||||
caves.getDryHeadroom() * column.reach().roofScaleAt(column.river().alongReach()),
|
||||
profile,
|
||||
roofOffset
|
||||
);
|
||||
return new IrisRiverTunnelSample(column.river(), bedY, waterHeadY, ceilingY);
|
||||
}
|
||||
|
||||
@@ -349,7 +351,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
IrisBiome sampledBiome = naturalBiome.get(center.x(), center.z());
|
||||
double head = waterSurface(reach, alongReach, isNaturalOcean(sampledBiome));
|
||||
double centerNaturalHeight = naturalHeight.get(center.x(), center.z());
|
||||
double centerBedHeight = head - reach.depth() + bedRoughness(center.x(), center.z());
|
||||
double centerBedHeight = head - reach.depthAt(alongReach) + bedRoughness(center.x(), center.z());
|
||||
EffectiveRiverSettings settings = settingsFor(sampledRegion, sampledBiome);
|
||||
double maximumIncision = Math.max(0D, terrain.getMaxIncision() * settings.maxIncisionMultiplier());
|
||||
double cappedSurface = incisedHeight(centerNaturalHeight, centerBedHeight, 1D, maximumIncision);
|
||||
@@ -627,8 +629,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
.routingNoiseWeight(0D)
|
||||
.flowAlignmentWeight(topology.getFlowAlignmentWeight())
|
||||
.confluenceWeight(topology.getConfluenceWeight())
|
||||
.branchSoftCap(topology.getBranchSoftCap())
|
||||
.branchChildShrinkFactor(topology.getBranchChildShrinkFactor())
|
||||
.oceanAttraction(topology.getOceanAttraction())
|
||||
.channelWidth(mid(riverTerrain.getChannelWidth(), 12D))
|
||||
.bankWidth(mid(riverTerrain.getBankWidth(), 8D))
|
||||
@@ -639,11 +639,63 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
.orderWidthFactor(riverTerrain.getOrderWidthFactor())
|
||||
.orderDepthFactor(riverTerrain.getOrderDepthFactor())
|
||||
.maximumReachRadius(maximumReachRadius(topology, riverTerrain))
|
||||
.meanderStrength(riverTerrain.getMeanderStrength())
|
||||
.meanderSubdivisions(riverTerrain.getMeanderSubdivisions())
|
||||
.worms(worms(riverTerrain))
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<RiverWorm> worms(IrisRiverTerrain riverTerrain) {
|
||||
if (riverTerrain.getWorms() == null || riverTerrain.getWorms().isEmpty()) {
|
||||
throw new IllegalArgumentException("River terrain must configure at least one Perlin worm");
|
||||
}
|
||||
ArrayList<RiverWorm> worms = new ArrayList<>(riverTerrain.getWorms().size());
|
||||
for (IrisRiverWorm configured : riverTerrain.getWorms()) {
|
||||
if (configured == null) {
|
||||
throw new IllegalArgumentException("River terrain worms must not contain null entries");
|
||||
}
|
||||
worms.add(worm(configured));
|
||||
}
|
||||
return List.copyOf(worms);
|
||||
}
|
||||
|
||||
private RiverWorm worm(IrisRiverWorm configured) {
|
||||
if (configured.getChildren() == null) {
|
||||
throw new IllegalArgumentException("River worm children must be an array");
|
||||
}
|
||||
ArrayList<RiverWorm> children = new ArrayList<>(configured.getChildren().size());
|
||||
for (IrisRiverWorm child : configured.getChildren()) {
|
||||
if (child == null) {
|
||||
throw new IllegalArgumentException("River worm children must not contain null entries");
|
||||
}
|
||||
children.add(worm(child));
|
||||
}
|
||||
return new RiverWorm(
|
||||
configured.getId(),
|
||||
configured.getSeed(),
|
||||
configured.getWeight(),
|
||||
configured.getWavelength(),
|
||||
configured.getDetailWavelength(),
|
||||
configured.getTortuosity(),
|
||||
configured.getDetailTortuosity(),
|
||||
configured.getMaxOffset(),
|
||||
configured.getSegments(),
|
||||
configured.getWidthMultiplier(),
|
||||
configured.getBankMultiplier(),
|
||||
configured.getDepthMultiplier(),
|
||||
configured.getBodyWavelength(),
|
||||
configured.getBodyDetailWavelength(),
|
||||
configured.getWidthVariation(),
|
||||
configured.getBankVariation(),
|
||||
configured.getDepthVariation(),
|
||||
configured.getRoofVariation(),
|
||||
configured.getBranchCap(),
|
||||
configured.getBranchDecay(),
|
||||
configured.getConfluenceMultiplier(),
|
||||
configured.getChildChance(),
|
||||
configured.getBranchChildChance(),
|
||||
List.copyOf(children)
|
||||
);
|
||||
}
|
||||
|
||||
private void addTerminalCaveAnchors(
|
||||
RiverTile tile,
|
||||
int minimumX,
|
||||
@@ -1141,11 +1193,6 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
return routingCost(context.midpointX(), context.midpointZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public double meanderNoise(RiverMeanderContext context) {
|
||||
return meanderNoise.fitDouble(-1D, 1D, context.x(), context.z());
|
||||
}
|
||||
|
||||
@Override
|
||||
public double flowNoise(double x, double z) {
|
||||
return routingNoise.fitDouble(-1D, 1D, x, z);
|
||||
@@ -1163,7 +1210,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
RiverRoutingContext context,
|
||||
double x,
|
||||
double z,
|
||||
double fallback
|
||||
double fallback
|
||||
) {
|
||||
EffectiveRiverSettings settings = settingsAt(x, z);
|
||||
return styled(
|
||||
@@ -1183,6 +1230,18 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
* settings.bankWidthMultiplier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double bankWidth(RiverRoutingContext context, double x, double z, double fallback) {
|
||||
EffectiveRiverSettings settings = settingsAt(x, z);
|
||||
return styled(
|
||||
terrain.getBankWidth(),
|
||||
bankNoise,
|
||||
(int) StrictMath.round(x),
|
||||
(int) StrictMath.round(z),
|
||||
fallback
|
||||
) * settings.bankWidthMultiplier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double depth(RiverRoutingContext context, double fallback) {
|
||||
EffectiveRiverSettings settings = settingsAt(context.midpointX(), context.midpointZ());
|
||||
@@ -1199,6 +1258,22 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double depth(RiverRoutingContext context, double x, double z, double fallback) {
|
||||
EffectiveRiverSettings settings = settingsAt(x, z);
|
||||
double configuredDepth = styled(
|
||||
terrain.getDepth(),
|
||||
depthNoise,
|
||||
(int) StrictMath.round(x),
|
||||
(int) StrictMath.round(z),
|
||||
fallback
|
||||
) * settings.depthMultiplier();
|
||||
return Math.min(
|
||||
terrain.getMaxDepth(),
|
||||
Math.max(1D + terrain.getBedRoughness(), configuredDepth)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
|
||||
EffectiveRiverSettings settings = settingsAt(blockX, blockZ);
|
||||
|
||||
@@ -50,19 +50,19 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener {
|
||||
}
|
||||
|
||||
public void l(Object l) {
|
||||
IrisLogging.info("[" + getName() + "]: " + l);
|
||||
IrisLogging.info(String.valueOf(l));
|
||||
}
|
||||
|
||||
public void w(Object l) {
|
||||
IrisLogging.warn("[" + getName() + "]: " + l);
|
||||
IrisLogging.warn(String.valueOf(l));
|
||||
}
|
||||
|
||||
public void f(Object l) {
|
||||
IrisLogging.error("[" + getName() + "]: " + l);
|
||||
IrisLogging.error(String.valueOf(l));
|
||||
}
|
||||
|
||||
public void v(Object l) {
|
||||
IrisLogging.debug("[" + getName() + "]: " + l);
|
||||
IrisLogging.debug(String.valueOf(l));
|
||||
}
|
||||
|
||||
public void onEnable() {
|
||||
|
||||
Reference in new issue
Block a user