Compiled scalar group heights

- Layer and generator programs sample single points through NoiseProgram.sample2D
- Scalar interpolator group means use the compiled programs when the seed matches
- Single-point program heights are property-tested against the generator path
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 23:57:11 -04:00
1 parent fa996a2e24
commit a25233edbc
5 files changed
+180 -2

No files matched your search

@@ -34,6 +34,41 @@ public final class TerrainProgram {
return new TerrainProgram(generators, programs, heightSeed);
}
public long heightSeed() {
return heightSeed;
}
public double average(int group, TerrainBatchScratch scratch, double low, double high, double x, double z) {
IrisGenerator[] members = generators[group];
if (members.length == 0) {
return 0D;
}
if (!Double.isFinite(low) || !Double.isFinite(high)) {
throw new IllegalStateException("Terrain generator bounds were not finite at " + x + "," + z
+ " (generator=" + members[0].getLoadKey() + ", bounds=" + low + ".." + high + ").");
}
double height = 0D;
if (low == high) {
for (int member = 0; member < members.length; member++) {
height += low;
}
return height / members.length;
}
GeneratorEvaluations.scalar(members.length);
GeneratorProgram[] compiled = programs[group];
for (int member = 0; member < members.length; member++) {
GeneratorProgram program = compiled[member];
double noise = program != null ? program.height(scratch, x, z) : members[member].getHeight(x, z, heightSeed);
if (!Double.isFinite(noise)) {
throw new IllegalStateException("Terrain generator noise was not finite at " + x + "," + z
+ " (generator=" + members[member].getLoadKey() + ", noise=" + noise
+ ", bounds=" + low + ".." + high + ").");
}
height += M.lerp(low, high, noise);
}
return height / members.length;
}
public int groups() {
return generators.length;
}
@@ -58,6 +58,70 @@ public final class GeneratorProgram {
surfaceHeights(scratch, xs, zs, count, out);
}
public double height(TerrainBatchScratch scratch, double x, double z) {
if (layers.length == 0) {
return 0D;
}
if (surfaceDetail == 1D) {
return sample(scratch, x, z);
}
double lowerX = Math.floor(x / SURFACE_GRID) * SURFACE_GRID;
double lowerZ = Math.floor(z / SURFACE_GRID) * SURFACE_GRID;
double dx = (x - lowerX) / SURFACE_GRID;
double dz = (z - lowerZ) / SURFACE_GRID;
IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes();
double northWest = lattice(scratch, stripes, lowerX, lowerZ);
if (dx == 0D && dz == 0D) {
return northWest;
}
double north = dx == 0D ? northWest
: IrisInterpolation.lerp(northWest, lattice(scratch, stripes, lowerX + SURFACE_GRID, lowerZ), dx);
double smooth = north;
if (dz != 0D) {
double southWest = lattice(scratch, stripes, lowerX, lowerZ + SURFACE_GRID);
double south = dx == 0D ? southWest
: IrisInterpolation.lerp(southWest, lattice(scratch, stripes, lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID), dx);
smooth = IrisInterpolation.lerp(north, south, dz);
}
return surfaceDetail == 0D ? smooth : smooth + (sample(scratch, x, z) - smooth) * surfaceDetail;
}
private double lattice(TerrainBatchScratch scratch, IrisGenerator.SurfaceCacheStripe[] stripes, double x, double z) {
if (x != (int) x || z != (int) z) {
return sample(scratch, x, z);
}
int mark = scratch.mark();
double[] cached = scratch.buffer(1);
boolean hit = IrisGenerator.cachedSurface(stripes, x, z, superSeed, cached, 0);
double value = cached[0];
scratch.reset(mark);
if (hit) {
return value;
}
double height = sample(scratch, x, z);
IrisGenerator.storeSurface(stripes, x, z, superSeed, height);
return height;
}
private double sample(TerrainBatchScratch scratch, double x, double z) {
double sampleX = (x + offsetX) / zoom;
double sampleZ = (z + offsetZ) / zoom;
double value = multiplicitive ? 1D : 0D;
for (NoiseLayerProgram layer : layers) {
double noise = layer.sample(scratch, sampleX, sampleZ);
value = multiplicitive ? value * noise : value + noise;
}
value = multiplicitive ? value * opacity : (value / totalOpacity) * opacity;
if (Double.isNaN(value)) {
value = 0D;
}
if (cliffs != null) {
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, cliffs.sample(scratch, sampleX, sampleZ));
value = (Math.round((value * 255D) / height) * height) / 255D;
}
return cellCracks ? generator.cell(x, z, value, 0D) : value;
}
private void surfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
int mark = scratch.mark();
int indexMark = scratch.indexMark();
@@ -125,6 +125,53 @@ public final class NoiseLayerProgram {
scratch.reset(mark);
}
public double sample(TerrainBatchScratch scratch, double x, double z) {
if (!enabled) {
return offsetY;
}
double sampleX = x;
double sampleZ = z;
if (children.length > 0) {
int mark = scratch.mark();
double[] pair = scratch.buffer(2);
for (int child = 0; child < children.length; child++) {
children[child].samplePair(scratch, x, z, pair);
double offset = childOffsets[child];
sampleX += pair[0] - offset;
sampleZ += pair[1] - offset;
}
scratch.reset(mark);
}
return post(program.sample2D((sampleX / zoom) + offsetX, (sampleZ / zoom) + offsetZ));
}
private void samplePair(TerrainBatchScratch scratch, double x, double z, double[] out) {
if (!enabled) {
out[0] = offsetY;
out[1] = offsetY;
return;
}
double forwardX = x;
double forwardZ = z;
double reverseX = z;
double reverseZ = x;
if (children.length > 0) {
int mark = scratch.mark();
double[] pair = scratch.buffer(2);
for (int child = 0; child < children.length; child++) {
children[child].samplePair(scratch, x, z, pair);
double offset = childOffsets[child];
forwardX += pair[0] - offset;
forwardZ += pair[1] - offset;
reverseX += pair[1] - offset;
reverseZ += pair[0] - offset;
}
scratch.reset(mark);
}
out[0] = post(program.sample2D((forwardX / zoom) + offsetX, (forwardZ / zoom) + offsetZ));
out[1] = post(program.sample2D((reverseX / zoom) + offsetX, (reverseZ / zoom) + offsetZ));
}
private double post(double raw) {
double n = raw * opacity;
n = negative ? (-n + opacity) : n;
@@ -2149,7 +2149,19 @@ public class IrisComplex implements DataProvider {
double hi = sampledBounds.max();
double lo = sampledBounds.min();
return averageGeneratorHeights(generators, lo, hi, x, z, seed + 239945);
return groupHeight(interpolatorIndex, generators, lo, hi, x, z, seed + 239945);
}
private double groupHeight(int group, IrisGenerator[] generators, double low, double high, double x, double z, long heightSeed) {
TerrainProgram program = terrainProgram;
if (program == null && terrainEngine != null) {
program = terrainProgram();
}
if (program == null || program.heightSeed() != heightSeed || group >= program.groups()
|| program.groupSize(group) != generators.length || generators != frozenGenerators[group]) {
return averageGeneratorHeights(generators, low, high, x, z, heightSeed);
}
return program.average(group, BATCH_SCRATCH.get(), low, high, x, z);
}
private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) {
@@ -2349,7 +2361,7 @@ public class IrisComplex implements DataProvider {
lo = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz);
hi = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz);
}
h += averageGeneratorHeights(generators, lo, hi, x, z, heightSeed);
h += groupHeight(interpolatorIndex, generators, lo, hi, x, z, heightSeed);
}
return h;
@@ -39,6 +39,26 @@ public class GeneratorProgramParityTest {
}
}
@Test
public void singlePointProgramHeightsMatchScalarHeightsBitForBit() {
SplittableRandom random = new SplittableRandom(0xFEEDL);
TerrainBatchScratch scratch = new TerrainBatchScratch();
for (int generatorIndex = 0; generatorIndex < 160; generatorIndex++) {
long shapeSeed = random.nextLong();
IrisGenerator scalar = generator(new SplittableRandom(shapeSeed));
IrisGenerator compiled = generator(new SplittableRandom(shapeSeed));
long superSeed = random.nextLong();
GeneratorProgram program = compiled.compileProgram(superSeed);
for (int sample = 0; sample < 400; sample++) {
double x = coordinate(random);
double z = random.nextInt(5) == 0 ? x : coordinate(random);
assertEquals("generator " + generatorIndex + " point " + x + "," + z,
Double.doubleToRawLongBits(scalar.getHeight(x, z, superSeed)),
Double.doubleToRawLongBits(program.height(scratch, x, z)));
}
}
}
@Test
public void chunkGridsMatchScalarHeightsBitForBit() {
SplittableRandom random = new SplittableRandom(0xC0FFEEL);