Basic terrain generation testing
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good

This commit is contained in:
austin 2024-10-29 13:52:31 -04:00
parent 4d6278d21f
commit 604380b31e
12 changed files with 405 additions and 27 deletions

View File

@ -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": [

10
pom.xml
View File

@ -294,12 +294,20 @@
<version>${lwjgl.version}</version>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-yoga</artifactId>
<version>${lwjgl.version}</version>
<classifier>${lwjgl.natives}</classifier>
</dependency>
<!--MathUtils-->
<!--License: MIT-->
<dependency>
<groupId>io.github.studiorailgun</groupId>
<artifactId>MathUtils</artifactId>
<version>1.0.1</version>
</dependency>
</dependencies>

View File

@ -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;
}
}

View File

@ -19,6 +19,11 @@ public class BiomeTypeMap {
*/
Map<String,BiomeData> idBiomeMap = new HashMap<String,BiomeData>();
/**
* The map of index -> biome data
*/
Map<Integer,BiomeData> indexBiomeMap = new HashMap<Integer,BiomeData>();
/**
* 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);
}
}

View File

@ -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);

View File

@ -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<String,HeightmapGenerator> tagGeneratorMap = new HashMap<String,HeightmapGenerator>();
/**
* 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;
}
}
}

View File

@ -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";
}
}

View File

@ -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();
}

View File

@ -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";
}
}

View File

@ -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";
}
}

View File

@ -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);

View File

@ -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<String,ModificationList> 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;
}
}