Include ocean generation algorithm

This commit replaces calls to Minecraft's shapeChunk with a custom ocean
generation algorithm. This allows us to remove Byte Buddy as a
dependency and avoid wasteful computations.

The main change consists of adding an `OceanOracle` class which now
handles ocean generation. The relevant algorithms were mostly pretty
straightforward to implement, though the `mergeNoises` function is a bit
complex.

Due to this change, we no longer need to deal with an entire chunk
worth of data; instead we now only allocate one or two layers. The
massive overhead from calling shapeChunk through reflection has also
been eliminated.

I added another config constant, INTERPOLATE_NOISE_VERTICALLY, that
could be disabled for even more performance at the cost of accuracy. I
consider the current performance good enough, so I didn't turn it on by
default.
This commit is contained in:
kahomayo 2021-01-17 15:29:09 +01:00 committed by moulins
parent 64d6095528
commit 1692199c0e
4 changed files with 261 additions and 122 deletions

View File

@ -124,10 +124,5 @@
<scope>provided</scope>
<type>maven-plugin</type>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.10.19</version>
</dependency>
</dependencies>
</project>

View File

@ -21,6 +21,7 @@ public enum BetaClassTranslator {
.thenDeclareRequired(BetaSymbolicNames.CLASS_BIOMEGENERATOR)
.requiredMethod(BetaSymbolicNames.METHOD_BIOMEGENERATOR_GET_BIOME, "a").symbolicArray(BetaSymbolicNames.CLASS_BIOME, 1).real("int").real("int").real("int").real("int").end()
.requiredField(BetaSymbolicNames.FIELD_BIOMEGENERATOR_TEMPERATURE, "a")
.requiredField(BetaSymbolicNames.FIELD_BIOMEGENERATOR_RAINFALL, "b")
.next()
.ifDetect(BetaClassTranslator::isDimensionBase)
.thenDeclareRequired(BetaSymbolicNames.CLASS_DIMENSION_BASE)
@ -55,6 +56,7 @@ public enum BetaClassTranslator {
)
.thenDeclareRequired(BetaSymbolicNames.CLASS_PERLIN_OCTAVE_NOISE)
.requiredMethod(BetaSymbolicNames.METHOD_PERLIN_OCTAVE_NOISE_SAMPLE_3D, "a").realArray("double", 1).real("double").real("double").real("double").real("int").real("int").real("int").real("double").real("double").real("double").end()
.requiredMethod(BetaSymbolicNames.METHOD_PERLIN_OCTAVE_NOISE_SAMPLE_2D, "a").realArray("double", 1).real("int").real("int").real("int").real("int").real("double").real("double").real("double").end()
.requiredField(BetaSymbolicNames.FIELD_PERLIN_OCTAVE_NOISE_OCTAVES, "a")
.next()
.ifDetect(c -> c.searchForDouble(109.0134))
@ -64,6 +66,8 @@ public enum BetaClassTranslator {
.requiredField(BetaSymbolicNames.FIELD_UPPER_INTERPOLATION_NOISE, "k")
.requiredField(BetaSymbolicNames.FIELD_LOWER_INTERPOLATION_NOISE, "l")
.requiredField(BetaSymbolicNames.FIELD_INTERPOLATION_NOISE, "m")
.requiredField(BetaSymbolicNames.FIELD_BIOME_NOISE, "a")
.requiredField(BetaSymbolicNames.FIELD_DEPTH_NOISE, "b")
.next()
.ifDetect(c -> c.searchForDouble(6.0) && c.searchForDouble(15.0)
&& c.getNumberOfFields() == 4

View File

@ -10,26 +10,26 @@ import amidst.mojangapi.world.Dimension;
import amidst.mojangapi.world.WorldOptions;
import amidst.mojangapi.world.versionfeatures.DefaultBiomes;
import amidst.util.ChunkBasedGen;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.function.Function;
public class BetaMinecraftInterface implements MinecraftInterface {
// Values in [0, 16] progressively increase the accuracy of ocean display (16 = real ocean generation)
// Set to -1 to disable ocean generation entirely.
// Values in [0, 16] progressively increase the accuracy of ocean display, set to -1 to
// disable ocean generation entirely.
// Vanilla = 16
public static final int OCEAN_PRECISION = 4;
// If set to false, only one layer of noise is generated at Y=64 and used directly, instead of
// interpolating between two at Y=56 and Y=64. This will introduce inaccuracies, but also
// half the amount of time spent on 3D noise.
// Vanilla = true
public static final boolean INTERPOLATE_NOISE_VERTICALLY = true;
public static final RecognisedVersion LAST_COMPATIBLE_VERSION = RecognisedVersion._b1_7_3;
private final RecognisedVersion recognisedVersion;
private final SymbolicClass dimensionBaseClass;
@ -37,7 +37,6 @@ public class BetaMinecraftInterface implements MinecraftInterface {
private final SymbolicClass overworldLevelSourceClass;
private final BiomeMapping biomeMapping;
private final SymbolicClass dimensionOverworldClass;
private final Class<?> hackedNoiseClass;
private final SymbolicClass perlinNoiseClass;
private BetaMinecraftInterface(
@ -46,7 +45,6 @@ public class BetaMinecraftInterface implements MinecraftInterface {
SymbolicClass dimensionOverworldClass,
SymbolicClass worldClass,
SymbolicClass overworldLevelSourceClass,
SymbolicClass perlinOctaveNoiseClass,
SymbolicClass perlinNoiseClass,
RecognisedVersion recognisedVersion) {
this.recognisedVersion = recognisedVersion;
@ -55,7 +53,6 @@ public class BetaMinecraftInterface implements MinecraftInterface {
this.overworldLevelSourceClass = overworldLevelSourceClass;
this.perlinNoiseClass = perlinNoiseClass;
this.dimensionOverworldClass = dimensionOverworldClass;
this.hackedNoiseClass = makeInterpolationNoiseClass(perlinOctaveNoiseClass);
try {
this.biomeMapping = new BiomeMapping(biomeClass);
} catch (IllegalAccessException | InvocationTargetException e) {
@ -71,7 +68,6 @@ public class BetaMinecraftInterface implements MinecraftInterface {
stringSymbolicClassMap.get(BetaSymbolicNames.CLASS_DIMENSION_OVERWORLD),
stringSymbolicClassMap.get(BetaSymbolicNames.CLASS_WORLD),
stringSymbolicClassMap.get(BetaSymbolicNames.CLASS_OVERWORLD_LEVEL_SOURCE),
stringSymbolicClassMap.get(BetaSymbolicNames.CLASS_PERLIN_OCTAVE_NOISE),
stringSymbolicClassMap.get(BetaSymbolicNames.CLASS_PERLIN_NOISE),
recognisedVersion);
}
@ -140,20 +136,31 @@ public class BetaMinecraftInterface implements MinecraftInterface {
return overworldLevelSourceClass.callConstructor(BetaSymbolicNames.CONSTRUCTOR_OVERWORLD_LEVEL_SOURCE, world.getObject(), worldOptions.getWorldSeed().getLong());
}
private OceanOracle makeOceanOracle(SymbolicObject levelSource, int precision) throws IllegalAccessException {
return new OceanOracle(
(SymbolicObject) levelSource.getFieldValue(BetaSymbolicNames.FIELD_BIOME_NOISE),
(SymbolicObject) levelSource.getFieldValue(BetaSymbolicNames.FIELD_DEPTH_NOISE),
makeOctaveNoise((SymbolicObject) levelSource.getFieldValue(BetaSymbolicNames.FIELD_INTERPOLATION_NOISE), precision / 2),
makeOctaveNoise((SymbolicObject) levelSource.getFieldValue(BetaSymbolicNames.FIELD_UPPER_INTERPOLATION_NOISE), precision),
makeOctaveNoise((SymbolicObject) levelSource.getFieldValue(BetaSymbolicNames.FIELD_LOWER_INTERPOLATION_NOISE), precision)
);
}
private PerlinOctaveNoise makeOctaveNoise(SymbolicObject fieldValue, int precision) throws IllegalAccessException {
return PerlinOctaveNoise.fromSymbolic(fieldValue, perlinNoiseClass, precision);
}
/** Provides both biomes and oceans, at a configurable precision. */
private class OceanProvidingWorldAccessor implements MinecraftInterface.WorldAccessor {
private final SymbolicObject biomeGenerator;
private final SymbolicObject overworldLevelSource;
private final boolean generateBlocks;
// instance variable to avoid allocations
private final byte[] blocks = new byte[32768];
private final OceanOracle oceanOracle;
private final boolean generateOceans;
public OceanProvidingWorldAccessor(SymbolicObject biomeGenerator, SymbolicObject overworldLevelSource, int precision) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
this.biomeGenerator = biomeGenerator;
this.overworldLevelSource = overworldLevelSource;
this.generateBlocks = precision >= 0;
adjustNoise(overworldLevelSource, precision);
this.oceanOracle = makeOceanOracle(overworldLevelSource, precision);
this.generateOceans = precision >= 0;
}
@Override
@ -171,11 +178,10 @@ public class BetaMinecraftInterface implements MinecraftInterface {
// Generate real biomes using the BiomeGenerator
Object[] biomes = getBiomes(chunkZ, chunkX);
int[] out;
if (generateBlocks) {
// Run ShapeChunk (using our own noise generators) to determine where water is
if (generateOceans) {
double[] temperatures = getTemperatures();
fillBlocks(chunkZ, chunkX, temperatures);
out = getBlocksAtY63(blocks);
double[] rainfall = getRainfall();
out = oceanOracle.determineOceans(chunkX, chunkZ, null, temperatures, rainfall);
} else {
out = new int[256];
}
@ -190,11 +196,8 @@ public class BetaMinecraftInterface implements MinecraftInterface {
return (double[]) biomeGenerator.getFieldValue(BetaSymbolicNames.FIELD_BIOMEGENERATOR_TEMPERATURE);
}
private void fillBlocks(int chunkZ, int chunkX, double[] temperatures) throws IllegalAccessException, InvocationTargetException {
// This call has ***massive*** overhead. The overhead of Method.invoke is literally 50% of this calls execution time.
// I tried throwing MethodHandle at it and even using ByteBuddy to generate code for invoking this, but both just made it worse.
// If you are looking for ways to speed up beta oceans, fix this overhead!
overworldLevelSource.callMethod(BetaSymbolicNames.METHOD_OVERWORLD_LEVEL_SOURCE_SHAPE_CHUNK, chunkX, chunkZ, blocks, null, temperatures);
private double[] getRainfall() throws IllegalAccessException {
return (double[]) biomeGenerator.getFieldValue(BetaSymbolicNames.FIELD_BIOMEGENERATOR_RAINFALL);
}
private Object[] getBiomes(int chunkZ, int chunkX) throws IllegalAccessException, InvocationTargetException {
@ -205,9 +208,9 @@ public class BetaMinecraftInterface implements MinecraftInterface {
for (int x = 0; x < 16; ++x) {
for (int z = 0; z < 16; ++z) {
int outIdx = x + z * 16;
if (out[outIdx] == 9) {
if (out[outIdx] == OceanOracle.OCEAN) {
out[outIdx] = DefaultBiomes.ocean;
} else if (out[outIdx] == 79) {
} else if (out[outIdx] == OceanOracle.FROZEN_OCEAN) {
out[outIdx] = DefaultBiomes.coldOcean;
} else {
out[outIdx] = biomeMapping.getBiomeInt(biomes[z + x * 16]);
@ -217,12 +220,6 @@ public class BetaMinecraftInterface implements MinecraftInterface {
return out;
}
private int[] getBlocksAtY63(byte[] blocks) {
return ChunkBasedGen.streamY63()
.map(i -> blocks[i])
.toArray();
}
@Override
public Set<Dimension> supportedDimensions() {
return Collections.singleton(Dimension.OVERWORLD);
@ -230,7 +227,7 @@ public class BetaMinecraftInterface implements MinecraftInterface {
}
/** Handles mapping of Minecraft's Biome objects to out biome IDs. */
private static final class BiomeMapping {
private static class BiomeMapping {
private final Object tundra;
private final Object taiga;
private final Object savanna;
@ -288,57 +285,242 @@ public class BetaMinecraftInterface implements MinecraftInterface {
}
}
private static class OceanOracle {
// Outputs of shapeChunk
public static final int OCEAN = -1;
public static final int FROZEN_OCEAN = -2;
public static final int LAND = -3;
// Arbitrary constants to make us generate the same noise
private static final int NOISE_POSITION_FACTOR = 4;
private static final double BIOME_NOISE_SCALE = 1.121;
private static final double DEPTH_NOISE_SCALE = 200.0;
private static final double MAIN_INTERPOLATION_SCALE_XZ = 8.555150000000001;
private static final double MAIN_INTERPOLATION_SCALE_Y = 4.277575000000001;
private static final double OTHER_INTERPOLATION_SCALE = 684.412;
// Offsets to deal with the fact that vanilla generates noise for all Y values
private static final double VANILLA_NOISE_HEIGHT = 17;
private static final double NOISE_HEIGHT_OFFSET = INTERPOLATE_NOISE_VERTICALLY ? 7 : 8;
// Dimensions of the noise arrays
private static final int NOISE_WIDTH = 5;
private static final int NOISE_HEIGHT = INTERPOLATE_NOISE_VERTICALLY ? 2 : 1;
private static final int NOISE_DEPTH = 5;
// Various noise sources
private final SymbolicObject biomeNoise;
private final SymbolicObject depthNoise;
private final PerlinOctaveNoise interpolationNoise;
private final PerlinOctaveNoise upperInterpolationNoise;
private final PerlinOctaveNoise lowerInterpolationNoise;
// Arrays that are stored to avoid re-allocating them constantly.
private double[] biomeNoises = null;
private double[] depthNoises = null;
private double[] interpolationNoises = null;
private double[] upperInterpolationNoises = null;
private double[] lowerInterpolationNoises = null;
private double[] noises = null;
public OceanOracle(SymbolicObject biomeNoise, SymbolicObject depthNoise, PerlinOctaveNoise interpolationNoise, PerlinOctaveNoise upperInterpolationNoise, PerlinOctaveNoise lowerInterpolationNoise) {
this.biomeNoise = biomeNoise;
this.depthNoise = depthNoise;
this.interpolationNoise = interpolationNoise;
this.upperInterpolationNoise = upperInterpolationNoise;
this.lowerInterpolationNoise = lowerInterpolationNoise;
}
public int[] determineOceans(int chunkX, int chunkZ, int[] oceansIn, double[] temperatureNoises, double[] rainfallNoises) throws InvocationTargetException, IllegalAccessException {
int[] oceans = (oceansIn != null && oceansIn.length >= 16 * 16) ? oceansIn : new int[16 * 16];
this.noises = this.calculateNoise(this.noises, chunkX, chunkZ, NOISE_WIDTH, NOISE_HEIGHT, NOISE_DEPTH, temperatureNoises, rainfallNoises);
for (int x = 0; x < 16; ++x) {
for (int z = 0; z < 16; ++z) {
double noiseAtPoint = NOISE_HEIGHT > 1
? interpolateNoise3d(x, 63 % 8, z, noises, NOISE_HEIGHT, NOISE_DEPTH)
: interpolateNoise2d(x, z, noises, NOISE_DEPTH);
boolean isOcean = noiseAtPoint <= 0;
boolean isFrozen = temperatureNoises[x * 16 + z] < 0.5;
oceans[z * 16 + x] = isOcean ? (isFrozen ? FROZEN_OCEAN : OCEAN) : LAND;
}
}
return oceans;
}
private double[] calculateNoise(double[] noisesIn, int chunkX, int chunkZ, int noiseWidth, int noiseHeight, int noiseDepth, double[] temperatureNoises, double[] rainfallNoises) throws InvocationTargetException, IllegalAccessException {
int noisesLength = noiseWidth * noiseHeight * noiseDepth;
double[] noises = (noisesIn != null && noisesIn.length >= noisesLength) ? noisesIn : new double[noisesLength];
int sampleX = chunkX * NOISE_POSITION_FACTOR;
int sampleZ = chunkZ * NOISE_POSITION_FACTOR;
biomeNoises = (double[]) biomeNoise.callMethod(BetaSymbolicNames.METHOD_PERLIN_OCTAVE_NOISE_SAMPLE_2D, biomeNoises, sampleX, sampleZ, noiseWidth, noiseDepth, BIOME_NOISE_SCALE, BIOME_NOISE_SCALE, 0);
depthNoises = (double[]) depthNoise.callMethod(BetaSymbolicNames.METHOD_PERLIN_OCTAVE_NOISE_SAMPLE_2D, depthNoises, sampleX, sampleZ, noiseWidth, noiseDepth, DEPTH_NOISE_SCALE, DEPTH_NOISE_SCALE, 0);
interpolationNoises = interpolationNoise.sample3d(interpolationNoises, sampleX, NOISE_HEIGHT_OFFSET, sampleZ, noiseWidth, noiseHeight, noiseDepth, MAIN_INTERPOLATION_SCALE_XZ, MAIN_INTERPOLATION_SCALE_Y, MAIN_INTERPOLATION_SCALE_XZ);
upperInterpolationNoises = upperInterpolationNoise.sample3d(upperInterpolationNoises, sampleX, NOISE_HEIGHT_OFFSET, sampleZ, noiseWidth, noiseHeight, noiseDepth, OTHER_INTERPOLATION_SCALE, OTHER_INTERPOLATION_SCALE, OTHER_INTERPOLATION_SCALE);
lowerInterpolationNoises = lowerInterpolationNoise.sample3d(lowerInterpolationNoises, sampleX, NOISE_HEIGHT_OFFSET, sampleZ, noiseWidth, noiseHeight, noiseDepth, OTHER_INTERPOLATION_SCALE, OTHER_INTERPOLATION_SCALE, OTHER_INTERPOLATION_SCALE);
for(int xNoiseIdx = 0; xNoiseIdx < noiseWidth; ++xNoiseIdx) {
for(int yNoiseIdx = 0; yNoiseIdx < noiseHeight; ++yNoiseIdx) {
for(int zNoiseIdx = 0; zNoiseIdx < noiseDepth; ++zNoiseIdx) {
int blockX = (int) Math.floor(16.0 / noiseWidth * (xNoiseIdx + 0.5));
int blockZ = (int) Math.floor(16.0 / noiseDepth * (zNoiseIdx + 0.5));
int climateIdx = blockX * 16 + blockZ;
double temperature = temperatureNoises[climateIdx];
double rainfall = rainfallNoises[climateIdx];
int noise2dIdx = xNoiseIdx * noiseDepth + zNoiseIdx;
double biome = biomeNoises[noise2dIdx];
double depth = depthNoises[noise2dIdx];
int noise3dIdx = (xNoiseIdx * noiseDepth + zNoiseIdx) * noiseHeight + yNoiseIdx;
double upperInter = upperInterpolationNoises[noise3dIdx];
double lowerInter = lowerInterpolationNoises[noise3dIdx];
double inter = interpolationNoises[noise3dIdx];
double mergedNoise = mergeNoises(yNoiseIdx,
biome, depth, upperInter, lowerInter, inter, temperature, rainfall);
noises[noise3dIdx] = mergedNoise;
}
}
}
return noises;
}
/** Combines the noise values in just the right way. */
private double mergeNoises(double yNoiseIdx, double biomeNoiseV, double depthNoiseV, double upperInterpolationNoiseV, double lowerInterpolationNoiseV, double interpolationNoiseV, double temperatureV, double rainfallV) {
// This took a lot of trial and error to get right...
double scaledRainfall = 1.0 - rainfallV * temperatureV;
double biomeNoiseValue = biomeNoiseV / 512.0 + 0.5;
double biomeFactor = Math.max(0, Math.min(1, biomeNoiseValue * (1 - Math.pow(scaledRainfall, 4))));
double upperInter = upperInterpolationNoiseV / 512.0;
double lowerInter = lowerInterpolationNoiseV / 512.0;
double mainInter = interpolationNoiseV / 20.0 + 0.5;
double interpolated = PerlinNoise.lerp(mainInter, upperInter, lowerInter);
double clampedInterpolated = Math.max(lowerInter, Math.min(upperInter, interpolated));
// Why are there so many conditionals for depth noise???
double depth1 = depthNoiseV / 8000.0;
double depth2 = depth1 * (depth1 < 0 ? -0.8999999999999999 : 3.0) - 2.0;
double depth3 = depth2 < 0 ? Math.max(-2, depth2) / 5.6 : Math.min(1, depth2) / 8.0;
double depthAdjustedBiomeFactor = depth2 < 0 ? 0.5 : biomeFactor;
double depth4 = (NOISE_HEIGHT_OFFSET + yNoiseIdx - VANILLA_NOISE_HEIGHT * (0.5 + depth3 * 0.25)) * 12.0 / depthAdjustedBiomeFactor;
double depth5 = depth4 < 0 ? depth4 * 4.0 : depth4;
return clampedInterpolated - depth5;
}
@SuppressWarnings("DuplicateExpressions")
private double interpolateNoise3d(int blockX, int blockY, int blockZ, double[] noises, int noiseHeight, int noiseDepth) {
int idxX = blockX / 4;
int idxY = blockY / 8;
int idxZ = blockZ / 4;
// Get the noise values surrounding this coordinate
// variable names are noise[XYZ corner]
// @formatter:off
double noise000 = noises[((idxX ) * noiseDepth + idxZ ) * noiseHeight + idxY ];
double noise001 = noises[((idxX ) * noiseDepth + idxZ + 1) * noiseHeight + idxY ];
double noise010 = noises[((idxX ) * noiseDepth + idxZ ) * noiseHeight + idxY + 1];
double noise011 = noises[((idxX ) * noiseDepth + idxZ + 1) * noiseHeight + idxY + 1];
double noise100 = noises[((idxX + 1) * noiseDepth + idxZ ) * noiseHeight + idxY ];
double noise101 = noises[((idxX + 1) * noiseDepth + idxZ + 1) * noiseHeight + idxY ];
double noise110 = noises[((idxX + 1) * noiseDepth + idxZ ) * noiseHeight + idxY + 1];
double noise111 = noises[((idxX + 1) * noiseDepth + idxZ + 1) * noiseHeight + idxY + 1];
// @formatter:on
double relX = blockX % 4;
double relY = blockY % 8;
double relZ = blockX % 4;
// interpolate X
double noiseX00 = PerlinNoise.lerp(relX / 4, noise000, noise100);
double noiseX01 = PerlinNoise.lerp(relX / 4, noise001, noise101);
double noiseX10 = PerlinNoise.lerp(relX / 4, noise010, noise110);
double noiseX11 = PerlinNoise.lerp(relX / 4, noise011, noise111);
// interpolate Y
double noiseXY0 = PerlinNoise.lerp(relY / 8, noiseX00, noiseX10);
double noiseXY1 = PerlinNoise.lerp(relY / 8, noiseX01, noiseX11);
// interpolate Z
return PerlinNoise.lerp(relZ / 4, noiseXY0, noiseXY1);
}
private double interpolateNoise2d(int blockX, int blockZ, double[] noises, int noiseDepth) {
int idxX = blockX / 4;
int idxZ = blockZ / 4;
double noise00 = noises[(idxX ) * noiseDepth + idxZ ];
double noise01 = noises[(idxX ) * noiseDepth + idxZ + 1];
double noise10 = noises[(idxX + 1) * noiseDepth + idxZ ];
double noise11 = noises[(idxX + 1) * noiseDepth + idxZ + 1];
double relX = blockX % 4;
double relZ = blockX % 4;
// interpolate X
double noiseX0 = PerlinNoise.lerp(relX / 4, noise00, noise10);
double noiseX1 = PerlinNoise.lerp(relX / 4, noise01, noise11);
// interpolate Z
return PerlinNoise.lerp(relZ / 4, noiseX0, noiseX1);
}
}
/**
* A custom implementation of perlin octave noise to speed up interpolation noises
* <p>
* At full precision, over 95% of the time in shapeChunk would be spent generating the 3D interpolation noise. Only
* 2 of the 16 generated y levels are relevant for oceans, so {@link PerlinNoise} skips calculating the others.
* Additionally, this class allows us to configure how many octaves to actually use. Each octave contributes only
* half as much as its successor, so using only 8 or even 4 of the 16 octaves can provide a significant speedup while
* remaining very accurate.
* 2 of the 16 generated y levels are relevant for oceans, so {@link PerlinNoise} is implemented to allow us to avoid
* calculating the others. Additionally, this class allows us to configure how many octaves to actually use. Each
* octave contributes only half as much as its successor, so using only 8 or even 4 of the 16 octaves can provide
* a significant speedup while remaining very accurate.
* <p>
* ByteBuddy is used to create a subclass of Minecraft's PerlinOctaveNoise which delegates to
* {@link TrimmedPerlinOctaveNoise#sample}. Instances of that subclass are then placed into the overworldLevelSource
* to replace its interpolation noise generators.
* We sadly are not able to just re-use Minecraft's perlin noise as that implementation is bugged so that
* selecting a different starting Y value alters the results. Minecraft's perlin octave noise only allows us to
* skip the n most significant octaves, which is counterproductive, so we had to re-implement that as well.
*/
public static class TrimmedPerlinOctaveNoise {
private static class PerlinOctaveNoise {
private final PerlinNoise[] octaves;
private final int firstOctave;
public TrimmedPerlinOctaveNoise(PerlinNoise[] octaves, int firstOctave) {
public PerlinOctaveNoise(PerlinNoise[] octaves, int firstOctave) {
this.octaves = octaves;
this.firstOctave = firstOctave;
}
public static TrimmedPerlinOctaveNoise fromSymbolic(SymbolicObject perlinOctaveNoise, SymbolicClass perlinNoiseClass, int octaveCount) throws IllegalAccessException {
/** Construct an instance by stealing the random state from Minecraft's implementation */
public static PerlinOctaveNoise fromSymbolic(SymbolicObject perlinOctaveNoise, SymbolicClass perlinNoiseClass, int octaveCount) throws IllegalAccessException {
Object[] octaveObjects = (Object[]) perlinOctaveNoise.getFieldValue(BetaSymbolicNames.FIELD_PERLIN_OCTAVE_NOISE_OCTAVES);
PerlinNoise[] octaves = new PerlinNoise[octaveObjects.length];
for (int i = 0; i < octaves.length; ++i) {
octaves[i] = PerlinNoise.fromSymbolic(new SymbolicObject(perlinNoiseClass, octaveObjects[i]));
}
return new TrimmedPerlinOctaveNoise(octaves, octaves.length - octaveCount);
return new PerlinOctaveNoise(octaves, octaves.length - octaveCount);
}
// Method gets injected by ByteBuddy
@SuppressWarnings("unused")
public double[] sample(double[] resultArr, double x, double y, double z,
int resX, int resY, int resZ, double scaleX, double scaleY,
double scaleZ) {
public double[] sample3d(double[] resultArr, double x, double y, double z,
int resX, int resY, int resZ, double scaleX, double scaleY,
double scaleZ) {
if (resultArr == null) {
resultArr = new double[resX * resY * resZ];
} else {
Arrays.fill(resultArr, 0.0);
}
double inverseIntensity = 1.0 / (1 << firstOctave);
for (int i = firstOctave; i < octaves.length; ++i) {
double inverseIntensity = 1.0 / (1 << i);
octaves[i].sample(resultArr,
x, y, z,
resX, resY, resZ,
scaleX * inverseIntensity, scaleY * inverseIntensity, scaleZ * inverseIntensity,
inverseIntensity);
inverseIntensity /= 2;
}
return resultArr;
}
@ -369,8 +551,7 @@ public class BetaMinecraftInterface implements MinecraftInterface {
public void sample(double[] array, double x, double y, double z, int resX, int resY, int resZ, double xScale, double yScale, double zScale, double scale) {
for (int xIdx = 0; xIdx < resX; ++xIdx) {
for (int zIdx = 0; zIdx < resZ; ++zIdx) {
// Range of Y values is hard-coded to avoid generating noise that is irrelevant to ocean gen.
for (int yIdx = 7; yIdx < 9; ++yIdx) {
for (int yIdx = 0; yIdx < resY; ++yIdx) {
double xPos = (x + xIdx) * xScale + xOffset;
double yPos = (y + yIdx) * yScale + yOffset;
double zPos = (z + zIdx) * zScale + zOffset;
@ -387,10 +568,10 @@ public class BetaMinecraftInterface implements MinecraftInterface {
double w = fade(zPos);
// Minecraft re-uses the lerps from the previous y value if cubeY didn't change.
// This is incorrect, as the lerps depend on yPos, not cubeY, so we need to figure which
// yPos minecraft would have used to generate the lerps for this index.
int lastY = findLastLerpIndex(y, yScale, yIdx, cubeY);
double[] lerps = calcBuggedLerps(y, yScale, xPos, zPos, cubeX, cubeZ, u, lastY);
// This is incorrect, as the lerps depend on yPos, not cubeY, so we need to figure out
// which yPos minecraft would have used to generate the lerps for this index.
int lastY = findLastLerpIndex(yScale, (int) Math.round(y + yIdx), cubeY);
double[] lerps = calcBuggedLerps(yScale, xPos, zPos, cubeX, cubeZ, u, lastY);
array[xIdx * resZ * resY + zIdx * resY + yIdx] += lerp(w, lerp(v, lerps[0], lerps[1]), lerp(v, lerps[2], lerps[3])) * (1 / scale);
}
@ -398,10 +579,10 @@ public class BetaMinecraftInterface implements MinecraftInterface {
}
}
private int findLastLerpIndex(double y, double yScale, int yIdx, int cubeY) {
private int findLastLerpIndex(double yScale, int yIdx, int cubeY) {
int searchIdx = yIdx;
while (true) {
double searchPos = (y + searchIdx) * yScale + yOffset;
double searchPos = searchIdx * yScale + yOffset;
int searchCube = (int) Math.floor(searchPos) & 255;
if (searchIdx < 0 || searchCube != cubeY)
break;
@ -410,8 +591,8 @@ public class BetaMinecraftInterface implements MinecraftInterface {
return searchIdx + 1;
}
private double[] calcBuggedLerps(double y, double yScale, double xPos, double zPos, int cubeX, int cubeZ, double u, int yIdx) {
double yPos = (y + yIdx) * yScale + yOffset;
private double[] calcBuggedLerps(double yScale, double xPos, double zPos, int cubeX, int cubeZ, double u, int yIdx) {
double yPos = yIdx * yScale + yOffset;
int cubeY = (int) Math.floor(yPos) & 255;
yPos -= Math.floor(yPos);
int A = permutations[cubeX] + cubeY;
@ -433,63 +614,19 @@ public class BetaMinecraftInterface implements MinecraftInterface {
return lerps;
}
private static double fade(double t) {
public static double fade(double t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
private static double lerp(double t, double a, double b) {
public static double lerp(double t, double a, double b) {
return a + t * (b - a);
}
private static double grad(int hash, double x, double y, double z) {
public static double grad(int hash, double x, double y, double z) {
int h = hash & 15;
double u = h < 8 ? x : y;
double v = h < 4 ? y : h == 12 || h == 14 ? x : z;
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}
}
public interface InterpolationNoiseSetter {
void setNoise(TrimmedPerlinOctaveNoise hacked);
}
public static Class<?> makeInterpolationNoiseClass(SymbolicClass perlinOctaveNoiseClass) {
return new ByteBuddy()
.subclass(perlinOctaveNoiseClass.getClazz())
.defineField("hacked", TrimmedPerlinOctaveNoise.class, Visibility.PUBLIC)
.implement(InterpolationNoiseSetter.class).intercept(FieldAccessor.ofField("hacked"))
.method(ElementMatchers.is(perlinOctaveNoiseClass.getMethod(BetaSymbolicNames.METHOD_PERLIN_OCTAVE_NOISE_SAMPLE_3D).getRawMethod()))
.intercept(MethodDelegation.toField("hacked"))
.make()
.load(perlinOctaveNoiseClass.getClazz().getClassLoader())
.getLoaded();
}
public Object makeInterpolationNoise(SymbolicObject octaveNoise, int octaveCount) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
// Create the object that will handle the actual noise generation
TrimmedPerlinOctaveNoise noise = TrimmedPerlinOctaveNoise.fromSymbolic(octaveNoise, perlinNoiseClass, octaveCount);
// Instantiate our delegating subclass
InterpolationNoiseSetter interpolationNoise = (InterpolationNoiseSetter) hackedNoiseClass.getConstructor(Random.class, int.class).newInstance(null, 0);
// Make the instance delegate to our noise object
interpolationNoise.setNoise(noise);
return interpolationNoise;
}
public void adjustNoise(SymbolicObject levelSource, int precision) throws IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException {
Object levelSourceObj = levelSource.getObject();
String[] symbolicFields = new String[]{
BetaSymbolicNames.FIELD_LOWER_INTERPOLATION_NOISE,
BetaSymbolicNames.FIELD_UPPER_INTERPOLATION_NOISE,
BetaSymbolicNames.FIELD_INTERPOLATION_NOISE,
};
int[] precisionFactor = new int[] {1, 1, 2};
for (int i = 0; i < symbolicFields.length; ++i) {
Field field = levelSource.getType().getField(symbolicFields[i]).getRawField();
int octaveCount = precision / precisionFactor[i];
Object newOctaveNoise = makeInterpolationNoise((SymbolicObject) levelSource.getFieldValue(symbolicFields[i]), octaveCount);
field.set(levelSourceObj, newOctaveNoise);
}
}
}

View File

@ -9,28 +9,31 @@ public enum BetaSymbolicNames {
public static final String CLASS_BIOMEGENERATOR = "BiomeGenerator";
public static final String METHOD_BIOMEGENERATOR_GET_BIOME = "getBiome";
public static final String FIELD_BIOMEGENERATOR_TEMPERATURE = "temperature";
public static final String FIELD_BIOMEGENERATOR_RAINFALL = "rainfall";
public static final String CLASS_WORLD = "World";
public static final String CONSTRUCTOR_WORLD = "<init>";
public static final String CLASS_DIMENSION_BASE = "DimensionBase";
public static final String FIELD_DIMENSION_WORLD = "world";
public static final String FIELD_DIMENSION_BIOMEGENERATOR = "biomeGenerator";
public static final String INTERFACE_SOMETHING = "ISomething";
public static final String CLASS_DIMENSION_OVERWORLD = "DimensionOverworld";
public static final String CLASS_PERLIN_OCTAVE_NOISE = "PerlinOctaveNoise";
public static final String METHOD_PERLIN_OCTAVE_NOISE_SAMPLE_3D = "sample";
public static final String METHOD_PERLIN_OCTAVE_NOISE_SAMPLE_3D = "sample3d";
public static final String METHOD_PERLIN_OCTAVE_NOISE_SAMPLE_2D = "sample2d";
public static final String FIELD_PERLIN_OCTAVE_NOISE_OCTAVES = "octaves";
public static final String CLASS_OVERWORLD_LEVEL_SOURCE = "OverworldLevelSource";
public static final String CONSTRUCTOR_OVERWORLD_LEVEL_SOURCE = "<init>";
public static final String METHOD_OVERWORLD_LEVEL_SOURCE_SHAPE_CHUNK = "shapeChunk";
public static final String FIELD_UPPER_INTERPOLATION_NOISE = "upperInterpolationNoise";
public static final String FIELD_LOWER_INTERPOLATION_NOISE = "lowerInterpolationNoise";
public static final String FIELD_INTERPOLATION_NOISE = "interpolationNoise";
public static final String FIELD_DIMENSION_BIOMEGENERATOR = "biomeGenerator";
public static final String FIELD_DEPTH_NOISE = "depthNoise";
public static final String FIELD_BIOME_NOISE = "biomeNoise";
public static final String CLASS_PERLIN_NOISE = "PerlinNoise";
public static final String FIELD_PERLIN_NOISE_PERMUTATIONS = "permutations";