mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Added chunk terrain bake
- Compiled generator programs evaluate layer trees in batches through NoiseProgram - TerrainProgram accumulates interpolator group means over chunk columns - ChunkBakeCache holds compute-once per-chunk planes with provisional results never published - Height, biome, region, rock, fluid and cave streams read the bake as thin facades - Sparse hydrology and bounds reads stay scalar with a direct-mapped base height memo - ColumnBake assembles id planes, heights, fluid surface, columns and the apron on demand
This commit is contained in:
1 parent
2471066937
commit
037156ab7d
22 files changed
+2032
-331
No files matched your search
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.generation.bake;
|
||||
|
||||
public interface BakeCache {
|
||||
ColumnBake get(int chunkX, int chunkZ);
|
||||
|
||||
ColumnBake getIfPresent(int chunkX, int chunkZ);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package art.arcane.iris.generation.bake;
|
||||
|
||||
public enum BakePlane {
|
||||
REGION,
|
||||
BASE_BIOME,
|
||||
BASE_HEIGHT,
|
||||
NATURAL_HEIGHT,
|
||||
NATURAL_TRUE_BIOME,
|
||||
HEIGHT,
|
||||
TRUE_BIOME,
|
||||
ROCK,
|
||||
FLUID,
|
||||
CAVE_BIOME;
|
||||
|
||||
static final BakePlane[] PLANES = values();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package art.arcane.iris.generation.bake;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class BakeRegistry<T> {
|
||||
private final Object lock = new Object();
|
||||
private volatile IdentityHashMap<T, Integer> ids = new IdentityHashMap<>();
|
||||
private volatile Object[] values = new Object[16];
|
||||
private int size;
|
||||
|
||||
public short id(T value) {
|
||||
Objects.requireNonNull(value, "Registered value");
|
||||
Integer known = ids.get(value);
|
||||
if (known != null) {
|
||||
return known.shortValue();
|
||||
}
|
||||
synchronized (lock) {
|
||||
Integer existing = ids.get(value);
|
||||
if (existing != null) {
|
||||
return existing.shortValue();
|
||||
}
|
||||
if (size == Short.MAX_VALUE) {
|
||||
throw new IllegalStateException("Bake registry is full");
|
||||
}
|
||||
Object[] grown = size == values.length ? Arrays.copyOf(values, size * 2) : values;
|
||||
grown[size] = value;
|
||||
values = grown;
|
||||
IdentityHashMap<T, Integer> next = new IdentityHashMap<>(ids);
|
||||
next.put(value, size);
|
||||
ids = next;
|
||||
return (short) size++;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public T value(int id) {
|
||||
return (T) values[id];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package art.arcane.iris.generation.bake;
|
||||
|
||||
import art.arcane.iris.generation.cache.ConcurrentClockCache;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.DoubleSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class ChunkBakeCache implements BakeCache {
|
||||
private static final ThreadLocal<int[]> DEPTH = ThreadLocal.withInitial(() -> new int[2]);
|
||||
private static final int BAKING = 0;
|
||||
private static final int SPARSE = 1;
|
||||
|
||||
private final PlaneSource source;
|
||||
private final PlaneSource tracked;
|
||||
private volatile ConcurrentClockCache<ChunkPlanes> entries;
|
||||
|
||||
public ChunkBakeCache(PlaneSource source, int capacityChunks) {
|
||||
this.source = Objects.requireNonNull(source, "Bake plane source");
|
||||
this.tracked = new TrackedSource(source);
|
||||
this.entries = new ConcurrentClockCache<>(Math.max(16, capacityChunks));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnBake get(int chunkX, int chunkZ) {
|
||||
return planes(chunkX, chunkZ).columnBake(tracked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnBake getIfPresent(int chunkX, int chunkZ) {
|
||||
ChunkPlanes planes = planesIfPresent(chunkX, chunkZ);
|
||||
return planes == null ? null : planes.columnBake();
|
||||
}
|
||||
|
||||
public int capacity() {
|
||||
return entries.capacity();
|
||||
}
|
||||
|
||||
public synchronized void ensureCapacity(int chunks) {
|
||||
ConcurrentClockCache<ChunkPlanes> current = entries;
|
||||
if (current.capacity() >= chunks) {
|
||||
return;
|
||||
}
|
||||
ConcurrentClockCache<ChunkPlanes> resized = new ConcurrentClockCache<>(chunks);
|
||||
current.forEach(planes -> resized.putIfAbsent(key(planes.chunkX(), planes.chunkZ()), planes));
|
||||
entries = resized;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
entries.clear();
|
||||
}
|
||||
|
||||
public ChunkPlanes planes(int chunkX, int chunkZ) {
|
||||
ConcurrentClockCache<ChunkPlanes> current = entries;
|
||||
long key = key(chunkX, chunkZ);
|
||||
ChunkPlanes existing = current.get(key);
|
||||
return existing != null ? existing : current.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ));
|
||||
}
|
||||
|
||||
public ChunkPlanes planesIfPresent(int chunkX, int chunkZ) {
|
||||
return entries.get(key(chunkX, chunkZ));
|
||||
}
|
||||
|
||||
public Object bake(BakePlane plane, int chunkX, int chunkZ) {
|
||||
return planes(chunkX, chunkZ).plane(plane, tracked);
|
||||
}
|
||||
|
||||
public Object read(BakePlane plane, int chunkX, int chunkZ) {
|
||||
ChunkPlanes planes = entries.get(key(chunkX, chunkZ));
|
||||
if (planes != null) {
|
||||
Object published = planes.published(plane);
|
||||
if (published != null) {
|
||||
return published;
|
||||
}
|
||||
}
|
||||
if (!denseReadsAllowed()) {
|
||||
return null;
|
||||
}
|
||||
return (planes != null ? planes : planes(chunkX, chunkZ)).plane(plane, tracked);
|
||||
}
|
||||
|
||||
public boolean denseReadsAllowed() {
|
||||
int[] depth = DEPTH.get();
|
||||
if (depth[SPARSE] > 0 || source.mainThread()) {
|
||||
return false;
|
||||
}
|
||||
return depth[BAKING] > 0 || source.generationContext();
|
||||
}
|
||||
|
||||
public boolean bakingAllowed() {
|
||||
return !inSparseScope() && !source.mainThread();
|
||||
}
|
||||
|
||||
public static boolean inSparseScope() {
|
||||
return DEPTH.get()[SPARSE] > 0;
|
||||
}
|
||||
|
||||
public static <T> T sparse(Supplier<T> action) {
|
||||
enterSparse();
|
||||
try {
|
||||
return action.get();
|
||||
} finally {
|
||||
exitSparse();
|
||||
}
|
||||
}
|
||||
|
||||
public static double sparseDouble(DoubleSupplier action) {
|
||||
enterSparse();
|
||||
try {
|
||||
return action.getAsDouble();
|
||||
} finally {
|
||||
exitSparse();
|
||||
}
|
||||
}
|
||||
|
||||
public static void sparse(Runnable action) {
|
||||
enterSparse();
|
||||
try {
|
||||
action.run();
|
||||
} finally {
|
||||
exitSparse();
|
||||
}
|
||||
}
|
||||
|
||||
public static void enterSparse() {
|
||||
DEPTH.get()[SPARSE]++;
|
||||
}
|
||||
|
||||
public static void exitSparse() {
|
||||
DEPTH.get()[SPARSE]--;
|
||||
}
|
||||
|
||||
public static long key(int chunkX, int chunkZ) {
|
||||
return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
|
||||
}
|
||||
|
||||
private static final class TrackedSource implements PlaneSource {
|
||||
private final PlaneSource delegate;
|
||||
|
||||
private TrackedSource(PlaneSource delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object compute(BakePlane plane, int chunkX, int chunkZ) {
|
||||
int[] depth = DEPTH.get();
|
||||
depth[BAKING]++;
|
||||
try {
|
||||
return delegate.compute(plane, chunkX, chunkZ);
|
||||
} finally {
|
||||
depth[BAKING]--;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnBake assemble(ChunkPlanes planes) {
|
||||
int[] depth = DEPTH.get();
|
||||
depth[BAKING]++;
|
||||
try {
|
||||
return delegate.assemble(planes);
|
||||
} finally {
|
||||
depth[BAKING]--;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mainThread() {
|
||||
return delegate.mainThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean generationContext() {
|
||||
return delegate.generationContext();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package art.arcane.iris.generation.bake;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
public final class ChunkPlanes {
|
||||
private final int chunkX;
|
||||
private final int chunkZ;
|
||||
private final AtomicReferenceArray<Object> planes = new AtomicReferenceArray<>(BakePlane.PLANES.length);
|
||||
private volatile ColumnBake columnBake;
|
||||
|
||||
ChunkPlanes(int chunkX, int chunkZ) {
|
||||
this.chunkX = chunkX;
|
||||
this.chunkZ = chunkZ;
|
||||
}
|
||||
|
||||
public int chunkX() {
|
||||
return chunkX;
|
||||
}
|
||||
|
||||
public int chunkZ() {
|
||||
return chunkZ;
|
||||
}
|
||||
|
||||
public Object published(BakePlane plane) {
|
||||
Object state = planes.getAcquire(plane.ordinal());
|
||||
return state instanceof Pending ? null : state;
|
||||
}
|
||||
|
||||
Object plane(BakePlane plane, PlaneSource source) {
|
||||
int slot = plane.ordinal();
|
||||
while (true) {
|
||||
Object state = planes.getAcquire(slot);
|
||||
if (state != null && !(state instanceof Pending)) {
|
||||
return state;
|
||||
}
|
||||
if (state instanceof Pending pending) {
|
||||
if (pending.owner == Thread.currentThread()) {
|
||||
return source.compute(plane, chunkX, chunkZ);
|
||||
}
|
||||
pending.await();
|
||||
continue;
|
||||
}
|
||||
Pending pending = new Pending(Thread.currentThread());
|
||||
if (!planes.compareAndSet(slot, null, pending)) {
|
||||
continue;
|
||||
}
|
||||
Object computed = null;
|
||||
boolean publishable = false;
|
||||
try {
|
||||
long revision = ProvisionalSampling.revision();
|
||||
computed = source.compute(plane, chunkX, chunkZ);
|
||||
publishable = revision == ProvisionalSampling.revision();
|
||||
} finally {
|
||||
planes.setRelease(slot, publishable ? computed : null);
|
||||
pending.complete();
|
||||
}
|
||||
return computed;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnBake columnBake() {
|
||||
return columnBake;
|
||||
}
|
||||
|
||||
ColumnBake columnBake(PlaneSource source) {
|
||||
ColumnBake bake = columnBake;
|
||||
if (bake != null) {
|
||||
return bake;
|
||||
}
|
||||
long revision = ProvisionalSampling.revision();
|
||||
bake = source.assemble(this);
|
||||
if (revision == ProvisionalSampling.revision()) {
|
||||
columnBake = bake;
|
||||
}
|
||||
return bake;
|
||||
}
|
||||
|
||||
private static final class Pending {
|
||||
private final Thread owner;
|
||||
private boolean done;
|
||||
|
||||
private Pending(Thread owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
private synchronized void complete() {
|
||||
done = true;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
private synchronized void await() {
|
||||
boolean interrupted = false;
|
||||
while (!done) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
interrupted = true;
|
||||
}
|
||||
}
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package art.arcane.iris.generation.bake;
|
||||
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
|
||||
public final class ColumnBake {
|
||||
public static final int APRON_WIDTH = 18;
|
||||
|
||||
public final int chunkX;
|
||||
public final int chunkZ;
|
||||
public final short[] regionId;
|
||||
public final short[] baseBiomeId;
|
||||
public final short[] trueBiomeId;
|
||||
public final short[] caveBiomeId;
|
||||
public final double[] baseHeight;
|
||||
public final double[] naturalHeight;
|
||||
public final double[] height;
|
||||
public final int[] roundedHeight;
|
||||
public final float[] fluidSurface;
|
||||
public final short[] rockState;
|
||||
public final short[] fluidState;
|
||||
public final Terrain3DColumn[] terrainColumns;
|
||||
public final double[] apronHeight;
|
||||
|
||||
public ColumnBake(Planes planes) {
|
||||
this.chunkX = planes.chunkX();
|
||||
this.chunkZ = planes.chunkZ();
|
||||
this.regionId = planes.regionId();
|
||||
this.baseBiomeId = planes.baseBiomeId();
|
||||
this.trueBiomeId = planes.trueBiomeId();
|
||||
this.caveBiomeId = planes.caveBiomeId();
|
||||
this.baseHeight = planes.baseHeight();
|
||||
this.naturalHeight = planes.naturalHeight();
|
||||
this.height = planes.height();
|
||||
this.roundedHeight = planes.roundedHeight();
|
||||
this.fluidSurface = planes.fluidSurface();
|
||||
this.rockState = planes.rockState();
|
||||
this.fluidState = planes.fluidState();
|
||||
this.terrainColumns = planes.terrainColumns();
|
||||
this.apronHeight = planes.apronHeight();
|
||||
}
|
||||
|
||||
public static int index(int localX, int localZ) {
|
||||
return (localZ << 4) | localX;
|
||||
}
|
||||
|
||||
public static int apronIndex(int localX, int localZ) {
|
||||
return (localZ + 1) * APRON_WIDTH + localX + 1;
|
||||
}
|
||||
|
||||
public record Planes(int chunkX, int chunkZ, short[] regionId, short[] baseBiomeId, short[] trueBiomeId,
|
||||
short[] caveBiomeId, double[] baseHeight, double[] naturalHeight, double[] height,
|
||||
int[] roundedHeight, float[] fluidSurface, short[] rockState, short[] fluidState,
|
||||
Terrain3DColumn[] terrainColumns, double[] apronHeight) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package art.arcane.iris.generation.bake;
|
||||
|
||||
public interface PlaneSource {
|
||||
Object compute(BakePlane plane, int chunkX, int chunkZ);
|
||||
|
||||
ColumnBake assemble(ChunkPlanes planes);
|
||||
|
||||
boolean mainThread();
|
||||
|
||||
boolean generationContext();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package art.arcane.iris.generation.bake;
|
||||
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
public final class SparseHeightMemo {
|
||||
private final AtomicReferenceArray<Node> nodes;
|
||||
private final int mask;
|
||||
|
||||
public SparseHeightMemo(int capacity) {
|
||||
if (capacity <= 0 || Integer.bitCount(capacity) != 1) {
|
||||
throw new IllegalArgumentException("Sparse height memo capacity must be a power of two");
|
||||
}
|
||||
nodes = new AtomicReferenceArray<>(capacity);
|
||||
mask = capacity - 1;
|
||||
}
|
||||
|
||||
public double get(int x, int z) {
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
Node node = nodes.getAcquire((int) CacheKey.mix(key) & mask);
|
||||
return node != null && node.key == key ? node.height : Double.NaN;
|
||||
}
|
||||
|
||||
public void put(int x, int z, double height) {
|
||||
if (!Double.isFinite(height)) {
|
||||
return;
|
||||
}
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
nodes.setRelease((int) CacheKey.mix(key) & mask, new Node(key, height));
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
for (int index = 0; index < nodes.length(); index++) {
|
||||
nodes.setRelease(index, null);
|
||||
}
|
||||
}
|
||||
|
||||
private record Node(long key, double height) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package art.arcane.iris.generation.bake;
|
||||
|
||||
import art.arcane.iris.generation.noise.GeneratorProgram;
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
import art.arcane.iris.generation.noise.TerrainBatchScratch;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
public final class TerrainProgram {
|
||||
private static final int SKIPPED = 0;
|
||||
private static final int CONSTANT = 1;
|
||||
private static final int ACTIVE = 2;
|
||||
|
||||
private final IrisGenerator[][] generators;
|
||||
private final GeneratorProgram[][] programs;
|
||||
private final long heightSeed;
|
||||
private final LongAdder evaluations = new LongAdder();
|
||||
|
||||
private TerrainProgram(IrisGenerator[][] generators, GeneratorProgram[][] programs, long heightSeed) {
|
||||
this.generators = generators;
|
||||
this.programs = programs;
|
||||
this.heightSeed = heightSeed;
|
||||
}
|
||||
|
||||
public static TerrainProgram compile(IrisGenerator[][] generators, long heightSeed) {
|
||||
GeneratorProgram[][] programs = new GeneratorProgram[generators.length][];
|
||||
for (int group = 0; group < generators.length; group++) {
|
||||
programs[group] = new GeneratorProgram[generators[group].length];
|
||||
for (int index = 0; index < generators[group].length; index++) {
|
||||
IrisGenerator generator = generators[group][index];
|
||||
programs[group][index] = generator.supportsBatch() ? generator.compileProgram(heightSeed) : null;
|
||||
}
|
||||
}
|
||||
return new TerrainProgram(generators, programs, heightSeed);
|
||||
}
|
||||
|
||||
public int groups() {
|
||||
return generators.length;
|
||||
}
|
||||
|
||||
public int groupSize(int group) {
|
||||
return generators[group].length;
|
||||
}
|
||||
|
||||
public long evaluations() {
|
||||
return evaluations.sum();
|
||||
}
|
||||
|
||||
public void countScalarEvaluations(int count) {
|
||||
evaluations.add(count);
|
||||
}
|
||||
|
||||
public void accumulate(int group, TerrainBatchScratch scratch, double[] xs, double[] zs, int count,
|
||||
double[] lows, double[] highs, double[] heights) {
|
||||
IrisGenerator[] members = generators[group];
|
||||
if (members.length == 0 || count == 0) {
|
||||
return;
|
||||
}
|
||||
int mark = scratch.mark();
|
||||
int indexMark = scratch.indexMark();
|
||||
int[] modes = scratch.indices(count);
|
||||
int activeCount = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
double low = lows[index];
|
||||
double high = highs[index];
|
||||
if (low == 0D && high == 0D) {
|
||||
modes[index] = SKIPPED;
|
||||
continue;
|
||||
}
|
||||
if (!Double.isFinite(low) || !Double.isFinite(high)) {
|
||||
throw new IllegalStateException("Terrain generator bounds were not finite at " + xs[index] + "," + zs[index]
|
||||
+ " (generator=" + members[0].getLoadKey() + ", bounds=" + low + ".." + high + ").");
|
||||
}
|
||||
if (low == high) {
|
||||
modes[index] = CONSTANT;
|
||||
double sum = 0D;
|
||||
for (int member = 0; member < members.length; member++) {
|
||||
sum += low;
|
||||
}
|
||||
heights[index] += sum / members.length;
|
||||
continue;
|
||||
}
|
||||
modes[index] = ACTIVE;
|
||||
activeCount++;
|
||||
}
|
||||
if (activeCount == 0) {
|
||||
scratch.resetIndices(indexMark);
|
||||
return;
|
||||
}
|
||||
double[] activeX = scratch.buffer(activeCount);
|
||||
double[] activeZ = scratch.buffer(activeCount);
|
||||
double[] activeLow = scratch.buffer(activeCount);
|
||||
double[] activeHigh = scratch.buffer(activeCount);
|
||||
int[] activeIndex = scratch.indices(activeCount);
|
||||
int cursor = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (modes[index] == ACTIVE) {
|
||||
activeX[cursor] = xs[index];
|
||||
activeZ[cursor] = zs[index];
|
||||
activeLow[cursor] = lows[index];
|
||||
activeHigh[cursor] = highs[index];
|
||||
activeIndex[cursor++] = index;
|
||||
}
|
||||
}
|
||||
double[] sums = scratch.buffer(activeCount);
|
||||
double[] noise = scratch.buffer(activeCount);
|
||||
Arrays.fill(sums, 0, activeCount, 0D);
|
||||
GeneratorProgram[] compiled = programs[group];
|
||||
for (int member = 0; member < members.length; member++) {
|
||||
GeneratorProgram program = compiled[member];
|
||||
if (program != null) {
|
||||
program.heights(scratch, activeX, activeZ, activeCount, noise);
|
||||
} else {
|
||||
for (int index = 0; index < activeCount; index++) {
|
||||
noise[index] = members[member].getHeight(activeX[index], activeZ[index], heightSeed);
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < activeCount; index++) {
|
||||
if (!Double.isFinite(noise[index])) {
|
||||
throw new IllegalStateException("Terrain generator noise was not finite at " + activeX[index] + "," + activeZ[index]
|
||||
+ " (generator=" + members[member].getLoadKey() + ", noise=" + noise[index]
|
||||
+ ", bounds=" + activeLow[index] + ".." + activeHigh[index] + ").");
|
||||
}
|
||||
sums[index] += M.lerp(activeLow[index], activeHigh[index], noise[index]);
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < activeCount; index++) {
|
||||
heights[activeIndex[index]] += sums[index] / members.length;
|
||||
}
|
||||
evaluations.add((long) activeCount * members.length);
|
||||
scratch.reset(mark);
|
||||
scratch.resetIndices(indexMark);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class GeneratorProgram {
|
||||
private static final int SURFACE_GRID = 6;
|
||||
|
||||
private final IrisGenerator generator;
|
||||
private final long superSeed;
|
||||
private final NoiseLayerProgram[] layers;
|
||||
private final double totalOpacity;
|
||||
private final NoiseLayerProgram cliffs;
|
||||
private final boolean multiplicitive;
|
||||
private final double opacity;
|
||||
private final double offsetX;
|
||||
private final double offsetZ;
|
||||
private final double zoom;
|
||||
private final double surfaceDetail;
|
||||
private final double cliffHeightMin;
|
||||
private final double cliffHeightMax;
|
||||
private final boolean cellCracks;
|
||||
|
||||
GeneratorProgram(IrisGenerator generator, long superSeed, NoiseLayerProgram[] layers, double totalOpacity, NoiseLayerProgram cliffs) {
|
||||
this.generator = generator;
|
||||
this.superSeed = superSeed;
|
||||
this.layers = layers;
|
||||
this.totalOpacity = totalOpacity;
|
||||
this.cliffs = cliffs;
|
||||
this.multiplicitive = generator.isMultiplicitive();
|
||||
this.opacity = generator.getOpacity();
|
||||
this.offsetX = generator.getOffsetX();
|
||||
this.offsetZ = generator.getOffsetZ();
|
||||
this.zoom = generator.getZoom();
|
||||
this.surfaceDetail = generator.getSurfaceDetail();
|
||||
this.cliffHeightMin = generator.getCliffHeightMin();
|
||||
this.cliffHeightMax = generator.getCliffHeightMax();
|
||||
this.cellCracks = generator.getCellFractureHeight() != 0;
|
||||
}
|
||||
|
||||
public IrisGenerator generator() {
|
||||
return generator;
|
||||
}
|
||||
|
||||
public void heights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
if (layers.length == 0) {
|
||||
Arrays.fill(out, 0, count, 0D);
|
||||
return;
|
||||
}
|
||||
if (surfaceDetail == 1D) {
|
||||
sample(scratch, xs, zs, count, out);
|
||||
return;
|
||||
}
|
||||
surfaceHeights(scratch, xs, zs, count, out);
|
||||
}
|
||||
|
||||
private void surfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
|
||||
int mark = scratch.mark();
|
||||
int indexMark = scratch.indexMark();
|
||||
int[] northWest = scratch.indices(count);
|
||||
int[] northEast = scratch.indices(count);
|
||||
int[] southWest = scratch.indices(count);
|
||||
int[] southEast = scratch.indices(count);
|
||||
double[] fractionX = scratch.buffer(count);
|
||||
double[] fractionZ = scratch.buffer(count);
|
||||
scratch.clearLattice(count * 4);
|
||||
int detailed = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
double lowerX = Math.floor(xs[index] / SURFACE_GRID) * SURFACE_GRID;
|
||||
double lowerZ = Math.floor(zs[index] / SURFACE_GRID) * SURFACE_GRID;
|
||||
double dx = (xs[index] - lowerX) / SURFACE_GRID;
|
||||
double dz = (zs[index] - lowerZ) / SURFACE_GRID;
|
||||
fractionX[index] = dx;
|
||||
fractionZ[index] = dz;
|
||||
northWest[index] = scratch.lattice(lowerX, lowerZ);
|
||||
northEast[index] = dx == 0D ? -1 : scratch.lattice(lowerX + SURFACE_GRID, lowerZ);
|
||||
southWest[index] = dz == 0D ? -1 : scratch.lattice(lowerX, lowerZ + SURFACE_GRID);
|
||||
southEast[index] = dx == 0D || dz == 0D ? -1 : scratch.lattice(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID);
|
||||
if (dx != 0D || dz != 0D) {
|
||||
detailed++;
|
||||
}
|
||||
}
|
||||
int nodes = scratch.latticeCount();
|
||||
double[] lattice = scratch.buffer(nodes);
|
||||
resolveLattice(scratch, nodes, lattice);
|
||||
double[] detail = null;
|
||||
int[] detailIndex = null;
|
||||
if (surfaceDetail != 0D && detailed > 0) {
|
||||
double[] detailX = scratch.buffer(detailed);
|
||||
double[] detailZ = scratch.buffer(detailed);
|
||||
detail = scratch.buffer(detailed);
|
||||
detailIndex = scratch.indices(count);
|
||||
int cursor = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (fractionX[index] != 0D || fractionZ[index] != 0D) {
|
||||
detailX[cursor] = xs[index];
|
||||
detailZ[cursor] = zs[index];
|
||||
detailIndex[index] = cursor++;
|
||||
}
|
||||
}
|
||||
sample(scratch, detailX, detailZ, detailed, detail);
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
double dx = fractionX[index];
|
||||
double dz = fractionZ[index];
|
||||
double nw = lattice[northWest[index]];
|
||||
if (dx == 0D && dz == 0D) {
|
||||
out[index] = nw;
|
||||
continue;
|
||||
}
|
||||
double north = dx == 0D ? nw : IrisInterpolation.lerp(nw, lattice[northEast[index]], dx);
|
||||
double smooth = north;
|
||||
if (dz != 0D) {
|
||||
double sw = lattice[southWest[index]];
|
||||
double south = dx == 0D ? sw : IrisInterpolation.lerp(sw, lattice[southEast[index]], dx);
|
||||
smooth = IrisInterpolation.lerp(north, south, dz);
|
||||
}
|
||||
out[index] = surfaceDetail == 0D ? smooth : smooth + (detail[detailIndex[index]] - smooth) * surfaceDetail;
|
||||
}
|
||||
scratch.resetIndices(indexMark);
|
||||
scratch.reset(mark);
|
||||
}
|
||||
|
||||
private void resolveLattice(TerrainBatchScratch scratch, int nodes, double[] lattice) {
|
||||
IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes();
|
||||
int indexMark = scratch.indexMark();
|
||||
int mark = scratch.mark();
|
||||
int[] missing = scratch.indices(nodes);
|
||||
double[] missingX = scratch.buffer(nodes);
|
||||
double[] missingZ = scratch.buffer(nodes);
|
||||
int missingCount = 0;
|
||||
for (int node = 0; node < nodes; node++) {
|
||||
double x = scratch.latticeX(node);
|
||||
double z = scratch.latticeZ(node);
|
||||
if (x == (int) x && z == (int) z && IrisGenerator.cachedSurface(stripes, x, z, superSeed, lattice, node)) {
|
||||
continue;
|
||||
}
|
||||
missing[missingCount] = node;
|
||||
missingX[missingCount] = x;
|
||||
missingZ[missingCount++] = z;
|
||||
}
|
||||
if (missingCount > 0) {
|
||||
double[] sampled = scratch.buffer(missingCount);
|
||||
sample(scratch, missingX, missingZ, missingCount, sampled);
|
||||
for (int index = 0; index < missingCount; index++) {
|
||||
int node = missing[index];
|
||||
lattice[node] = sampled[index];
|
||||
if (missingX[index] == (int) missingX[index] && missingZ[index] == (int) missingZ[index]) {
|
||||
IrisGenerator.storeSurface(stripes, missingX[index], missingZ[index], superSeed, sampled[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
scratch.reset(mark);
|
||||
scratch.resetIndices(indexMark);
|
||||
}
|
||||
|
||||
private void sample(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
|
||||
int mark = scratch.mark();
|
||||
double[] sampleX = scratch.buffer(count);
|
||||
double[] sampleZ = scratch.buffer(count);
|
||||
double[] noise = scratch.buffer(count);
|
||||
for (int index = 0; index < count; index++) {
|
||||
sampleX[index] = (xs[index] + offsetX) / zoom;
|
||||
sampleZ[index] = (zs[index] + offsetZ) / zoom;
|
||||
}
|
||||
Arrays.fill(out, 0, count, multiplicitive ? 1D : 0D);
|
||||
for (NoiseLayerProgram layer : layers) {
|
||||
layer.evaluate(scratch, sampleX, sampleZ, count, noise);
|
||||
if (multiplicitive) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
out[index] *= noise[index];
|
||||
}
|
||||
} else {
|
||||
for (int index = 0; index < count; index++) {
|
||||
out[index] += noise[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
double value = multiplicitive ? out[index] * opacity : (out[index] / totalOpacity) * opacity;
|
||||
out[index] = Double.isNaN(value) ? 0D : value;
|
||||
}
|
||||
if (cliffs != null) {
|
||||
cliffs.evaluate(scratch, sampleX, sampleZ, count, noise);
|
||||
for (int index = 0; index < count; index++) {
|
||||
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, noise[index]);
|
||||
out[index] = (Math.round((out[index] * 255D) / height) * height) / 255D;
|
||||
}
|
||||
}
|
||||
if (cellCracks) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
out[index] = generator.cell(xs[index], zs[index], out[index], 0D);
|
||||
}
|
||||
}
|
||||
scratch.reset(mark);
|
||||
}
|
||||
}
|
||||
@@ -41,10 +41,7 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.DoubleBinaryOperator;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@@ -211,7 +208,7 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
if (surfaceDetail == 1D) {
|
||||
return sampleHeight(rx, rz, superSeed, data, engine);
|
||||
}
|
||||
return surfaceHeight(rx, rz, superSeed, data, engine, null);
|
||||
return surfaceHeight(rx, rz, superSeed, data, engine);
|
||||
}
|
||||
|
||||
public boolean supportsBatch() {
|
||||
@@ -227,90 +224,84 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
return !hasCliffs() || cliffHeightGenerator.supportsBatch();
|
||||
}
|
||||
|
||||
public void getHeights(double[] xs, double[] zs, long superSeed, double[] target, int count) {
|
||||
Objects.checkFromIndexSize(0, count, xs.length);
|
||||
Objects.checkFromIndexSize(0, count, zs.length);
|
||||
Objects.checkFromIndexSize(0, count, target.length);
|
||||
if (count > 4096) {
|
||||
throw new IllegalArgumentException("Generator batch exceeds 4096 samples");
|
||||
public GeneratorProgram compileProgram(long superSeed) {
|
||||
if (!Double.isFinite(surfaceDetail) || surfaceDetail < 0D || surfaceDetail > 1D) {
|
||||
throw new IllegalArgumentException("Generator surfaceDetail must be finite and between 0 and 1");
|
||||
}
|
||||
if (count < 8 || !supportsBatch()) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
target[index] = getHeight(xs[index], zs[index], superSeed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (composite.isEmpty()) {
|
||||
Arrays.fill(target, 0, count, 0D);
|
||||
return;
|
||||
if (!supportsBatch()) {
|
||||
throw new IllegalStateException("Generator " + getLoadKey() + " cannot be compiled into a batch program");
|
||||
}
|
||||
IrisData data = getLoader();
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
DoubleBinaryOperator prepared = prepareHeight(superSeed, data, engine);
|
||||
for (int index = 0; index < count; index++) {
|
||||
target[index] = surfaceDetail == 1D ? prepared.applyAsDouble(xs[index], zs[index])
|
||||
: surfaceHeight(xs[index], zs[index], superSeed, data, engine, prepared);
|
||||
}
|
||||
}
|
||||
|
||||
private DoubleBinaryOperator prepareHeight(long superSeed, IrisData data, Engine engine) {
|
||||
int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ);
|
||||
DoubleBinaryOperator[] layers = new DoubleBinaryOperator[composite.size()];
|
||||
NoiseLayerProgram[] layers = new NoiseLayerProgram[composite.size()];
|
||||
double totalOpacity = 0D;
|
||||
for (int index = 0; index < layers.length; index++) {
|
||||
IrisNoiseGenerator layer = composite.get(index);
|
||||
layers[index] = layer.prepareBatch(getSeed() + superSeed + hc, data, engine);
|
||||
layers[index] = layer.compileProgram(getSeed() + superSeed + hc, data, engine);
|
||||
if (!multiplicitive) {
|
||||
totalOpacity += layer.getOpacity();
|
||||
}
|
||||
}
|
||||
double sumOpacity = totalOpacity;
|
||||
double cliffSeed = (double) (superSeed + 294596 + hc) - 34857;
|
||||
DoubleBinaryOperator cliffs = hasCliffs() ? cliffHeightGenerator.prepareBatch(
|
||||
(long) (getSeed() + cliffSeed + hc), data, engine) : null;
|
||||
return (rx, rz) -> {
|
||||
double sampleX = (rx + offsetX) / zoom;
|
||||
double sampleZ = (rz + offsetZ) / zoom;
|
||||
double h = multiplicitive ? 1D : 0D;
|
||||
for (DoubleBinaryOperator layer : layers) {
|
||||
double sampled = layer.applyAsDouble(sampleX, sampleZ);
|
||||
h = multiplicitive ? h * sampled : h + sampled;
|
||||
}
|
||||
double value = multiplicitive ? h * opacity : (h / sumOpacity) * opacity;
|
||||
if (Double.isNaN(value)) {
|
||||
value = 0D;
|
||||
}
|
||||
if (cliffs != null) {
|
||||
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax,
|
||||
cliffs.applyAsDouble(sampleX, sampleZ));
|
||||
value = (Math.round((value * 255D) / height) * height) / 255D;
|
||||
}
|
||||
return hasCellCracks() ? cell(rx, rz, value, superSeed + 48622 + hc) : value;
|
||||
};
|
||||
NoiseLayerProgram cliffs = hasCliffs()
|
||||
? cliffHeightGenerator.compileProgram((long) (getSeed() + cliffSeed + hc), data, engine)
|
||||
: null;
|
||||
return new GeneratorProgram(this, superSeed, layers, totalOpacity, cliffs);
|
||||
}
|
||||
|
||||
private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine,
|
||||
DoubleBinaryOperator prepared) {
|
||||
SurfaceCacheStripe[] surfaceStripes() {
|
||||
IrisData data = getLoader();
|
||||
return surfaceCache(data, data == null ? null : data.getEngine()).stripes();
|
||||
}
|
||||
|
||||
static boolean cachedSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double[] target, int index) {
|
||||
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||
long mixed = surfaceMix(key);
|
||||
SurfaceSample cached = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)]
|
||||
.get(key, superSeed, (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1));
|
||||
if (cached == null) {
|
||||
return false;
|
||||
}
|
||||
target[index] = cached.height();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void storeSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double height) {
|
||||
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||
long mixed = surfaceMix(key);
|
||||
stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)]
|
||||
.put((int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1), new SurfaceSample(key, superSeed, height));
|
||||
}
|
||||
|
||||
private static long surfaceMix(long key) {
|
||||
long mixed = key ^ (key >>> 33);
|
||||
mixed *= 0xff51afd7ed558ccdL;
|
||||
mixed ^= mixed >>> 33;
|
||||
return mixed;
|
||||
}
|
||||
|
||||
private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
|
||||
SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes();
|
||||
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
|
||||
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
|
||||
double dx = (rx - lowerX) / SURFACE_GRID;
|
||||
double dz = (rz - lowerZ) / SURFACE_GRID;
|
||||
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine, prepared);
|
||||
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine);
|
||||
if (dx == 0D && dz == 0D) {
|
||||
return northWest;
|
||||
}
|
||||
double north = dx == 0D ? northWest : IrisInterpolation.lerp(northWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine, prepared), dx);
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine), dx);
|
||||
double smooth = north;
|
||||
if (dz != 0D) {
|
||||
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared);
|
||||
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine);
|
||||
double south = dx == 0D ? southWest : IrisInterpolation.lerp(southWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared), dx);
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine), dx);
|
||||
smooth = IrisInterpolation.lerp(north, south, dz);
|
||||
}
|
||||
return surfaceDetail == 0D ? smooth
|
||||
: smooth + ((prepared == null ? sampleHeight(rx, rz, superSeed, data, engine) : prepared.applyAsDouble(rx, rz)) - smooth) * surfaceDetail;
|
||||
: smooth + (sampleHeight(rx, rz, superSeed, data, engine) - smooth) * surfaceDetail;
|
||||
}
|
||||
|
||||
private double sampleHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
|
||||
@@ -350,21 +341,19 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
return v;
|
||||
}
|
||||
|
||||
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine, DoubleBinaryOperator prepared) {
|
||||
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine) {
|
||||
if (x != (int) x || z != (int) z) {
|
||||
return prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z);
|
||||
return sampleHeight(x, z, superSeed, data, engine);
|
||||
}
|
||||
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||
long mixed = key ^ (key >>> 33);
|
||||
mixed *= 0xff51afd7ed558ccdL;
|
||||
mixed ^= mixed >>> 33;
|
||||
long mixed = surfaceMix(key);
|
||||
SurfaceCacheStripe stripe = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)];
|
||||
int slot = (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1);
|
||||
SurfaceSample cached = stripe.get(key, superSeed, slot);
|
||||
if (cached != null) {
|
||||
return cached.height();
|
||||
}
|
||||
double height = prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z);
|
||||
double height = sampleHeight(x, z, superSeed, data, engine);
|
||||
stripe.put(slot, new SurfaceSample(key, superSeed, height));
|
||||
return height;
|
||||
}
|
||||
@@ -457,7 +446,7 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
private record SurfaceSample(long key, long seed, double height) {
|
||||
}
|
||||
|
||||
private static final class SurfaceCacheStripe {
|
||||
static final class SurfaceCacheStripe {
|
||||
private final AtomicReferenceArray<SurfaceSample> entries = new AtomicReferenceArray<>(SURFACE_CACHE_STRIPE_SIZE);
|
||||
|
||||
private SurfaceSample get(long key, long seed, int slot) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import art.arcane.volmlib.util.noise.NoiseGenerator;
|
||||
import art.arcane.volmlib.util.noise.OffsetNoiseGenerator;
|
||||
import art.arcane.volmlib.util.noise.PerlinNoise;
|
||||
import art.arcane.volmlib.util.noise.SimplexNoise;
|
||||
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@@ -47,8 +48,8 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.DoubleBinaryOperator;
|
||||
|
||||
@Snippet("generator")
|
||||
@Accessors(chain = true)
|
||||
@@ -198,44 +199,29 @@ public class IrisNoiseGenerator {
|
||||
return true;
|
||||
}
|
||||
|
||||
DoubleBinaryOperator prepareBatch(long superSeed, IrisData data, Engine engine) {
|
||||
NoiseLayerProgram compileProgram(long superSeed, IrisData data, Engine engine) {
|
||||
if (!enabled) {
|
||||
double value = offsetY;
|
||||
return (x, z) -> value;
|
||||
return NoiseLayerProgram.disabled(this);
|
||||
}
|
||||
if (hasDeepFracture()) {
|
||||
FractureSamples samples = new FractureSamples();
|
||||
if (samples.prepare(this, superSeed, data, engine)) {
|
||||
return samples::sample;
|
||||
int enabledChildren = 0;
|
||||
for (IrisNoiseGenerator child : fracture) {
|
||||
if (child.isEnabled()) {
|
||||
enabledChildren++;
|
||||
}
|
||||
}
|
||||
DoubleBinaryOperator[] children = new DoubleBinaryOperator[fracture.size()];
|
||||
double[] offsets = new double[fracture.size()];
|
||||
NoiseLayerProgram[] children = new NoiseLayerProgram[enabledChildren];
|
||||
double[] offsets = new double[enabledChildren];
|
||||
int salt = 33;
|
||||
for (int index = 0; index < children.length; index++) {
|
||||
IrisNoiseGenerator child = fracture.get(index);
|
||||
int compiled = 0;
|
||||
for (IrisNoiseGenerator child : fracture) {
|
||||
if (child.isEnabled()) {
|
||||
children[index] = child.prepareBatch(superSeed + seed + salt, data, engine);
|
||||
offsets[index] = child.getOpacity() / 2D;
|
||||
children[compiled] = child.compileProgram(superSeed + seed + salt, data, engine);
|
||||
offsets[compiled++] = child.getOpacity() / 2D;
|
||||
}
|
||||
salt += 819;
|
||||
}
|
||||
CNG generator = getGenerator(superSeed, data, engine);
|
||||
return (x, z) -> samplePrepared(generator, children, offsets, x, z);
|
||||
}
|
||||
|
||||
private double samplePrepared(CNG generator, DoubleBinaryOperator[] children, double[] offsets,
|
||||
double xv, double zv) {
|
||||
double x = xv;
|
||||
double z = zv;
|
||||
for (int index = 0; index < children.length; index++) {
|
||||
DoubleBinaryOperator child = children[index];
|
||||
if (child != null) {
|
||||
x += child.applyAsDouble(xv, zv) - offsets[index];
|
||||
z += child.applyAsDouble(zv, xv) - offsets[index];
|
||||
}
|
||||
}
|
||||
return sample(generator, x, z);
|
||||
CNG generator = Objects.requireNonNull(getGenerator(superSeed, data, engine), "Noise layer generator");
|
||||
return NoiseLayerProgram.of(this, NoiseProgram.compile2D(generator), children, offsets);
|
||||
}
|
||||
|
||||
public KList<IrisNoiseGenerator> getAllComposites() {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
|
||||
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class NoiseLayerProgram {
|
||||
private final boolean enabled;
|
||||
private final double offsetY;
|
||||
private final NoiseProgram program;
|
||||
private final double zoom;
|
||||
private final double opacity;
|
||||
private final double offsetX;
|
||||
private final double offsetZ;
|
||||
private final double exponent;
|
||||
private final boolean negative;
|
||||
private final boolean parametric;
|
||||
private final boolean bezier;
|
||||
private final boolean sinCentered;
|
||||
private final NoiseLayerProgram[] children;
|
||||
private final double[] childOffsets;
|
||||
|
||||
private NoiseLayerProgram(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) {
|
||||
this.enabled = program != null;
|
||||
this.offsetY = layer.getOffsetY();
|
||||
this.program = program;
|
||||
this.zoom = layer.getZoom();
|
||||
this.opacity = layer.getOpacity();
|
||||
this.offsetX = layer.getOffsetX();
|
||||
this.offsetZ = layer.getOffsetZ();
|
||||
this.exponent = layer.getExponent();
|
||||
this.negative = layer.isNegative();
|
||||
this.parametric = layer.isParametric();
|
||||
this.bezier = layer.isBezier();
|
||||
this.sinCentered = layer.isSinCentered();
|
||||
this.children = children;
|
||||
this.childOffsets = childOffsets;
|
||||
}
|
||||
|
||||
static NoiseLayerProgram disabled(IrisNoiseGenerator layer) {
|
||||
return new NoiseLayerProgram(layer, null, new NoiseLayerProgram[0], new double[0]);
|
||||
}
|
||||
|
||||
static NoiseLayerProgram of(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) {
|
||||
return new NoiseLayerProgram(layer, program, children, childOffsets);
|
||||
}
|
||||
|
||||
public void evaluate(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
|
||||
if (!enabled) {
|
||||
Arrays.fill(out, 0, count, offsetY);
|
||||
return;
|
||||
}
|
||||
int mark = scratch.mark();
|
||||
double[] sampleX = scratch.buffer(count);
|
||||
double[] sampleZ = scratch.buffer(count);
|
||||
if (children.length == 0) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
sampleX[index] = (xs[index] / zoom) + offsetX;
|
||||
sampleZ[index] = (zs[index] / zoom) + offsetZ;
|
||||
}
|
||||
} else {
|
||||
System.arraycopy(xs, 0, sampleX, 0, count);
|
||||
System.arraycopy(zs, 0, sampleZ, 0, count);
|
||||
double[] forward = scratch.buffer(count);
|
||||
double[] reverse = scratch.buffer(count);
|
||||
for (int child = 0; child < children.length; child++) {
|
||||
children[child].evaluatePair(scratch, xs, zs, count, forward, reverse);
|
||||
double offset = childOffsets[child];
|
||||
for (int index = 0; index < count; index++) {
|
||||
sampleX[index] += forward[index] - offset;
|
||||
sampleZ[index] += reverse[index] - offset;
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
sampleX[index] = (sampleX[index] / zoom) + offsetX;
|
||||
sampleZ[index] = (sampleZ[index] / zoom) + offsetZ;
|
||||
}
|
||||
}
|
||||
program.fill2D(scratch.noise(), sampleX, sampleZ, count, out);
|
||||
for (int index = 0; index < count; index++) {
|
||||
out[index] = post(out[index]);
|
||||
}
|
||||
scratch.reset(mark);
|
||||
}
|
||||
|
||||
void evaluatePair(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] forward, double[] reverse) {
|
||||
if (!enabled) {
|
||||
Arrays.fill(forward, 0, count, offsetY);
|
||||
Arrays.fill(reverse, 0, count, offsetY);
|
||||
return;
|
||||
}
|
||||
int mark = scratch.mark();
|
||||
int pairs = count * 2;
|
||||
double[] sampleX = scratch.buffer(pairs);
|
||||
double[] sampleZ = scratch.buffer(pairs);
|
||||
System.arraycopy(xs, 0, sampleX, 0, count);
|
||||
System.arraycopy(zs, 0, sampleZ, 0, count);
|
||||
System.arraycopy(zs, 0, sampleX, count, count);
|
||||
System.arraycopy(xs, 0, sampleZ, count, count);
|
||||
if (children.length > 0) {
|
||||
double[] childForward = scratch.buffer(count);
|
||||
double[] childReverse = scratch.buffer(count);
|
||||
for (int child = 0; child < children.length; child++) {
|
||||
children[child].evaluatePair(scratch, xs, zs, count, childForward, childReverse);
|
||||
double offset = childOffsets[child];
|
||||
for (int index = 0; index < count; index++) {
|
||||
sampleX[index] += childForward[index] - offset;
|
||||
sampleZ[index] += childReverse[index] - offset;
|
||||
sampleX[count + index] += childReverse[index] - offset;
|
||||
sampleZ[count + index] += childForward[index] - offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < pairs; index++) {
|
||||
sampleX[index] = (sampleX[index] / zoom) + offsetX;
|
||||
sampleZ[index] = (sampleZ[index] / zoom) + offsetZ;
|
||||
}
|
||||
double[] values = scratch.buffer(pairs);
|
||||
program.fill2D(scratch.noise(), sampleX, sampleZ, pairs, values);
|
||||
for (int index = 0; index < count; index++) {
|
||||
forward[index] = post(values[index]);
|
||||
reverse[index] = post(values[count + index]);
|
||||
}
|
||||
scratch.reset(mark);
|
||||
}
|
||||
|
||||
private double post(double raw) {
|
||||
double n = raw * opacity;
|
||||
n = negative ? (-n + opacity) : n;
|
||||
n = (exponent != 1 ? n < 0 ? -Math.pow(-n, exponent) : Math.pow(n, exponent) : n) + offsetY;
|
||||
n = parametric ? IrisInterpolation.parametric(n, 2D) : n;
|
||||
n = bezier ? IrisInterpolation.bezier(n) : n;
|
||||
n = sinCentered ? IrisInterpolation.sinCenter(n) : n;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.volmlib.util.noise.program.NoiseScratch;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class TerrainBatchScratch {
|
||||
private static final int MINIMUM_BUFFER = 256;
|
||||
|
||||
private final NoiseScratch noise = new NoiseScratch();
|
||||
private double[][] buffers = new double[32][];
|
||||
private int[][] indexBuffers = new int[8][];
|
||||
private int top;
|
||||
private int indexTop;
|
||||
private long[] latticeX = new long[64];
|
||||
private long[] latticeZ = new long[64];
|
||||
private int[] latticeSlots = new int[64];
|
||||
private int latticeCapacity = 64;
|
||||
private int latticeCount;
|
||||
|
||||
public NoiseScratch noise() {
|
||||
return noise;
|
||||
}
|
||||
|
||||
public int mark() {
|
||||
return top;
|
||||
}
|
||||
|
||||
public int indexMark() {
|
||||
return indexTop;
|
||||
}
|
||||
|
||||
public void reset(int mark) {
|
||||
top = mark;
|
||||
}
|
||||
|
||||
public void resetIndices(int mark) {
|
||||
indexTop = mark;
|
||||
}
|
||||
|
||||
public double[] buffer(int length) {
|
||||
if (top == buffers.length) {
|
||||
buffers = Arrays.copyOf(buffers, top * 2);
|
||||
}
|
||||
double[] buffer = buffers[top];
|
||||
if (buffer == null || buffer.length < length) {
|
||||
buffer = new double[Math.max(length, MINIMUM_BUFFER)];
|
||||
buffers[top] = buffer;
|
||||
}
|
||||
top++;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public int[] indices(int length) {
|
||||
if (indexTop == indexBuffers.length) {
|
||||
indexBuffers = Arrays.copyOf(indexBuffers, indexTop * 2);
|
||||
}
|
||||
int[] buffer = indexBuffers[indexTop];
|
||||
if (buffer == null || buffer.length < length) {
|
||||
buffer = new int[Math.max(length, MINIMUM_BUFFER)];
|
||||
indexBuffers[indexTop] = buffer;
|
||||
}
|
||||
indexTop++;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void clearLattice(int maximumEntries) {
|
||||
int capacity = Integer.highestOneBit(Math.max(16, maximumEntries * 2) - 1) << 1;
|
||||
if (latticeSlots.length < capacity) {
|
||||
latticeSlots = new int[capacity];
|
||||
} else {
|
||||
Arrays.fill(latticeSlots, 0, capacity, 0);
|
||||
}
|
||||
latticeCapacity = capacity;
|
||||
latticeCount = 0;
|
||||
}
|
||||
|
||||
int lattice(double x, double z) {
|
||||
long xBits = Double.doubleToRawLongBits(x);
|
||||
long zBits = Double.doubleToRawLongBits(z);
|
||||
long mixed = xBits * 0x9E3779B97F4A7C15L ^ Long.rotateLeft(zBits * 0xC2B2AE3D27D4EB4FL, 31);
|
||||
mixed ^= mixed >>> 29;
|
||||
int mask = latticeCapacity - 1;
|
||||
int slot = (int) mixed & mask;
|
||||
while (true) {
|
||||
int stored = latticeSlots[slot];
|
||||
if (stored == 0) {
|
||||
if (latticeCount == latticeX.length) {
|
||||
latticeX = Arrays.copyOf(latticeX, latticeCount * 2);
|
||||
latticeZ = Arrays.copyOf(latticeZ, latticeCount * 2);
|
||||
}
|
||||
latticeX[latticeCount] = xBits;
|
||||
latticeZ[latticeCount] = zBits;
|
||||
latticeSlots[slot] = ++latticeCount;
|
||||
return latticeCount - 1;
|
||||
}
|
||||
if (latticeX[stored - 1] == xBits && latticeZ[stored - 1] == zBits) {
|
||||
return stored - 1;
|
||||
}
|
||||
slot = (slot + 1) & mask;
|
||||
}
|
||||
}
|
||||
|
||||
int latticeCount() {
|
||||
return latticeCount;
|
||||
}
|
||||
|
||||
double latticeX(int index) {
|
||||
return Double.longBitsToDouble(latticeX[index]);
|
||||
}
|
||||
|
||||
double latticeZ(int index) {
|
||||
return Double.longBitsToDouble(latticeZ[index]);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,19 @@
|
||||
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.generation.bake.BakePlane;
|
||||
import art.arcane.iris.generation.bake.BakeRegistry;
|
||||
import art.arcane.iris.generation.bake.ChunkBakeCache;
|
||||
import art.arcane.iris.generation.bake.ChunkPlanes;
|
||||
import art.arcane.iris.generation.bake.ColumnBake;
|
||||
import art.arcane.iris.generation.bake.PlaneSource;
|
||||
import art.arcane.iris.generation.bake.SparseHeightMemo;
|
||||
import art.arcane.iris.generation.bake.TerrainProgram;
|
||||
import art.arcane.iris.generation.noise.TerrainBatchScratch;
|
||||
import art.arcane.iris.generation.simd.SimdSupport;
|
||||
import art.arcane.iris.generation.stream.BakedDoubleStream;
|
||||
import art.arcane.iris.generation.stream.BakedHeightStream;
|
||||
import art.arcane.iris.generation.stream.BakedStream;
|
||||
import art.arcane.iris.generation.stream.GenerationStreams;
|
||||
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
|
||||
import art.arcane.iris.generation.stream.CachedStream2D;
|
||||
@@ -118,14 +131,15 @@ import java.nio.file.Path;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "sparseBaseHeights", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "sparseBaseHeights", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
||||
public class IrisComplex implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
||||
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
|
||||
private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288;
|
||||
private static final int SPARSE_BASE_HEIGHT_CAPACITY = 1 << 17;
|
||||
/** One million corners: about 20 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
|
||||
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
|
||||
/** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */
|
||||
@@ -141,6 +155,7 @@ public class IrisComplex implements DataProvider {
|
||||
IrisGenerator::getLoadKey,
|
||||
Comparator.nullsFirst(Comparator.naturalOrder())
|
||||
);
|
||||
private static final ThreadLocal<TerrainBatchScratch> BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new);
|
||||
private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = {
|
||||
InferredType.LAND,
|
||||
InferredType.CAVE,
|
||||
@@ -153,6 +168,35 @@ public class IrisComplex implements DataProvider {
|
||||
private final int biomeBoundsSamplingStep;
|
||||
private transient volatile SharedCornerBounds sharedCornerBounds;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient volatile TerrainProgram terrainProgram;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ChunkBakeCache bakeCache;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient SparseHeightMemo sparseBaseHeights = new SparseHeightMemo(SPARSE_BASE_HEIGHT_CAPACITY);
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient BakeRegistry<IrisBiome> biomeRegistry = new BakeRegistry<>();
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient BakeRegistry<IrisRegion> regionRegistry = new BakeRegistry<>();
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient BakeRegistry<NativeBlockState> blockRegistry = new BakeRegistry<>();
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<IrisRegion> regionSource;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<IrisBiome> baseBiomeSource;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<IrisBiome> caveSource;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<NativeBlockState> rockSource;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<NativeBlockState> fluidSource;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<Double> naturalHeightSource;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<IrisBiome> naturalTrueBiomeSource;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<IrisBiome> trueBiomeSource;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient IrisInterpolator[] frozenInterpolators;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient IrisGenerator[][] frozenGenerators;
|
||||
@@ -213,9 +257,6 @@ public class IrisComplex implements DataProvider {
|
||||
private ProceduralStream<Double> unblendedNaturalHeightStream;
|
||||
private final TransitionDisplacementField transitionDisplacement;
|
||||
private ProceduralStream<Double> heightStream;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private CachedDoubleStream2D cachedHeightStream;
|
||||
private ProceduralStream<Integer> roundedHeighteightStream;
|
||||
private ProceduralStream<Double> maxHeightStream;
|
||||
private ProceduralStream<Double> overlayStream;
|
||||
@@ -365,54 +406,58 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
return value;
|
||||
});
|
||||
rockStream = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream()
|
||||
bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize);
|
||||
rockSource = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream()
|
||||
.select(engine.getDimension().getRockPalette().getBlockData(data));
|
||||
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
|
||||
rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource);
|
||||
fluidSource = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
|
||||
.select(engine.getDimension().getFluidPalette().getBlockData(data));
|
||||
fluidStream = new BakedStream<>(bakeCache, BakePlane.FLUID, fluidSource);
|
||||
hydrologyFluidStreams = createHydrologyFluidStreams(engine.getDimension().getHydrology());
|
||||
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
||||
.zoom(engine.getDimension().getRegionZoom());
|
||||
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||
regionStream = focusedRegions != null ? focusedRegions
|
||||
: GenerationStreams.cache2D(RegionSelection.select(new RegionSelection.Options(
|
||||
engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)),
|
||||
"regionStream", engine, cacheSize);
|
||||
caveBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
regionSource = focusedRegions != null ? focusedRegions
|
||||
: RegionSelection.select(new RegionSelection.Options(
|
||||
engine.getDimension(), data, regionStyleStream, imageMapRuntime, null));
|
||||
regionStream = focusedRegions != null ? focusedRegions : new BakedStream<>(bakeCache, BakePlane.REGION, regionSource);
|
||||
caveSource = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.CAVE))
|
||||
.convertAware2D(ProceduralStream::get), "caveBiomeStream", engine, cacheSize);
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource);
|
||||
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
|
||||
landBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
landBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.LAND))
|
||||
.convertAware2D(ProceduralStream::get), "landBiomeStream", engine, cacheSize);
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||
seaBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
seaBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.SEA))
|
||||
.convertAware2D(ProceduralStream::get), "seaBiomeStream", engine, cacheSize);
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||
shoreBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
shoreBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.SHORE))
|
||||
.convertAware2D(ProceduralStream::get), "shoreBiomeStream", engine, cacheSize);
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||
bridgeStream = focusedBiomes != null ? focusedBiomes.convert(IrisBiome::getInferredType) :
|
||||
GenerationStreams.cache2D(ContinentGenerator.create(engine.getDimension(), rng, getData()).stream()
|
||||
.convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND), "bridgeStream", engine, cacheSize);
|
||||
ContinentGenerator.create(engine.getDimension(), rng, getData()).stream()
|
||||
.convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND);
|
||||
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes :
|
||||
GenerationStreams.cache2D(bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
||||
.convertAware2D(this::implode), "baseBiomeStream", engine, cacheSize);
|
||||
baseBiomeStream = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME)
|
||||
? proceduralBaseBiomeStream : GenerationStreams.cache2D(ProceduralStream.of((x, z) -> {
|
||||
bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
||||
.convertAware2D(this::implode);
|
||||
baseBiomeSource = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME)
|
||||
? proceduralBaseBiomeStream : ProceduralStream.of((x, z) -> {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null ? proceduralBaseBiomeStream.get(x, z) : mapped;
|
||||
}, (x, y, z) -> {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null ? proceduralBaseBiomeStream.get(x, y, z) : mapped;
|
||||
}, proceduralBaseBiomeStream), "imageMappedBaseBiomeStream", engine, cacheSize);
|
||||
}, proceduralBaseBiomeStream);
|
||||
baseBiomeStream = focusedBiomes != null ? focusedBiomes : new BakedStream<>(bakeCache, BakePlane.BASE_BIOME, baseBiomeSource);
|
||||
proceduralTerrainHeight = new ProceduralTerrainHeightSampler(
|
||||
(x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()),
|
||||
engine.getDimension().getTerrainSamplingStep());
|
||||
baseTerrainHeightStream = GenerationStreams.cache2DDouble(
|
||||
ProceduralStream.ofDouble(this::sampleUnblendedNaturalTerrainHeight),
|
||||
"baseTerrainHeightStream", engine, cacheSize);
|
||||
baseTerrainHeightStream = new BakedDoubleStream(bakeCache, BakePlane.BASE_HEIGHT,
|
||||
ProceduralStream.ofDouble(this::sparseBaseTerrainHeight));
|
||||
boolean terrain3DEnabled = false;
|
||||
for (IrisBiome biome : generatorBiomes) {
|
||||
if (biome.getTerrain3D() != null) {
|
||||
@@ -427,18 +472,20 @@ public class IrisComplex implements DataProvider {
|
||||
new Terrain3DRuntime.Options(engine.getSeedManager().getTerrain(), engine.getHeight(),
|
||||
fluidHeight, data, terrain3DEnabled, TERRAIN_COLUMN_CACHE_SIZE));
|
||||
unblendedNaturalHeightStream = terrain3DEnabled
|
||||
? GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::sampleTerrain3DHeight), "unblendedNaturalHeightStream", engine, cacheSize)
|
||||
? ProceduralStream.ofDouble(this::sampleTerrain3DHeight)
|
||||
: baseTerrainHeightStream;
|
||||
transitionDisplacement = transitionGenerationPlan == null ? null : new TransitionDisplacementField(
|
||||
transitionGenerationPlan, new TransitionDisplacementField.Sources(
|
||||
(x, z) -> unblendedNaturalHeightStream.getDouble(x, z), engine.getHeight()));
|
||||
naturalHeightStream = transitionDisplacement == null ? unblendedNaturalHeightStream
|
||||
: GenerationStreams.cache2DDouble(ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height(
|
||||
blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z))),
|
||||
"naturalHeightStream", engine, cacheSize);
|
||||
naturalTrueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "naturalTrueBiomeStream-focus", engine, cacheSize)
|
||||
: GenerationStreams.cache2D(ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome,
|
||||
Interpolated.of(value -> 0D, value -> null)), "naturalTrueBiomeStream", engine, cacheSize);
|
||||
naturalHeightSource = transitionDisplacement == null
|
||||
? unblendedNaturalHeightStream
|
||||
: ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height(
|
||||
blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z)));
|
||||
naturalHeightStream = new BakedDoubleStream(bakeCache, BakePlane.NATURAL_HEIGHT, naturalHeightSource);
|
||||
naturalTrueBiomeSource = focusedBiomes != null ? focusedBiomes
|
||||
: ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome,
|
||||
Interpolated.of(value -> 0D, value -> null));
|
||||
naturalTrueBiomeStream = new BakedStream<>(bakeCache, BakePlane.NATURAL_TRUE_BIOME, naturalTrueBiomeSource);
|
||||
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
|
||||
if (hydrologyActive(configuredHydrology)) {
|
||||
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
|
||||
@@ -446,9 +493,9 @@ public class IrisComplex implements DataProvider {
|
||||
engine.getHeight(),
|
||||
engine.getDimension(),
|
||||
data,
|
||||
(x, z, naturalHeight) -> sampleHydrologyNatural(engine, x, z, naturalHeight),
|
||||
(x, z, naturalHeight) -> ChunkBakeCache.sparse(() -> sampleHydrologyNatural(engine, x, z, naturalHeight)),
|
||||
new HydrologyHeightProvider(),
|
||||
(x, z) -> describeNaturalHeight(engine, x, z),
|
||||
(x, z) -> ChunkBakeCache.sparse(() -> describeNaturalHeight(engine, x, z)),
|
||||
this::sampleNaturalOcean,
|
||||
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint),
|
||||
() -> engine.getPlatformHooks().isMainThread(),
|
||||
@@ -467,8 +514,8 @@ public class IrisComplex implements DataProvider {
|
||||
Math.max(4096, cacheSize), data, banks3D));
|
||||
}
|
||||
}
|
||||
cachedHeightStream = new TerrainHeightStream(engine, cacheSize);
|
||||
heightStream = cachedHeightStream;
|
||||
heightStream = new BakedHeightStream(bakeCache, () -> terrainEngine.getPlatformHooks().isMainThread(),
|
||||
this::nonblockingTerrainHeight, this::resolveHydrologyTerrainHeight);
|
||||
roundedHeighteightStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.round();
|
||||
slopeStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
@@ -476,8 +523,9 @@ public class IrisComplex implements DataProvider {
|
||||
naturalSlopeStream = GenerationStreams.cache2DDouble(naturalHeightStream.slope(3), "naturalSlopeStream", engine, cacheSize);
|
||||
// Terrain3D column heights resolve the surface biome, so the shore band reads the pre-terrain3D slope or it recurses.
|
||||
shoreSlopeStream = terrain3DEnabled ? baseTerrainHeightStream.slope(3) : naturalSlopeStream;
|
||||
trueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "trueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(heightStream
|
||||
.convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z)), "trueBiomeStream", engine, cacheSize);
|
||||
trueBiomeSource = focusedBiomes != null ? focusedBiomes
|
||||
: heightStream.convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z));
|
||||
trueBiomeStream = new BakedStream<>(bakeCache, BakePlane.TRUE_BIOME, trueBiomeSource);
|
||||
riverDistanceStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyDistance), "riverDistanceStream", engine, cacheSize);
|
||||
riverFlowStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyFlow), "riverFlowStream", engine, cacheSize);
|
||||
riverCarveWeightStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyCarveWeight), "riverCarveWeightStream", engine, cacheSize);
|
||||
@@ -518,17 +566,11 @@ public class IrisComplex implements DataProvider {
|
||||
return engine.isStudio() && !detached ? Math.max(configuredSize, STUDIO_NOISE_CACHE_SIZE) : configuredSize;
|
||||
}
|
||||
|
||||
public synchronized void ensureTerrainNoiseCacheSize(int minimumChunks) {
|
||||
public void ensureTerrainNoiseCacheSize(int minimumChunks) {
|
||||
if (minimumChunks <= 0) {
|
||||
throw new IllegalArgumentException("Terrain noise cache size must be positive");
|
||||
}
|
||||
CachedDoubleStream2D naturalCache = (CachedDoubleStream2D) naturalHeightStream;
|
||||
if (naturalCache.getMaxSize() < minimumChunks * 256L) {
|
||||
naturalCache.setMaximumChunks(minimumChunks);
|
||||
}
|
||||
if (cachedHeightStream.getMaxSize() < minimumChunks * 256L) {
|
||||
cachedHeightStream.setMaximumChunks(minimumChunks);
|
||||
}
|
||||
bakeCache.ensureCapacity(minimumChunks);
|
||||
}
|
||||
|
||||
void expandHydrologyNoiseCaches() {
|
||||
@@ -787,96 +829,220 @@ public class IrisComplex implements DataProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void sampleBaseHeightBatch(long[] coordinates, int count, double[] target) {
|
||||
((CachedDoubleStream2D) baseTerrainHeightStream).getBatch(coordinates, count, target, this::resolveBaseHeightBatch);
|
||||
private void resolveBaseHeightBatch(long[] missing, int size, double[] values) {
|
||||
TerrainBatchScratch scratch = BATCH_SCRATCH.get();
|
||||
int mark = scratch.mark();
|
||||
int indexMark = scratch.indexMark();
|
||||
try {
|
||||
double[] xs = scratch.buffer(size);
|
||||
double[] zs = scratch.buffer(size);
|
||||
double[] lows = scratch.buffer(size);
|
||||
double[] highs = scratch.buffer(size);
|
||||
Arrays.fill(values, 0, size, 0D);
|
||||
for (int index = 0; index < size; index++) {
|
||||
xs[index] = RiverFootprint.unpackX(missing[index]);
|
||||
zs[index] = RiverFootprint.unpackZ(missing[index]);
|
||||
}
|
||||
TerrainProgram program = terrainProgram();
|
||||
for (int group = 0; group < frozenInterpolators.length; group++) {
|
||||
if (frozenGenerators[group].length == 0) {
|
||||
continue;
|
||||
}
|
||||
for (int index = 0; index < size; index++) {
|
||||
NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group,
|
||||
frozenGenerators[group], xs[index], zs[index]);
|
||||
lows[index] = bounds.min();
|
||||
highs[index] = bounds.max();
|
||||
}
|
||||
program.accumulate(group, scratch, xs, zs, size, lows, highs, values);
|
||||
}
|
||||
for (int index = 0; index < size; index++) {
|
||||
values[index] = finishBaseHeight(xs[index], zs[index], values[index]);
|
||||
}
|
||||
} finally {
|
||||
scratch.reset(mark);
|
||||
scratch.resetIndices(indexMark);
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveBaseHeightBatch(long[] missing, int size, double[] values) {
|
||||
double[] xs = new double[size];
|
||||
double[] zs = new double[size];
|
||||
double[] lows = new double[size];
|
||||
double[] highs = new double[size];
|
||||
double[] activeXs = new double[size];
|
||||
double[] activeZs = new double[size];
|
||||
double[] noise = new double[size];
|
||||
double[] groupHeights = new double[size];
|
||||
int[] active = new int[size];
|
||||
for (int index = 0; index < size; index++) {
|
||||
xs[index] = RiverFootprint.unpackX(missing[index]);
|
||||
zs[index] = RiverFootprint.unpackZ(missing[index]);
|
||||
private double[] bakeBaseHeights(int chunkX, int chunkZ) {
|
||||
double[] values = new double[256];
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
if (!supportsChunkHeightBatch()) {
|
||||
for (int index = 0; index < 256; index++) {
|
||||
values[index] = sampleUnblendedNaturalTerrainHeight(originX + (index & 15), originZ + (index >> 4));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
long seed = terrainEngine.getSeedManager().getHeight() + 239945;
|
||||
for (int group = 0; group < frozenInterpolators.length; group++) {
|
||||
IrisGenerator[] generators = frozenGenerators[group];
|
||||
if (generators.length == 0) {
|
||||
TerrainBatchScratch scratch = BATCH_SCRATCH.get();
|
||||
int mark = scratch.mark();
|
||||
int indexMark = scratch.indexMark();
|
||||
try {
|
||||
double[] xs = scratch.buffer(256);
|
||||
double[] zs = scratch.buffer(256);
|
||||
double[] lows = scratch.buffer(256);
|
||||
double[] highs = scratch.buffer(256);
|
||||
for (int index = 0; index < 256; index++) {
|
||||
xs[index] = originX + (index & 15);
|
||||
zs[index] = originZ + (index >> 4);
|
||||
}
|
||||
TerrainProgram program = terrainProgram();
|
||||
GridBoundsCache cache = gridBoundsCache.get();
|
||||
int grid = biomeBoundsSamplingStep;
|
||||
boolean corners = ChunkCorners.supports(grid);
|
||||
for (int group = 0; group < frozenInterpolators.length; group++) {
|
||||
if (frozenGenerators[group].length == 0) {
|
||||
continue;
|
||||
}
|
||||
if (corners) {
|
||||
if (!chunkCornerBounds(cache, group, grid, originX, originZ, xs, zs, lows, highs)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
for (int index = 0; index < 256; index++) {
|
||||
NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group,
|
||||
frozenGenerators[group], xs[index], zs[index]);
|
||||
lows[index] = bounds.min();
|
||||
highs[index] = bounds.max();
|
||||
}
|
||||
}
|
||||
program.accumulate(group, scratch, xs, zs, 256, lows, highs, values);
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
values[index] = finishBaseHeight(xs[index], zs[index], values[index]);
|
||||
}
|
||||
} finally {
|
||||
scratch.reset(mark);
|
||||
scratch.resetIndices(indexMark);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private boolean chunkCornerBounds(GridBoundsCache cache, int group, int grid, int originX, int originZ,
|
||||
double[] xs, double[] zs, double[] lows, double[] highs) {
|
||||
int cells = 16 / grid;
|
||||
int stride = cells + 1;
|
||||
long[] packed = new long[stride * stride];
|
||||
boolean silent = true;
|
||||
for (int row = 0; row < stride; row++) {
|
||||
for (int column = 0; column < stride; column++) {
|
||||
long corner = cornerBounds(cache, terrainEngine, frozenInterpolators[group], group,
|
||||
frozenGenerators[group], originX + column * grid, originZ + row * grid);
|
||||
packed[row * stride + column] = corner;
|
||||
silent &= silentBounds(corner);
|
||||
}
|
||||
}
|
||||
if (silent) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
double x = xs[index];
|
||||
double z = zs[index];
|
||||
int gx = ((int) Math.floor(x)) & ~(grid - 1);
|
||||
int gz = ((int) Math.floor(z)) & ~(grid - 1);
|
||||
double fx = (x - gx) / grid;
|
||||
double fz = (z - gz) / grid;
|
||||
int cornerIndex = ((gz - originZ) / grid) * stride + (gx - originX) / grid;
|
||||
long b00 = packed[cornerIndex];
|
||||
if (fx == 0D && fz == 0D) {
|
||||
lows[index] = boundsLow(b00);
|
||||
highs[index] = boundsHigh(b00);
|
||||
} else if (fz == 0D) {
|
||||
long b10 = packed[cornerIndex + 1];
|
||||
lows[index] = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b00), boundsLow(b10), fx, fz);
|
||||
highs[index] = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b00), boundsHigh(b10), fx, fz);
|
||||
} else if (fx == 0D) {
|
||||
long b01 = packed[cornerIndex + stride];
|
||||
lows[index] = biLerp(boundsLow(b00), boundsLow(b00), boundsLow(b01), boundsLow(b01), fx, fz);
|
||||
highs[index] = biLerp(boundsHigh(b00), boundsHigh(b00), boundsHigh(b01), boundsHigh(b01), fx, fz);
|
||||
} else {
|
||||
long b10 = packed[cornerIndex + 1];
|
||||
long b01 = packed[cornerIndex + stride];
|
||||
long b11 = packed[cornerIndex + stride + 1];
|
||||
lows[index] = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz);
|
||||
highs[index] = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean supportsChunkHeightBatch() {
|
||||
return terrainEngine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty()
|
||||
&& (biomeBuffet == null || biomeBuffet.cells().isEmpty());
|
||||
}
|
||||
|
||||
private double finishBaseHeight(double x, double z, double interpolated) {
|
||||
double height = Math.max(Math.min(interpolated + fluidHeight + overlayStream.get(x, z), terrainEngine.getHeight()), 0);
|
||||
return imageMapRuntime.sampleTerrainHeight(x, z, height);
|
||||
}
|
||||
|
||||
private TerrainProgram terrainProgram() {
|
||||
TerrainProgram program = terrainProgram;
|
||||
if (program == null) {
|
||||
synchronized (this) {
|
||||
program = terrainProgram;
|
||||
if (program == null) {
|
||||
program = TerrainProgram.compile(frozenGenerators, terrainEngine.getSeedManager().getHeight() + 239945);
|
||||
terrainProgram = program;
|
||||
}
|
||||
}
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
private double sparseBaseTerrainHeight(double x, double z) {
|
||||
int blockX = (int) x;
|
||||
int blockZ = (int) z;
|
||||
if (x != blockX || z != blockZ) {
|
||||
return sampleUnblendedNaturalTerrainHeight(x, z);
|
||||
}
|
||||
double memoized = sparseBaseHeights.get(blockX, blockZ);
|
||||
if (!Double.isNaN(memoized)) {
|
||||
return memoized;
|
||||
}
|
||||
double height = sampleUnblendedNaturalTerrainHeight(x, z);
|
||||
sparseBaseHeights.put(blockX, blockZ, height);
|
||||
return height;
|
||||
}
|
||||
|
||||
private void sampleNaturalBatch(long[] coordinates, int count, double[] heights) {
|
||||
long[] required = new long[count];
|
||||
int requiredCount = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
int x = RiverFootprint.unpackX(coordinates[index]);
|
||||
int z = RiverFootprint.unpackZ(coordinates[index]);
|
||||
if (publishedPlane(BakePlane.NATURAL_HEIGHT, x, z) != null
|
||||
|| terrain3D != null && !terrain3D.requiresBaseHeight(x, z)
|
||||
|| publishedPlane(BakePlane.BASE_HEIGHT, x, z) != null
|
||||
|| !Double.isNaN(sparseBaseHeights.get(x, z))) {
|
||||
continue;
|
||||
}
|
||||
int activeCount = 0;
|
||||
Arrays.fill(groupHeights, 0D);
|
||||
for (int index = 0; index < size; index++) {
|
||||
NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group,
|
||||
generators, xs[index], zs[index]);
|
||||
lows[index] = bounds.min();
|
||||
highs[index] = bounds.max();
|
||||
if (!Double.isFinite(lows[index]) || !Double.isFinite(highs[index])) {
|
||||
averageGeneratorHeights(generators, lows[index], highs[index], xs[index], zs[index], seed);
|
||||
}
|
||||
if (lows[index] != highs[index]) {
|
||||
active[activeCount] = index;
|
||||
activeXs[activeCount] = xs[index];
|
||||
activeZs[activeCount++] = zs[index];
|
||||
} else {
|
||||
for (int generator = 0; generator < generators.length; generator++) {
|
||||
groupHeights[index] += lows[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (IrisGenerator generator : generators) {
|
||||
if (activeCount == 0) {
|
||||
break;
|
||||
}
|
||||
generator.getHeights(activeXs, activeZs, seed, noise, activeCount);
|
||||
for (int sample = 0; sample < activeCount; sample++) {
|
||||
int index = active[sample];
|
||||
if (!Double.isFinite(noise[sample])) {
|
||||
throw new IllegalStateException("Terrain generator noise was not finite at " + xs[index] + "," + zs[index]
|
||||
+ " (generator=" + generator.getLoadKey() + ", noise=" + noise[sample]
|
||||
+ ", bounds=" + lows[index] + ".." + highs[index] + ").");
|
||||
}
|
||||
groupHeights[index] += M.lerp(lows[index], highs[index], noise[sample]);
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < size; index++) {
|
||||
values[index] += groupHeights[index] / generators.length;
|
||||
required[requiredCount++] = coordinates[index];
|
||||
}
|
||||
if (requiredCount > 0) {
|
||||
double[] baseHeights = new double[requiredCount];
|
||||
resolveBaseHeightBatch(required, requiredCount, baseHeights);
|
||||
for (int index = 0; index < requiredCount; index++) {
|
||||
sparseBaseHeights.put(RiverFootprint.unpackX(required[index]), RiverFootprint.unpackZ(required[index]), baseHeights[index]);
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < size; index++) {
|
||||
double height = Math.max(Math.min(values[index] + fluidHeight + overlayStream.get(xs[index], zs[index]),
|
||||
terrainEngine.getHeight()), 0);
|
||||
values[index] = imageMapRuntime.sampleTerrainHeight(xs[index], zs[index], height);
|
||||
BakedDoubleStream naturalHeights = (BakedDoubleStream) naturalHeightStream;
|
||||
for (int index = 0; index < count; index++) {
|
||||
heights[index] = naturalHeights.sparse(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index]));
|
||||
}
|
||||
}
|
||||
|
||||
private void sampleUnblendedHeightBatch(long[] coordinates, int count, double[] target) {
|
||||
if (terrain3D == null) {
|
||||
sampleBaseHeightBatch(coordinates, count, target);
|
||||
return;
|
||||
}
|
||||
((CachedDoubleStream2D) unblendedNaturalHeightStream).getBatch(coordinates, count, target, (missing, size, values) -> {
|
||||
long[] required = new long[size];
|
||||
int requiredCount = 0;
|
||||
for (int index = 0; index < size; index++) {
|
||||
if (terrain3D.requiresBaseHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index]))) {
|
||||
required[requiredCount++] = missing[index];
|
||||
}
|
||||
}
|
||||
if (requiredCount > 0) {
|
||||
sampleBaseHeightBatch(required, requiredCount, new double[requiredCount]);
|
||||
}
|
||||
for (int index = 0; index < size; index++) {
|
||||
values[index] = sampleTerrain3DHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index]));
|
||||
}
|
||||
});
|
||||
@SuppressWarnings("unchecked")
|
||||
private IrisBiome sparseBaseBiome(double x, double z) {
|
||||
return baseBiomeStream instanceof BakedStream<?> baked
|
||||
? ((BakedStream<IrisBiome>) baked).sparse((int) x, (int) z)
|
||||
: baseBiomeStream.get(x, z);
|
||||
}
|
||||
|
||||
private Object publishedPlane(BakePlane plane, int blockX, int blockZ) {
|
||||
ChunkPlanes planes = bakeCache.planesIfPresent(blockX >> 4, blockZ >> 4);
|
||||
return planes == null ? null : planes.published(plane);
|
||||
}
|
||||
|
||||
private final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider {
|
||||
@@ -884,7 +1050,7 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
@Override
|
||||
public double sample(int blockX, int blockZ) {
|
||||
return naturalHeightStream.getDouble(blockX, blockZ);
|
||||
return ((BakedDoubleStream) naturalHeightStream).sparse(blockX, blockZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -896,20 +1062,165 @@ public class IrisComplex implements DataProvider {
|
||||
public void sampleBatch(long[] coordinates, int count, double[] heights) {
|
||||
if (!batch) {
|
||||
IrisHydrologyNaturalHeightProvider.super.sampleBatch(coordinates, count, heights);
|
||||
} else if (transitionDisplacement == null) {
|
||||
sampleUnblendedHeightBatch(coordinates, count, heights);
|
||||
} else {
|
||||
((CachedDoubleStream2D) naturalHeightStream).getBatch(coordinates, count, heights, (missing, size, values) -> {
|
||||
sampleUnblendedHeightBatch(missing, size, values);
|
||||
for (int index = 0; index < size; index++) {
|
||||
values[index] = transitionDisplacement.height(RiverFootprint.unpackX(missing[index]),
|
||||
RiverFootprint.unpackZ(missing[index]), values[index]);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
ChunkBakeCache.enterSparse();
|
||||
try {
|
||||
sampleNaturalBatch(coordinates, count, heights);
|
||||
} finally {
|
||||
ChunkBakeCache.exitSparse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class BakeSource implements PlaneSource {
|
||||
@Override
|
||||
public Object compute(BakePlane plane, int chunkX, int chunkZ) {
|
||||
return switch (plane) {
|
||||
case REGION -> objectPlane(regionSource, chunkX, chunkZ);
|
||||
case BASE_BIOME -> objectPlane(baseBiomeSource, chunkX, chunkZ);
|
||||
case BASE_HEIGHT -> bakeBaseHeights(chunkX, chunkZ);
|
||||
case NATURAL_HEIGHT -> doublePlane(naturalHeightSource, chunkX, chunkZ);
|
||||
case NATURAL_TRUE_BIOME -> objectPlane(naturalTrueBiomeSource, chunkX, chunkZ);
|
||||
case HEIGHT -> heightPlane(chunkX, chunkZ);
|
||||
case TRUE_BIOME -> objectPlane(trueBiomeSource, chunkX, chunkZ);
|
||||
case ROCK -> objectPlane(rockSource, chunkX, chunkZ);
|
||||
case FLUID -> objectPlane(fluidSource, chunkX, chunkZ);
|
||||
case CAVE_BIOME -> objectPlane(caveSource, chunkX, chunkZ);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnBake assemble(ChunkPlanes planes) {
|
||||
return assembleColumnBake(planes.chunkX(), planes.chunkZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mainThread() {
|
||||
return terrainEngine.getPlatformHooks().isMainThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean generationContext() {
|
||||
IrisContext context = IrisContext.get();
|
||||
if (context == null || context.getEngine() != terrainEngine) {
|
||||
return false;
|
||||
}
|
||||
ChunkContext chunk = context.getChunkContext();
|
||||
return chunk != null && chunk.getComplex() == IrisComplex.this;
|
||||
}
|
||||
|
||||
private Object[] objectPlane(ProceduralStream<?> source, int chunkX, int chunkZ) {
|
||||
Object[] values = new Object[256];
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
for (int index = 0; index < 256; index++) {
|
||||
values[index] = source.get(originX + (index & 15), originZ + (index >> 4));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private double[] heightPlane(int chunkX, int chunkZ) {
|
||||
double[] values = new double[256];
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
boolean main = terrainEngine.getPlatformHooks().isMainThread();
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = originX + (index & 15);
|
||||
int z = originZ + (index >> 4);
|
||||
values[index] = main ? nonblockingTerrainHeight(x, z) : resolveHydrologyTerrainHeight(x, z);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private double[] doublePlane(ProceduralStream<Double> source, int chunkX, int chunkZ) {
|
||||
double[] values = new double[256];
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
for (int index = 0; index < 256; index++) {
|
||||
values[index] = source.getDouble(originX + (index & 15), originZ + (index >> 4));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
public Object bakePlane(BakePlane plane, int chunkX, int chunkZ) {
|
||||
return bakeCache.bake(plane, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
public ColumnBake columnBake(int chunkX, int chunkZ) {
|
||||
return bakeCache.get(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
public ColumnBake columnBakeIfPresent(int chunkX, int chunkZ) {
|
||||
return bakeCache.getIfPresent(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
public IrisBiome biome(int id) {
|
||||
return id < 0 ? null : biomeRegistry.value(id);
|
||||
}
|
||||
|
||||
public short biomeId(IrisBiome biome) {
|
||||
return biome == null ? -1 : biomeRegistry.id(biome);
|
||||
}
|
||||
|
||||
public IrisRegion region(int id) {
|
||||
return id < 0 ? null : regionRegistry.value(id);
|
||||
}
|
||||
|
||||
public short regionId(IrisRegion region) {
|
||||
return region == null ? -1 : regionRegistry.id(region);
|
||||
}
|
||||
|
||||
public NativeBlockState blockState(int id) {
|
||||
return id < 0 ? null : blockRegistry.value(id);
|
||||
}
|
||||
|
||||
private ColumnBake assembleColumnBake(int chunkX, int chunkZ) {
|
||||
double[] height = (double[]) bakeCache.bake(BakePlane.HEIGHT, chunkX, chunkZ);
|
||||
int[] rounded = new int[256];
|
||||
SimdSupport.kernels().roundToInt(height, rounded, 256);
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
float[] fluidSurface = new float[256];
|
||||
Terrain3DColumn[] terrainColumns = new Terrain3DColumn[256];
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = originX + (index & 15);
|
||||
int z = originZ + (index >> 4);
|
||||
fluidSurface[index] = (float) riverWaterSurfaceStream.getDouble(x, z);
|
||||
terrainColumns[index] = terrainColumn(x, z);
|
||||
}
|
||||
double[] apron = new double[ColumnBake.APRON_WIDTH * ColumnBake.APRON_WIDTH];
|
||||
for (int row = 0; row < ColumnBake.APRON_WIDTH; row++) {
|
||||
for (int column = 0; column < ColumnBake.APRON_WIDTH; column++) {
|
||||
apron[row * ColumnBake.APRON_WIDTH + column] = heightStream.getDouble(originX - 1 + column, originZ - 1 + row);
|
||||
}
|
||||
}
|
||||
return new ColumnBake(new ColumnBake.Planes(chunkX, chunkZ,
|
||||
ids((Object[]) bakeCache.bake(BakePlane.REGION, chunkX, chunkZ), regionRegistry),
|
||||
ids((Object[]) bakeCache.bake(BakePlane.BASE_BIOME, chunkX, chunkZ), biomeRegistry),
|
||||
ids((Object[]) bakeCache.bake(BakePlane.TRUE_BIOME, chunkX, chunkZ), biomeRegistry),
|
||||
ids((Object[]) bakeCache.bake(BakePlane.CAVE_BIOME, chunkX, chunkZ), biomeRegistry),
|
||||
(double[]) bakeCache.bake(BakePlane.BASE_HEIGHT, chunkX, chunkZ),
|
||||
(double[]) bakeCache.bake(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ),
|
||||
height,
|
||||
rounded,
|
||||
fluidSurface,
|
||||
ids((Object[]) bakeCache.bake(BakePlane.ROCK, chunkX, chunkZ), blockRegistry),
|
||||
ids((Object[]) bakeCache.bake(BakePlane.FLUID, chunkX, chunkZ), blockRegistry),
|
||||
terrainColumns,
|
||||
apron));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> short[] ids(Object[] values, BakeRegistry<T> registry) {
|
||||
short[] ids = new short[values.length];
|
||||
for (int index = 0; index < values.length; index++) {
|
||||
ids[index] = values[index] == null ? -1 : registry.id((T) values[index]);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
|
||||
double proceduralHeight = proceduralTerrainHeight.sample(x, z);
|
||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
|
||||
@@ -2495,7 +2806,8 @@ public class IrisComplex implements DataProvider {
|
||||
this.engine = engine;
|
||||
this.generators = generators;
|
||||
this.cachedBounds = cachedBounds;
|
||||
this.integerCoordinates = complex.baseBiomeStream instanceof CachedStream2D<?>;
|
||||
this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream<?>
|
||||
|| complex.baseBiomeStream instanceof CachedStream2D<?>;
|
||||
this.inUse = true;
|
||||
if (sampleSource.get() != complex.baseBiomeStream) {
|
||||
sampleCache.clear();
|
||||
@@ -2522,7 +2834,7 @@ public class IrisComplex implements DataProvider {
|
||||
double sampleZ = integerCoordinates ? (int) zz : zz;
|
||||
IrisBiome bx = sampleCache.get(sampleX, sampleZ);
|
||||
if (bx == null) {
|
||||
bx = complex.baseBiomeStream.get(xx, zz);
|
||||
bx = complex.sparseBaseBiome(xx, zz);
|
||||
sampleCache.put(sampleX, sampleZ, bx);
|
||||
}
|
||||
|
||||
@@ -2592,62 +2904,6 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private final class TerrainHeightStream extends CachedDoubleStream2D {
|
||||
private TerrainHeightStream(Engine engine, int cacheSize) {
|
||||
super("heightStream", engine, ProceduralStream.ofDouble(IrisComplex.this::resolveHydrologyTerrainHeight), cacheSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double z) {
|
||||
return terrainEngine.getPlatformHooks().isMainThread()
|
||||
? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(double x, double y, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double y, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
|
||||
boolean mainThread = terrainEngine.getPlatformHooks().isMainThread();
|
||||
if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) {
|
||||
super.fillChunkDoubles(worldX, worldZ, target);
|
||||
return;
|
||||
}
|
||||
if (target == null || target.length != 256) {
|
||||
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = worldX + (index & 15);
|
||||
int z = worldZ + (index >> 4);
|
||||
target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
||||
boolean mainThread = terrainEngine.getPlatformHooks().isMainThread();
|
||||
if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) {
|
||||
super.fillChunkRaw(worldX, worldZ, target);
|
||||
return;
|
||||
}
|
||||
if (target == null || target.length != 256) {
|
||||
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = worldX + (index & 15);
|
||||
int z = worldZ + (index >> 4);
|
||||
target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class ChildSelectionPlan {
|
||||
private final IrisRaritySelection<IrisBiome> selection;
|
||||
|
||||
@@ -2698,6 +2954,8 @@ public class IrisComplex implements DataProvider {
|
||||
hydrologyRuntime.close();
|
||||
}
|
||||
releaseHydrologyNoiseCaches();
|
||||
bakeCache.clear();
|
||||
sparseBaseHeights.clear();
|
||||
if (transitionDisplacement != null) {
|
||||
transitionDisplacement.clear();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package art.arcane.iris.generation.stream;
|
||||
|
||||
import art.arcane.iris.generation.bake.BakePlane;
|
||||
import art.arcane.iris.generation.bake.ChunkBakeCache;
|
||||
import art.arcane.iris.generation.bake.ChunkPlanes;
|
||||
import art.arcane.volmlib.util.stream.BasicStream;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class BakedDoubleStream extends BasicStream<Double> implements ChunkFillableStream2D, ChunkFillableDoubleStream2D {
|
||||
private final ChunkBakeCache cache;
|
||||
private final BakePlane plane;
|
||||
private final ProceduralStream<Double> scalar;
|
||||
|
||||
public BakedDoubleStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream<Double> scalar) {
|
||||
super();
|
||||
this.cache = Objects.requireNonNull(cache, "Bake cache");
|
||||
this.plane = Objects.requireNonNull(plane, "Bake plane");
|
||||
this.scalar = Objects.requireNonNull(scalar, "Scalar source");
|
||||
}
|
||||
|
||||
public BakePlane plane() {
|
||||
return plane;
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> scalar() {
|
||||
return scalar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(double x, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double z) {
|
||||
int blockX = (int) x;
|
||||
int blockZ = (int) z;
|
||||
Object values = cache.read(plane, blockX >> 4, blockZ >> 4);
|
||||
return values == null ? scalar.getDouble(blockX, blockZ) : ((double[]) values)[((blockZ & 15) << 4) | (blockX & 15)];
|
||||
}
|
||||
|
||||
public double sparse(int blockX, int blockZ) {
|
||||
Object published = publishedPlane(blockX >> 4, blockZ >> 4);
|
||||
if (published != null) {
|
||||
return ((double[]) published)[((blockZ & 15) << 4) | (blockX & 15)];
|
||||
}
|
||||
ChunkBakeCache.enterSparse();
|
||||
try {
|
||||
return scalar.getDouble(blockX, blockZ);
|
||||
} finally {
|
||||
ChunkBakeCache.exitSparse();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(double x, double y, double z) {
|
||||
return scalar.get(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double y, double z) {
|
||||
return scalar.getDouble(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toDouble(Double t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double fromDouble(double d) {
|
||||
return d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
|
||||
if (target == null || target.length != 256) {
|
||||
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
||||
}
|
||||
int chunkX = worldX >> 4;
|
||||
int chunkZ = worldZ >> 4;
|
||||
Object values = cache.bakingAllowed() ? cache.bake(plane, chunkX, chunkZ) : publishedPlane(chunkX, chunkZ);
|
||||
if (values != null) {
|
||||
System.arraycopy(values, 0, target, 0, 256);
|
||||
return;
|
||||
}
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
for (int index = 0; index < 256; index++) {
|
||||
target[index] = scalar.getDouble(originX + (index & 15), originZ + (index >> 4));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
||||
double[] values = new double[256];
|
||||
fillChunkDoubles(worldX, worldZ, values);
|
||||
for (int index = 0; index < 256; index++) {
|
||||
target[index] = values[index];
|
||||
}
|
||||
}
|
||||
|
||||
private Object publishedPlane(int chunkX, int chunkZ) {
|
||||
ChunkPlanes planes = cache.planesIfPresent(chunkX, chunkZ);
|
||||
return planes == null ? null : planes.published(plane);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package art.arcane.iris.generation.stream;
|
||||
|
||||
import art.arcane.iris.generation.bake.BakePlane;
|
||||
import art.arcane.iris.generation.bake.ChunkBakeCache;
|
||||
import art.arcane.iris.generation.bake.ChunkPlanes;
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
import art.arcane.volmlib.util.stream.BasicStream;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
public final class BakedHeightStream extends BasicStream<Double> implements ChunkFillableStream2D, ChunkFillableDoubleStream2D {
|
||||
private final ChunkBakeCache cache;
|
||||
private final BooleanSupplier mainThread;
|
||||
private final NoiseProvider nonblocking;
|
||||
private final NoiseProvider resolved;
|
||||
|
||||
public BakedHeightStream(ChunkBakeCache cache, BooleanSupplier mainThread, NoiseProvider nonblocking, NoiseProvider resolved) {
|
||||
super();
|
||||
this.cache = Objects.requireNonNull(cache, "Bake cache");
|
||||
this.mainThread = Objects.requireNonNull(mainThread, "Main thread check");
|
||||
this.nonblocking = Objects.requireNonNull(nonblocking, "Nonblocking height");
|
||||
this.resolved = Objects.requireNonNull(resolved, "Resolved height");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(double x, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double z) {
|
||||
if (mainThread.getAsBoolean()) {
|
||||
return nonblocking.noise(x, z);
|
||||
}
|
||||
int blockX = (int) x;
|
||||
int blockZ = (int) z;
|
||||
Object values = cache.read(BakePlane.HEIGHT, blockX >> 4, blockZ >> 4);
|
||||
return values == null ? resolved.noise(blockX, blockZ) : ((double[]) values)[((blockZ & 15) << 4) | (blockX & 15)];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(double x, double y, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double y, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toDouble(Double t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double fromDouble(double d) {
|
||||
return d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
|
||||
if (target == null || target.length != 256) {
|
||||
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
||||
}
|
||||
boolean main = mainThread.getAsBoolean();
|
||||
if (!main && (worldX & 15) == 0 && (worldZ & 15) == 0) {
|
||||
Object values = ChunkBakeCache.inSparseScope() ? published(worldX >> 4, worldZ >> 4)
|
||||
: cache.bake(BakePlane.HEIGHT, worldX >> 4, worldZ >> 4);
|
||||
if (values != null) {
|
||||
System.arraycopy(values, 0, target, 0, 256);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = worldX + (index & 15);
|
||||
int z = worldZ + (index >> 4);
|
||||
target[index] = main ? nonblocking.noise(x, z) : column(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
||||
double[] values = new double[256];
|
||||
fillChunkDoubles(worldX, worldZ, values);
|
||||
for (int index = 0; index < 256; index++) {
|
||||
target[index] = values[index];
|
||||
}
|
||||
}
|
||||
|
||||
private double column(int x, int z) {
|
||||
Object values = published(x >> 4, z >> 4);
|
||||
return values == null ? resolved.noise(x, z) : ((double[]) values)[((z & 15) << 4) | (x & 15)];
|
||||
}
|
||||
|
||||
private Object published(int chunkX, int chunkZ) {
|
||||
ChunkPlanes planes = cache.planesIfPresent(chunkX, chunkZ);
|
||||
return planes == null ? null : planes.published(BakePlane.HEIGHT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package art.arcane.iris.generation.stream;
|
||||
|
||||
import art.arcane.iris.generation.bake.BakePlane;
|
||||
import art.arcane.iris.generation.bake.ChunkBakeCache;
|
||||
import art.arcane.iris.generation.bake.ChunkPlanes;
|
||||
import art.arcane.volmlib.util.stream.BasicStream;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class BakedStream<T> extends BasicStream<T> implements ChunkFillableStream2D {
|
||||
private final ChunkBakeCache cache;
|
||||
private final BakePlane plane;
|
||||
private final ProceduralStream<T> scalar;
|
||||
|
||||
public BakedStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream<T> scalar) {
|
||||
super();
|
||||
this.cache = Objects.requireNonNull(cache, "Bake cache");
|
||||
this.plane = Objects.requireNonNull(plane, "Bake plane");
|
||||
this.scalar = Objects.requireNonNull(scalar, "Scalar source");
|
||||
}
|
||||
|
||||
public BakePlane plane() {
|
||||
return plane;
|
||||
}
|
||||
|
||||
public ProceduralStream<T> scalar() {
|
||||
return scalar;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public T get(double x, double z) {
|
||||
int blockX = (int) x;
|
||||
int blockZ = (int) z;
|
||||
Object values = cache.read(plane, blockX >> 4, blockZ >> 4);
|
||||
return values == null ? scalar.get(blockX, blockZ) : (T) ((Object[]) values)[((blockZ & 15) << 4) | (blockX & 15)];
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public T sparse(int blockX, int blockZ) {
|
||||
Object published = publishedPlane(blockX >> 4, blockZ >> 4);
|
||||
if (published != null) {
|
||||
return (T) ((Object[]) published)[((blockZ & 15) << 4) | (blockX & 15)];
|
||||
}
|
||||
ChunkBakeCache.enterSparse();
|
||||
try {
|
||||
return scalar.get(blockX, blockZ);
|
||||
} finally {
|
||||
ChunkBakeCache.exitSparse();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get(double x, double y, double z) {
|
||||
return scalar.get(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toDouble(T t) {
|
||||
return scalar.toDouble(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T fromDouble(double d) {
|
||||
return scalar.fromDouble(d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
||||
int chunkX = worldX >> 4;
|
||||
int chunkZ = worldZ >> 4;
|
||||
Object values = cache.bakingAllowed() ? cache.bake(plane, chunkX, chunkZ) : publishedPlane(chunkX, chunkZ);
|
||||
if (values != null) {
|
||||
System.arraycopy(values, 0, target, 0, 256);
|
||||
return;
|
||||
}
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
for (int index = 0; index < 256; index++) {
|
||||
target[index] = scalar.get(originX + (index & 15), originZ + (index >> 4));
|
||||
}
|
||||
}
|
||||
|
||||
private Object publishedPlane(int chunkX, int chunkZ) {
|
||||
ChunkPlanes planes = cache.planesIfPresent(chunkX, chunkZ);
|
||||
return planes == null ? null : planes.published(plane);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.SplittableRandom;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class GeneratorProgramParityTest {
|
||||
private static final NoiseStyle[] STYLES = {
|
||||
NoiseStyle.SIMPLEX, NoiseStyle.PERLIN, NoiseStyle.IRIS, NoiseStyle.IRIS_HALF, NoiseStyle.FRACTAL_SMOKE,
|
||||
NoiseStyle.FRACTAL_WATER, NoiseStyle.CLOVER, NoiseStyle.CELLULAR, NoiseStyle.NOWHERE,
|
||||
NoiseStyle.CELLULAR_IRIS_DOUBLE, NoiseStyle.SIMPLEX_VASCULAR, NoiseStyle.FLAT, NoiseStyle.STATIC,
|
||||
NoiseStyle.GLOB_IRIS_THICK, NoiseStyle.FRACTAL_BILLOW_PERLIN, NoiseStyle.PERLIN_IRIS
|
||||
};
|
||||
|
||||
@Test
|
||||
public void programHeightsMatchScalarHeightsBitForBit() {
|
||||
SplittableRandom random = new SplittableRandom(0x5EED1E55L);
|
||||
TerrainBatchScratch scratch = new TerrainBatchScratch();
|
||||
for (int generatorIndex = 0; generatorIndex < 160; generatorIndex++) {
|
||||
long shapeSeed = random.nextLong();
|
||||
IrisGenerator scalar = generator(new SplittableRandom(shapeSeed));
|
||||
IrisGenerator batch = generator(new SplittableRandom(shapeSeed));
|
||||
long superSeed = random.nextLong();
|
||||
GeneratorProgram program = batch.compileProgram(superSeed);
|
||||
int count = 1 + random.nextInt(600);
|
||||
double[] xs = new double[count];
|
||||
double[] zs = new double[count];
|
||||
double[] out = new double[count];
|
||||
fillCoordinates(random, xs, zs, count);
|
||||
program.heights(scratch, xs, zs, count, out);
|
||||
for (int index = 0; index < count; index++) {
|
||||
assertEquals("generator " + generatorIndex + " point " + xs[index] + "," + zs[index],
|
||||
Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)),
|
||||
Double.doubleToRawLongBits(out[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkGridsMatchScalarHeightsBitForBit() {
|
||||
SplittableRandom random = new SplittableRandom(0xC0FFEEL);
|
||||
TerrainBatchScratch scratch = new TerrainBatchScratch();
|
||||
double[] xs = new double[256];
|
||||
double[] zs = new double[256];
|
||||
double[] out = new double[256];
|
||||
for (int generatorIndex = 0; generatorIndex < 60; generatorIndex++) {
|
||||
long shapeSeed = random.nextLong();
|
||||
IrisGenerator scalar = generator(new SplittableRandom(shapeSeed));
|
||||
IrisGenerator batch = generator(new SplittableRandom(shapeSeed));
|
||||
long superSeed = random.nextLong();
|
||||
GeneratorProgram program = batch.compileProgram(superSeed);
|
||||
for (int chunk = 0; chunk < 4; chunk++) {
|
||||
int originX = random.nextInt(-200_000, 200_000) << 4;
|
||||
int originZ = random.nextInt(-200_000, 200_000) << 4;
|
||||
for (int index = 0; index < 256; index++) {
|
||||
xs[index] = originX + (index & 15);
|
||||
zs[index] = originZ + (index >> 4);
|
||||
}
|
||||
program.heights(scratch, xs, zs, 256, out);
|
||||
for (int index = 0; index < 256; index++) {
|
||||
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)),
|
||||
Double.doubleToRawLongBits(out[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillCoordinates(SplittableRandom random, double[] xs, double[] zs, int count) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
xs[index] = coordinate(random);
|
||||
zs[index] = random.nextInt(5) == 0 ? xs[index] : coordinate(random);
|
||||
}
|
||||
}
|
||||
|
||||
private static double coordinate(SplittableRandom random) {
|
||||
return switch (random.nextInt(6)) {
|
||||
case 0 -> random.nextInt(-4096, 4096);
|
||||
case 1 -> random.nextInt(-30_000_000, 30_000_000);
|
||||
case 2 -> random.nextInt(-512, 512) + random.nextDouble();
|
||||
case 3 -> random.nextInt(-64, 64) * 6;
|
||||
case 4 -> random.nextDouble(-30_000_000D, 30_000_000D);
|
||||
default -> random.nextInt(-16, 16) * 0.25D;
|
||||
};
|
||||
}
|
||||
|
||||
private static IrisGenerator generator(SplittableRandom random) {
|
||||
int layers = 1 + random.nextInt(3);
|
||||
KList<IrisNoiseGenerator> composite = new KList<>();
|
||||
for (int layer = 0; layer < layers; layer++) {
|
||||
composite.add(layer(random, 0));
|
||||
}
|
||||
double[] details = {0D, 0.25D, 0.5D, 1D};
|
||||
IrisGenerator generator = new IrisGenerator().setComposite(composite)
|
||||
.setSurfaceDetail(details[random.nextInt(details.length)])
|
||||
.setMultiplicitive(random.nextInt(4) == 0)
|
||||
.setSeed(random.nextLong(-5000L, 5000L))
|
||||
.setZoom(0.2D + random.nextDouble() * 3D)
|
||||
.setOffsetX(random.nextInt(-40, 40) * 0.25D)
|
||||
.setOffsetZ(random.nextInt(-40, 40) * 0.25D)
|
||||
.setOpacity(0.1D + random.nextDouble());
|
||||
if (random.nextInt(3) == 0) {
|
||||
generator.setCliffHeightMin(random.nextDouble() * 4D).setCliffHeightMax(4D + random.nextDouble() * 12D)
|
||||
.setCliffHeightGenerator(layer(random, 2));
|
||||
}
|
||||
if (random.nextInt(5) == 0) {
|
||||
generator.setCellFractureHeight(random.nextDouble()).setCellFractureZoom(0.5D + random.nextDouble());
|
||||
}
|
||||
return generator;
|
||||
}
|
||||
|
||||
private static IrisNoiseGenerator layer(SplittableRandom random, int depth) {
|
||||
IrisNoiseGenerator layer = new IrisNoiseGenerator()
|
||||
.setStyle(STYLES[random.nextInt(STYLES.length)].style())
|
||||
.setSeed(random.nextLong(-100_000L, 100_000L))
|
||||
.setZoom(0.3D + random.nextDouble() * 4D)
|
||||
.setOpacity(0.05D + random.nextDouble())
|
||||
.setOffsetX(random.nextInt(-20, 20) * 0.5D)
|
||||
.setOffsetZ(random.nextInt(-20, 20) * 0.5D)
|
||||
.setOffsetY(random.nextInt(4) == 0 ? random.nextDouble() * 0.1D : 0D)
|
||||
.setNegative(random.nextInt(4) == 0)
|
||||
.setExponent(random.nextInt(4) == 0 ? 0.5D + random.nextDouble() : 1D)
|
||||
.setParametric(random.nextInt(6) == 0)
|
||||
.setBezier(random.nextInt(6) == 0)
|
||||
.setSinCentered(random.nextInt(8) == 0)
|
||||
.setOctaves(random.nextInt(5) == 0 ? 2 : 1)
|
||||
.setEnabled(depth == 0 || random.nextInt(8) != 0);
|
||||
if (depth < 4 && random.nextInt(3) != 0) {
|
||||
int children = random.nextInt(4) == 0 ? 2 + random.nextInt(2) : 1;
|
||||
KList<IrisNoiseGenerator> fracture = new KList<>();
|
||||
for (int child = 0; child < children; child++) {
|
||||
fracture.add(layer(random, depth + 1));
|
||||
}
|
||||
layer.setFracture(fracture);
|
||||
}
|
||||
return layer;
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,6 @@ package art.arcane.iris.generation.noise;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
@@ -34,7 +31,7 @@ public class IrisGeneratorBatchTest {
|
||||
assertTrue(batch.supportsBatch());
|
||||
double[] sampled = new double[XS.length];
|
||||
for (long seed : seeds) {
|
||||
batch.getHeights(XS, ZS, seed, sampled, sampled.length);
|
||||
batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, sampled.length, sampled);
|
||||
for (int index = 0; index < sampled.length; index++) {
|
||||
assertEquals(style + " detail=" + detail + " seed=" + seed + " index=" + index,
|
||||
Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
|
||||
@@ -54,7 +51,7 @@ public class IrisGeneratorBatchTest {
|
||||
IrisGenerator batch = generator(style, detail, false).setCliffHeightMax(0D).setCellFractureHeight(0D);
|
||||
double[] values = new double[XS.length];
|
||||
for (long seed : new long[]{-1337L, Long.MAX_VALUE - 37L}) {
|
||||
batch.getHeights(XS, ZS, seed, values, XS.length);
|
||||
batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, XS.length, values);
|
||||
for (int index = 0; index < XS.length; index++) {
|
||||
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
|
||||
Double.doubleToRawLongBits(values[index]));
|
||||
@@ -65,13 +62,15 @@ public class IrisGeneratorBatchTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesLayerContextOncePerBatch() {
|
||||
public void resolvesLayerContextOncePerCompile() {
|
||||
IrisNoiseGenerator layer = spy(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()));
|
||||
IrisGenerator generator = new IrisGenerator().setComposite(new KList<>(layer));
|
||||
double[] sampled = new double[XS.length];
|
||||
generator.getHeights(XS, ZS, 99L, sampled, XS.length);
|
||||
GeneratorProgram program = generator.compileProgram(99L);
|
||||
verify(layer, times(1)).getGenerator(anyLong(), isNull(), isNull());
|
||||
clearInvocations(layer);
|
||||
program.heights(new TerrainBatchScratch(), XS, ZS, XS.length, sampled);
|
||||
verify(layer, times(0)).getGenerator(anyLong(), isNull(), isNull());
|
||||
for (int index = 0; index < XS.length; index++) {
|
||||
generator.getHeight(XS[index], ZS[index], 99L);
|
||||
}
|
||||
@@ -79,24 +78,15 @@ public class IrisGeneratorBatchTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customGeneratorFallbackKeepsCoordinateOrder() {
|
||||
List<Double> sampled = new ArrayList<>();
|
||||
public void customGeneratorsCannotCompile() {
|
||||
IrisGenerator generator = new IrisGenerator() {
|
||||
@Override
|
||||
public double getHeight(double x, double z, long seed) {
|
||||
sampled.add(x);
|
||||
return sampled.size();
|
||||
return x;
|
||||
}
|
||||
};
|
||||
assertFalse(generator.supportsBatch());
|
||||
double[] values = new double[XS.length];
|
||||
generator.getHeights(XS, ZS, 0L, values, values.length);
|
||||
for (int index = 0; index < values.length; index++) {
|
||||
assertEquals(Double.valueOf(XS[index]), sampled.get(index));
|
||||
assertEquals(index + 1D, values[index], 0D);
|
||||
}
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> generator.getHeights(new double[4097], new double[4097], 0L, new double[4097], 4097));
|
||||
assertThrows(IllegalStateException.class, () -> generator.compileProgram(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+3
-8
@@ -44,6 +44,7 @@ import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.atMost;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -89,16 +90,10 @@ public class IrisComplexHeightStreamTest {
|
||||
try (Fixture fixture = new Fixture(pack())) {
|
||||
IrisBiome mapped = new IrisBiome();
|
||||
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(mapped);
|
||||
MeteredCache bridge = (MeteredCache) fixture.complex.getBridgeStream();
|
||||
MeteredCache regions = (MeteredCache) fixture.complex.getRegionStream();
|
||||
MeteredCache heights = (MeteredCache) fixture.complex.getNaturalHeightStream();
|
||||
assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16, 32));
|
||||
assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16.75, 10, 32.5));
|
||||
assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16, 32));
|
||||
assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16.75, 10, 32.5));
|
||||
assertEquals(0L, bridge.getSize());
|
||||
assertEquals(0L, regions.getSize());
|
||||
assertEquals(0L, heights.getSize());
|
||||
assertNull(fixture.complex.getSharedCornerBounds());
|
||||
|
||||
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(null);
|
||||
@@ -116,7 +111,7 @@ public class IrisComplexHeightStreamTest {
|
||||
int[] rounded = new int[256];
|
||||
clearInvocations(fixture.hooks);
|
||||
cache.fillRounded(rounded);
|
||||
verify(fixture.hooks, times(1)).isMainThread();
|
||||
verify(fixture.hooks, atMost(2)).isMainThread();
|
||||
assertEquals(256, samples.get());
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = index & 15;
|
||||
@@ -127,7 +122,7 @@ public class IrisComplexHeightStreamTest {
|
||||
}
|
||||
clearInvocations(fixture.hooks);
|
||||
cache.fill();
|
||||
verify(fixture.hooks, times(1)).isMainThread();
|
||||
verify(fixture.hooks, atMost(2)).isMainThread();
|
||||
assertEquals(256, samples.get());
|
||||
}
|
||||
}
|
||||
|
||||
+10
-9
@@ -2,6 +2,9 @@ package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
|
||||
import art.arcane.iris.generation.bake.ChunkBakeCache;
|
||||
import art.arcane.iris.generation.bake.PlaneSource;
|
||||
import art.arcane.iris.generation.bake.SparseHeightMemo;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyPlanner;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
|
||||
@@ -48,17 +51,14 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
try (Fixture fixture = new Fixture(1_024)) {
|
||||
ProceduralStream<Double> wrapper = fixture.complex.getRawHeightStream();
|
||||
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
|
||||
assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D);
|
||||
|
||||
PregenPerformanceProfile.apply(fixture.engine);
|
||||
|
||||
assertEquals(4_096L * 256L, fixture.height.getMaxSize());
|
||||
assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize());
|
||||
assertTrue(fixture.bakes.capacity() >= 4_096);
|
||||
assertEquals(1_024L * 256L, fixture.biomes.getMaxSize());
|
||||
assertSame(wrapper, fixture.complex.getRawHeightStream());
|
||||
assertEquals(47D, wrapper.getDouble(18, 5), 0D);
|
||||
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
|
||||
assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D);
|
||||
} finally {
|
||||
PregenPerformanceProfile.restore();
|
||||
IrisSettings.settings = previousSettings;
|
||||
@@ -75,7 +75,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
|
||||
|
||||
fixture.assertCapacities(32_768);
|
||||
assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize());
|
||||
assertTrue(fixture.bakes.capacity() >= 4_096);
|
||||
assertEquals(reservedBytes, budget.reservedBytes());
|
||||
fixture.complex.close();
|
||||
assertEquals(0L, budget.reservedBytes());
|
||||
@@ -88,7 +88,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
|
||||
|
||||
fixture.assertCapacities(65_536);
|
||||
assertEquals(65_536L * 256L, fixture.rawHeight.getMaxSize());
|
||||
assertTrue(fixture.bakes.capacity() >= 65_536);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
private final CachedStream2D<IrisRegion> regions;
|
||||
private final CachedStream2D<InferredType> bridge;
|
||||
private final CachedDoubleStream2D height;
|
||||
private final CachedDoubleStream2D rawHeight;
|
||||
private final ChunkBakeCache bakes;
|
||||
|
||||
private Fixture(int capacity) throws Exception {
|
||||
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
|
||||
@@ -305,10 +305,11 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
regions = objectCache(capacity);
|
||||
bridge = objectCache(capacity);
|
||||
height = heightCache(capacity);
|
||||
rawHeight = heightCache(capacity);
|
||||
bakes = new ChunkBakeCache(mock(PlaneSource.class), capacity);
|
||||
complex.setNaturalHeightStream(height);
|
||||
complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 47D));
|
||||
setField(complex, "cachedHeightStream", rawHeight);
|
||||
setField(complex, "bakeCache", bakes);
|
||||
setField(complex, "sparseBaseHeights", new SparseHeightMemo(16));
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
complex.setBaseBiomeStream(biomes);
|
||||
complex.setRegionStream(regions);
|
||||
|
||||
Reference in new issue
Block a user