From 604380b31e61afdcadb1908128cbca844e3ec325 Mon Sep 17 00:00:00 2001 From: austin Date: Tue, 29 Oct 2024 13:52:31 -0400 Subject: [PATCH] Basic terrain generation testing --- assets/Data/game/biomes.json | 34 +++---- pom.xml | 10 +- .../game/data/biome/BiomeData.java | 13 +++ .../game/data/biome/BiomeTypeMap.java | 15 +++ .../game/server/world/ServerWorldData.java | 18 +++- .../TestGenerationChunkGenerator.java | 96 +++++++++++++++++-- .../generation/heightmap/EmptySkyGen.java | 15 +++ .../heightmap/HeightmapGenerator.java | 23 +++++ .../generation/heightmap/HillsGen.java | 84 ++++++++++++++++ .../generation/heightmap/PlainsGen.java | 66 +++++++++++++ .../terrain/manager/ServerTerrainManager.java | 10 ++ .../server/terrain/models/TerrainModel.java | 48 +++++++++- 12 files changed, 405 insertions(+), 27 deletions(-) create mode 100644 src/main/java/electrosphere/server/terrain/generation/heightmap/EmptySkyGen.java create mode 100644 src/main/java/electrosphere/server/terrain/generation/heightmap/HeightmapGenerator.java create mode 100644 src/main/java/electrosphere/server/terrain/generation/heightmap/HillsGen.java create mode 100644 src/main/java/electrosphere/server/terrain/generation/heightmap/PlainsGen.java diff --git a/assets/Data/game/biomes.json b/assets/Data/game/biomes.json index ab93f97f..fd8e83b8 100644 --- a/assets/Data/game/biomes.json +++ b/assets/Data/game/biomes.json @@ -1,20 +1,5 @@ { "biomes": [ - { - "id": "sky", - "displayName": "Sky", - "isAerial": true, - "isSurface": false, - "isSubterranean": false, - "regions": [ - { - "frequency": 1.0, - "baseFloorVoxel": 0, - "floorVariants": [], - "foliageDescription": [] - } - ] - }, { "id": "plains", "displayName": "Plains", @@ -36,7 +21,24 @@ "foliageDescription": [ ] } - ] + ], + "heightGenerator": "plains" + }, + { + "id": "sky", + "displayName": "Sky", + "isAerial": true, + "isSurface": false, + "isSubterranean": false, + "regions": [ + { + "frequency": 1.0, + "baseFloorVoxel": 0, + "floorVariants": [], + "foliageDescription": [] + } + ], + "heightGenerator": "empty" } ], "files": [ diff --git a/pom.xml b/pom.xml index 23653a4a..c262b5ea 100644 --- a/pom.xml +++ b/pom.xml @@ -294,12 +294,20 @@ ${lwjgl.version} - org.lwjgl + org.lwjgl lwjgl-yoga ${lwjgl.version} ${lwjgl.natives} + + + + io.github.studiorailgun + MathUtils + 1.0.1 + + diff --git a/src/main/java/electrosphere/game/data/biome/BiomeData.java b/src/main/java/electrosphere/game/data/biome/BiomeData.java index 4eb5d50a..75bf5da1 100644 --- a/src/main/java/electrosphere/game/data/biome/BiomeData.java +++ b/src/main/java/electrosphere/game/data/biome/BiomeData.java @@ -37,6 +37,11 @@ public class BiomeData { */ Boolean isSubterranean; + /** + * Tag for the heightmap generator to source from + */ + String heightGenerator; + /** @@ -86,5 +91,13 @@ public class BiomeData { public Boolean isSubterranean(){ return isSubterranean; } + + /** + * Gets the tag for the heightmap generator to source from + * @return The tag for the heightmap generator to source from + */ + public String getHeightGenerator(){ + return heightGenerator; + } } diff --git a/src/main/java/electrosphere/game/data/biome/BiomeTypeMap.java b/src/main/java/electrosphere/game/data/biome/BiomeTypeMap.java index 283ac246..f4a4baa2 100644 --- a/src/main/java/electrosphere/game/data/biome/BiomeTypeMap.java +++ b/src/main/java/electrosphere/game/data/biome/BiomeTypeMap.java @@ -19,6 +19,11 @@ public class BiomeTypeMap { */ Map idBiomeMap = new HashMap(); + /** + * The map of index -> biome data + */ + Map indexBiomeMap = new HashMap(); + /** * The list of surface biomes */ @@ -51,6 +56,7 @@ public class BiomeTypeMap { if(biome.isSubterranean()){ this.subterraneanBiomes.add(biome); } + indexBiomeMap.put(indexBiomeMap.size(),biome); } /** @@ -138,4 +144,13 @@ public class BiomeTypeMap { return this.subterraneanBiomes; } + /** + * Gets the biome by its index + * @param index The index + * @return The biome if the index exists, null otherwise + */ + public BiomeData getBiomeByIndex(int index){ + return indexBiomeMap.get(index); + } + } diff --git a/src/main/java/electrosphere/game/server/world/ServerWorldData.java b/src/main/java/electrosphere/game/server/world/ServerWorldData.java index 7b60192a..c1440419 100644 --- a/src/main/java/electrosphere/game/server/world/ServerWorldData.java +++ b/src/main/java/electrosphere/game/server/world/ServerWorldData.java @@ -150,7 +150,13 @@ public class ServerWorldData { serverWorldData = ServerWorldData.createFixedWorldData(new Vector3d(0),new Vector3d(16 * 4 * 4)); serverWorldData.worldSizeDiscrete = TestGenerationChunkGenerator.GENERATOR_REALM_SIZE; serverWorldData.worldSizeDiscreteVertical = TestGenerationChunkGenerator.GENERATOR_REALM_SIZE; - serverTerrainManager = new ServerTerrainManager(serverWorldData, 0, new TestGenerationChunkGenerator()); + + //test terrain gen + { + TestGenerationChunkGenerator chunkGen = new TestGenerationChunkGenerator(serverWorldData); + serverTerrainManager = new ServerTerrainManager(serverWorldData, 0, chunkGen); + serverTerrainManager.genTestData(chunkGen); + } serverFluidManager = new ServerFluidManager(serverWorldData, serverTerrainManager, 0, new DefaultFluidGenerator()); serverWorldData.setManagers(serverTerrainManager, serverFluidManager); return serverWorldData; @@ -184,6 +190,16 @@ public class ServerWorldData { public float convertChunkToRealSpace(int chunk){ return chunk * ServerTerrainChunk.CHUNK_DIMENSION; } + + /** + * Converts a chunk space coordinate to a real space coordinate + * @param chunk The position within the chunk + * @param worldPos The world pos of the chunk + * @return The real pos + */ + public double convertVoxelToRealSpace(int chunk, int worldPos){ + return chunk + this.convertWorldToReal(worldPos); + } public double getRelativeLocation(double real, int world){ return real - (world * dynamicInterpolationRatio); diff --git a/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java b/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java index 767eda01..d6b90be9 100644 --- a/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java +++ b/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java @@ -1,7 +1,15 @@ package electrosphere.server.terrain.generation; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import electrosphere.game.data.biome.BiomeData; +import electrosphere.game.server.world.ServerWorldData; +import electrosphere.server.terrain.generation.heightmap.EmptySkyGen; +import electrosphere.server.terrain.generation.heightmap.HeightmapGenerator; +import electrosphere.server.terrain.generation.heightmap.HillsGen; +import electrosphere.server.terrain.generation.heightmap.PlainsGen; import electrosphere.server.terrain.generation.interfaces.ChunkGenerator; import electrosphere.server.terrain.manager.ServerTerrainChunk; import electrosphere.server.terrain.models.TerrainModel; @@ -21,6 +29,35 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { */ TerrainModel terrainModel; + /** + * The server world data + */ + ServerWorldData serverWorldData; + + /** + * Map of generator tag to the generator + */ + Map tagGeneratorMap = new HashMap(); + + /** + * Constructor + */ + public TestGenerationChunkGenerator(ServerWorldData serverWorldData){ + this.serverWorldData = serverWorldData; + registerGenerator(new EmptySkyGen()); + registerGenerator(new HillsGen()); + registerGenerator(new PlainsGen()); + + } + + /** + * Registers a heightmap generator + * @param generator The heightmap generator + */ + private void registerGenerator(HeightmapGenerator generator){ + tagGeneratorMap.put(generator.getTag(),generator); + } + @Override public ServerTerrainChunk generateChunk(int worldX, int worldY, int worldZ) { ServerTerrainChunk rVal = null; @@ -51,11 +88,15 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { //actual generation algo weights = new float[ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION]; values = new int[ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION]; + + //biome of the current chunk + BiomeData biome = this.terrainModel.getSurfaceBiome(worldX, worldY, worldZ); + for(int x = 0; x < ServerTerrainChunk.CHUNK_DIMENSION; x++){ for(int y = 0; y < ServerTerrainChunk.CHUNK_DIMENSION; y++){ for(int z = 0; z < ServerTerrainChunk.CHUNK_DIMENSION; z++){ - weights[x][y][z] = this.getChunkWeight(worldX, worldY, worldZ, x, y, z, this.terrainModel); - values[x][y][z] = this.getChunkValue(worldX, worldY, worldZ, x, y, z, this.terrainModel); + weights[x][y][z] = this.getChunkWeight(worldX, worldY, worldZ, x, y, z, this.terrainModel, biome); + values[x][y][z] = this.getChunkValue(worldX, worldY, worldZ, x, y, z, this.terrainModel, biome); } } } @@ -80,10 +121,30 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { * @param chunkY The chunk y pos * @param chunkZ The chunk z pos * @param terrainModel The terrain model + * @param surfaceBiome The surface biome of the chunk * @return The value of the chunk */ - private int getChunkValue(int worldX, int worldY, int worldZ, int chunkX, int chunkY, int chunkZ, TerrainModel terrainModel){ - return 1; + private int getChunkValue( + int worldX, int worldY, int worldZ, + int chunkX, int chunkY, int chunkZ, + TerrainModel terrainModel, + BiomeData surfaceBiome + ){ + HeightmapGenerator heightmapGen = this.tagGeneratorMap.get(surfaceBiome.getHeightGenerator()); + if(heightmapGen == null){ + throw new Error("Undefined heightmap generator in biome! " + surfaceBiome.getId() + " " + surfaceBiome.getDisplayName() + " " + surfaceBiome.getHeightGenerator()); + } + + double realX = this.serverWorldData.convertVoxelToRealSpace(chunkX,worldX); + double realY = this.serverWorldData.convertVoxelToRealSpace(chunkY,worldY); + double realZ = this.serverWorldData.convertVoxelToRealSpace(chunkZ,worldZ); + + float surfaceHeight = heightmapGen.getHeight(terrainModel.getSeed(), realX, realZ); + if(realY <= surfaceHeight){ + return 1; + } else { + return 0; + } } /** @@ -95,10 +156,33 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { * @param chunkY The chunk y pos * @param chunkZ The chunk z pos * @param terrainModel The terrain model + * @param surfaceBiome The surface biome of the chunk * @return The weight of the chunk */ - private float getChunkWeight(int worldX, int worldY, int worldZ, int chunkX, int chunkY, int chunkZ, TerrainModel terrainModel){ - return 0.1f; + private float getChunkWeight( + int worldX, int worldY, int worldZ, + int chunkX, int chunkY, int chunkZ, + TerrainModel terrainModel, + BiomeData surfaceBiome + ){ + HeightmapGenerator heightmapGen = this.tagGeneratorMap.get(surfaceBiome.getHeightGenerator()); + if(heightmapGen == null){ + throw new Error("Undefined heightmap generator in biome! " + surfaceBiome.getId() + " " + surfaceBiome.getDisplayName() + " " + surfaceBiome.getHeightGenerator()); + } + + double realX = this.serverWorldData.convertVoxelToRealSpace(chunkX,worldX); + double realY = this.serverWorldData.convertVoxelToRealSpace(chunkY,worldY); + double realZ = this.serverWorldData.convertVoxelToRealSpace(chunkZ,worldZ); + + float surfaceHeight = heightmapGen.getHeight(terrainModel.getSeed(), realX, realZ); + double flooredSurfaceHeight = Math.floor(surfaceHeight); + if(realY < flooredSurfaceHeight){ + return 1; + } else if(realY > flooredSurfaceHeight) { + return -1; + } else { + return (float)(surfaceHeight - flooredSurfaceHeight) * 2 - 1; + } } } diff --git a/src/main/java/electrosphere/server/terrain/generation/heightmap/EmptySkyGen.java b/src/main/java/electrosphere/server/terrain/generation/heightmap/EmptySkyGen.java new file mode 100644 index 00000000..a30955d3 --- /dev/null +++ b/src/main/java/electrosphere/server/terrain/generation/heightmap/EmptySkyGen.java @@ -0,0 +1,15 @@ +package electrosphere.server.terrain.generation.heightmap; + +public class EmptySkyGen implements HeightmapGenerator { + + @Override + public float getHeight(long SEED, double x, double y) { + return 0; + } + + @Override + public String getTag() { + return "empty"; + } + +} diff --git a/src/main/java/electrosphere/server/terrain/generation/heightmap/HeightmapGenerator.java b/src/main/java/electrosphere/server/terrain/generation/heightmap/HeightmapGenerator.java new file mode 100644 index 00000000..4989ff10 --- /dev/null +++ b/src/main/java/electrosphere/server/terrain/generation/heightmap/HeightmapGenerator.java @@ -0,0 +1,23 @@ +package electrosphere.server.terrain.generation.heightmap; + +/** + * Generates height values for terrain + */ +public interface HeightmapGenerator { + + /** + * Gets the height of a given position, given a SEED value + * @param SEED The seed for the terrain + * @param x The x value + * @param y The y value + * @return The height + */ + public float getHeight(long SEED, double x, double y); + + /** + * Gets the tag associated with this generator + * @return The tag + */ + public String getTag(); + +} diff --git a/src/main/java/electrosphere/server/terrain/generation/heightmap/HillsGen.java b/src/main/java/electrosphere/server/terrain/generation/heightmap/HillsGen.java new file mode 100644 index 00000000..c16256a0 --- /dev/null +++ b/src/main/java/electrosphere/server/terrain/generation/heightmap/HillsGen.java @@ -0,0 +1,84 @@ +package electrosphere.server.terrain.generation.heightmap; + +import electrosphere.util.noise.OpenSimplex2S; + +/** + * Generates hilly heightmaps + */ +public class HillsGen implements HeightmapGenerator { + + /** + * Offset from baseline to place the noisemap at + */ + static final float HEIGHT_OFFSET = 10; + + /** + * The different scales of noise to sample from + */ + static final double[][] GRAD_NOISE = new double[][]{ + {0.01, 2.0}, + {0.02, 2.0}, + {0.05, 1.0}, + {0.1, 1.0}, + {0.3, 1.0}, + }; + + //distance from origin to sample for gradient calculation + public static float GRADIENT_DIST = 0.01f; + + //param for controlling how pointer the initial layers are + public static float GRAD_INFLUENCE_DROPOFF = 0.35f; + + + /** + * Gets the height at a given position for this generation approach + * @param SEED The seed + * @param x The x position + * @param y The y position + * @return The height + */ + public float getHeight(long SEED, double x, double y){ + return gradientHeight(SEED, x, y); + } + + + /** + * Applies a gradient approach to heightfield generation + * @param SEED The seed + * @param x The x value + * @param y The y value + * @return The elevation at x,y + */ + static float gradientHeight(long SEED, double x, double y){ + float rVal = 0; + + float gradXAccum = 0; + float gradYAccum = 0; + for(int n = 0; n < GRAD_NOISE.length; n++){ + //get noise samples + float noiseOrigin = (float)(OpenSimplex2S.noise2_ImproveX(SEED, x * GRAD_NOISE[n][0], y * GRAD_NOISE[n][0]) * GRAD_NOISE[n][1]); + float noiseX = (float)(OpenSimplex2S.noise2_ImproveX(SEED, x * GRAD_NOISE[n][0] + GRADIENT_DIST, y * GRAD_NOISE[n][0]) * GRAD_NOISE[n][1]); + float noiseY = (float)(OpenSimplex2S.noise2_ImproveX(SEED, x * GRAD_NOISE[n][0], y * GRAD_NOISE[n][0] + GRADIENT_DIST) * GRAD_NOISE[n][1]); + //calculate gradient accumulation + float gradX = (noiseX - noiseOrigin) / GRADIENT_DIST; + float gradY = (noiseY - noiseOrigin) / GRADIENT_DIST; + gradXAccum = gradXAccum + gradX; + gradYAccum = gradYAccum + gradY; + //determine current noise's influence based on gradient + float gradientMagnitude = (float)Math.sqrt(gradXAccum * gradXAccum + gradYAccum * gradYAccum); + float influence = 1.0f / (1.0f + gradientMagnitude * GRAD_INFLUENCE_DROPOFF); + + //add to height + rVal = rVal + (float)(OpenSimplex2S.noise2_ImproveX(SEED, x * GRAD_NOISE[n][0], y * GRAD_NOISE[n][0]) * GRAD_NOISE[n][1]) * influence; + } + rVal = rVal + HEIGHT_OFFSET; + return rVal; + } + + + @Override + public String getTag() { + return "hills"; + } + +} diff --git a/src/main/java/electrosphere/server/terrain/generation/heightmap/PlainsGen.java b/src/main/java/electrosphere/server/terrain/generation/heightmap/PlainsGen.java new file mode 100644 index 00000000..29c902fb --- /dev/null +++ b/src/main/java/electrosphere/server/terrain/generation/heightmap/PlainsGen.java @@ -0,0 +1,66 @@ +package electrosphere.server.terrain.generation.heightmap; + +import electrosphere.util.noise.OpenSimplex2S; + +/** + * Generator for plains + */ +public class PlainsGen implements HeightmapGenerator { + + /** + * Offset from baseline to place the noisemap at + */ + static final float HEIGHT_OFFSET = 10; + + /** + * The scale to apply to the coordinates + */ + static final float GEN_SCALE = 0.2f; + + //the different scales of noise to sample from + static final double[][] NOISE_SCALES = new double[][]{ + {0.01, 3.0}, + {0.02, 2.0}, + {0.05, 0.8}, + {0.1, 0.3}, + {0.3, 0.2}, + }; + + + /** + * Gets the height at a given position for this generation approach + * @param SEED The seed + * @param x The x position + * @param y The y position + * @return The height + */ + public float getHeight(long SEED, double x, double y){ + return sampleAllNoise(SEED, x, y); + } + + + /** + * Samples all noise values directly + * @param SEED The seed + * @param x The x value + * @param y The y value + * @return The elevation at x,y + */ + static float sampleAllNoise(long SEED, double x, double y){ + float rVal = 0; + double scaledX = x * GEN_SCALE; + double scaledY = y * GEN_SCALE; + for(int n = 0; n < NOISE_SCALES.length; n++){ + rVal = rVal + (float)(OpenSimplex2S.noise2_ImproveX(SEED, scaledX * NOISE_SCALES[n][0], scaledY * NOISE_SCALES[n][0]) * NOISE_SCALES[n][1]); + } + rVal = rVal + HEIGHT_OFFSET; + return rVal; + } + + + @Override + public String getTag() { + return "plains"; + } + +} diff --git a/src/main/java/electrosphere/server/terrain/manager/ServerTerrainManager.java b/src/main/java/electrosphere/server/terrain/manager/ServerTerrainManager.java index 49bc9ff8..c9bcf770 100644 --- a/src/main/java/electrosphere/server/terrain/manager/ServerTerrainManager.java +++ b/src/main/java/electrosphere/server/terrain/manager/ServerTerrainManager.java @@ -2,6 +2,7 @@ package electrosphere.server.terrain.manager; import electrosphere.game.server.world.ServerWorldData; import electrosphere.server.terrain.diskmap.ChunkDiskMap; +import electrosphere.server.terrain.generation.TestGenerationChunkGenerator; import electrosphere.server.terrain.generation.continentphase.TerrainGenerator; import electrosphere.server.terrain.generation.interfaces.ChunkGenerator; import electrosphere.server.terrain.models.TerrainModel; @@ -151,6 +152,15 @@ public class ServerTerrainManager { chunkDiskMap = new ChunkDiskMap(); chunkDiskMap.init(saveName); } + + /** + * Generates a test terrain model + * @param chunkGen The chunk generator + */ + public void genTestData(TestGenerationChunkGenerator chunkGen){ + this.model = TerrainModel.generateTestModel(); + chunkGen.setModel(model); + } public float[][] getTerrainAtChunk(int x, int y){ return model.getElevationForChunk(x, y); diff --git a/src/main/java/electrosphere/server/terrain/models/TerrainModel.java b/src/main/java/electrosphere/server/terrain/models/TerrainModel.java index 60bf3715..dc7bf8ba 100644 --- a/src/main/java/electrosphere/server/terrain/models/TerrainModel.java +++ b/src/main/java/electrosphere/server/terrain/models/TerrainModel.java @@ -5,12 +5,19 @@ import java.util.Map; import electrosphere.engine.Globals; import electrosphere.game.data.biome.BiomeData; +import electrosphere.server.terrain.generation.TestGenerationChunkGenerator; import electrosphere.util.annotation.Exclude; /** * The model of the terrain */ public class TerrainModel { + + + /** + * The scale of the macro data + */ + static final int MACRO_DATA_SCALE = 32; /** @@ -33,6 +40,12 @@ public class TerrainModel { */ @Exclude private float[][] elevation; + + /** + * The macro level biome data + */ + @Exclude + private short[][] biome; /** * The real coordinate mountain threshold @@ -48,6 +61,11 @@ public class TerrainModel { * The map of modifications applied to the model */ Map modifications; + + /** + * The seed of the terrain + */ + long seed = 0; /** * Private constructor @@ -92,6 +110,18 @@ public class TerrainModel { rVal.dynamicInterpolationRatio = dynamicInterpolationRatio; return rVal; } + + /** + * Generates a test terrain model + * @return The test terrain model + */ + public static TerrainModel generateTestModel(){ + TerrainModel rVal = new TerrainModel(); + rVal.discreteArrayDimension = TestGenerationChunkGenerator.GENERATOR_REALM_SIZE; + rVal.dynamicInterpolationRatio = 1; + rVal.biome = new short[2][2]; + return rVal; + } /** * Gets the macro elevation data for the terrain model @@ -460,14 +490,26 @@ public class TerrainModel { } /** - * Gets the biome for a given world position + * Gets the surface biome for a given world position * @param worldX The world X * @param worldY The world Y * @param worldZ The world Z * @return The biome */ - public BiomeData getBiome(int worldX, int worldY, int worldZ){ - return Globals.gameConfigCurrent.getBiomeMap().getTypes().iterator().next(); + public BiomeData getSurfaceBiome(int worldX, int worldY, int worldZ){ + int macroX = worldX / MACRO_DATA_SCALE; + int macroZ = worldZ / MACRO_DATA_SCALE; + int surfaceBiomeIndex = this.biome[macroX][macroZ]; + BiomeData biome = Globals.gameConfigCurrent.getBiomeMap().getBiomeByIndex(surfaceBiomeIndex); + return biome; + } + + /** + * Gets the seed of the terrain model + * @return The seed + */ + public long getSeed(){ + return seed; } }