mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 06:50:02 +00:00
Volumetric surface terrain
- Add an optional per-biome `terrain3D` profile with density and fissure noise styles, amplitude, horizontal and vertical feature scales, crack depth, width and scale, slope and fluid-clearance gates, and a seed offset, each range-checked before use - Add a terrain runtime that samples the signed density and fissure fields on a four-block lattice around the biome's generated height and resolves each column into solid spans, so surfaces gain projecting rock, undercuts, ledges and narrow cracks that fade out on flat ground and near the fluid line - Remove unsupported rock by flood-filling span components across neighbouring columns and dropping any fragment of 512 blocks or fewer, while larger connected masses are kept - Report the shaped column top as the natural terrain height, keep the unshaped generator height available as a separate stream, and measure surface slope between neighbouring ledge floors instead of the flat heightmap - Write air through the gaps in the normal terrain actuator, generate a fresh surface palette for every ledge floor, and paint ceiling layers beneath undercuts - Carry each layer's own column through the dimension stack and upper dimension so stack top heights, solidity and surface lookups follow the spans, and each exposed face draws its palette from its own source height and slope - Sample a layer's slope condition once per column at the surface being generated, and gate surface decorators on the slope of the ledge they sit on rather than the column heightmap - Decorate every exposed ledge floor and the ceiling above it during the decorant pass - Treat shaped openings as surface rather than cave when resolving biomes, skip them during cave carving, suppress cave zone markers inside them, and resolve their boundary biome from the surface biome stream - Include shaped openings in mantle carve queries and carved columns, fall back to terrain solidity for surface-solid checks across stack, upper and normal terrain, and answer both queries from the island mask when placing objects on floating islands - Let the hydrology planned surface report whether it owns a column, so the cave voxel view consults natural terrain solidity where hydrology does not own the terrain, and split replayed plan observations into height and ownership records - Validate `terrain3D` on biomes and `snippet/terrain-3d` snippets as a blocking pack error, rejecting unknown fields, wrong types, out-of-range numbers, unknown noise styles, style nesting past 32 levels and snippet references that escape the pack - Copy the whole snippet tree during modded studio pack export through one shared routine instead of only `snippet/style`, so terrain and nested style snippets survive export - Add a terrain probe and Gradle task that generates real pack chunks, compares predicted column geometry against the actual blocks, requires retained overhang coverage for named biomes, reports detached-component topology, and writes cross-section images with a JSON summary - Enumerate image-mapped regions and skip compat-excluded ones when collecting biomes for stacked and upper dimension terrain
This commit is contained in:
1 parent
7841efed54
commit
c39a6403bc
63 files changed
+4761
-145
No files matched your search
@@ -24,9 +24,16 @@ import art.arcane.iris.engine.object.IrisObjectMarker;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Shared key collection for the pack packagers. Both the Bukkit re-serializing compiler and the
|
||||
@@ -38,6 +45,26 @@ public final class PackExportClosure {
|
||||
private PackExportClosure() {
|
||||
}
|
||||
|
||||
public static String copySnippets(File packFolder, File targetFolder) throws IOException {
|
||||
Path sourceRoot = new File(packFolder, "snippet").toPath();
|
||||
if (!Files.isDirectory(sourceRoot)) {
|
||||
return "";
|
||||
}
|
||||
Path targetRoot = new File(targetFolder, "snippet").toPath();
|
||||
StringBuilder hashes = new StringBuilder();
|
||||
try (Stream<Path> files = Files.walk(sourceRoot)) {
|
||||
for (Path source : files.filter(Files::isRegularFile)
|
||||
.filter(path -> path.getFileName().toString().endsWith(".json"))
|
||||
.sorted()
|
||||
.toList()) {
|
||||
String json = new JSONObject(IO.readAll(source.toFile())).toString(0);
|
||||
IO.writeAll(targetRoot.resolve(sourceRoot.relativize(source)).toFile(), json);
|
||||
hashes.append(IO.hash(json));
|
||||
}
|
||||
}
|
||||
return hashes.toString();
|
||||
}
|
||||
|
||||
public static KSet<String> collectDimensionKeys(IrisDimension dimension) {
|
||||
return collectDimensionKeys(dimension, key -> dimension == null || dimension.getLoader() == null
|
||||
? null
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisTerrain3D;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
final class PackTerrain3DValidator {
|
||||
private static final Gson GSON = new Gson();
|
||||
private static final Set<String> PROFILE_FIELDS = Set.of("$schema", "enabled", "seed", "amplitude",
|
||||
"horizontalScale", "verticalScale", "densityStyle", "crackDepth", "crackWidth", "crackScale",
|
||||
"crackStyle", "minimumSlope", "slopeFade", "fluidClearance", "fluidFade");
|
||||
private static final Set<String> STYLE_FIELDS = Set.of("$schema", "style", "cellularFrequency",
|
||||
"cellularZoom", "zoom", "expression", "imageMap", "multiplier", "fracture", "exponent", "cacheSize");
|
||||
|
||||
private PackTerrain3DValidator() {
|
||||
}
|
||||
|
||||
static List<String> validate(File packFolder) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
if (packFolder == null || !packFolder.isDirectory()) {
|
||||
return errors;
|
||||
}
|
||||
File biomeFolder = new File(packFolder, "biomes");
|
||||
for (File biomeFile : files(biomeFolder)) {
|
||||
JsonObject biome = readObject(biomeFile, "Biome '" + PackValidationIo.deriveKey(biomeFolder, biomeFile)
|
||||
+ "'", errors);
|
||||
if (biome != null && biome.has("terrain3D") && !biome.get("terrain3D").isJsonNull()) {
|
||||
validateProfile(packFolder, biome.get("terrain3D"),
|
||||
"Biome '" + PackValidationIo.deriveKey(biomeFolder, biomeFile) + "' terrain3D", errors);
|
||||
}
|
||||
}
|
||||
File snippetFolder = new File(packFolder, "snippet/terrain-3d");
|
||||
for (File snippetFile : files(snippetFolder)) {
|
||||
String location = "Terrain3D snippet '" + PackValidationIo.deriveKey(snippetFolder, snippetFile) + "'";
|
||||
JsonObject profile = readObject(snippetFile, location, errors);
|
||||
if (profile != null) {
|
||||
validateProfile(packFolder, profile, location, errors);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
private static List<File> files(File folder) {
|
||||
if (!folder.isDirectory()) {
|
||||
return List.of();
|
||||
}
|
||||
List<File> files = PackValidationIo.listJsonRecursive(folder);
|
||||
files.sort(Comparator.comparing(File::getPath));
|
||||
return files;
|
||||
}
|
||||
|
||||
private static void validateProfile(File packFolder, JsonElement raw, String location, List<String> errors) {
|
||||
JsonObject object = resolveObject(packFolder, raw, "terrain-3d", location, errors);
|
||||
if (object == null) {
|
||||
return;
|
||||
}
|
||||
JsonObject normalized = object.deepCopy();
|
||||
int initialErrors = errors.size();
|
||||
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
|
||||
String field = entry.getKey();
|
||||
JsonElement value = entry.getValue();
|
||||
String path = location + "." + field;
|
||||
if (!PROFILE_FIELDS.contains(field)) {
|
||||
errors.add(path + " is not a terrain3D field.");
|
||||
continue;
|
||||
}
|
||||
switch (field) {
|
||||
case "$schema" -> requireString(value, path, false, errors);
|
||||
case "enabled" -> {
|
||||
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean()) {
|
||||
errors.add(path + " must be a JSON boolean.");
|
||||
}
|
||||
}
|
||||
case "seed" -> requireInteger(value, path, errors);
|
||||
case "densityStyle", "crackStyle" -> {
|
||||
JsonObject style = validateStyle(packFolder, value, path, 0, errors);
|
||||
if (style != null) {
|
||||
normalized.add(field, style);
|
||||
}
|
||||
}
|
||||
default -> requireNumber(value, path, errors);
|
||||
}
|
||||
}
|
||||
if (initialErrors != errors.size()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
GSON.fromJson(normalized, IrisTerrain3D.class).validate();
|
||||
} catch (JsonParseException | IllegalArgumentException invalid) {
|
||||
errors.add(location + ": " + invalid.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonObject validateStyle(File packFolder, JsonElement raw, String location,
|
||||
int depth, List<String> errors) {
|
||||
if (depth > 32) {
|
||||
errors.add(location + " exceeds the maximum style nesting depth of 32.");
|
||||
return null;
|
||||
}
|
||||
JsonObject object = resolveObject(packFolder, raw, "style", location, errors);
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
JsonObject normalized = object.deepCopy();
|
||||
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
|
||||
String field = entry.getKey();
|
||||
JsonElement value = entry.getValue();
|
||||
String path = location + "." + field;
|
||||
if (!STYLE_FIELDS.contains(field)) {
|
||||
errors.add(path + " is not a generator style field.");
|
||||
continue;
|
||||
}
|
||||
switch (field) {
|
||||
case "$schema" -> requireString(value, path, false, errors);
|
||||
case "expression", "imageMap" -> requireString(value, path, true, errors);
|
||||
case "style" -> {
|
||||
if (requireString(value, path, false, errors)) {
|
||||
try {
|
||||
NoiseStyle.valueOf(value.getAsString());
|
||||
} catch (IllegalArgumentException invalid) {
|
||||
errors.add(path + " is not a known noise style.");
|
||||
}
|
||||
}
|
||||
}
|
||||
case "fracture" -> {
|
||||
if (!value.isJsonNull()) {
|
||||
JsonObject fracture = validateStyle(packFolder, value, path, depth + 1, errors);
|
||||
if (fracture != null) {
|
||||
normalized.add(field, fracture);
|
||||
}
|
||||
}
|
||||
}
|
||||
case "cacheSize" -> {
|
||||
if (requireInteger(value, path, errors)) {
|
||||
requireRange(value, path, 0D, 8192D, errors);
|
||||
}
|
||||
}
|
||||
case "exponent" -> requireRange(value, path, 0.01562D, 64D, errors);
|
||||
case "cellularFrequency" -> requireRange(value, path, 0D, Double.MAX_VALUE, errors);
|
||||
default -> requireRange(value, path, 0.00001D, Double.MAX_VALUE, errors);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static JsonObject resolveObject(File packFolder, JsonElement raw, String snippetType,
|
||||
String location, List<String> errors) {
|
||||
if (raw.isJsonObject()) {
|
||||
return raw.getAsJsonObject();
|
||||
}
|
||||
if (!raw.isJsonPrimitive() || !raw.getAsJsonPrimitive().isString()
|
||||
|| !raw.getAsString().startsWith("snippet/")) {
|
||||
errors.add(location + " must be an object or snippet reference.");
|
||||
return null;
|
||||
}
|
||||
String reference = raw.getAsString();
|
||||
String prefix = "snippet/" + snippetType + "/";
|
||||
String resolved = reference.startsWith(prefix) ? reference : prefix + reference.substring(8);
|
||||
Path root = packFolder.toPath().toAbsolutePath().normalize();
|
||||
Path path = root.resolve(resolved + ".json").normalize();
|
||||
if (!path.startsWith(root.resolve(prefix)) || !Files.isRegularFile(path)) {
|
||||
errors.add(location + " references an unavailable " + snippetType + " snippet '" + reference + "'.");
|
||||
return null;
|
||||
}
|
||||
return readObject(path.toFile(), location + " (" + reference + ")", errors);
|
||||
}
|
||||
|
||||
private static JsonObject readObject(File file, String location, List<String> errors) {
|
||||
try {
|
||||
JsonElement parsed = JsonParser.parseString(Files.readString(file.toPath(), StandardCharsets.UTF_8));
|
||||
if (parsed.isJsonObject()) {
|
||||
return parsed.getAsJsonObject();
|
||||
}
|
||||
errors.add(location + " must contain a JSON object.");
|
||||
} catch (IOException | JsonParseException invalid) {
|
||||
errors.add(location + " contains unreadable JSON: " + invalid.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean requireString(JsonElement value, String path, boolean nullable, List<String> errors) {
|
||||
if (nullable && value.isJsonNull()) {
|
||||
return true;
|
||||
}
|
||||
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) {
|
||||
errors.add(path + " must be a JSON string" + (nullable ? " or null." : "."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean requireNumber(JsonElement value, String path, List<String> errors) {
|
||||
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber()
|
||||
|| !Double.isFinite(value.getAsDouble())) {
|
||||
errors.add(path + " must be a finite JSON number.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean requireInteger(JsonElement value, String path, List<String> errors) {
|
||||
if (!requireNumber(value, path, errors)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
value.getAsBigDecimal().longValueExact();
|
||||
return true;
|
||||
} catch (ArithmeticException invalid) {
|
||||
errors.add(path + " must be an integer in the signed 64-bit range.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireRange(JsonElement value, String path, double minimum,
|
||||
double maximum, List<String> errors) {
|
||||
if (requireNumber(value, path, errors)
|
||||
&& (value.getAsDouble() < minimum || value.getAsDouble() > maximum)) {
|
||||
errors.add(path + " must be between " + minimum + " and " + maximum + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,7 @@ public final class PackValidator {
|
||||
addDistinct(blockingErrors, hydrologyValidation.errors());
|
||||
addDistinct(warnings, hydrologyValidation.warnings());
|
||||
blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder));
|
||||
addDistinct(blockingErrors, PackTerrain3DValidator.validate(packFolder));
|
||||
PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder);
|
||||
addDistinct(blockingErrors, lootIssues.errors());
|
||||
addDistinct(warnings, lootIssues.warnings());
|
||||
|
||||
@@ -30,6 +30,7 @@ public final class DimensionStackContext {
|
||||
private final List<DimensionTerrainContext> layersBottomToTop;
|
||||
private final List<SeamOffsetSampler> seamOffsetSamplersBottomToTop;
|
||||
private final Cache<Long, DimensionStackLayout> layoutCache;
|
||||
private final boolean volumetric;
|
||||
|
||||
private DimensionStackContext(ContextState state) {
|
||||
engine = state.engine();
|
||||
@@ -41,6 +42,7 @@ public final class DimensionStackContext {
|
||||
layersTopToBottom = List.copyOf(reversed);
|
||||
seamOffsetSamplersBottomToTop = List.copyOf(state.seamOffsetSamplersBottomToTop());
|
||||
layoutCache = Caffeine.newBuilder().maximumSize(LAYOUT_CACHE_SIZE).build();
|
||||
volumetric = layersBottomToTop.stream().anyMatch(DimensionTerrainContext::hasTerrain3D);
|
||||
}
|
||||
|
||||
public static DimensionStackContext create(Engine engine, IrisDimensionStack stack) {
|
||||
@@ -191,7 +193,8 @@ public final class DimensionStackContext {
|
||||
column.fluidBlock(),
|
||||
column.surfaceBlock(),
|
||||
normalTerrainHeight,
|
||||
fluidHeight
|
||||
fluidHeight,
|
||||
column.terrainColumn()
|
||||
));
|
||||
}
|
||||
|
||||
@@ -244,6 +247,11 @@ public final class DimensionStackContext {
|
||||
}
|
||||
|
||||
private long sampleTopHeights(int x, int z) {
|
||||
if (volumetric) {
|
||||
DimensionStackLayout layout = getLayout(x, z);
|
||||
return ((long) layout.clippedStackTerrainTopY() << 32)
|
||||
| (layout.clippedStackTopY() & 0xFFFFFFFFL);
|
||||
}
|
||||
boolean naturalFallback = usesTemporaryNaturalFallback(x, z);
|
||||
int baseY = 0;
|
||||
int renderedTerrainTopY = Integer.MIN_VALUE;
|
||||
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -15,6 +16,7 @@ public final class DimensionStackLayout {
|
||||
private final int stackTopY;
|
||||
private final int clippedStackTerrainTopY;
|
||||
private final int clippedStackTopY;
|
||||
private final boolean volumetric;
|
||||
|
||||
private DimensionStackLayout(LayoutState state) {
|
||||
layersBottomToTop = List.copyOf(state.layersBottomToTop());
|
||||
@@ -25,6 +27,7 @@ public final class DimensionStackLayout {
|
||||
stackTopY = state.stackTopY();
|
||||
clippedStackTerrainTopY = state.clippedStackTerrainTopY();
|
||||
clippedStackTopY = state.clippedStackTopY();
|
||||
volumetric = layersBottomToTop.stream().anyMatch(layer -> layer.terrainColumn() != null);
|
||||
}
|
||||
|
||||
static DimensionStackLayout create(
|
||||
@@ -80,6 +83,15 @@ public final class DimensionStackLayout {
|
||||
int renderMinY = Math.max(0, baseY);
|
||||
int renderMaxY = Math.min(outputMaxY, contentTopY);
|
||||
boolean visible = renderMinY <= renderMaxY;
|
||||
int clippedSurfaceY = clip(surfaceY, outputMaxY);
|
||||
if (input.terrainColumn() != null) {
|
||||
int maximumSourceY = Math.clamp((long) renderMaxY - baseY,
|
||||
Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||
int sourceY = input.terrainColumn().highestSolidY(maximumSourceY);
|
||||
int visibleY = saturatedAdd(baseY, sourceY);
|
||||
clippedSurfaceY = sourceY >= 0 && visibleY >= renderMinY && visible
|
||||
? visibleY : -1;
|
||||
}
|
||||
Layer layer = new Layer(
|
||||
input.terrainContext(),
|
||||
input.biome(),
|
||||
@@ -97,9 +109,10 @@ public final class DimensionStackLayout {
|
||||
seamOffsetBelow,
|
||||
renderMinY,
|
||||
renderMaxY,
|
||||
clip(surfaceY, outputMaxY),
|
||||
clippedSurfaceY,
|
||||
clip(fluidY, outputMaxY),
|
||||
visible
|
||||
visible,
|
||||
input.terrainColumn()
|
||||
);
|
||||
layers.add(layer);
|
||||
stackTerrainTopY = Math.max(stackTerrainTopY, surfaceY);
|
||||
@@ -122,6 +135,14 @@ public final class DimensionStackLayout {
|
||||
}
|
||||
}
|
||||
|
||||
boolean volumetric = false;
|
||||
for (Layer layer : layers) {
|
||||
volumetric |= layer.terrainColumn() != null;
|
||||
}
|
||||
if (volumetric) {
|
||||
renderedTerrainTopY = highestVisibleY(layers, outputMaxY, false);
|
||||
renderedContentTopY = highestVisibleY(layers, outputMaxY, true);
|
||||
}
|
||||
return new DimensionStackLayout(new LayoutState(
|
||||
layers,
|
||||
stackTerrainTopY,
|
||||
@@ -182,6 +203,33 @@ public final class DimensionStackLayout {
|
||||
return layer.clippedSurfaceY();
|
||||
}
|
||||
|
||||
private static int highestVisibleY(List<Layer> layers, int maximumY, boolean includeFluid) {
|
||||
int highest = -1;
|
||||
for (int sourceIndex = 0; sourceIndex < layers.size(); sourceIndex++) {
|
||||
Layer source = layers.get(sourceIndex);
|
||||
int candidate = includeFluid ? source.highestContentY(maximumY) : source.highestSolidY(maximumY);
|
||||
int index = sourceIndex + 1;
|
||||
while (candidate > highest && index < layers.size()) {
|
||||
Layer overlay = layers.get(index);
|
||||
Layer lower = layers.get(index - 1);
|
||||
int nextMaximum = candidate;
|
||||
if (overlay.containsRenderedY(candidate)) {
|
||||
nextMaximum = overlay.renderMinY() - 1;
|
||||
} else if ((long) candidate > lower.contentTopY() && (long) candidate < overlay.localBaseY()) {
|
||||
nextMaximum = lower.contentTopY();
|
||||
}
|
||||
if (nextMaximum < candidate) {
|
||||
candidate = includeFluid ? source.highestContentY(nextMaximum) : source.highestSolidY(nextMaximum);
|
||||
index = sourceIndex + 1;
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
highest = Math.max(highest, candidate);
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
|
||||
public List<Layer> layersBottomToTop() {
|
||||
return layersBottomToTop;
|
||||
}
|
||||
@@ -207,6 +255,10 @@ public final class DimensionStackLayout {
|
||||
}
|
||||
|
||||
public Layer topTerrainLayer() {
|
||||
if (volumetric) {
|
||||
Layer owner = layerAt(clippedStackTerrainTopY);
|
||||
return isSolid(clippedStackTerrainTopY) ? owner : null;
|
||||
}
|
||||
Layer highest = null;
|
||||
int highestSurfaceY = Integer.MIN_VALUE;
|
||||
for (int layerIndex = 0; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
@@ -278,6 +330,20 @@ public final class DimensionStackLayout {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public boolean isSolid(int y) {
|
||||
for (int index = layersBottomToTop.size() - 1; index > 0; index--) {
|
||||
Layer layer = layersBottomToTop.get(index);
|
||||
if (layer.containsRenderedY(y)) {
|
||||
return layer.isSolid(y);
|
||||
}
|
||||
Layer lower = layersBottomToTop.get(index - 1);
|
||||
if ((long) y > lower.contentTopY() && (long) y < layer.localBaseY()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return layersBottomToTop.get(0).isSolid(y);
|
||||
}
|
||||
|
||||
public boolean containsUpperLayerY(int y) {
|
||||
for (int layerIndex = 1; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
if (layersBottomToTop.get(layerIndex).containsRenderedY(y)) {
|
||||
@@ -307,7 +373,8 @@ public final class DimensionStackLayout {
|
||||
PlatformBlockState fluidBlock,
|
||||
PlatformBlockState surfaceBlock,
|
||||
int normalTerrainHeight,
|
||||
int fluidHeight
|
||||
int fluidHeight,
|
||||
Terrain3DColumn terrainColumn
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -330,12 +397,51 @@ public final class DimensionStackLayout {
|
||||
int renderMaxY,
|
||||
int clippedSurfaceY,
|
||||
int clippedFluidY,
|
||||
boolean visible
|
||||
boolean visible,
|
||||
Terrain3DColumn terrainColumn
|
||||
) {
|
||||
public boolean containsRenderedY(int y) {
|
||||
return visible && y >= renderMinY && y <= renderMaxY;
|
||||
}
|
||||
|
||||
public boolean isSolid(int y) {
|
||||
if (!containsRenderedY(y)) {
|
||||
return false;
|
||||
}
|
||||
int sourceY = y - localBaseY;
|
||||
return terrainColumn == null ? sourceY <= normalTerrainHeight : terrainColumn.isSolid(sourceY);
|
||||
}
|
||||
|
||||
public int surfaceAt(int y) {
|
||||
if (!isSolid(y)) {
|
||||
return -1;
|
||||
}
|
||||
return terrainColumn == null ? surfaceY
|
||||
: saturatedAdd(localBaseY, terrainColumn.surfaceY(y - localBaseY));
|
||||
}
|
||||
|
||||
public int highestSolidY(int maximumY) {
|
||||
if (!visible || maximumY < renderMinY) {
|
||||
return -1;
|
||||
}
|
||||
int maximumSourceY = Math.clamp((long) Math.min(maximumY, renderMaxY) - localBaseY,
|
||||
Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||
int sourceY = terrainColumn == null ? Math.min(normalTerrainHeight, maximumSourceY)
|
||||
: terrainColumn.highestSolidY(maximumSourceY);
|
||||
int worldY = saturatedAdd(localBaseY, sourceY);
|
||||
return sourceY >= 0 && worldY >= renderMinY ? worldY : -1;
|
||||
}
|
||||
|
||||
public int highestContentY(int maximumY) {
|
||||
if (visible && fluidHeight > normalTerrainHeight) {
|
||||
int fluidTop = Math.min(maximumY, Math.min(renderMaxY, fluidY));
|
||||
if (fluidTop >= renderMinY && fluidTop > surfaceY) {
|
||||
return fluidTop;
|
||||
}
|
||||
}
|
||||
return highestSolidY(maximumY);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private record LayoutState(
|
||||
|
||||
@@ -11,6 +11,8 @@ import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DRuntime;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
@@ -48,6 +50,8 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
private final IrisImageMapRuntime imageMapRuntime;
|
||||
private final boolean selfReferencing;
|
||||
private final SelfFallback selfFallback;
|
||||
private final Terrain3DRuntime terrain3D;
|
||||
private final boolean naturalSelf;
|
||||
|
||||
private DimensionTerrainContext(ContextState state) {
|
||||
engine = state.engine();
|
||||
@@ -64,6 +68,8 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
imageMapRuntime = state.imageMapRuntime();
|
||||
selfReferencing = state.selfReferencing();
|
||||
selfFallback = state.selfFallback();
|
||||
terrain3D = state.terrain3D();
|
||||
naturalSelf = state.naturalSelf();
|
||||
}
|
||||
|
||||
public static DimensionTerrainContext forStack(Engine engine, IrisDimension dimension) {
|
||||
@@ -130,7 +136,9 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
fluidBlockSampler,
|
||||
complex.getImageMapRuntime(),
|
||||
true,
|
||||
selfFallback
|
||||
selfFallback,
|
||||
null,
|
||||
natural
|
||||
));
|
||||
}
|
||||
|
||||
@@ -175,7 +183,7 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
focusRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
} else {
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
for (IrisRegion region : terrainRegions(dimension.getAllRegions(dataProvider), imageMapRuntime)) {
|
||||
preparedRegions.add(region);
|
||||
region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
@@ -351,9 +359,6 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
return mappedTerrainHeight(imageMapRuntime, proceduralHeight, x, z);
|
||||
}, Interpolated.DOUBLE).cache2DDouble(
|
||||
cachePrefix + "ImageMappedHeightStream", engine, cacheSize);
|
||||
ProceduralStream<Double> slopeStream = heightStream.slope(3)
|
||||
.cache2DDouble(cachePrefix + "SlopeStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<IrisBiome> finalBiomeStream = focusBiome == null
|
||||
? heightStream.convertAware2D((height, x, z) -> {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(x, z);
|
||||
@@ -381,6 +386,24 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
Interpolated.of(value -> 0D, value -> focusBiome)
|
||||
).cache2D(cachePrefix + "FinalBiomeStreamFocus", engine, cacheSize);
|
||||
|
||||
boolean terrain3DEnabled = false;
|
||||
for (IrisBiome biome : allBiomes) {
|
||||
if (biome.getTerrain3D() != null) {
|
||||
biome.getTerrain3D().validate();
|
||||
terrain3DEnabled |= biome.getTerrain3D().isEnabled();
|
||||
}
|
||||
}
|
||||
Terrain3DRuntime terrain3D = !terrain3DEnabled ? null : new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources(heightStream::getDouble, finalBiomeStream::get),
|
||||
new Terrain3DRuntime.Options(engine.getSeedManager().getTerrain() ^ seedOffset,
|
||||
localHeight, fluidHeight, dimensionData, true, Math.max(4096, cacheSize))
|
||||
);
|
||||
ProceduralStream<Double> shapedHeightStream = terrain3D == null ? heightStream
|
||||
: ProceduralStream.ofDouble((x, z) -> shapedHeight(terrain3D, x, z))
|
||||
.cache2DDouble(cachePrefix + "Terrain3DHeightStream", engine, cacheSize);
|
||||
ProceduralStream<Double> slopeStream = shapedHeightStream.slope(3)
|
||||
.cache2DDouble(cachePrefix + "SlopeStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<PlatformBlockState> rockStream = dimension.getRockPalette()
|
||||
.getLayerGenerator(rng.nextParallelRNG(45), dimensionData).stream()
|
||||
.select(dimension.getRockPalette().getBlockData(dimensionData));
|
||||
@@ -399,7 +422,7 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
dimension,
|
||||
dimensionData,
|
||||
localHeight,
|
||||
heightStream,
|
||||
shapedHeightStream,
|
||||
slopeStream,
|
||||
ProceduralStream.ofDouble((x, z) -> fluidHeight),
|
||||
finalBiomeStream,
|
||||
@@ -408,7 +431,9 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
fluidBlockSampler,
|
||||
imageMapRuntime,
|
||||
false,
|
||||
null
|
||||
null,
|
||||
terrain3D,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
@@ -427,6 +452,22 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
});
|
||||
}
|
||||
|
||||
static KList<IrisRegion> terrainRegions(Iterable<IrisRegion> declared, IrisImageMapRuntime imageMaps) {
|
||||
KList<IrisRegion> regions = new KList<>();
|
||||
Set<IrisRegion> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
for (IrisRegion region : declared) {
|
||||
if (!region.isCompatExcluded() && seen.add(region)) {
|
||||
regions.add(region);
|
||||
}
|
||||
}
|
||||
for (IrisRegion region : imageMaps.getMappedRegions()) {
|
||||
if (!region.isCompatExcluded() && seen.add(region)) {
|
||||
regions.add(region);
|
||||
}
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
|
||||
static IrisRegion mappedRegion(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
IrisRegion proceduralRegion,
|
||||
@@ -629,6 +670,32 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
return getNormalTerrainHeight(x, z, usesNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
public boolean hasTerrain3D() {
|
||||
return selfReferencing ? engine.getComplex().hasTerrain3D() : terrain3D != null;
|
||||
}
|
||||
|
||||
public Terrain3DColumn terrainColumn(int x, int z) {
|
||||
return terrainColumn(x, z, usesNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
Terrain3DColumn terrainColumn(int x, int z, boolean naturalFallback) {
|
||||
if (selfReferencing) {
|
||||
return naturalSelf || naturalFallback
|
||||
? engine.getComplex().naturalTerrainColumn(x, z)
|
||||
: engine.getComplex().terrainColumn(x, z);
|
||||
}
|
||||
if (terrain3D == null) {
|
||||
return null;
|
||||
}
|
||||
Terrain3DColumn column = terrain3D.column(x, z);
|
||||
return column.shaped() ? column : null;
|
||||
}
|
||||
|
||||
private static double shapedHeight(Terrain3DRuntime runtime, double x, double z) {
|
||||
Terrain3DColumn column = runtime.column((int) Math.floor(x), (int) Math.floor(z));
|
||||
return column.shaped() ? column.topY() : column.baseHeight();
|
||||
}
|
||||
|
||||
double getNormalTerrainHeight(double x, double z, boolean naturalFallback) {
|
||||
if (naturalFallback && selfFallback != null) {
|
||||
return selfFallback.heightStream().getDouble(x, z);
|
||||
@@ -651,6 +718,25 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
return slopeStream;
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> getSurfaceSlopeStream(int sourceY) {
|
||||
return !hasTerrain3D() ? slopeStream : ProceduralStream.ofDouble((x, z) ->
|
||||
surfaceSlope((int) Math.floor(x), sourceY, (int) Math.floor(z)));
|
||||
}
|
||||
|
||||
double surfaceSlope(int x, int sourceY, int z) {
|
||||
Terrain3DColumn column = terrainColumn(x, z);
|
||||
if (column == null || sourceY >= column.topY() || column.surfaceY(sourceY) != sourceY) {
|
||||
return slopeStream.getDouble(x, z);
|
||||
}
|
||||
return IrisComplex.calculateNaturalSlope(sourceY,
|
||||
nearbySurfaceHeight(x + 3, sourceY, z), nearbySurfaceHeight(x, sourceY, z + 3));
|
||||
}
|
||||
|
||||
private double nearbySurfaceHeight(int x, int sourceY, int z) {
|
||||
Terrain3DColumn column = terrainColumn(x, z);
|
||||
return column == null ? getNormalTerrainHeight(x, z) : column.nearestSurfaceY(sourceY);
|
||||
}
|
||||
|
||||
public IrisBiome getBiome(double x, double z) {
|
||||
return getBiome(x, z, usesNaturalFallback(x, z));
|
||||
}
|
||||
@@ -710,7 +796,8 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
getRegion(x, z),
|
||||
getRockBlock(x, z),
|
||||
getFluidBlock(x, z, naturalFallback),
|
||||
getSurfaceBlock(x, z)
|
||||
getSurfaceBlock(x, z),
|
||||
terrainColumn((int) Math.floor(x), (int) Math.floor(z), naturalFallback)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -739,7 +826,9 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
FluidBlockSampler fluidBlockSampler,
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
boolean selfReferencing,
|
||||
SelfFallback selfFallback
|
||||
SelfFallback selfFallback,
|
||||
Terrain3DRuntime terrain3D,
|
||||
boolean naturalSelf
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -758,7 +847,8 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
IrisRegion region,
|
||||
PlatformBlockState rockBlock,
|
||||
PlatformBlockState fluidBlock,
|
||||
PlatformBlockState surfaceBlock
|
||||
PlatformBlockState surfaceBlock,
|
||||
Terrain3DColumn terrainColumn
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,8 @@ import art.arcane.iris.engine.object.IrisHydrology;
|
||||
import art.arcane.iris.engine.object.IrisRiverHydrology;
|
||||
import art.arcane.iris.engine.object.IrisRiverProfile;
|
||||
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DRuntime;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
@@ -94,8 +96,8 @@ import java.nio.file.Path;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime"})
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D"})
|
||||
public class IrisComplex implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||
@@ -149,6 +151,11 @@ public class IrisComplex implements DataProvider {
|
||||
private ProceduralStream<IrisBiome> trueBiomeStream;
|
||||
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
|
||||
private ProceduralStream<Double> naturalHeightStream;
|
||||
private ProceduralStream<Double> baseTerrainHeightStream;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient Engine terrainEngine;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private transient Terrain3DRuntime terrain3D;
|
||||
private ProceduralStream<Double> unblendedNaturalHeightStream;
|
||||
private final ResolvedTerrainProvider resolvedTerrain;
|
||||
private ProceduralStream<Double> placementHeightStream;
|
||||
@@ -199,6 +206,7 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan) {
|
||||
terrainEngine = engine;
|
||||
this.transitionGenerationPlan = transitionGenerationPlan;
|
||||
this.resolvedTerrain = new ResolvedTerrainProvider(engine);
|
||||
int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize());
|
||||
@@ -324,10 +332,27 @@ public class IrisComplex implements DataProvider {
|
||||
return mapped == null ? biome : mapped;
|
||||
})
|
||||
.cache2D("imageMappedBaseBiomeStream", engine, cacheSize);
|
||||
unblendedNaturalHeightStream = ProceduralStream.of(
|
||||
baseTerrainHeightStream = ProceduralStream.of(
|
||||
(x, z) -> sampleUnblendedNaturalTerrainHeight(engine, x, z),
|
||||
Interpolated.DOUBLE
|
||||
).cache2DDouble("unblendedNaturalHeightStream", engine, cacheSize);
|
||||
).cache2DDouble("baseTerrainHeightStream", engine, cacheSize);
|
||||
boolean terrain3DEnabled = false;
|
||||
for (IrisBiome biome : generatorBiomes) {
|
||||
if (biome.getTerrain3D() != null) {
|
||||
biome.getTerrain3D().validate();
|
||||
terrain3DEnabled |= biome.getTerrain3D().isEnabled();
|
||||
}
|
||||
}
|
||||
terrain3D = !terrain3DEnabled ? null : new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources(
|
||||
(x, z) -> baseTerrainHeightStream.getDouble(x, z),
|
||||
this::sampleTerrain3DBiome),
|
||||
new Terrain3DRuntime.Options(engine.getSeedManager().getTerrain(), engine.getHeight(),
|
||||
fluidHeight, data, terrain3DEnabled, Math.max(4_096, cacheSize)));
|
||||
unblendedNaturalHeightStream = terrain3DEnabled
|
||||
? ProceduralStream.ofDouble(this::sampleTerrain3DHeight)
|
||||
.cache2DDouble("unblendedNaturalHeightStream", engine, cacheSize)
|
||||
: baseTerrainHeightStream;
|
||||
naturalHeightStream = unblendedNaturalHeightStream;
|
||||
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||
b -> focusBiome))
|
||||
@@ -491,7 +516,95 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
private double sampleNaturalTerrainHeight(Engine engine, double x, double z) {
|
||||
return sampleUnblendedNaturalTerrainHeight(engine, x, z);
|
||||
return terrain3D != null && terrain3D.active()
|
||||
? sampleTerrain3DHeight(x, z)
|
||||
: sampleUnblendedNaturalTerrainHeight(engine, x, z);
|
||||
}
|
||||
|
||||
private double sampleTerrain3DHeight(double x, double z) {
|
||||
Terrain3DColumn column = terrain3D.column(blockCoordinate(x), blockCoordinate(z));
|
||||
return column.shaped() ? column.topY() : column.baseHeight();
|
||||
}
|
||||
|
||||
private IrisBiome sampleTerrain3DBiome(int x, int z) {
|
||||
if (focusBiome != null) {
|
||||
return focusBiome;
|
||||
}
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null
|
||||
? fixBiomeType(baseTerrainHeightStream.getDouble(x, z), baseBiomeStream.get(x, z),
|
||||
regionStream.get(x, z), (double) x, (double) z, fluidHeight)
|
||||
: mapped;
|
||||
}
|
||||
|
||||
public Terrain3DColumn naturalTerrainColumn(int x, int z) {
|
||||
if (terrain3D == null || !terrain3D.active()) {
|
||||
return null;
|
||||
}
|
||||
Terrain3DColumn column = terrain3D.column(x, z);
|
||||
return column.shaped() ? column : null;
|
||||
}
|
||||
|
||||
public boolean hasTerrain3D() {
|
||||
return terrain3D != null && terrain3D.active();
|
||||
}
|
||||
|
||||
public Terrain3DColumn terrainColumn(int x, int z, HydrologyColumnSample hydrology) {
|
||||
if (hydrology != null) {
|
||||
HydrologyColumnLayer layer = hydrology.primarySurfaceLayer().orElse(null);
|
||||
if (layer != null && layer.terrainOwned()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return naturalTerrainColumn(x, z);
|
||||
}
|
||||
|
||||
public Terrain3DColumn terrainColumn(int x, int z) {
|
||||
if (terrain3D == null || !terrain3D.active()) {
|
||||
return null;
|
||||
}
|
||||
if (terrainEngine != null && terrainEngine.answersFromNaturalTerrain(x, z)) {
|
||||
return naturalTerrainColumn(x, z);
|
||||
}
|
||||
return terrainColumn(x, z, sampleHydrologyColumn(x, z));
|
||||
}
|
||||
|
||||
public boolean isTerrain3DOpening(int x, int y, int z) {
|
||||
Terrain3DColumn column = terrainColumn(x, z);
|
||||
return column != null && y >= column.minY() && y < column.topY() && !column.isSolid(y)
|
||||
&& (terrainEngine == null || !terrainEngine.isAdditionalTerrainOwned(x, y, z));
|
||||
}
|
||||
|
||||
public boolean isNaturalTerrainSolid(int x, int y, int z) {
|
||||
Terrain3DColumn column = naturalTerrainColumn(x, z);
|
||||
return column == null ? y <= naturalTrueHeight(x, z) : column.isSolid(y);
|
||||
}
|
||||
|
||||
public boolean isTerrain3DSurface(int x, int y, int z) {
|
||||
Terrain3DColumn column = terrainColumn(x, z);
|
||||
if (column == null || y < column.minY()) {
|
||||
return false;
|
||||
}
|
||||
if (!column.isSolid(y)) {
|
||||
return true;
|
||||
}
|
||||
int surface = column.surfaceY(y);
|
||||
return surface >= y && surface - y < 2;
|
||||
}
|
||||
|
||||
public double terrainSurfaceSlope(int x, int surfaceY, int z) {
|
||||
Terrain3DColumn column = terrainColumn(x, z);
|
||||
if (column == null || surfaceY >= column.topY() || column.surfaceY(surfaceY) != surfaceY) {
|
||||
return getSlopeStream().getDouble(x, z);
|
||||
}
|
||||
return calculateNaturalSlope(surfaceY,
|
||||
nearbyTerrainSurfaceHeight(x + 3, surfaceY, z),
|
||||
nearbyTerrainSurfaceHeight(x, surfaceY, z + 3));
|
||||
}
|
||||
|
||||
private double nearbyTerrainSurfaceHeight(int x, int surfaceY, int z) {
|
||||
Terrain3DColumn column = terrainColumn(x, z);
|
||||
return column == null ? getPlacementHeightStream().getDouble(x, z) : column.nearestSurfaceY(surfaceY);
|
||||
}
|
||||
|
||||
private double sampleUnblendedNaturalTerrainHeight(Engine engine, double x, double z) {
|
||||
@@ -1735,6 +1848,9 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (terrain3D != null) {
|
||||
terrain3D.clear();
|
||||
}
|
||||
if (hydrologyRuntime != null) {
|
||||
hydrologyRuntime.close();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
@@ -93,6 +94,26 @@ public class UpperDimensionContext implements DataProvider {
|
||||
}
|
||||
|
||||
public int getEffectiveSurfaceY(int x, int z) {
|
||||
int surfaceY = effectiveEnvelopeY(x, z);
|
||||
Terrain3DColumn column = terrainContext.terrainColumn(x, z);
|
||||
return clipSurfaceY(column, ceilingLayout.height(), surfaceY);
|
||||
}
|
||||
|
||||
public Column sampleColumn(int x, int z) {
|
||||
Terrain3DColumn column = terrainContext.terrainColumn(x, z);
|
||||
return new Column(ceilingLayout.height(),
|
||||
clipSurfaceY(column, ceilingLayout.height(), effectiveEnvelopeY(x, z)), column);
|
||||
}
|
||||
|
||||
private static int clipSurfaceY(Terrain3DColumn column, int height, int surfaceY) {
|
||||
if (column == null || surfaceY >= height) {
|
||||
return surfaceY;
|
||||
}
|
||||
int top = column.highestSolidY(height - 1 - surfaceY);
|
||||
return top <= 0 ? height : height - 1 - top;
|
||||
}
|
||||
|
||||
private int effectiveEnvelopeY(int x, int z) {
|
||||
double depth = Math.max(
|
||||
0D,
|
||||
Math.min(ceilingLayout.height() - 1D, terrainContext.getNormalTerrainHeight(x, z))
|
||||
@@ -127,6 +148,10 @@ public class UpperDimensionContext implements DataProvider {
|
||||
return terrainContext.getSurfaceBlock(x, z);
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> getSurfaceSlopeStream(int sourceY) {
|
||||
return terrainContext.getSurfaceSlopeStream(sourceY);
|
||||
}
|
||||
|
||||
public IrisDimension getDimension() {
|
||||
return terrainContext.getDimension();
|
||||
}
|
||||
@@ -140,6 +165,24 @@ public class UpperDimensionContext implements DataProvider {
|
||||
return terrainContext.isSelfReferencing();
|
||||
}
|
||||
|
||||
public record Column(int height, int surfaceY, Terrain3DColumn terrain) {
|
||||
public boolean ownsY(int y) {
|
||||
return y >= surfaceY && y < height;
|
||||
}
|
||||
|
||||
public boolean isSolid(int y) {
|
||||
return ownsY(y) && (terrain == null || terrain.isSolid(height - 1 - y));
|
||||
}
|
||||
|
||||
public int faceY(int y) {
|
||||
if (!isSolid(y)) {
|
||||
return -1;
|
||||
}
|
||||
return terrain == null ? surfaceY
|
||||
: Math.max(surfaceY, height - 1 - terrain.surfaceY(height - 1 - y));
|
||||
}
|
||||
}
|
||||
|
||||
record CeilingLayout(int height, int minimumGap) {
|
||||
CeilingLayout {
|
||||
if (height <= 0 || minimumGap < 0) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
@@ -228,6 +229,11 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
getSurfaceDecorator().decorate(i, j, realX, realZ, output, biome, height, getEngine().getHeight() - height);
|
||||
}
|
||||
|
||||
Terrain3DColumn terrainColumn = getComplex().terrainColumn(realX, realZ, hydrology);
|
||||
if (terrainColumn != null) {
|
||||
decorateTerrainLedges(output, terrainColumn, biome, i, j, realX, realZ);
|
||||
}
|
||||
|
||||
|
||||
if (cave != null && cave.getDecorators().isNotEmpty()) {
|
||||
for (int k = Math.min(height, output.getHeight() - 1); k > 0; k--) {
|
||||
@@ -256,6 +262,24 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
return false; // TODO CAVES
|
||||
}
|
||||
|
||||
private void decorateTerrainLedges(Hunk<PlatformBlockState> output, Terrain3DColumn column,
|
||||
IrisBiome biome, int localX, int localZ, int worldX, int worldZ) {
|
||||
for (int span = 0; span + 1 < column.spanCount(); span++) {
|
||||
int floorY = column.floor(span);
|
||||
int ceilingY = column.ceiling(span + 1);
|
||||
int headroom = ceilingY - floorY - 1;
|
||||
if (headroom < 1 || !PREDICATE_SOLID.test(output.getRaw(localX, floorY, localZ))) {
|
||||
continue;
|
||||
}
|
||||
getSurfaceDecorator().decorate(localX, localZ, worldX, worldZ,
|
||||
output, biome, floorY, headroom);
|
||||
if (PREDICATE_SOLID.test(output.getRaw(localX, ceilingY, localZ))) {
|
||||
getCeilingDecorator().decorate(localX, localZ, worldX, worldZ,
|
||||
output, biome, ceilingY - 1, headroom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ShorelineDecorationMode {
|
||||
NONE,
|
||||
LEGACY,
|
||||
|
||||
@@ -180,7 +180,7 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
|
||||
IrisBiome biome = layer.biome();
|
||||
int surfaceDepth = Math.max(0, layer.normalTerrainHeight());
|
||||
int fluidDepth = Math.max(0, layer.fluidHeight() - layer.normalTerrainHeight());
|
||||
KList<PlatformBlockState> surfaceBlocks = biome == null
|
||||
KList<PlatformBlockState> surfaceBlocks = biome == null || layer.terrainColumn() != null
|
||||
? null
|
||||
: biome.generateLayersWithSlope(
|
||||
dimension,
|
||||
@@ -192,6 +192,7 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
|
||||
data,
|
||||
terrainContext.getSlopeStream()
|
||||
);
|
||||
int paletteSurfaceY = layer.normalTerrainHeight();
|
||||
KList<PlatformBlockState> seaBlocks = biome == null || fluidDepth == 0
|
||||
? null
|
||||
: biome.generateSeaLayers(worldX, worldZ, rng, fluidDepth, data);
|
||||
@@ -215,13 +216,26 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
|
||||
if (sourceY > layer.normalTerrainHeight()) {
|
||||
continue;
|
||||
}
|
||||
int depth = layer.surfaceY() - y;
|
||||
if (!layer.isSolid(y)) {
|
||||
writeBlock(output, metadata, localX, y, localZ, AIR.get());
|
||||
continue;
|
||||
}
|
||||
int depth = layer.surfaceAt(y) - y;
|
||||
int sourceSurfaceY = sourceY + depth;
|
||||
if (sourceSurfaceY != paletteSurfaceY) {
|
||||
paletteSurfaceY = sourceSurfaceY;
|
||||
surfaceBlocks = null;
|
||||
}
|
||||
if (depth == 0 && layer.surfaceBlock() != null) {
|
||||
writeBlock(output, metadata, localX, y, localZ, layer.surfaceBlock());
|
||||
} else if (surfaceBlocks != null && surfaceBlocks.hasIndex(depth)) {
|
||||
writeBlock(output, metadata, localX, y, localZ, surfaceBlocks.get(depth));
|
||||
} else {
|
||||
writeBlock(output, metadata, localX, y, localZ, layer.rockBlock());
|
||||
if (surfaceBlocks == null && biome != null) {
|
||||
surfaceBlocks = biome.generateLayersWithSlope(dimension, worldX, worldZ, rng,
|
||||
sourceSurfaceY, sourceSurfaceY, data, terrainContext.getSurfaceSlopeStream(sourceSurfaceY));
|
||||
}
|
||||
writeBlock(output, metadata, localX, y, localZ,
|
||||
surfaceBlocks != null && surfaceBlocks.hasIndex(depth)
|
||||
? surfaceBlocks.get(depth) : layer.rockBlock());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import art.arcane.iris.engine.object.IrisOreGeneratorBounds;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisRiverMaterialConfig;
|
||||
import art.arcane.iris.engine.object.IrisSurfaceRiverBankConfig;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.util.common.data.BoundBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.project.context.ChunkedDataCache;
|
||||
@@ -47,6 +48,7 @@ import lombok.Getter;
|
||||
|
||||
public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBlockState> {
|
||||
private static final BoundBlockState BEDROCK = BoundBlockState.of("BEDROCK");
|
||||
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
|
||||
@Getter
|
||||
private final RNG rng;
|
||||
@Getter
|
||||
@@ -132,6 +134,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
HydrologyColumnLayer hydrologyTerrain = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
Terrain3DColumn terrainColumn = complex.terrainColumn(realX, realZ, hydrology);
|
||||
int terrainSpan = terrainColumn == null ? -1 : terrainColumn.spanCount() - 1;
|
||||
int layerSurfaceY = he;
|
||||
int layerCeilingY = 0;
|
||||
int cut = exposeCutStrata
|
||||
&& hydrologyTerrain != null
|
||||
&& hydrologyTerrain.terrainOwned()
|
||||
@@ -157,6 +163,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
boolean hasSurfaceOres = biomeSurfaceOreBounds.hasOres() || regionSurfaceOreBounds.hasOres() || dimensionSurfaceOreBounds.hasOres();
|
||||
boolean hasUndergroundOres = biomeUndergroundOreBounds.hasOres() || regionUndergroundOreBounds.hasOres() || dimensionUndergroundOreBounds.hasOres();
|
||||
KList<PlatformBlockState> blocks = null;
|
||||
KList<PlatformBlockState> ceilingBlocks = null;
|
||||
KList<PlatformBlockState> fblocks = null;
|
||||
|
||||
for (int i = topY; i >= 0; i--) {
|
||||
@@ -166,6 +173,20 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
continue;
|
||||
}
|
||||
|
||||
if (terrainColumn != null && i <= he) {
|
||||
while (terrainSpan >= 0 && i < terrainColumn.ceiling(terrainSpan)) {
|
||||
terrainSpan--;
|
||||
blocks = null;
|
||||
ceilingBlocks = null;
|
||||
}
|
||||
if (terrainSpan < 0 || i > terrainColumn.floor(terrainSpan)) {
|
||||
h.setRaw(xf, i, zf, AIR.get());
|
||||
continue;
|
||||
}
|
||||
layerSurfaceY = terrainColumn.floor(terrainSpan);
|
||||
layerCeilingY = terrainColumn.ceiling(terrainSpan);
|
||||
}
|
||||
|
||||
PlatformBlockState ore = null;
|
||||
if (hasSurfaceOres) {
|
||||
if (biomeSurfaceOreBounds.contains(i)) {
|
||||
@@ -198,13 +219,25 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
}
|
||||
|
||||
if (i <= he) {
|
||||
int depth = he - i;
|
||||
int depth = layerSurfaceY - i;
|
||||
if (depth == 0 && mappedSurfaceBlock != null) {
|
||||
h.setRaw(xf, i, zf, mappedSurfaceBlock);
|
||||
continue;
|
||||
}
|
||||
if (blocks == null) {
|
||||
blocks = biome.generateLayers(dimension, realX, realZ, localRng, he + cut, he + cut, data, complex);
|
||||
blocks = biome.generateLayers(dimension, realX, realZ, localRng,
|
||||
layerSurfaceY + cut, layerSurfaceY + cut, data, complex);
|
||||
}
|
||||
|
||||
if (layerCeilingY > 0 && depth >= 2 && i - layerCeilingY < 2) {
|
||||
if (ceilingBlocks == null) {
|
||||
ceilingBlocks = biome.generateCeilingLayers(dimension, realX, realZ, localRng,
|
||||
2, layerCeilingY, data, complex);
|
||||
}
|
||||
int ceilingDepth = i - layerCeilingY;
|
||||
h.setRaw(xf, i, zf, ceilingBlocks.hasIndex(ceilingDepth)
|
||||
? ceilingBlocks.get(ceilingDepth) : rock);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blocks.hasIndex(depth + cut)) {
|
||||
@@ -241,29 +274,41 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
}
|
||||
|
||||
if (upperContext != null) {
|
||||
int upperSurfaceY = upperContext.getEffectiveSurfaceY(realX, realZ);
|
||||
UpperDimensionContext.Column upperColumn = upperContext.sampleColumn(realX, realZ);
|
||||
int upperSurfaceY = upperColumn.surfaceY();
|
||||
|
||||
if (upperSurfaceY < chunkHeight - 1) {
|
||||
IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ);
|
||||
PlatformBlockState upperRock = upperContext.getRockBlock(realX, realZ);
|
||||
PlatformBlockState upperMappedSurface = upperContext.getSurfaceBlock(realX, realZ);
|
||||
int upperThickness = chunkHeight - 1 - upperSurfaceY;
|
||||
KList<PlatformBlockState> upperBlocks = upperBiome != null
|
||||
? upperBiome.generateLayers(upperContext.getDimension(),
|
||||
realX, realZ, localRng, upperThickness, upperThickness,
|
||||
upperContext.getData(), complex)
|
||||
: null;
|
||||
KList<PlatformBlockState> upperBlocks = null;
|
||||
int paletteSourceY = -1;
|
||||
|
||||
for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) {
|
||||
if (y == chunkHeight - 1 && bedrockEnabled) {
|
||||
h.setRaw(xf, y, zf, BEDROCK.get());
|
||||
continue;
|
||||
}
|
||||
if (y == upperSurfaceY && upperMappedSurface != null) {
|
||||
if (!upperColumn.isSolid(y)) {
|
||||
h.setRaw(xf, y, zf, AIR.get());
|
||||
continue;
|
||||
}
|
||||
int faceY = upperColumn.faceY(y);
|
||||
int sourceSurfaceY = upperColumn.height() - 1 - faceY;
|
||||
if (sourceSurfaceY != paletteSourceY) {
|
||||
paletteSourceY = sourceSurfaceY;
|
||||
upperBlocks = null;
|
||||
}
|
||||
if (y == faceY && upperMappedSurface != null) {
|
||||
h.setRaw(xf, y, zf, upperMappedSurface);
|
||||
continue;
|
||||
}
|
||||
int depthFromFace = y - upperSurfaceY;
|
||||
int depthFromFace = y - faceY;
|
||||
if (upperBlocks == null && upperBiome != null) {
|
||||
upperBlocks = upperBiome.generateLayersWithSlope(upperContext.getDimension(),
|
||||
realX, realZ, localRng, sourceSurfaceY, sourceSurfaceY,
|
||||
upperContext.getData(), upperContext.getSurfaceSlopeStream(sourceSurfaceY));
|
||||
}
|
||||
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
|
||||
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
|
||||
} else {
|
||||
|
||||
@@ -43,11 +43,14 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.NONE));
|
||||
}
|
||||
|
||||
protected boolean isSlopeValid(IrisDecorator decorator, int realX, int realZ) {
|
||||
protected boolean isSlopeValid(IrisDecorator decorator, int realX, int height, int realZ) {
|
||||
if (decorator.isForcePlace() || decorator.getSlopeCondition().isDefault()) {
|
||||
return true;
|
||||
}
|
||||
return decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ));
|
||||
double slope = getComplex().hasTerrain3D()
|
||||
? getComplex().terrainSurfaceSlope(realX, height, realZ)
|
||||
: getComplex().getSlopeStream().getDouble(realX, realZ);
|
||||
return decorator.getSlopeCondition().isValid(slope);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
@@ -73,7 +76,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
RNG rng = getRNG(realX, realZ);
|
||||
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
||||
|
||||
if (decorator == null || !isSlopeValid(decorator, realX, realZ)) {
|
||||
if (decorator == null || !isSlopeValid(decorator, realX, height, realZ)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
|
||||
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
@@ -118,6 +120,34 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return null;
|
||||
}
|
||||
|
||||
default boolean isAdditionalTerrainOwned(int x, int y, int z) {
|
||||
DimensionStackContext stack = getDimensionStackContext();
|
||||
if (stack != null && stack.getLayout(x, z).isHostFeatureProtectedY(y)) {
|
||||
return true;
|
||||
}
|
||||
UpperDimensionContext upper = getUpperContext();
|
||||
return upper != null && y >= upper.getEffectiveSurfaceY(x, z) && y < getHeight();
|
||||
}
|
||||
|
||||
default boolean isTerrainSurfaceSolid(int x, int y, int z) {
|
||||
DimensionStackContext stack = getDimensionStackContext();
|
||||
if (stack != null) {
|
||||
DimensionStackLayout layout = stack.getLayout(x, z);
|
||||
if (layout.isHostFeatureProtectedY(y)) {
|
||||
return layout.isSolid(y);
|
||||
}
|
||||
}
|
||||
UpperDimensionContext upper = getUpperContext();
|
||||
if (upper != null) {
|
||||
UpperDimensionContext.Column column = upper.sampleColumn(x, z);
|
||||
if (column.ownsY(y)) {
|
||||
return column.isSolid(y);
|
||||
}
|
||||
}
|
||||
Terrain3DColumn column = getComplex().terrainColumn(x, z);
|
||||
return column == null || column.isSolid(y);
|
||||
}
|
||||
|
||||
EngineMode getMode();
|
||||
|
||||
EnginePlatformHooks getPlatformHooks();
|
||||
@@ -615,12 +645,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
if (y > bottomLayer.renderMaxY()) {
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= bottomLayer.clippedSurfaceY() - 2) {
|
||||
if (y <= bottomLayer.clippedSurfaceY() - 2 && !getComplex().isTerrain3DSurface(x, y, z)) {
|
||||
return getCaveBiome(x, y, z);
|
||||
}
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= getHeight(x, z) - 2) {
|
||||
if (y <= getHeight(x, z) - 2 && !getComplex().isTerrain3DSurface(x, y, z)) {
|
||||
return getCaveBiome(x, y, z);
|
||||
}
|
||||
|
||||
@@ -639,12 +669,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
if (y > bottomLayer.renderMaxY()) {
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= bottomLayer.clippedSurfaceY() - 2) {
|
||||
if (y <= bottomLayer.clippedSurfaceY() - 2 && !getComplex().isTerrain3DSurface(x, y, z)) {
|
||||
return getCaveOrMantleBiome(x, y, z);
|
||||
}
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= getHeight(x, z) - 2) {
|
||||
if (y <= getHeight(x, z) - 2 && !getComplex().isTerrain3DSurface(x, y, z)) {
|
||||
return getCaveOrMantleBiome(x, y, z);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -6,8 +6,9 @@ import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
|
||||
public interface HydrologyCaveVoxelViewFactory {
|
||||
CaveVoxelView create(PlannedSurface plannedSurface);
|
||||
|
||||
@FunctionalInterface
|
||||
interface PlannedSurface {
|
||||
int resolve(int x, int z, int naturalHeight);
|
||||
|
||||
boolean ownsTerrain(int x, int z);
|
||||
}
|
||||
}
|
||||
+20
-3
@@ -2039,9 +2039,7 @@ final class HydrologyFootprintCompiler {
|
||||
if (materializedSurface == null) {
|
||||
return surfaceRaster;
|
||||
}
|
||||
return (int x, int z, int naturalHeight) -> materializedSurface.sample(x, z)
|
||||
.map(HydrologyColumnSample::terrainHeight)
|
||||
.orElse(naturalHeight);
|
||||
return new MaterializedSurface(materializedSurface);
|
||||
}
|
||||
|
||||
HydrologyColumnSample surfaceColumnAt(int x, int z, int naturalHeight) {
|
||||
@@ -2100,6 +2098,19 @@ final class HydrologyFootprintCompiler {
|
||||
}
|
||||
}
|
||||
|
||||
private record MaterializedSurface(RiverFootprint footprint) implements HydrologyCaveVoxelViewFactory.PlannedSurface {
|
||||
@Override
|
||||
public int resolve(int x, int z, int naturalHeight) {
|
||||
return footprint.sample(x, z).map(HydrologyColumnSample::terrainHeight).orElse(naturalHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ownsTerrain(int x, int z) {
|
||||
HydrologyColumnSample sample = footprint.sample(x, z).orElse(null);
|
||||
return sample != null && sample.primarySurfaceLayer().isPresent();
|
||||
}
|
||||
}
|
||||
|
||||
private final class SurfaceRasterIndex implements HydrologyCaveVoxelViewFactory.PlannedSurface {
|
||||
private final Long2ObjectOpenHashMap<HydrologyColumnSample> surfaceColumns;
|
||||
private final Long2IntOpenHashMap validationNaturalHeights;
|
||||
@@ -2140,6 +2151,12 @@ final class HydrologyFootprintCompiler {
|
||||
return naturalHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ownsTerrain(int x, int z) {
|
||||
HydrologyColumnSample sample = surfaceColumns.get(RiverFootprint.pack(x, z));
|
||||
return sample != null && sample.primarySurfaceLayer().isPresent();
|
||||
}
|
||||
|
||||
private HydrologyColumnSample surfaceColumnAt(int x, int z, int naturalHeight) {
|
||||
HydrologyColumnSample sample = surfaceColumns.get(RiverFootprint.pack(x, z));
|
||||
if (sample != null && sample.naturalHeight() != naturalHeight) {
|
||||
|
||||
+37
-4
@@ -25,24 +25,57 @@ public final class HydrologyObservedPlannedSurface implements HydrologyCaveVoxel
|
||||
observations.put(key, column);
|
||||
}
|
||||
for (Observation observation : column) {
|
||||
if (observation.naturalHeight() == naturalHeight) {
|
||||
if (observation instanceof HeightObservation height && height.naturalHeight() == naturalHeight) {
|
||||
return resolvedHeight;
|
||||
}
|
||||
}
|
||||
column.add(new Observation(x, z, naturalHeight, resolvedHeight));
|
||||
column.add(new HeightObservation(x, z, naturalHeight, resolvedHeight));
|
||||
return resolvedHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ownsTerrain(int x, int z) {
|
||||
boolean owned = delegate.ownsTerrain(x, z);
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
ArrayList<Observation> column = observations.get(key);
|
||||
if (column == null) {
|
||||
column = new ArrayList<>(1);
|
||||
observations.put(key, column);
|
||||
}
|
||||
for (Observation observation : column) {
|
||||
if (observation instanceof OwnershipObservation) {
|
||||
return owned;
|
||||
}
|
||||
}
|
||||
column.add(new OwnershipObservation(x, z, owned));
|
||||
return owned;
|
||||
}
|
||||
|
||||
public List<Observation> observationsAt(int x, int z) {
|
||||
ArrayList<Observation> column = observations.get(RiverFootprint.pack(x, z));
|
||||
return column == null ? List.of() : List.copyOf(column);
|
||||
}
|
||||
|
||||
public record Observation(
|
||||
public sealed interface Observation permits HeightObservation, OwnershipObservation {
|
||||
boolean matches(HydrologyCaveVoxelViewFactory.PlannedSurface surface);
|
||||
}
|
||||
|
||||
public record HeightObservation(
|
||||
int x,
|
||||
int z,
|
||||
int naturalHeight,
|
||||
int resolvedHeight
|
||||
) {
|
||||
) implements Observation {
|
||||
@Override
|
||||
public boolean matches(HydrologyCaveVoxelViewFactory.PlannedSurface surface) {
|
||||
return surface.resolve(x, z, naturalHeight) == resolvedHeight;
|
||||
}
|
||||
}
|
||||
|
||||
public record OwnershipObservation(int x, int z, boolean terrainOwned) implements Observation {
|
||||
@Override
|
||||
public boolean matches(HydrologyCaveVoxelViewFactory.PlannedSurface surface) {
|
||||
return surface.ownsTerrain(x, z) == terrainOwned;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-6
@@ -508,12 +508,7 @@ public final class HydrologyCaveContainmentPlanner {
|
||||
|
||||
private boolean matches(HydrologyObservedPlannedSurface plannedSurface) {
|
||||
for (HydrologyObservedPlannedSurface.Observation observation : observations) {
|
||||
int resolvedHeight = plannedSurface.resolve(
|
||||
observation.x(),
|
||||
observation.z(),
|
||||
observation.naturalHeight()
|
||||
);
|
||||
if (resolvedHeight != observation.resolvedHeight()) {
|
||||
if (!observation.matches(plannedSurface)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,8 @@ public interface EngineMantle extends MatterGenerator {
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
}
|
||||
return getMantle().get(x, h, z, MatterCavern.class) != null;
|
||||
return getMantle().get(x, h, z, MatterCavern.class) != null
|
||||
|| getComplex().isTerrain3DOpening(x, h, z);
|
||||
}
|
||||
|
||||
default PlatformBlockState get(int x, int y, int z) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||
import art.arcane.iris.engine.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
|
||||
@@ -499,7 +500,8 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
}
|
||||
return getPrerequisiteDataIfPresent(x, y, z, MatterCavern.class) != null;
|
||||
return getPrerequisiteDataIfPresent(x, y, z, MatterCavern.class) != null
|
||||
|| engineMantle.getComplex().isTerrain3DOpening(x, y, z);
|
||||
}
|
||||
|
||||
public byte[] getPrerequisiteCarvedColumn(int x, int z, int height) {
|
||||
@@ -513,6 +515,17 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
return carvedColumn;
|
||||
}
|
||||
|
||||
Terrain3DColumn terrainColumn = engineMantle.getComplex().terrainColumn(x, z);
|
||||
if (terrainColumn != null) {
|
||||
int maximumY = Math.min(cappedHeight, terrainColumn.topY());
|
||||
for (int y = Math.max(0, terrainColumn.minY()); y < maximumY; y++) {
|
||||
if (!terrainColumn.isSolid(y)
|
||||
&& !engineMantle.getEngine().isAdditionalTerrainOwned(x, y, z)) {
|
||||
carvedColumn[y] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
|
||||
if (chunk == null) {
|
||||
return carvedColumn;
|
||||
@@ -885,7 +898,8 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
}
|
||||
return getDataIfPresent(x, y, z, MatterCavern.class) != null;
|
||||
return getDataIfPresent(x, y, z, MatterCavern.class) != null
|
||||
|| engineMantle.getComplex().isTerrain3DOpening(x, y, z);
|
||||
}
|
||||
|
||||
private byte[] resolvedCarvedColumn(TerrainBoundarySignature column, int height) {
|
||||
@@ -933,6 +947,16 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
return carvedColumn;
|
||||
}
|
||||
|
||||
Terrain3DColumn terrainColumn = engineMantle.getComplex().terrainColumn(x, z);
|
||||
if (terrainColumn != null) {
|
||||
int maximumY = Math.min(cappedHeight, terrainColumn.topY());
|
||||
for (int y = terrainColumn.minY(); y < maximumY; y++) {
|
||||
if (!terrainColumn.isSolid(y) && !engineMantle.getEngine().isAdditionalTerrainOwned(x, y, z)) {
|
||||
carvedColumn[y] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
|
||||
if (chunk == null) {
|
||||
return carvedColumn;
|
||||
@@ -980,8 +1004,11 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
@Override
|
||||
public boolean isSurfaceSolid(int x, int y, int z) {
|
||||
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
|
||||
return resolved.isEmpty() || resolved.get().geometry()
|
||||
.voxelAt(y + engineMantle.getEngine().getMinHeight()).phase() == BoundaryColumnGeometry.Phase.SOLID;
|
||||
if (resolved.isPresent()) {
|
||||
return resolved.get().geometry()
|
||||
.voxelAt(y + engineMantle.getEngine().getMinHeight()).phase() == BoundaryColumnGeometry.Phase.SOLID;
|
||||
}
|
||||
return engineMantle.getEngine().isTerrainSurfaceSolid(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -98,12 +98,16 @@ public final class IslandObjectPlacer implements IObjectPlacer {
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
FloatingIslandSample sample = samples.sample(x, z);
|
||||
if (sample != null && y >= sample.islandBaseY && y < sample.islandBaseY + sample.solidMask.length) {
|
||||
return !sample.solidMask[y - sample.islandBaseY];
|
||||
}
|
||||
return wrapped.isCarved(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSurfaceSolid(int x, int y, int z) {
|
||||
return wrapped.isSurfaceSolid(x, y, z);
|
||||
return samples.sample(x, z) == null ? wrapped.isSurfaceSolid(x, y, z) : isSolid(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+52
-24
@@ -15,13 +15,13 @@ import art.arcane.iris.engine.mantle.TerrainMatterView;
|
||||
import art.arcane.iris.engine.object.IrisProceduralBlocks;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.function.Function2;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -33,11 +33,12 @@ public final class MantleHydrologyCaveVoxelView implements CaveVoxelView {
|
||||
|
||||
private final Mantle<Matter> mantle;
|
||||
private final int worldHeight;
|
||||
private final Function2<Integer, Integer, Integer> surfaceHeight;
|
||||
private final ColumnSource terrainColumns;
|
||||
private final SolidSource naturalSolid;
|
||||
private final BiConsumer<Integer, Integer> chunkLoader;
|
||||
private final LongOpenHashSet loadedChunks;
|
||||
private final Long2IntOpenHashMap openFloorCache;
|
||||
private final Long2IntOpenHashMap surfaceHeightCache;
|
||||
private final Long2ObjectOpenHashMap<TerrainColumn> terrainCache;
|
||||
|
||||
public MantleHydrologyCaveVoxelView(
|
||||
Engine engine,
|
||||
@@ -71,8 +72,10 @@ public final class MantleHydrologyCaveVoxelView implements CaveVoxelView {
|
||||
this(
|
||||
engineMantle.getMantle(),
|
||||
engineMantle.getMantle().getWorldHeight(),
|
||||
(x, z) -> plannedSurfaceHeight(complex, footprint, x, z),
|
||||
(chunkX, chunkZ) -> generateCarvingInput(engineMantle, complex, chunkX, chunkZ)
|
||||
new TerrainSources(
|
||||
(x, z) -> terrainColumn(complex, footprint.sample(x, z).orElse(null), x, z),
|
||||
complex::isNaturalTerrainSolid,
|
||||
(chunkX, chunkZ) -> generateCarvingInput(engineMantle, complex, chunkX, chunkZ))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,33 +87,32 @@ public final class MantleHydrologyCaveVoxelView implements CaveVoxelView {
|
||||
this(
|
||||
engineMantle.getMantle(),
|
||||
engineMantle.getMantle().getWorldHeight(),
|
||||
(x, z) -> plannedSurface.resolve(
|
||||
new TerrainSources((x, z) -> new TerrainColumn(plannedSurface.resolve(
|
||||
x,
|
||||
z,
|
||||
(int) Math.round(complex.getNaturalHeightStream().getDouble(x, z))
|
||||
),
|
||||
(chunkX, chunkZ) -> generateCarvingInput(engineMantle, complex, chunkX, chunkZ)
|
||||
), plannedSurface.ownsTerrain(x, z)), complex::isNaturalTerrainSolid,
|
||||
(chunkX, chunkZ) -> generateCarvingInput(engineMantle, complex, chunkX, chunkZ))
|
||||
);
|
||||
}
|
||||
|
||||
MantleHydrologyCaveVoxelView(
|
||||
Mantle<Matter> mantle,
|
||||
int worldHeight,
|
||||
Function2<Integer, Integer, Integer> surfaceHeight,
|
||||
BiConsumer<Integer, Integer> chunkLoader
|
||||
TerrainSources sources
|
||||
) {
|
||||
this.mantle = Objects.requireNonNull(mantle);
|
||||
if (worldHeight < 3) {
|
||||
throw new IllegalArgumentException("worldHeight must be at least three");
|
||||
}
|
||||
this.worldHeight = worldHeight;
|
||||
this.surfaceHeight = Objects.requireNonNull(surfaceHeight);
|
||||
this.chunkLoader = Objects.requireNonNull(chunkLoader);
|
||||
this.terrainColumns = Objects.requireNonNull(sources.columns());
|
||||
this.naturalSolid = Objects.requireNonNull(sources.naturalSolid());
|
||||
this.chunkLoader = Objects.requireNonNull(sources.chunkLoader());
|
||||
this.loadedChunks = new LongOpenHashSet();
|
||||
this.openFloorCache = new Long2IntOpenHashMap();
|
||||
this.openFloorCache.defaultReturnValue(CACHE_MISS);
|
||||
this.surfaceHeightCache = new Long2IntOpenHashMap();
|
||||
this.surfaceHeightCache.defaultReturnValue(CACHE_MISS);
|
||||
this.terrainCache = new Long2ObjectOpenHashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,7 +142,9 @@ public final class MantleHydrologyCaveVoxelView implements CaveVoxelView {
|
||||
? CaveVoxel.LAVA
|
||||
: CaveVoxel.INCOMPATIBLE_FLUID;
|
||||
}
|
||||
return position.y() > surfaceY(position.x(), position.z())
|
||||
TerrainColumn terrain = column(position.x(), position.z());
|
||||
return position.y() > terrain.surfaceHeight()
|
||||
|| !terrain.terrainOwned() && !naturalSolid.test(position.x(), position.y(), position.z())
|
||||
? CaveVoxel.CAVE_AIR
|
||||
: CaveVoxel.SOLID;
|
||||
}
|
||||
@@ -187,14 +191,21 @@ public final class MantleHydrologyCaveVoxelView implements CaveVoxelView {
|
||||
}
|
||||
|
||||
private int surfaceY(int x, int z) {
|
||||
return column(x, z).surfaceHeight();
|
||||
}
|
||||
|
||||
private TerrainColumn column(int x, int z) {
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
int cached = surfaceHeightCache.get(key);
|
||||
if (cached != CACHE_MISS) {
|
||||
TerrainColumn cached = terrainCache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
int resolved = Math.max(1, Math.min(worldHeight - 2, surfaceHeight.apply(x, z)));
|
||||
surfaceHeightCache.put(key, resolved);
|
||||
return resolved;
|
||||
TerrainColumn sampled = Objects.requireNonNull(terrainColumns.sample(x, z));
|
||||
int height = Math.clamp(sampled.surfaceHeight(), 1, worldHeight - 2);
|
||||
TerrainColumn bounded = height == sampled.surfaceHeight() ? sampled
|
||||
: new TerrainColumn(height, sampled.terrainOwned());
|
||||
terrainCache.put(key, bounded);
|
||||
return bounded;
|
||||
}
|
||||
|
||||
private <T> T dataIfPresent(CavePosition position, Class<T> type) {
|
||||
@@ -207,16 +218,16 @@ public final class MantleHydrologyCaveVoxelView implements CaveVoxelView {
|
||||
return TerrainMatterView.get(mantle, position.x(), position.y(), position.z(), type);
|
||||
}
|
||||
|
||||
private static int plannedSurfaceHeight(
|
||||
static TerrainColumn terrainColumn(
|
||||
IrisComplex complex,
|
||||
RiverFootprint footprint,
|
||||
HydrologyColumnSample sample,
|
||||
int x,
|
||||
int z
|
||||
) {
|
||||
HydrologyColumnSample sample = footprint.sample(x, z).orElse(null);
|
||||
return sample == null
|
||||
int surfaceHeight = sample == null
|
||||
? (int) Math.round(complex.getNaturalHeightStream().getDouble(x, z))
|
||||
: sample.terrainHeight();
|
||||
return new TerrainColumn(surfaceHeight, sample != null && sample.primarySurfaceLayer().isPresent());
|
||||
}
|
||||
|
||||
static void generateCarvingInput(
|
||||
@@ -263,4 +274,21 @@ public final class MantleHydrologyCaveVoxelView implements CaveVoxelView {
|
||||
static boolean requiresCarvingInput(Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||
return !mantle.hasFlag(chunkX, chunkZ, ReservedFlag.CARVED);
|
||||
}
|
||||
|
||||
record TerrainSources(ColumnSource columns,
|
||||
SolidSource naturalSolid, BiConsumer<Integer, Integer> chunkLoader) {
|
||||
}
|
||||
|
||||
record TerrainColumn(int surfaceHeight, boolean terrainOwned) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ColumnSource {
|
||||
TerrainColumn sample(int x, int z);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface SolidSource {
|
||||
boolean test(int x, int y, int z);
|
||||
}
|
||||
}
|
||||
+3
-7
@@ -96,18 +96,14 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
|
||||
CaveVoxelView caveView = new MantleHydrologyCaveVoxelView(
|
||||
writer.getMantle(),
|
||||
writer.getMantle().getWorldHeight(),
|
||||
(x, z) -> {
|
||||
HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z);
|
||||
return sample == null
|
||||
? (int) Math.round(complex.getNaturalHeightStream().getDouble(x, z))
|
||||
: sample.terrainHeight();
|
||||
},
|
||||
new MantleHydrologyCaveVoxelView.TerrainSources((x, z) -> MantleHydrologyCaveVoxelView.terrainColumn(
|
||||
complex, complex.sampleHydrologyColumn(x, z), x, z), complex::isNaturalTerrainSolid,
|
||||
(inputChunkX, inputChunkZ) -> MantleHydrologyCaveVoxelView.generateCarvingInput(
|
||||
getEngineMantle(),
|
||||
complex,
|
||||
inputChunkX,
|
||||
inputChunkZ
|
||||
)
|
||||
))
|
||||
);
|
||||
Publication publication = compilePublication(
|
||||
chunkX,
|
||||
|
||||
@@ -22,6 +22,7 @@ import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedModifier;
|
||||
@@ -97,12 +98,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
int[] upperSurfaceHeights = protectUpper ? scratch.getOrCreateUpperSurfaceHeights() : null;
|
||||
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
|
||||
DimensionStackLayout[] stackLayouts = stackContext == null ? null : new DimensionStackLayout[256];
|
||||
Terrain3DColumn[] terrainColumns = getComplex().hasTerrain3D() ? new Terrain3DColumn[256] : null;
|
||||
int chunkBlockX = PowerOfTwoCoordinates.chunkToBlock(x);
|
||||
int chunkBlockZ = PowerOfTwoCoordinates.chunkToBlock(z);
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
|
||||
int localZ = columnIndex & 15;
|
||||
surfaceHeights[columnIndex] = context.getRoundedHeight(localX, localZ);
|
||||
if (terrainColumns != null) {
|
||||
terrainColumns[columnIndex] = getComplex().terrainColumn(chunkBlockX + localX, chunkBlockZ + localZ);
|
||||
}
|
||||
if (protectUpper) {
|
||||
int worldX = localX + chunkBlockX;
|
||||
int worldZ = localZ + chunkBlockZ;
|
||||
@@ -125,6 +130,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
columnMasks,
|
||||
upperSurfaceHeights,
|
||||
stackLayouts,
|
||||
terrainColumns,
|
||||
worldHeightSpan,
|
||||
caveLavaHeight,
|
||||
chunkBlockX,
|
||||
@@ -363,6 +369,12 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
}
|
||||
|
||||
PlatformBlockState current = context.output().getRaw(localX, y, localZ);
|
||||
if (hydrology == null && context.terrainColumns() != null) {
|
||||
Terrain3DColumn column = context.terrainColumns()[columnIndex];
|
||||
if (column != null && y >= column.minY() && y < column.topY() && !column.isSolid(y)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (hydrology != null && hydrology.action() == HydrologyCaveAction.SEAL_GUARD) {
|
||||
PlatformBlockState normalized = resolveHydrologyState(hydrology, current, null, AIR);
|
||||
if (normalized != current) {
|
||||
@@ -419,6 +431,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
CarveColumnMask[] columnMasks,
|
||||
int[] upperSurfaceHeights,
|
||||
DimensionStackLayout[] stackLayouts,
|
||||
Terrain3DColumn[] terrainColumns,
|
||||
int worldHeightSpan,
|
||||
int caveLavaHeight,
|
||||
int chunkBlockX,
|
||||
@@ -889,7 +902,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
CaveZone zone = new CaveZone();
|
||||
zone.setFloor(floor);
|
||||
zone.setCeiling(y - 1);
|
||||
if (zone.isValid(getEngine())) {
|
||||
if (zone.isValid(getEngine())
|
||||
&& !getComplex().isTerrain3DOpening(worldX, floor, worldZ)) {
|
||||
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
|
||||
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
|
||||
}
|
||||
@@ -979,6 +993,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
}
|
||||
|
||||
IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
if (getComplex().isTerrain3DOpening(worldX, y, worldZ)) {
|
||||
return getComplex().getTrueBiomeStream().get(worldX, worldZ);
|
||||
}
|
||||
if (cavern != null && !cavern.getCustomBiome().isEmpty()) {
|
||||
return resolveCustomBiome(customBiomeCache, cavern.getCustomBiome());
|
||||
}
|
||||
|
||||
@@ -207,6 +207,9 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
@ArrayType(min = 1, type = IrisBiomeGeneratorLink.class)
|
||||
@Desc("Generators for this biome. Multiple generators with different interpolation sizes will mix with other biomes how you would expect. This defines your biome height relative to the fluid height. Use negative for oceans.")
|
||||
private KList<IrisBiomeGeneratorLink> generators = new KList<IrisBiomeGeneratorLink>().qadd(new IrisBiomeGeneratorLink());
|
||||
|
||||
@Desc("Optional volumetric surface terrain profile. Shapes projecting ledges, undercuts, arches, and fissures around this biome's height generators.")
|
||||
private IrisTerrain3D terrain3D = null;
|
||||
@ArrayType(min = 1, type = IrisDepositGenerator.class)
|
||||
@Desc("Define biome deposit generators that add onto the existing regional and global deposit generators")
|
||||
private KList<IrisDepositGenerator> deposits = new KList<>();
|
||||
|
||||
@@ -68,6 +68,8 @@ final class IrisBiomeLayerGenerator {
|
||||
}
|
||||
|
||||
KList<CNG> heightGenerators = getLayerHeightGenerators(biome, random, rdata);
|
||||
double surfaceSlope = 0D;
|
||||
boolean sampledSlope = false;
|
||||
|
||||
for (int i = 0; i < layerCount; i++) {
|
||||
IrisBiomePaletteLayer layer = layers.get(i);
|
||||
@@ -78,7 +80,11 @@ final class IrisBiomeLayerGenerator {
|
||||
IrisSlopeClip sc = layer.getSlopeCondition();
|
||||
|
||||
if (!sc.isDefault()) {
|
||||
if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
|
||||
if (!sampledSlope) {
|
||||
surfaceSlope = resolveSurfaceSlope(complex, slopeStream, wx, height, wz);
|
||||
sampledSlope = true;
|
||||
}
|
||||
if (!sc.isValid(surfaceSlope)) {
|
||||
d = 0;
|
||||
}
|
||||
}
|
||||
@@ -171,6 +177,8 @@ final class IrisBiomeLayerGenerator {
|
||||
|
||||
if (layerCount > 0) {
|
||||
KList<CNG> heightGenerators = getLayerHeightGenerators(biome, random, rdata);
|
||||
double surfaceSlope = 0D;
|
||||
boolean sampledSlope = false;
|
||||
|
||||
for (int i = 0; i < layerCount; i++) {
|
||||
IrisBiomePaletteLayer layer = layers.get(i);
|
||||
@@ -181,7 +189,11 @@ final class IrisBiomeLayerGenerator {
|
||||
IrisSlopeClip sc = layer.getSlopeCondition();
|
||||
|
||||
if (!sc.isDefault()) {
|
||||
if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
|
||||
if (!sampledSlope) {
|
||||
surfaceSlope = resolveSurfaceSlope(complex, slopeStream, wx, height, wz);
|
||||
sampledSlope = true;
|
||||
}
|
||||
if (!sc.isValid(surfaceSlope)) {
|
||||
d = 0;
|
||||
}
|
||||
}
|
||||
@@ -206,11 +218,19 @@ final class IrisBiomeLayerGenerator {
|
||||
return real;
|
||||
}
|
||||
|
||||
private static ProceduralStream<Double> resolveSlopeStream(
|
||||
private static double resolveSurfaceSlope(
|
||||
IrisComplex complex,
|
||||
ProceduralStream<Double> slopeStream
|
||||
ProceduralStream<Double> slopeStream,
|
||||
double x,
|
||||
int height,
|
||||
double z
|
||||
) {
|
||||
return slopeStream == null ? complex.getSlopeStream() : slopeStream;
|
||||
if (slopeStream != null) {
|
||||
return slopeStream.getDouble(x, z);
|
||||
}
|
||||
return complex.hasTerrain3D()
|
||||
? complex.terrainSurfaceSlope((int) Math.floor(x), height, (int) Math.floor(z))
|
||||
: complex.getSlopeStream().getDouble(x, z);
|
||||
}
|
||||
|
||||
static KList<PlatformBlockState> generateSeaLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepth, IrisData rdata) {
|
||||
|
||||
@@ -219,7 +219,7 @@ public class IrisDimension extends IrisRegistrant {
|
||||
@MaxNumber(256)
|
||||
@Desc("Minimum air gap in blocks between the lower terrain surface and the upper terrain surface.")
|
||||
private int upperDimensionGap = 32;
|
||||
@Desc("When true, cave carving will cut through the upper dimension terrain. When false, the upper terrain is a solid untouched mass.")
|
||||
@Desc("When true, cave carving cuts through the upper dimension terrain. When false, cave carving leaves the upper terrain unchanged, including its biome terrain3D openings.")
|
||||
private boolean upperDimensionCarving = false;
|
||||
@Desc("When true, objects from the mantle (structures, trees, etc.) can be placed in the upper dimension terrain zone. When false, the upper terrain is protected from object placement.")
|
||||
private boolean upperDimensionObjects = false;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
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.Snippet;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Snippet("terrain-3d")
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Desc("Volumetric surface terrain that adds projecting rock and removes undercuts and fissures around the biome's height generators.")
|
||||
public class IrisTerrain3D {
|
||||
@Desc("Enable this biome's volumetric terrain profile.")
|
||||
private boolean enabled = true;
|
||||
|
||||
@Desc("Seed offset for the surface density and fissure fields.")
|
||||
private long seed = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(128)
|
||||
@Desc("Maximum positive or negative surface density displacement in blocks.")
|
||||
private double amplitude = 32;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(4096)
|
||||
@Desc("Horizontal feature size in blocks for a density style with its default zoom.")
|
||||
private double horizontalScale = 96;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(4096)
|
||||
@Desc("Vertical feature size in blocks. Shorter scales relative to amplitude permit overlapping ledges and overhangs.")
|
||||
private double verticalScale = 24;
|
||||
|
||||
@Desc("Signed three-dimensional noise that displaces terrain density. Style zoom multiplies the configured feature sizes.")
|
||||
private IrisGeneratorStyle densityStyle = new IrisGeneratorStyle(NoiseStyle.SIMPLEX);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(128)
|
||||
@Desc("Maximum additional fissure depth in blocks. Zero disables fissures.")
|
||||
private double crackDepth = 0;
|
||||
|
||||
@MinNumber(0.25)
|
||||
@MaxNumber(64)
|
||||
@Desc("Approximate fissure half-width in blocks around the fissure field's zero crossings.")
|
||||
private double crackWidth = 4;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(4096)
|
||||
@Desc("Horizontal fissure feature size in blocks. Fissures use four times this vertical scale.")
|
||||
private double crackScale = 96;
|
||||
|
||||
@Desc("Noise whose zero crossings define tall narrow fissures.")
|
||||
private IrisGeneratorStyle crackStyle = new IrisGeneratorStyle(NoiseStyle.SIMPLEX);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("Minimum original terrain slope, measured as rise divided by horizontal distance, before shaping starts. Zero disables slope gating.")
|
||||
private double minimumSlope = 0.15;
|
||||
|
||||
@MinNumber(0.001)
|
||||
@MaxNumber(16)
|
||||
@Desc("Slope interval above minimumSlope over which shaping reaches full strength.")
|
||||
private double slopeFade = 0.35;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(128)
|
||||
@Desc("Height above the dimension fluid level below which terrain remains solid and unchanged.")
|
||||
private double fluidClearance = 8;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(128)
|
||||
@Desc("Vertical distance over which shaping grows from zero above the fluid clearance.")
|
||||
private double fluidFade = 24;
|
||||
|
||||
public void validate() {
|
||||
requireRange("amplitude", amplitude, 0D, 128D);
|
||||
requireRange("horizontalScale", horizontalScale, 8D, 4096D);
|
||||
requireRange("verticalScale", verticalScale, 8D, 4096D);
|
||||
requireRange("crackDepth", crackDepth, 0D, 128D);
|
||||
requireRange("crackWidth", crackWidth, 0.25D, 64D);
|
||||
requireRange("crackScale", crackScale, 8D, 4096D);
|
||||
requireRange("minimumSlope", minimumSlope, 0D, 16D);
|
||||
requireRange("slopeFade", slopeFade, 0.001D, 16D);
|
||||
requireRange("fluidClearance", fluidClearance, 0D, 128D);
|
||||
requireRange("fluidFade", fluidFade, 1D, 128D);
|
||||
if (densityStyle == null || crackStyle == null) {
|
||||
throw new IllegalArgumentException("terrain3D densityStyle and crackStyle must be style objects or snippets");
|
||||
}
|
||||
validateStyle(densityStyle, "densityStyle", 0);
|
||||
validateStyle(crackStyle, "crackStyle", 0);
|
||||
}
|
||||
|
||||
private static void validateStyle(IrisGeneratorStyle style, String path, int depth) {
|
||||
if (depth > 32 || style.getStyle() == null) {
|
||||
throw new IllegalArgumentException("terrain3D." + path + " must use a known style with at most 32 nested fractures");
|
||||
}
|
||||
requireRange(path + ".zoom", style.getZoom(), 0.00001D, Double.MAX_VALUE);
|
||||
requireRange(path + ".cellularFrequency", style.getCellularFrequency(), 0D, Double.MAX_VALUE);
|
||||
requireRange(path + ".cellularZoom", style.getCellularZoom(), 0.00001D, Double.MAX_VALUE);
|
||||
requireRange(path + ".multiplier", style.getMultiplier(), 0.00001D, Double.MAX_VALUE);
|
||||
requireRange(path + ".exponent", style.getExponent(), 0.01562D, 64D);
|
||||
requireRange(path + ".cacheSize", style.getCacheSize(), 0D, 8192D);
|
||||
if (style.getFracture() != null) {
|
||||
validateStyle(style.getFracture(), path + ".fracture", depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireRange(String name, double value, double minimum, double maximum) {
|
||||
if (!Double.isFinite(value) || value < minimum || value > maximum) {
|
||||
throw new IllegalArgumentException("terrain3D." + name + " must be finite and between "
|
||||
+ minimum + " and " + maximum);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package art.arcane.iris.engine.terrain;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class Terrain3DColumn {
|
||||
private final double baseHeight;
|
||||
private final int minY;
|
||||
private final boolean shaped;
|
||||
private final int[] boundaries;
|
||||
|
||||
Terrain3DColumn(double baseHeight, int minY, boolean shaped, int[] boundaries) {
|
||||
this.baseHeight = baseHeight;
|
||||
this.minY = minY;
|
||||
this.shaped = shaped;
|
||||
this.boundaries = boundaries.clone();
|
||||
}
|
||||
|
||||
public static Terrain3DColumn unshaped(double baseHeight, int height) {
|
||||
int top = Math.clamp(Math.round(baseHeight), 0, height - 1);
|
||||
return new Terrain3DColumn(baseHeight, top + 1, false, new int[]{0, top});
|
||||
}
|
||||
|
||||
public double baseHeight() {
|
||||
return baseHeight;
|
||||
}
|
||||
|
||||
public int topY() {
|
||||
return boundaries[boundaries.length - 1];
|
||||
}
|
||||
|
||||
public int minY() {
|
||||
return minY;
|
||||
}
|
||||
|
||||
public boolean shaped() {
|
||||
return shaped;
|
||||
}
|
||||
|
||||
public int spanCount() {
|
||||
return boundaries.length / 2;
|
||||
}
|
||||
|
||||
public int floor(int span) {
|
||||
return boundaries[span * 2 + 1];
|
||||
}
|
||||
|
||||
public int ceiling(int span) {
|
||||
return boundaries[span * 2];
|
||||
}
|
||||
|
||||
public boolean isSolid(int y) {
|
||||
return surfaceY(y) >= 0;
|
||||
}
|
||||
|
||||
public int highestSolidY(int maximumY) {
|
||||
for (int index = boundaries.length - 2; index >= 0; index -= 2) {
|
||||
if (maximumY >= boundaries[index]) {
|
||||
return Math.min(maximumY, boundaries[index + 1]);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int surfaceY(int y) {
|
||||
for (int index = 0; index < boundaries.length; index += 2) {
|
||||
if (y < boundaries[index]) {
|
||||
return -1;
|
||||
}
|
||||
if (y <= boundaries[index + 1]) {
|
||||
return boundaries[index + 1];
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int nearestSurfaceY(int y) {
|
||||
int nearest = boundaries[1];
|
||||
long distance = Math.abs((long) nearest - y);
|
||||
for (int index = 3; index < boundaries.length; index += 2) {
|
||||
long candidateDistance = Math.abs((long) boundaries[index] - y);
|
||||
if (candidateDistance < distance) {
|
||||
nearest = boundaries[index];
|
||||
distance = candidateDistance;
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object value) {
|
||||
return this == value || value instanceof Terrain3DColumn other
|
||||
&& Double.compare(baseHeight, other.baseHeight) == 0
|
||||
&& minY == other.minY && shaped == other.shaped
|
||||
&& Arrays.equals(boundaries, other.boundaries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Double.hashCode(baseHeight);
|
||||
result = 31 * result + minY;
|
||||
result = 31 * result + Boolean.hashCode(shaped);
|
||||
return 31 * result + Arrays.hashCode(boundaries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package art.arcane.iris.engine.terrain;
|
||||
|
||||
import it.unimi.dsi.fastutil.HashCommon;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicIntegerArray;
|
||||
|
||||
final class Terrain3DFragmentFilter {
|
||||
private static final int MAXIMUM_FRAGMENT_VOLUME = 512;
|
||||
private static final int VISITED_CAPACITY = 2048;
|
||||
private static final int UNKNOWN = 0;
|
||||
private static final int KEEP = 1;
|
||||
private static final int REMOVE = 2;
|
||||
|
||||
private final ColumnSource source;
|
||||
private final ThreadLocal<Traversal> traversals = ThreadLocal.withInitial(Traversal::new);
|
||||
|
||||
Terrain3DFragmentFilter(ColumnSource source) {
|
||||
this.source = Objects.requireNonNull(source, "Raw density columns");
|
||||
}
|
||||
|
||||
Terrain3DColumn column(int x, int z) {
|
||||
DensityColumn density = source.column(x, z);
|
||||
Terrain3DColumn resolved = density.resolved;
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
Terrain3DColumn raw = density.raw;
|
||||
int removed = 0;
|
||||
for (int span = 1; span < raw.spanCount(); span++) {
|
||||
if (density.state(span) == UNKNOWN) {
|
||||
traversals.get().classify(x, z, density, span);
|
||||
}
|
||||
if (density.state(span) == REMOVE) {
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
resolved = removed == 0 ? raw : retainSpans(density, removed);
|
||||
density.resolved = resolved;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
traversals.remove();
|
||||
}
|
||||
|
||||
private Terrain3DColumn retainSpans(DensityColumn density, int removed) {
|
||||
Terrain3DColumn raw = density.raw;
|
||||
int[] boundaries = new int[(raw.spanCount() - removed) * 2];
|
||||
int index = 0;
|
||||
for (int span = 0; span < raw.spanCount(); span++) {
|
||||
if (density.state(span) != REMOVE) {
|
||||
boundaries[index++] = raw.ceiling(span);
|
||||
boundaries[index++] = raw.floor(span);
|
||||
}
|
||||
}
|
||||
return new Terrain3DColumn(raw.baseHeight(), raw.minY(), raw.shaped(), boundaries);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ColumnSource {
|
||||
DensityColumn column(int x, int z);
|
||||
}
|
||||
|
||||
static final class DensityColumn {
|
||||
private final Terrain3DColumn raw;
|
||||
private final AtomicIntegerArray states;
|
||||
private volatile Terrain3DColumn resolved;
|
||||
|
||||
DensityColumn(Terrain3DColumn raw) {
|
||||
this.raw = Objects.requireNonNull(raw, "Raw density column");
|
||||
states = raw.spanCount() == 1 ? null : new AtomicIntegerArray(raw.spanCount() - 1);
|
||||
resolved = states == null ? raw : null;
|
||||
}
|
||||
|
||||
private int state(int span) {
|
||||
return span == 0 ? KEEP : states.get(span - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private final class Traversal {
|
||||
private final int[] xs = new int[MAXIMUM_FRAGMENT_VOLUME + 1];
|
||||
private final int[] zs = new int[MAXIMUM_FRAGMENT_VOLUME + 1];
|
||||
private final int[] spans = new int[MAXIMUM_FRAGMENT_VOLUME + 1];
|
||||
private final DensityColumn[] columns = new DensityColumn[MAXIMUM_FRAGMENT_VOLUME + 1];
|
||||
private final int[] visited = new int[VISITED_CAPACITY];
|
||||
private final int[] visitedSlots = new int[MAXIMUM_FRAGMENT_VOLUME + 1];
|
||||
private int count;
|
||||
private int volume;
|
||||
private int decision;
|
||||
|
||||
private void classify(int x, int z, DensityColumn density, int span) {
|
||||
try {
|
||||
visit(x, z, density, span);
|
||||
for (int cursor = 0; cursor < count && decision == UNKNOWN; cursor++) {
|
||||
Terrain3DColumn raw = columns[cursor].raw;
|
||||
int bottom = raw.ceiling(spans[cursor]);
|
||||
int top = raw.floor(spans[cursor]);
|
||||
inspect((long) xs[cursor] - 1, zs[cursor], bottom, top);
|
||||
inspect((long) xs[cursor] + 1, zs[cursor], bottom, top);
|
||||
inspect(xs[cursor], (long) zs[cursor] - 1, bottom, top);
|
||||
inspect(xs[cursor], (long) zs[cursor] + 1, bottom, top);
|
||||
}
|
||||
int result = decision == UNKNOWN ? REMOVE : decision;
|
||||
for (int index = 0; index < count; index++) {
|
||||
columns[index].states.set(spans[index] - 1, result);
|
||||
}
|
||||
} finally {
|
||||
for (int index = 0; index < count; index++) {
|
||||
visited[visitedSlots[index]] = 0;
|
||||
}
|
||||
Arrays.fill(columns, 0, count, null);
|
||||
count = 0;
|
||||
volume = 0;
|
||||
decision = UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
private void inspect(long x, long z, int bottom, int top) {
|
||||
if (decision != UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
if (x < Integer.MIN_VALUE || x > Integer.MAX_VALUE
|
||||
|| z < Integer.MIN_VALUE || z > Integer.MAX_VALUE) {
|
||||
decision = KEEP;
|
||||
return;
|
||||
}
|
||||
DensityColumn density = source.column((int) x, (int) z);
|
||||
Terrain3DColumn raw = density.raw;
|
||||
for (int span = 0; span < raw.spanCount(); span++) {
|
||||
if (raw.ceiling(span) > top || decision != UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
if (raw.floor(span) >= bottom) {
|
||||
visit((int) x, (int) z, density, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void visit(int x, int z, DensityColumn density, int span) {
|
||||
int state = density.state(span);
|
||||
if (state != UNKNOWN) {
|
||||
decision = state;
|
||||
return;
|
||||
}
|
||||
int slot = visitedSlot(x, z, span);
|
||||
if (visited[slot] != 0) {
|
||||
return;
|
||||
}
|
||||
visited[slot] = count + 1;
|
||||
visitedSlots[count] = slot;
|
||||
xs[count] = x;
|
||||
zs[count] = z;
|
||||
spans[count] = span;
|
||||
columns[count++] = density;
|
||||
volume += density.raw.floor(span) - density.raw.ceiling(span) + 1;
|
||||
if (volume > MAXIMUM_FRAGMENT_VOLUME) {
|
||||
decision = KEEP;
|
||||
}
|
||||
}
|
||||
|
||||
private int visitedSlot(int x, int z, int span) {
|
||||
long coordinate = (long) x << 32 ^ z & 0xffffffffL;
|
||||
int slot = (int) HashCommon.mix(coordinate ^ span * 0x9e3779b97f4a7c15L)
|
||||
& (VISITED_CAPACITY - 1);
|
||||
while (visited[slot] != 0) {
|
||||
int index = visited[slot] - 1;
|
||||
if (xs[index] == x && zs[index] == z && spans[index] == span) {
|
||||
return slot;
|
||||
}
|
||||
slot = (slot + 1) & (VISITED_CAPACITY - 1);
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
package art.arcane.iris.engine.terrain;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisGeneratorStyle;
|
||||
import art.arcane.iris.engine.object.IrisTerrain3D;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
public final class Terrain3DRuntime {
|
||||
private static final int STEP = 4;
|
||||
private static final int CACHE_STRIPES = 16;
|
||||
private static final long DENSITY_SALT = 0x536E4A11C924B3D7L;
|
||||
private static final long CRACK_SALT = 0x7839A16DC4052EFBL;
|
||||
private static final NodeSample EMPTY_SAMPLE = new NodeSample(0D, 0D, 0D);
|
||||
private static final CompiledProfile DISABLED = new CompiledProfile(null, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, null, null);
|
||||
|
||||
private final Sources sources;
|
||||
private final Options options;
|
||||
private final NoiseFactory noiseFactory;
|
||||
private final BoundedCache<Terrain3DFragmentFilter.DensityColumn> columns;
|
||||
private final BoundedCache<Anchor> anchors;
|
||||
private final Terrain3DFragmentFilter fragments;
|
||||
private final Map<IrisTerrain3D, CompiledProfile> profiles = new IdentityHashMap<>();
|
||||
private final ThreadLocal<ColumnMemo> localColumn = ThreadLocal.withInitial(ColumnMemo::new);
|
||||
private volatile Object cacheGeneration = new Object();
|
||||
|
||||
public Terrain3DRuntime(Sources sources, Options options) {
|
||||
this(sources, options, (style, seed) -> {
|
||||
CNG noise = style.createNoCache(new RNG(seed), options.data());
|
||||
return noise::noiseFastSigned3D;
|
||||
});
|
||||
}
|
||||
|
||||
Terrain3DRuntime(Sources sources, Options options, NoiseFactory noiseFactory) {
|
||||
this.sources = Objects.requireNonNull(sources, "Terrain sources");
|
||||
this.options = Objects.requireNonNull(options, "Terrain options");
|
||||
this.noiseFactory = Objects.requireNonNull(noiseFactory, "Terrain noise factory");
|
||||
columns = new BoundedCache<>(options.maximumColumns());
|
||||
anchors = new BoundedCache<>(Math.max(CACHE_STRIPES, options.maximumColumns() / 4));
|
||||
fragments = new Terrain3DFragmentFilter(this::rawColumn);
|
||||
}
|
||||
|
||||
public boolean active() {
|
||||
return options.enabled();
|
||||
}
|
||||
|
||||
public Terrain3DColumn column(int x, int z) {
|
||||
if (!active()) {
|
||||
return Terrain3DColumn.unshaped(baseHeight(x, z), options.height());
|
||||
}
|
||||
long key = pack(x, z);
|
||||
Object generation = cacheGeneration;
|
||||
ColumnMemo memo = localColumn.get();
|
||||
if (memo.generation == generation && memo.key == key) {
|
||||
return memo.column;
|
||||
}
|
||||
Terrain3DColumn column = fragments.column(x, z);
|
||||
memo.key = key;
|
||||
memo.column = column;
|
||||
memo.generation = generation;
|
||||
return column;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
columns.clear();
|
||||
anchors.clear();
|
||||
fragments.clear();
|
||||
synchronized (profiles) {
|
||||
profiles.clear();
|
||||
}
|
||||
cacheGeneration = new Object();
|
||||
localColumn.remove();
|
||||
}
|
||||
|
||||
private Terrain3DFragmentFilter.DensityColumn rawColumn(int x, int z) {
|
||||
long key = pack(x, z);
|
||||
Terrain3DFragmentFilter.DensityColumn cached = columns.get(key);
|
||||
return cached == null ? columns.putIfAbsent(key,
|
||||
new Terrain3DFragmentFilter.DensityColumn(createColumn(x, z))) : cached;
|
||||
}
|
||||
|
||||
private Terrain3DColumn createColumn(int x, int z) {
|
||||
double baseHeight = baseHeight(x, z);
|
||||
int gridX = Math.floorDiv(x, STEP) * STEP;
|
||||
int gridZ = Math.floorDiv(z, STEP) * STEP;
|
||||
Anchor northWest = anchor(gridX, gridZ);
|
||||
Anchor northEast = anchor(gridX + STEP, gridZ);
|
||||
Anchor southWest = anchor(gridX, gridZ + STEP);
|
||||
Anchor southEast = anchor(gridX + STEP, gridZ + STEP);
|
||||
double dx = (x - gridX) / (double) STEP;
|
||||
double dz = (z - gridZ) / (double) STEP;
|
||||
double amplitude = interpolate(northWest.amplitude(), northEast.amplitude(),
|
||||
southWest.amplitude(), southEast.amplitude(), dx, dz);
|
||||
double crackDepth = interpolate(northWest.crackDepth(), northEast.crackDepth(),
|
||||
southWest.crackDepth(), southEast.crackDepth(), dx, dz);
|
||||
if (amplitude + crackDepth <= 0D) {
|
||||
return Terrain3DColumn.unshaped(baseHeight, options.height());
|
||||
}
|
||||
double crackWidth = interpolate(northWest.profile.crackWidth, northEast.profile.crackWidth,
|
||||
southWest.profile.crackWidth, southEast.profile.crackWidth, dx, dz);
|
||||
int minimum = Math.max(Math.max(1, (int) Math.floor(options.fluidHeight()) + 1),
|
||||
(int) Math.floor(baseHeight - amplitude - crackDepth));
|
||||
int maximum = Math.min(options.height() - 1, (int) Math.ceil(baseHeight + amplitude));
|
||||
if (minimum > maximum) {
|
||||
return Terrain3DColumn.unshaped(baseHeight, options.height());
|
||||
}
|
||||
int[] boundaries = new int[16];
|
||||
int count = 1;
|
||||
boundaries[0] = 0;
|
||||
boolean previousSolid = true;
|
||||
for (int gridY = Math.floorDiv(minimum, STEP) * STEP; gridY <= maximum; gridY += STEP) {
|
||||
NodeSample lowerNW = northWest.sample(gridY);
|
||||
NodeSample lowerNE = northEast.sample(gridY);
|
||||
NodeSample lowerSW = southWest.sample(gridY);
|
||||
NodeSample lowerSE = southEast.sample(gridY);
|
||||
NodeSample upperNW = northWest.sample(gridY + STEP);
|
||||
NodeSample upperNE = northEast.sample(gridY + STEP);
|
||||
NodeSample upperSW = southWest.sample(gridY + STEP);
|
||||
NodeSample upperSE = southEast.sample(gridY + STEP);
|
||||
double lowerDisplacement = interpolate(lowerNW.displacement, lowerNE.displacement,
|
||||
lowerSW.displacement, lowerSE.displacement, dx, dz);
|
||||
double upperDisplacement = interpolate(upperNW.displacement, upperNE.displacement,
|
||||
upperSW.displacement, upperSE.displacement, dx, dz);
|
||||
double lowerCrack = interpolate(lowerNW.crackDistance, lowerNE.crackDistance,
|
||||
lowerSW.crackDistance, lowerSE.crackDistance, dx, dz);
|
||||
double upperCrack = interpolate(upperNW.crackDistance, upperNE.crackDistance,
|
||||
upperSW.crackDistance, upperSE.crackDistance, dx, dz);
|
||||
double lowerDepth = interpolate(lowerNW.crackDepth, lowerNE.crackDepth,
|
||||
lowerSW.crackDepth, lowerSE.crackDepth, dx, dz);
|
||||
double upperDepth = interpolate(upperNW.crackDepth, upperNE.crackDepth,
|
||||
upperSW.crackDepth, upperSE.crackDepth, dx, dz);
|
||||
int lastY = Math.min(maximum, gridY + STEP - 1);
|
||||
for (int y = Math.max(minimum, gridY); y <= lastY; y++) {
|
||||
double dy = (y - gridY) / (double) STEP;
|
||||
double displacement = lerp(lowerDisplacement, upperDisplacement, dy);
|
||||
double fissure = 0D;
|
||||
if (crackDepth > 0D && crackWidth > 0D) {
|
||||
double distance = Math.abs(lerp(lowerCrack, upperCrack, dy));
|
||||
double ridge = Math.max(0D, 1D - distance / crackWidth);
|
||||
fissure = lerp(lowerDepth, upperDepth, dy) * ridge * ridge;
|
||||
}
|
||||
boolean solid = baseHeight + 0.5D - y + displacement - fissure >= 0D;
|
||||
if (solid != previousSolid) {
|
||||
if (count == boundaries.length) {
|
||||
boundaries = Arrays.copyOf(boundaries, count * 2);
|
||||
}
|
||||
boundaries[count++] = solid ? y : y - 1;
|
||||
previousSolid = solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (previousSolid) {
|
||||
if (count == boundaries.length) {
|
||||
boundaries = Arrays.copyOf(boundaries, count + 1);
|
||||
}
|
||||
boundaries[count++] = maximum;
|
||||
}
|
||||
return new Terrain3DColumn(baseHeight, minimum, true, Arrays.copyOf(boundaries, count));
|
||||
}
|
||||
|
||||
private Anchor anchor(int x, int z) {
|
||||
long key = pack(x, z);
|
||||
Anchor cached = anchors.get(key);
|
||||
return cached == null ? anchors.putIfAbsent(key, createAnchor(x, z)) : cached;
|
||||
}
|
||||
|
||||
private Anchor createAnchor(int x, int z) {
|
||||
IrisBiome biome = sources.biomes().sample(x, z);
|
||||
CompiledProfile profile = profile(biome == null ? null : biome.getTerrain3D());
|
||||
if (profile == DISABLED) {
|
||||
return new Anchor(x, z, profile, 0D);
|
||||
}
|
||||
double height = baseHeight(x, z);
|
||||
double elevationStrength = smooth((height - options.fluidHeight() - profile.fluidClearance)
|
||||
/ profile.fluidFade);
|
||||
if (elevationStrength <= 0D) {
|
||||
return new Anchor(x, z, profile, 0D);
|
||||
}
|
||||
double slopeStrength = 1D;
|
||||
if (profile.minimumSlope > 0D) {
|
||||
double east = (baseHeight(x + STEP, z) - height) / STEP;
|
||||
double south = (baseHeight(x, z + STEP) - height) / STEP;
|
||||
double slope = StrictMath.sqrt(east * east + south * south);
|
||||
slopeStrength = smooth((slope - profile.minimumSlope) / profile.slopeFade);
|
||||
}
|
||||
return new Anchor(x, z, profile, elevationStrength * slopeStrength);
|
||||
}
|
||||
|
||||
private CompiledProfile profile(IrisTerrain3D config) {
|
||||
if (config == null || !config.isEnabled()) {
|
||||
return DISABLED;
|
||||
}
|
||||
synchronized (profiles) {
|
||||
CompiledProfile cached = profiles.get(config);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
CompiledProfile compiled = compile(config);
|
||||
synchronized (profiles) {
|
||||
CompiledProfile existing = profiles.get(config);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
profiles.put(config, compiled);
|
||||
return compiled;
|
||||
}
|
||||
}
|
||||
|
||||
private CompiledProfile compile(IrisTerrain3D config) {
|
||||
config.validate();
|
||||
double amplitude = config.getAmplitude();
|
||||
double crackDepth = config.getCrackDepth();
|
||||
double horizontalScale = config.getHorizontalScale();
|
||||
double verticalScale = config.getVerticalScale();
|
||||
double crackScale = config.getCrackScale();
|
||||
double crackWidth = config.getCrackWidth();
|
||||
double minimumSlope = config.getMinimumSlope();
|
||||
double slopeFade = config.getSlopeFade();
|
||||
double fluidClearance = config.getFluidClearance();
|
||||
double fluidFade = config.getFluidFade();
|
||||
if (amplitude + crackDepth == 0D) {
|
||||
return DISABLED;
|
||||
}
|
||||
long seed = options.seed() ^ config.getSeed();
|
||||
NoiseSource density = amplitude == 0D ? null : noiseFactory.create(
|
||||
Objects.requireNonNull(config.getDensityStyle(), "terrain3D.densityStyle"), seed ^ DENSITY_SALT);
|
||||
NoiseSource cracks = crackDepth == 0D ? null : noiseFactory.create(
|
||||
Objects.requireNonNull(config.getCrackStyle(), "terrain3D.crackStyle"), seed ^ CRACK_SALT);
|
||||
return new CompiledProfile(config, amplitude, crackDepth, crackWidth, crackScale,
|
||||
64D / horizontalScale, 64D / verticalScale, 64D / crackScale,
|
||||
minimumSlope, slopeFade, fluidClearance, fluidFade, density, cracks);
|
||||
}
|
||||
|
||||
private double baseHeight(int x, int z) {
|
||||
double height = sources.heights().sample(x, z);
|
||||
if (!Double.isFinite(height)) {
|
||||
throw new IllegalStateException("Nonfinite terrain height at " + x + "," + z);
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
private static double signed(NoiseSource source, double x, double y, double z) {
|
||||
double sampled = source.sample(x, y, z);
|
||||
if (!Double.isFinite(sampled)) {
|
||||
throw new IllegalStateException("Nonfinite terrain3D density at " + x + "," + y + "," + z);
|
||||
}
|
||||
return Math.clamp(sampled, -1D, 1D);
|
||||
}
|
||||
|
||||
private static double interpolate(double northWest, double northEast, double southWest,
|
||||
double southEast, double x, double z) {
|
||||
return lerp(lerp(northWest, northEast, x), lerp(southWest, southEast, x), z);
|
||||
}
|
||||
|
||||
private static double lerp(double lower, double upper, double fraction) {
|
||||
return lower + (upper - lower) * fraction;
|
||||
}
|
||||
|
||||
private static double smooth(double value) {
|
||||
double bounded = Math.clamp(value, 0D, 1D);
|
||||
return bounded * bounded * (3D - 2D * bounded);
|
||||
}
|
||||
|
||||
private static long pack(int x, int z) {
|
||||
return (long) x << 32 ^ z & 0xffffffffL;
|
||||
}
|
||||
|
||||
public record Sources(BaseHeightSource heights, BiomeSource biomes) {
|
||||
public Sources {
|
||||
Objects.requireNonNull(heights, "Base terrain height source");
|
||||
Objects.requireNonNull(biomes, "Base terrain biome source");
|
||||
}
|
||||
}
|
||||
|
||||
public record Options(long seed, int height, double fluidHeight, IrisData data,
|
||||
boolean enabled, int maximumColumns) {
|
||||
public Options {
|
||||
if (height < 2 || height > 4096 || !Double.isFinite(fluidHeight)
|
||||
|| maximumColumns < CACHE_STRIPES) {
|
||||
throw new IllegalArgumentException("Invalid volumetric terrain runtime options");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface BaseHeightSource {
|
||||
double sample(int x, int z);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface BiomeSource {
|
||||
IrisBiome sample(int x, int z);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface NoiseFactory {
|
||||
NoiseSource create(IrisGeneratorStyle style, long seed);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface NoiseSource {
|
||||
double sample(double x, double y, double z);
|
||||
}
|
||||
|
||||
private static final class ColumnMemo {
|
||||
private Object generation;
|
||||
private long key;
|
||||
private Terrain3DColumn column;
|
||||
}
|
||||
|
||||
private final class Anchor {
|
||||
private final int x;
|
||||
private final int z;
|
||||
private final CompiledProfile profile;
|
||||
private final double strength;
|
||||
private final AtomicReferenceArray<NodeSample> samples;
|
||||
|
||||
private Anchor(int x, int z, CompiledProfile profile, double strength) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.profile = profile;
|
||||
this.strength = strength;
|
||||
samples = strength == 0D ? null
|
||||
: new AtomicReferenceArray<>(Math.ceilDiv(options.height(), STEP) + 1);
|
||||
}
|
||||
|
||||
private double amplitude() {
|
||||
return profile.amplitude * strength;
|
||||
}
|
||||
|
||||
private double crackDepth() {
|
||||
return profile.crackDepth * strength;
|
||||
}
|
||||
|
||||
private NodeSample sample(int y) {
|
||||
if (samples == null) {
|
||||
return EMPTY_SAMPLE;
|
||||
}
|
||||
int index = y / STEP;
|
||||
NodeSample cached = samples.get(index);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
double fade = strength * smooth((y - options.fluidHeight() - profile.fluidClearance)
|
||||
/ profile.fluidFade);
|
||||
double displacement = profile.density == null || fade == 0D ? 0D
|
||||
: profile.amplitude * fade * signed(profile.density,
|
||||
x * profile.horizontalFrequency, y * profile.verticalFrequency, z * profile.horizontalFrequency);
|
||||
double crackDistance = profile.cracks == null || fade == 0D ? 0D
|
||||
: profile.crackScale * 0.5D * signed(profile.cracks,
|
||||
x * profile.crackFrequency, y * profile.crackFrequency * 0.25D, z * profile.crackFrequency);
|
||||
NodeSample computed = new NodeSample(displacement, crackDistance, profile.crackDepth * fade);
|
||||
return samples.compareAndSet(index, null, computed) ? computed : samples.get(index);
|
||||
}
|
||||
}
|
||||
|
||||
private record NodeSample(double displacement, double crackDistance, double crackDepth) {
|
||||
}
|
||||
|
||||
private record CompiledProfile(IrisTerrain3D config, double amplitude, double crackDepth,
|
||||
double crackWidth, double crackScale, double horizontalFrequency,
|
||||
double verticalFrequency, double crackFrequency,
|
||||
double minimumSlope, double slopeFade, double fluidClearance,
|
||||
double fluidFade, NoiseSource density, NoiseSource cracks) {
|
||||
}
|
||||
|
||||
private static final class BoundedCache<T> {
|
||||
private final CacheStripe<T>[] stripes;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private BoundedCache(int maximumSize) {
|
||||
stripes = (CacheStripe<T>[]) new CacheStripe<?>[CACHE_STRIPES];
|
||||
for (int index = 0; index < stripes.length; index++) {
|
||||
stripes[index] = new CacheStripe<>(Math.ceilDiv(maximumSize, CACHE_STRIPES));
|
||||
}
|
||||
}
|
||||
|
||||
private T get(long key) {
|
||||
return stripe(key).get(key);
|
||||
}
|
||||
|
||||
private T putIfAbsent(long key, T value) {
|
||||
return stripe(key).putIfAbsent(key, value);
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
for (CacheStripe<T> stripe : stripes) {
|
||||
stripe.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private CacheStripe<T> stripe(long key) {
|
||||
long mixed = key ^ (key >>> 33);
|
||||
mixed *= 0xff51afd7ed558ccdL;
|
||||
mixed ^= mixed >>> 33;
|
||||
return stripes[(int) mixed & (CACHE_STRIPES - 1)];
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CacheStripe<T> {
|
||||
private final int maximumSize;
|
||||
private final Long2ObjectLinkedOpenHashMap<T> entries = new Long2ObjectLinkedOpenHashMap<>(16);
|
||||
|
||||
private CacheStripe(int maximumSize) {
|
||||
this.maximumSize = maximumSize;
|
||||
}
|
||||
|
||||
private synchronized T get(long key) {
|
||||
return entries.getAndMoveToLast(key);
|
||||
}
|
||||
|
||||
private synchronized T putIfAbsent(long key, T value) {
|
||||
T existing = entries.getAndMoveToLast(key);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
entries.putAndMoveToLast(key, value);
|
||||
if (entries.size() > maximumSize) {
|
||||
entries.removeFirst();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private synchronized void clear() {
|
||||
entries.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user