/* * This file is part of Cubic Chunks Mod, licensed under the MIT License (MIT). * * Copyright (c) 2015 contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package cubicchunks.worldgen.generator.custom; import net.minecraft.block.state.IBlockState; import net.minecraft.init.Blocks; import net.minecraft.world.biome.Biome; import java.util.Random; import cubicchunks.world.ICubicWorld; import cubicchunks.world.cube.Cube; import cubicchunks.worldgen.generator.GlobalGeneratorConfig; import cubicchunks.worldgen.generator.ICubePrimer; import cubicchunks.worldgen.generator.custom.builder.BasicBuilder; import cubicchunks.worldgen.generator.custom.builder.IBuilder; import static cubicchunks.util.Coords.localToBlock; import static cubicchunks.util.MathUtil.lerp; import static cubicchunks.worldgen.generator.GlobalGeneratorConfig.MAX_ELEV; import static cubicchunks.worldgen.generator.GlobalGeneratorConfig.X_SECTIONS; import static cubicchunks.worldgen.generator.GlobalGeneratorConfig.X_SECTION_SIZE; import static cubicchunks.worldgen.generator.GlobalGeneratorConfig.Y_SECTIONS; import static cubicchunks.worldgen.generator.GlobalGeneratorConfig.Y_SECTION_SIZE; import static cubicchunks.worldgen.generator.GlobalGeneratorConfig.Z_SECTIONS; import static cubicchunks.worldgen.generator.GlobalGeneratorConfig.Z_SECTION_SIZE; /** * A terrain generator that supports infinite(*) worlds */ public class CustomTerrainGenerator { // Number of octaves for the noise function private static final int OCTAVES = 16; private final ICubicWorld world; private final long seed; private final Random rand; private final double[][][] noiseArrayHigh; private final double[][][] noiseArrayLow; private final double[][][] noiseArrayAlpha; private final double[][][] rawDensity; private final double[][][] expandedDensity; private final IBuilder builderHigh; private final IBuilder builderLow; private final IBuilder builderAlpha; private final int maxSmoothRadius; private final int maxSmoothDiameter; private final double[][] noiseArrayHeight; private final double[] nearBiomeWeightArray; private final BasicBuilder builderHeight; private final boolean needsScaling = true; private Biome[] biomes; private double biomeVolatility; private double biomeHeight; public CustomTerrainGenerator(ICubicWorld world, final long seed) { this.seed = seed; this.rand = new Random(seed); this.maxSmoothRadius = 2*(int) (MAX_ELEV/64); this.maxSmoothDiameter = this.maxSmoothRadius*2 + 1; this.world = world; this.noiseArrayHigh = new double[X_SECTIONS][Y_SECTIONS][Z_SECTIONS]; this.noiseArrayLow = new double[X_SECTIONS][Y_SECTIONS][Z_SECTIONS]; this.noiseArrayAlpha = new double[X_SECTIONS][Y_SECTIONS][Z_SECTIONS]; this.rawDensity = new double[X_SECTIONS][Y_SECTIONS][Z_SECTIONS]; this.expandedDensity = new double[Cube.SIZE][Cube.SIZE][Cube.SIZE]; this.builderHigh = createHighBuilder(); this.builderLow = createLowBuilder(); this.builderAlpha = createAlphaBuilder(); this.noiseArrayHeight = new double[X_SECTIONS][Z_SECTIONS]; this.nearBiomeWeightArray = new double[this.maxSmoothDiameter*this.maxSmoothDiameter]; for (int x = -this.maxSmoothRadius; x <= this.maxSmoothRadius; x++) { for (int z = -this.maxSmoothRadius; z <= this.maxSmoothRadius; z++) { final double f1 = 10.0F/Math.sqrt(x*x + z*z + 0.2F); this.nearBiomeWeightArray[x + this.maxSmoothRadius + (z + this.maxSmoothRadius) *this.maxSmoothDiameter] = f1; } } double freq = 200.0/Math.pow(2, 10)/(MAX_ELEV/64); this.builderHeight = new BasicBuilder(); this.builderHeight.setSeed(this.rand.nextInt()); this.builderHeight.setOctaves(10); this.builderHeight.setMaxElev(8); this.builderHeight.setFreq(freq); this.builderHeight.build(); } /** * Generate the cube as the specified location * * @param cube cube primer to use * @param cubeX cube x location * @param cubeY cube y location * @param cubeZ cube z location */ public void generate(final ICubePrimer cube, int cubeX, int cubeY, int cubeZ) { generateNoiseArrays(cubeX, cubeY, cubeZ); generateTerrainArray(cube, cubeX, cubeY, cubeZ); generateTerrain(cube, this.rawDensity, cubeX, cubeY, cubeZ); } /** * Generate terrain at the specified location * * @param cube cube primer to use * @param input generated noise to use * @param cubeX cube x position * @param cubeY cube y position * @param cubeZ cube z position */ private void generateTerrain(ICubePrimer cube, double[][][] input, int cubeX, int cubeY, int cubeZ) { int xSteps = X_SECTION_SIZE - 1; int ySteps = Y_SECTION_SIZE - 1; int zSteps = Z_SECTION_SIZE - 1; // use the noise to generate the generator for (int noiseX = 0; noiseX < X_SECTIONS - 1; noiseX++) { for (int noiseZ = 0; noiseZ < Z_SECTIONS - 1; noiseZ++) { for (int noiseY = 0; noiseY < Y_SECTIONS - 1; noiseY++) { // get the noise samples double x0y0z0 = input[noiseX][noiseY][noiseZ]; double x0y0z1 = input[noiseX][noiseY][noiseZ + 1]; double x1y0z0 = input[noiseX + 1][noiseY][noiseZ]; double x1y0z1 = input[noiseX + 1][noiseY][noiseZ + 1]; double x0y1z0 = input[noiseX][noiseY + 1][noiseZ]; double x0y1z1 = input[noiseX][noiseY + 1][noiseZ + 1]; double x1y1z0 = input[noiseX + 1][noiseY + 1][noiseZ]; double x1y1z1 = input[noiseX + 1][noiseY + 1][noiseZ + 1]; for (int x = 0; x < xSteps; x++) { int xRel = noiseX*xSteps + x; double xd = (double) x/xSteps; // interpolate along x double xy0z0 = lerp(xd, x0y0z0, x1y0z0); double xy0z1 = lerp(xd, x0y0z1, x1y0z1); double xy1z0 = lerp(xd, x0y1z0, x1y1z0); double xy1z1 = lerp(xd, x0y1z1, x1y1z1); for (int z = 0; z < zSteps; z++) { int zRel = noiseZ*zSteps + z; double zd = (double) z/zSteps; // interpolate along z double xy0z = lerp(zd, xy0z0, xy0z1); double xy1z = lerp(zd, xy1z0, xy1z1); for (int y = 0; y < ySteps; y++) { int yRel = noiseY*ySteps + y; double yd = (double) y/ySteps; // interpolate along y double xyz = lerp(yd, xy0z, xy1z); //values needed to calculate gradient vector double xyz0 = lerp(yd, xy0z0, xy1z0); double xyz1 = lerp(yd, xy0z1, xy1z1); double x0y0z = lerp(zd, x0y0z0, x0y0z1); double x0y1z = lerp(zd, x0y1z0, x0y1z1); double x1y0z = lerp(zd, x1y0z0, x1y0z1); double x1y1z = lerp(zd, x1y1z0, x1y1z1); double x0yz = lerp(yd, x0y0z, x0y1z); double x1yz = lerp(yd, x1y0z, x1y1z); //calculate gradient vector double xGrad = (x1yz - x0yz)/xSteps; double yGrad = (xy1z - xy0z)/ySteps; double zGrad = (xyz1 - xyz0)/zSteps; IBlockState state = getBlockStateFor(localToBlock(cubeY, yRel), xyz, xGrad, yGrad, zGrad); cube.setBlockState(xRel, yRel, zRel, state); } } } } } } } /** * Retrieve the blockstate appropriate for the specified noise parameters * * @param height Height of the block being generated * @param density Generated density at the specified location. A density > 0 typically indicates a solid block * @param xGrad Gradient of density in x direction * @param yGrad Gradient of density in y direction * @param zGrad Gradient of density in z direction * * @return The block state */ private IBlockState getBlockStateFor(int height, double density, double xGrad, double yGrad, double zGrad) { final double dirtDepth = 4; IBlockState state = Blocks.AIR.getDefaultState(); if (density > 0) { state = Blocks.STONE.getDefaultState(); //if the block above would be empty: if (density + yGrad <= 0) { state = Blocks.GRASS.getDefaultState(); //if density decreases as we go up && density < dirtDepth } else if (yGrad < 0 && density < dirtDepth) { state = Blocks.DIRT.getDefaultState(); } } else if (height < 64) { // TODO replace check with GlobalGeneratorConfig.SEA_LEVEL state = Blocks.WATER.getDefaultState(); } return state; } private IBuilder createHighBuilder() { Random rand = new Random(this.seed*2); double freq = 684.412D/Math.pow(2, OCTAVES)/(MAX_ELEV/64.0); BasicBuilder builderHigh = new BasicBuilder(); builderHigh.setSeed(rand.nextInt()); builderHigh.setOctaves(OCTAVES); builderHigh.setPersistance(0.5); // with 16 octaves probability of getting 1 is too low builderHigh.setMaxElev(2); builderHigh.setClamp(-1, 1); builderHigh.setFreq(freq, freq, freq); builderHigh.build(); return builderHigh; } private IBuilder createLowBuilder() { Random rand = new Random(this.seed*3); double freq = 684.412D/Math.pow(2, OCTAVES)/(MAX_ELEV/64.0); BasicBuilder builderLow = new BasicBuilder(); builderLow.setSeed(rand.nextInt()); builderLow.setOctaves(OCTAVES); builderLow.setPersistance(0.5); builderLow.setMaxElev(2); builderLow.setClamp(-1, 1); builderLow.setFreq(freq, freq, freq); builderLow.build(); return builderLow; } private IBuilder createAlphaBuilder() { Random rand = new Random(this.seed*4); double freq = 8.55515/Math.pow(2, 8)/(MAX_ELEV/64.0); BasicBuilder builderAlpha = new BasicBuilder(); builderAlpha.setSeed(rand.nextInt()); builderAlpha.setOctaves(8); builderAlpha.setPersistance(0.5); builderAlpha.setMaxElev(25.6); builderAlpha.setSeaLevel(0.5); builderAlpha.setClamp(0, 1); builderAlpha.setFreq(freq, freq*2, freq); builderAlpha.build(); return builderAlpha; } /* * (non-Javadoc) * @see cubicchunks.worldgen.generator.ITerrainGenerator#generateNoiseArrays(cubicchunks.world.cube.Cube) */ private void generateNoiseArrays(int cubeX, int cubeY, int cubeZ) { int cubeXMin = cubeX*(X_SECTIONS - 1); int cubeYMin = cubeY*(Y_SECTIONS - 1); int cubeZMin = cubeZ*(Z_SECTIONS - 1); for (int x = 0; x < X_SECTIONS; x++) { int xPos = cubeXMin + x; for (int z = 0; z < Z_SECTIONS; z++) { int zPos = cubeZMin + z; for (int y = 0; y < Y_SECTIONS; y++) { int yPos = cubeYMin + y; this.noiseArrayHigh[x][y][z] = this.builderHigh.getValue(xPos, yPos, zPos); this.noiseArrayLow[x][y][z] = this.builderLow.getValue(xPos, yPos, zPos); this.noiseArrayAlpha[x][y][z] = this.builderAlpha.getValue(xPos, yPos, zPos); } } } } /* * (non-Javadoc) * @see cubicchunks.worldgen.generator.ITerrainGenerator#generateTerrainArray(cubicchunks.world.cube.Cube) */ private void generateTerrainArray(final ICubePrimer cube, int cubeX, int cubeY, int cubeZ) { this.biomes = getBiomeMap(cubeX, cubeZ); fillHeightArray(cubeX, cubeZ); for (int x = 0; x < X_SECTIONS; x++) { for (int z = 0; z < Z_SECTIONS; z++) { // TODO: Remove addHeight? double addHeight = getAddHeight(x, z); biomeFactor(x, z, addHeight); for (int y = 0; y < Y_SECTIONS; y++) { final double vol1Low = this.noiseArrayLow[x][y][z]; final double vol2High = this.noiseArrayHigh[x][y][z]; final double noiseAlpha = this.noiseArrayAlpha[x][y][z]; double output = lerp(noiseAlpha, vol1Low, vol2High); double heightModifier = this.biomeHeight; double volatilityModifier = this.biomeVolatility; final double yAbs = (cubeY*16.0 + y*8.0)/MAX_ELEV; if (yAbs < heightModifier) { // generator below average biome geight is more flat volatilityModifier /= 4.0; } // NOTE: Multiplication by nonnegative number and addition when using 3d noise effects are the same // as with heightmap. // make height range lower output *= volatilityModifier; // height shift output += heightModifier; // Since in TWM we don't have height limit we could skip it but PLATEAU biomes need it int maxYSections = (int) Math.round(MAX_ELEV/Y_SECTION_SIZE); if (yAbs*MAX_ELEV > maxYSections - 4) { // TODO: Convert Y cutoff to work correctly with noise between -1 and 1 // final double a = ( yAbs - ( maxYSections - 4 ) ) / 3.0F; // output = output * ( 1.0D - a ) - 10.0D * a; } this.rawDensity[x][y][z] = output*GlobalGeneratorConfig.MAX_ELEV + 64 - yAbs*MAX_ELEV; } } } } /** * Retrieve biomes at the specified column location * * @param cubeX column x * @param cubeZ column z * * @return biomes in that column */ private Biome[] getBiomeMap(int cubeX, int cubeZ) { return world.getProvider().getBiomeProvider().getBiomesForGeneration(this.biomes, cubeX*4 - this.maxSmoothRadius, cubeZ*4 - this.maxSmoothRadius, X_SECTION_SIZE + this.maxSmoothDiameter, Z_SECTION_SIZE + this.maxSmoothDiameter); } /** * Calculates biome height and volatility and adds addHeight to result. * <p> * It converts vanilla biome values to some more predictable format: * <p> * biome volatility == 0 will generate flat generator * <p> * biome volatility == 0.5 means that max difference between the actual height and average height is 0.5 of max * generation height from sea level. High volatility will generate overhangs * <p> * biome height == 0 will generate generator at sea level * <p> * biome height == 1 will generate generator will generate at max generation height above sea level. * <p> * Volatility Note: Terrain below biome height has volatility divided by 4, probably to add some flat generator to * mountanious biomes */ private void biomeFactor(final int x, final int z, final double addHeight) { // Calculate weighted average of nearby biomes height and volatility float smoothVolatility = 0.0F; float smoothHeight = 0.0F; float biomeWeightSum = 0.0F; final Biome centerBiomeConfig = getCenterBiome(x, z); final int lookRadius = this.maxSmoothRadius; for (int nextX = -lookRadius; nextX <= lookRadius; nextX++) { for (int nextZ = -lookRadius; nextZ <= lookRadius; nextZ++) { final Biome biome = getOffsetBiome(x, z, nextX, nextZ); final float biomeHeight = biome.getBaseHeight(); final float biomeVolatility = biome.getHeightVariation(); double biomeWeight = calcBiomeWeight(nextX, nextZ, biomeHeight); biomeWeight = Math.abs(biomeWeight); if (biomeHeight > centerBiomeConfig.getBaseHeight()) { // prefer biomes with lower height? biomeWeight /= 2.0F; } smoothVolatility += biomeVolatility*biomeWeight; smoothHeight += biomeHeight*biomeWeight; biomeWeightSum += biomeWeight; } } smoothVolatility /= biomeWeightSum; smoothHeight /= biomeWeightSum; // Convert from vanilla height/volatility format // to something easier to predict this.biomeVolatility = smoothVolatility*0.9 + 0.1; this.biomeVolatility *= 4.0/3.0; // divide everything by 64, then it will be multpllied by maxElev // vanilla sea level: 63.75 / 64.00 // sea level 0.75/64 of height above sea level (63.75 = 63+0.75) this.biomeHeight = 0.75/64.0; this.biomeHeight += smoothHeight*17.0/64.0; // TODO: Remove addHeight? it changes the result by at most 1 block this.biomeHeight += 0.2*addHeight*17.0/64.0; } private Biome getCenterBiome(final int x, final int z) { return this.biomes[x + this.maxSmoothRadius + (z + this.maxSmoothRadius) *(X_SECTION_SIZE + this.maxSmoothDiameter)]; } private Biome getOffsetBiome(final int x, final int z, int nextX, int nextZ) { return this.biomes[x + nextX + this.maxSmoothRadius + (z + nextZ + this.maxSmoothRadius) *(X_SECTION_SIZE + this.maxSmoothDiameter)]; } private double calcBiomeWeight(int nextX, int nextZ, float biomeHeight) { return this.nearBiomeWeightArray[nextX + this.maxSmoothRadius + (nextZ + this.maxSmoothRadius) *this.maxSmoothDiameter] /(biomeHeight + 2.0F); } private void fillHeightArray(int cubeX, int cubeZ) { int cubeXMin = cubeX*(X_SECTION_SIZE - 1); int cubeZMin = cubeZ*(Z_SECTION_SIZE - 1); for (int x = 0; x < X_SECTIONS; x++) { int xPos = cubeXMin + x; for (int z = 0; z < Z_SECTIONS; z++) { int zPos = cubeZMin + z; this.noiseArrayHeight[x][z] = this.builderHeight.getValue(xPos, 0, zPos); } } } /** * This method is there only because the code exists in vanilla, it affects generator height by at most 1 block * (+/-0.425 blocks). In Minecraft beta it was base generator height, but as of beta 1.8 it doesn't have any * significant effect. It's multiplied 0.2 before it's used. */ private double getAddHeight(final int x, final int z) { double noiseHeight = this.noiseArrayHeight[x][z]; assert noiseHeight <= 8 && noiseHeight >= -8; if (noiseHeight < 0.0D) { noiseHeight = -noiseHeight*0.3D; } noiseHeight = noiseHeight*3.0D - 2.0D; if (noiseHeight < 0.0D) { noiseHeight /= 2.0D; if (noiseHeight < -1.0D) { noiseHeight = -1.0D; } noiseHeight /= 1.4D; noiseHeight /= 2.0D; } else { if (noiseHeight > 1.0D) { noiseHeight = 1.0D; } noiseHeight /= 8.0D; } return noiseHeight; } }