mirror of
https://github.com/toolbox4minecraft/amidst.git
synced 2025-01-09 04:28:44 +08:00
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:
parent
64d6095528
commit
1692199c0e
5
pom.xml
5
pom.xml
@ -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>
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
Loading…
Reference in New Issue
Block a user