From baeba9c98aa6386ab086b02605770ca2dcc24b04 Mon Sep 17 00:00:00 2001 From: dags- Date: Thu, 16 Jan 2020 10:06:28 +0000 Subject: [PATCH] core + app --- TerraForgedApp/build.gradle | 18 + .../main/java/com/terraforged/app/Applet.java | 136 ++++ .../main/java/com/terraforged/app/Cache.java | 98 +++ .../java/com/terraforged/app/Controller.java | 211 +++++ .../main/java/com/terraforged/app/Main.java | 239 ++++++ .../com/terraforged/app/biome/BiomeColor.java | 34 + .../terraforged/app/biome/BiomeProvider.java | 109 +++ .../java/com/terraforged/app/mesh/Mesh.java | 116 +++ .../com/terraforged/app/mesh/NoiseMesh.java | 42 + .../terraforged/app/mesh/TestRenderer.java | 33 + .../app/renderer/MeshRenderer.java | 50 ++ .../terraforged/app/renderer/Renderer.java | 105 +++ .../app/renderer/VoxelRenderer.java | 130 +++ .../src/main/resources/biome_data.json | 764 ++++++++++++++++++ .../src/main/resources/biome_groups.json | 168 ++++ TerraForgedApp/src/main/resources/grass.png | Bin 0 -> 7146 bytes TerraForgedCore/build.gradle | 5 + .../java/com/terraforged/core/cell/Cell.java | 98 +++ .../com/terraforged/core/cell/Extent.java | 10 + .../com/terraforged/core/cell/Populator.java | 23 + .../java/com/terraforged/core/cell/Tag.java | 6 + .../terraforged/core/decorator/Decorator.java | 9 + .../core/decorator/DesertStacks.java | 55 ++ .../core/decorator/SwampPools.java | 59 ++ .../com/terraforged/core/filter/Erosion.java | 234 ++++++ .../com/terraforged/core/filter/Filter.java | 32 + .../terraforged/core/filter/Filterable.java | 15 + .../com/terraforged/core/filter/Modifier.java | 36 + .../terraforged/core/filter/Smoothing.java | 60 ++ .../terraforged/core/filter/Steepness.java | 53 ++ .../com/terraforged/core/module/Blender.java | 102 +++ .../terraforged/core/module/CellLookup.java | 21 + .../core/module/CellLookupOffset.java | 30 + .../com/terraforged/core/module/Lerp.java | 54 ++ .../terraforged/core/module/MultiBlender.java | 108 +++ .../com/terraforged/core/module/Select.java | 18 + .../com/terraforged/core/module/Selector.java | 66 ++ .../com/terraforged/core/region/Region.java | 311 +++++++ .../terraforged/core/region/RegionCache.java | 88 ++ .../core/region/RegionCacheFactory.java | 8 + .../core/region/RegionGenerator.java | 145 ++++ .../com/terraforged/core/region/Size.java | 50 ++ .../core/region/chunk/ChunkGenTask.java | 23 + .../core/region/chunk/ChunkHolder.java | 14 + .../core/region/chunk/ChunkReader.java | 44 + .../core/region/chunk/ChunkWriter.java | 17 + .../core/region/chunk/ChunkZoomTask.java | 26 + .../core/settings/BiomeSettings.java | 79 ++ .../core/settings/FilterSettings.java | 45 ++ .../core/settings/GeneratorSettings.java | 200 +++++ .../terraforged/core/settings/Settings.java | 15 + .../core/settings/TerrainSettings.java | 50 ++ .../java/com/terraforged/core/util/Cache.java | 72 ++ .../terraforged/core/util/FutureValue.java | 38 + .../terraforged/core/util/PosIterator.java | 131 +++ .../java/com/terraforged/core/util/Seed.java | 42 + .../core/util/VariablePredicate.java | 31 + .../core/util/concurrent/ObjectPool.java | 78 ++ .../core/util/concurrent/ThreadPool.java | 110 +++ .../terraforged/core/util/grid/FixedGrid.java | 134 +++ .../terraforged/core/util/grid/FixedList.java | 67 ++ .../core/util/grid/MappedList.java | 25 + .../serialization/annotation/Comment.java | 13 + .../util/serialization/annotation/Option.java | 12 + .../util/serialization/annotation/Range.java | 15 + .../annotation/Serializable.java | 11 + .../serializer/Deserializer.java | 85 ++ .../util/serialization/serializer/Reader.java | 58 ++ .../serialization/serializer/Serializer.java | 146 ++++ .../util/serialization/serializer/Writer.java | 24 + .../core/world/GeneratorContext.java | 41 + .../core/world/WorldDecorators.java | 26 + .../terraforged/core/world/WorldFilters.java | 46 ++ .../core/world/WorldGenerator.java | 28 + .../core/world/WorldGeneratorFactory.java | 44 + .../core/world/biome/BiomeData.java | 134 +++ .../core/world/biome/BiomeManager.java | 10 + .../core/world/biome/BiomeType.java | 111 +++ .../core/world/biome/BiomeTypeColors.java | 50 ++ .../core/world/biome/BiomeTypeLoader.java | 179 ++++ .../core/world/climate/Climate.java | 141 ++++ .../core/world/climate/ClimateModule.java | 134 +++ .../core/world/climate/Compressor.java | 56 ++ .../world/continent/ContinentBlender.java | 21 + .../core/world/continent/ContinentModule.java | 7 + .../continent/ContinentMultiBlender.java | 22 + .../continent/VoronoiContinentModule.java | 138 ++++ .../core/world/geology/Geology.java | 37 + .../core/world/geology/Strata.java | 120 +++ .../core/world/geology/Stratum.java | 40 + .../core/world/heightmap/Heightmap.java | 55 ++ .../core/world/heightmap/Levels.java | 54 ++ .../core/world/heightmap/RegionConfig.java | 20 + .../core/world/heightmap/RegionExtent.java | 70 ++ .../core/world/heightmap/WorldHeightmap.java | 206 +++++ .../terraforged/core/world/river/Lake.java | 80 ++ .../core/world/river/LakeConfig.java | 51 ++ .../core/world/river/PosGenerator.java | 151 ++++ .../terraforged/core/world/river/River.java | 172 ++++ .../core/world/river/RiverBounds.java | 66 ++ .../core/world/river/RiverConfig.java | 87 ++ .../core/world/river/RiverManager.java | 86 ++ .../core/world/river/RiverNode.java | 48 ++ .../core/world/river/RiverRegion.java | 207 +++++ .../core/world/terrain/LandForms.java | 254 ++++++ .../core/world/terrain/Terrain.java | 151 ++++ .../core/world/terrain/TerrainPopulator.java | 77 ++ .../core/world/terrain/Terrains.java | 115 +++ .../core/world/terrain/VolcanoModule.java | 80 ++ .../core/world/terrain/VolcanoPopulator.java | 116 +++ .../provider/StandardTerrainProvider.java | 126 +++ .../terrain/provider/TerrainProvider.java | 61 ++ .../provider/TerrainProviderFactory.java | 10 + TerraForgedCore/src/main/resources/biomes.png | Bin 0 -> 14615 bytes TerraForgedCore/src/main/resources/biomes.txt | 13 + .../src/main/resources/terraforged.png | Bin 0 -> 27685 bytes settings.gradle | 2 +- 117 files changed, 9296 insertions(+), 1 deletion(-) create mode 100644 TerraForgedApp/build.gradle create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/Applet.java create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/Cache.java create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/Controller.java create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/Main.java create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/biome/BiomeColor.java create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/biome/BiomeProvider.java create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/mesh/Mesh.java create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/mesh/NoiseMesh.java create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/mesh/TestRenderer.java create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/renderer/MeshRenderer.java create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/renderer/Renderer.java create mode 100644 TerraForgedApp/src/main/java/com/terraforged/app/renderer/VoxelRenderer.java create mode 100644 TerraForgedApp/src/main/resources/biome_data.json create mode 100644 TerraForgedApp/src/main/resources/biome_groups.json create mode 100644 TerraForgedApp/src/main/resources/grass.png create mode 100644 TerraForgedCore/build.gradle create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/cell/Cell.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/cell/Extent.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/cell/Populator.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/cell/Tag.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/decorator/Decorator.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/decorator/DesertStacks.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/decorator/SwampPools.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/filter/Erosion.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/filter/Filter.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/filter/Filterable.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/filter/Modifier.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/filter/Smoothing.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/filter/Steepness.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/module/Blender.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/module/CellLookup.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/module/CellLookupOffset.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/module/Lerp.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/module/MultiBlender.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/module/Select.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/module/Selector.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/region/Region.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/region/RegionCache.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/region/RegionCacheFactory.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/region/RegionGenerator.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/region/Size.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkGenTask.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkHolder.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkReader.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkWriter.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkZoomTask.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/settings/BiomeSettings.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/settings/FilterSettings.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/settings/GeneratorSettings.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/settings/Settings.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/settings/TerrainSettings.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/Cache.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/FutureValue.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/PosIterator.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/Seed.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/VariablePredicate.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/concurrent/ObjectPool.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/concurrent/ThreadPool.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/grid/FixedGrid.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/grid/FixedList.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/grid/MappedList.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Comment.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Option.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Range.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Serializable.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Deserializer.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Reader.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Serializer.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Writer.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/GeneratorContext.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/WorldDecorators.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/WorldFilters.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/WorldGenerator.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/WorldGeneratorFactory.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeData.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeManager.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeType.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeTypeColors.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeTypeLoader.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/climate/Climate.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/climate/ClimateModule.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/climate/Compressor.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentBlender.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentModule.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentMultiBlender.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/continent/VoronoiContinentModule.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Geology.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Strata.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Stratum.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/Heightmap.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/Levels.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/RegionConfig.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/RegionExtent.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/WorldHeightmap.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/river/Lake.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/river/LakeConfig.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/river/PosGenerator.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/river/River.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverBounds.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverConfig.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverManager.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverNode.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverRegion.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/LandForms.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/Terrain.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/TerrainPopulator.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/Terrains.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/VolcanoModule.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/VolcanoPopulator.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/StandardTerrainProvider.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/TerrainProvider.java create mode 100644 TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/TerrainProviderFactory.java create mode 100644 TerraForgedCore/src/main/resources/biomes.png create mode 100644 TerraForgedCore/src/main/resources/biomes.txt create mode 100644 TerraForgedCore/src/main/resources/terraforged.png diff --git a/TerraForgedApp/build.gradle b/TerraForgedApp/build.gradle new file mode 100644 index 0000000..1537abc --- /dev/null +++ b/TerraForgedApp/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "java" +} + +repositories { + mavenCentral() + jcenter() + maven { url "https://jitpack.io" } +} + +dependencies { + compile "org.processing:core:3.3.7" + compile project(":TerraForgedCore") +} + +jar { + manifest { attributes "Main-Class": "com.terraforged.app.Main" } +} \ No newline at end of file diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/Applet.java b/TerraForgedApp/src/main/java/com/terraforged/app/Applet.java new file mode 100644 index 0000000..5784df1 --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/Applet.java @@ -0,0 +1,136 @@ +package com.terraforged.app; + +import com.terraforged.app.renderer.MeshRenderer; +import com.terraforged.app.renderer.Renderer; +import com.terraforged.app.renderer.VoxelRenderer; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.terrain.Terrain; +import processing.core.PApplet; +import processing.event.KeyEvent; +import processing.event.MouseEvent; + +public abstract class Applet extends PApplet { + + public static final int ELEVATION = 1; + public static final int BIOME_TYPE = 2; + public static final int TEMPERATURE = 3; + public static final int MOISTURE = 4; + public static final int BIOME = 5; + public static final int STEEPNESS = 6; + public static final int TERRAIN_TYPE = 7; + public static final int EROSION = 8; + public static final int CONTINENT = 9; + + protected Renderer mesh = new MeshRenderer(this); + protected Renderer voxel = new VoxelRenderer(this); + + public final Controller controller = new Controller(); + + public abstract Cache getCache(); + + public abstract float color(Cell cell); + + @Override + public void settings() { + size(800, 800, P3D); + } + + @Override + public void mousePressed(MouseEvent event) { + controller.mousePress(event); + } + + @Override + public void mouseReleased(MouseEvent event) { + controller.mouseRelease(event); + } + + @Override + public void mouseDragged(MouseEvent event) { + controller.mouseDrag(event); + } + + @Override + public void mouseWheel(MouseEvent event) { + controller.mouseWheel(event); + } + + @Override + public void keyPressed(KeyEvent event) { + controller.keyPress(event); + } + + @Override + public void keyReleased(KeyEvent event) { + controller.keyRelease(event); + } + + public void leftAlignText(int margin, int top, int lineHeight, String... lines) { + noLights(); + fill(0, 0, 100); + for (int i = 0; i < lines.length; i++) { + int y = top + (i * lineHeight); + text(lines[i], margin, y); + } + } + + + public void drawTerrain(float zoom) { + if (controller.getRenderMode() == 0) { + voxel.render(zoom); + } else { + mesh.render(zoom); + } + } + + public String colorModeName() { + switch (controller.getColorMode()) { + case STEEPNESS: + return "GRADIENT"; + case TEMPERATURE: + return "TEMPERATURE"; + case MOISTURE: + return "MOISTURE"; + case TERRAIN_TYPE: + return "TERRAIN TYPE"; + case ELEVATION: + return "ELEVATION"; + case BIOME_TYPE: + return "BIOME TYPE"; + case BIOME: + return "BIOME"; + case EROSION: + return "EROSION"; + case CONTINENT: + return "CONTINENT"; + default: + return "-"; + } + } + + public static float hue(float value, int steps, int max) { + value = Math.round(value * (steps - 1)); + value /= (steps - 1); + return value * max; + } + + public void drawCompass() { + pushStyle(); + pushMatrix(); + textSize(200); + fill(100, 0, 100); + + char[] chars = {'N', 'E', 'S', 'W'}; + for (int r = 0; r < 4; r++) { + char c = chars[r]; + float x = -textWidth(c) / 2; + float y = -width * 1.2F; + text(c, x, y); + rotateZ(0.5F * PI); + } + + popMatrix(); + popStyle(); + textSize(16); + } +} diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/Cache.java b/TerraForgedApp/src/main/java/com/terraforged/app/Cache.java new file mode 100644 index 0000000..27e5858 --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/Cache.java @@ -0,0 +1,98 @@ +package com.terraforged.app; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.region.Region; +import com.terraforged.core.region.RegionGenerator; +import com.terraforged.core.settings.Settings; +import com.terraforged.core.util.concurrent.ThreadPool; +import com.terraforged.core.world.GeneratorContext; +import com.terraforged.core.world.WorldGeneratorFactory; +import com.terraforged.core.world.biome.BiomeType; +import com.terraforged.core.world.terrain.Terrain; +import com.terraforged.core.world.terrain.Terrains; + +public class Cache { + + private float offsetX = 0; + private float offsetZ = 0; + private float zoom = 0F; + private boolean filter = true; + private Terrains terrain; + private Settings settings; + private GeneratorContext context; + private Region region; + private RegionGenerator renderer; + + public Cache(int seed) { + Settings settings = new Settings(); + settings.generator.seed = seed; + this.settings = settings; + this.terrain = Terrains.create(settings); + this.context = new GeneratorContext(terrain, settings); + this.renderer = RegionGenerator.builder() + .factory(new WorldGeneratorFactory(context)) + .pool(ThreadPool.getCommon()) + .size(3, 0) + .build(); + } + + public Settings getSettings() { + return settings; + } + + public Terrains getTerrain() { + return terrain; + } + + public Terrain getCenterTerrain() { + Terrain tag = getCenterCell().tag; + return tag == null ? terrain.ocean : tag; + } + + public BiomeType getCenterBiomeType() { + return getCenterCell().biomeType; + } + + public int getCenterHeight() { + return (int) (context.levels.worldHeight * getCenterCell().value); + } + + public Cell getCenterCell() { + int center = region.getBlockSize().size / 2; + return region.getCell(center, center); + } + + public Region getRegion() { + return region; + } + + public void update(float offsetX, float offsetZ, float zoom, boolean filters) { + if (region == null) { + record(offsetX, offsetZ, zoom, filters); + return; + } + if (this.offsetX != offsetX || this.offsetZ != offsetZ) { + record(offsetX, offsetZ, zoom, filters); + return; + } + if (this.zoom != zoom) { + record(offsetX, offsetZ, zoom, filters); + return; + } + if (this.filter != filters) { + record(offsetX, offsetZ, zoom, filters); + } + } + + private void record(float offsetX, float offsetZ, float zoom, boolean filters) { + this.zoom = zoom; + this.filter = filters; + this.offsetX = offsetX; + this.offsetZ = offsetZ; + try { + this.region = renderer.generateRegion(offsetX, offsetZ, zoom, filters); + } catch (Throwable t) { + t.printStackTrace(); + } + } +} diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/Controller.java b/TerraForgedApp/src/main/java/com/terraforged/app/Controller.java new file mode 100644 index 0000000..4bb2ec5 --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/Controller.java @@ -0,0 +1,211 @@ +package com.terraforged.app; + +import processing.core.PApplet; +import processing.event.KeyEvent; +import processing.event.MouseEvent; + +import java.awt.*; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; + +public class Controller { + + private static final int BUTTON_NONE = -1; + private static final int BUTTON_1 = 37; + private static final int BUTTON_2 = 39; + private static final int BUTTON_3 = 3; + + private static final float cameraSpeed = 100F; + private static final float zoomSpeed = 0.01F; + private static final float rotateSpeed = 0.002F; + private static final float translateSpeed = 2F; + private static final float moveSpeed = 10F; + + private int mouseButton = BUTTON_NONE; + private int lastX = 0; + private int lastY = 0; + + private float yaw = -0.2F; + private float pitch = 0.85F; + private float translateX = 0F; + private float translateY = 0F; + private float translateZ = -800; + private float velocityX = 0F; + private float velocityY = 0F; + + private int colorMode = 1; + private int renderMode = 0; + private int newSeed = 0; + private int left = 0; + private int right = 0; + private int up = 0; + private int down = 0; + private float zoom = 16; + private boolean filters = true; + + public void apply(PApplet applet) { + applet.translate(translateX, translateY, translateZ); + applet.translate(applet.width / 2, applet.height / 2, 0); + applet.rotateX(pitch); + applet.rotateZ(yaw); + update(); + } + + public void update() { + float forward = up + down; + float strafe = left + right; + velocityX = forward * (float) Math.sin(yaw); + velocityY = forward * (float) Math.cos(yaw); + velocityX += strafe * (float) Math.sin(yaw + Math.toRadians(90)); + velocityY += strafe * (float) Math.cos(yaw + Math.toRadians(90)); + if (velocityX != 0 || velocityY != 0) { + float magnitude = (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY); + velocityX /= magnitude; + velocityY /= magnitude; + } + } + + public int getColorMode() { + return colorMode; + } + + public int getRenderMode() { + return renderMode; + } + + public float velocityX() { + return velocityX * moveSpeed / zoom; + } + + public float velocityY() { + return velocityY * moveSpeed / zoom; + } + + public float zoomLevel() { + return zoom; + } + + public boolean filters() { + return filters; + } + + public int getNewSeed() { + if (newSeed == 1) { + newSeed = 0; + return 1; + } + if (newSeed != 0) { + int val = newSeed; + newSeed = 0; + return val; + } + return 0; + } + + public void keyPress(KeyEvent event) { + switch (event.getKey()) { + case 'w': + up = -1; + break; + case 'a': + left = -1; + break; + case 's': + down = 1; + break; + case 'd': + right = 1; + break; + } + } + + public void keyRelease(KeyEvent event) { + switch (event.getKey()) { + case 'w': + up = 0; + return; + case 'a': + left = 0; + return; + case 's': + down = 0; + return; + case 'd': + right = 0; + return; + case 'r': + renderMode = renderMode == 0 ? 1 : 0; + return; + case 'n': + newSeed = 1; + return; + case 'm': + newSeed = Main.seed; + return; + case 'f': + filters = !filters; + return; + case 'c': + StringSelection selection = new StringSelection("" + Main.seed); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null); + return; + case 'v': + try { + Object data = Toolkit.getDefaultToolkit().getSystemClipboard().getData(DataFlavor.stringFlavor); + newSeed = Integer.parseInt(data.toString()); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + if (event.getKey() >= '1' && event.getKey() <= '9') { + colorMode = event.getKey() - '0'; + return; + } + } + + public void mousePress(MouseEvent event) { + if (mouseButton == BUTTON_NONE) { + lastX = event.getX(); + lastY = event.getY(); + mouseButton = event.getButton(); + } + } + + public void mouseRelease(MouseEvent event) { + mouseButton = BUTTON_NONE; + } + + public void mouseWheel(MouseEvent event) { + translateZ -= event.getCount() * cameraSpeed; + } + + public void mouseDrag(MouseEvent event) { + int dx = event.getX() - lastX; + int dy = event.getY() - lastY; + boolean ctrl = (event.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK; + + lastX = event.getX(); + lastY = event.getY(); + + if (mouseButton == BUTTON_1) { + yaw -= dx * rotateSpeed; + pitch -= dy * rotateSpeed; + } + + if (mouseButton == BUTTON_2) { + translateX += dx * translateSpeed; + translateY += dy * translateSpeed; + } + + if (mouseButton == BUTTON_3) { + if (ctrl) { + zoom += (dy - dx) * zoom * zoomSpeed; + zoom = Math.max(1F, zoom); + } else { + translateZ -= (dy - dx) * cameraSpeed * 0.1F; + } + } + } +} diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/Main.java b/TerraForgedApp/src/main/java/com/terraforged/app/Main.java new file mode 100644 index 0000000..9e8d4ac --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/Main.java @@ -0,0 +1,239 @@ +package com.terraforged.app; + +import com.terraforged.app.biome.BiomeProvider; +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.biome.BiomeData; +import com.terraforged.core.world.biome.BiomeType; +import com.terraforged.core.world.terrain.Terrain; + +import java.awt.*; +import java.util.Random; + +public class Main extends Applet { + + public static void main(String[] args) { + Main.start(-1); + } + + public static void start(long seed) { + Main.seed = (int) seed; + Main.random = seed == -1; + main(Main.class.getName()); + } + + private static boolean random = false; + public static int seed = -1; + + private Cache cache; + private float offsetX = 0; + private float offsetZ = 0; + private final String bits = System.getProperty("sun.arch.data.model"); + private final BiomeProvider biomeProvider = new BiomeProvider(); + + @Override + public Cache getCache() { + return cache; + } + + @Override + public void setup() { + super.setup(); + if (random) { + setSeed(new Random(System.currentTimeMillis()).nextInt()); + offsetX = 0; + offsetZ = 0; + } + setSeed(seed); + } + + public void setSeed(int seed) { + Main.seed = seed; + cache = new Cache(seed); + System.out.println(seed); + } + + @Override + public float color(Cell cell) { + switch (controller.getColorMode()) { + case STEEPNESS: + return hue(1 - cell.steepness, 64, 70); + case TEMPERATURE: + return hue(1 - cell.temperature, 64, 70); + case MOISTURE: + return hue(cell.moisture, 64, 70); + case TERRAIN_TYPE: + if (cell.tag == getCache().getTerrain().volcano) { + return 0F; + } + return 20 + (cell.tag.getId() / (float) Terrain.MAX_ID.get()) * 80; + case ELEVATION: + float value = (cell.value - 0.245F) / 0.65F; + return (1 - value) * 30; + case BIOME: + BiomeData biome = biomeProvider.getBiome(cell); + if (biome == null) { + return 0F; + } + return cell.biome * 70; + case CONTINENT: + return cell.continent * 70; + default: + return 50; + } + } + + @Override + public void draw() { + int nextSeed = controller.getNewSeed(); + if (nextSeed == 1) { + setup(); + } else if (nextSeed != 0) { + setSeed(nextSeed); + } + + offsetX += controller.velocityX() * controller.zoomLevel() * controller.zoomLevel(); + offsetZ += controller.velocityY() * controller.zoomLevel() * controller.zoomLevel(); + cache.update(offsetX, offsetZ, controller.zoomLevel(), controller.filters()); + + // color stuff + noStroke(); + background(0); + colorMode(HSB, 100); + + // lighting + ambientLight(0, 0, 75, width / 2, -height, height / 2); + pointLight(0, 0, 50, width / 2, -height * 100, height / 2); + + // render + pushMatrix(); + controller.apply(this); +// translate(-width / 2F, -height / 2F); + drawTerrain(controller.zoomLevel()); + drawCompass(); +// translate(0, 0, 255 * (width / (float) controller.resolution()) / controller.zoomLevel()); +// mesh.renderWind(controller.resolution(), controller.zoomLevel()); + popMatrix(); + + pushMatrix(); + translate(0, 0, -1); +// drawGradient(0, height - 150, 100F, width, 150); + popMatrix(); + + drawStats(); + drawBiomeKey(); + drawControls(); + } + + private void drawGradient(int x, int y, float d, float w, float h) { + noFill(); + for (int dy = 0; dy <= h; dy++) { + float dist = Math.min(1, dy / d); + stroke(0, 0, 0, dist * 100F); + line(x, y + dy, x + w, y + dy); + } + noStroke(); + } + + private void drawStats() { + int resolution = cache.getRegion().getBlockSize().size; + int blocks = NoiseUtil.round(resolution * controller.zoomLevel()); + + String[][] info = { + {"Java:", String.format("x%s", bits)}, + {"Fps: ", String.format("%.3f", frameRate)}, + {"Seed:", String.format("%s", seed)}, + {"Zoom: ", String.format("%.2f", controller.zoomLevel())}, + {"Area: ", String.format("%sx%s [%sx%s]", blocks, blocks, resolution, resolution)}, + {"Center: ", String.format("x=%.0f, y=%s, z=%.0f", offsetX, cache.getCenterHeight(), offsetZ)}, + {"Terrain: ", String.format("%s:%s", cache.getCenterTerrain().getName(), + cache.getCenterBiomeType().name())}, + {"Biome: ", String.format("%s", biomeProvider.getBiome(cache.getCenterCell()).name)}, + {"Overlay: ", colorModeName()}, + }; + + int widest = 0; + for (String[] s : info) { + widest = Math.max(widest, (int) textWidth(s[0])); + } + + int top = 20; + int lineHeight = 15; + for (String[] s : info) { + leftAlignText(10, top, 0, s[0]); + top += lineHeight; + } + + top = 20; + for (String[] s : info) { + if (s.length == 2) { + leftAlignText(12 + widest, top, 0, s[1]); + top += lineHeight; + } + } + } + + private void drawBiomeKey() { + int top = 20; + int lineHeight = 15; + int widest = 0; + for (BiomeType type : BiomeType.values()) { + widest = Math.max(widest, (int) textWidth(type.name())); + } + + int left = width - widest - lineHeight - 15; + for (BiomeType type : BiomeType.values()) { + leftAlignText(left, top, 0, type.name()); + float[] hsb = Color.RGBtoHSB(type.getColor().getRed(), type.getColor().getGreen(), + type.getColor().getBlue(), null); + fill(0, 0, 100); + rect(width - lineHeight - 11, top - lineHeight + 1, lineHeight + 2, lineHeight + 2); + + fill(hsb[0] * 100, hsb[1] * 100, hsb[2] * 100); + rect(width - lineHeight - 10, top - lineHeight + 2, lineHeight, lineHeight); + top += lineHeight; + } + } + + private void drawControls() { + String[][][] columns = { + { + {"Mouse-Left + Move", " - Rotate terrain"}, + {"Mouse-Right + Move", " - Pan terrain"}, + {"Mouse-Scroll + Move", " - Zoom camera"}, + {"Mouse-Scroll + LCTRL + Move", " - Zoom terrain"}, + {"WASD", "- Move terrain"} + }, { + {"Key 1-8", "- Select overlay"}, + {"Key R", "- Toggle mesh renderer"}, + {"Key F", "- Toggle filters"}, + {"Key N", "- Generate new world"}, + {"Key C", "- Copy seed to clipboard"}, + {"Key V", "- Paste seed from clipboard"}, + }}; + + int lineHeight = 15; + int rows = 0; + for (String[][] column : columns) { + rows = Math.max(rows, column.length); + } + + int left = 10; + int widest = 0; + for (String[][] column : columns) { + int top = (height - 10) - ((rows - 1) * lineHeight); + + for (String[] row : column) { + int width = 0; + for (String cell : row) { + leftAlignText(left + width, top, 0, cell); + width += (int) textWidth(cell); + } + top += lineHeight; + widest = Math.max(widest, width); + } + + left += widest + 10; + } + } +} diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/biome/BiomeColor.java b/TerraForgedApp/src/main/java/com/terraforged/app/biome/BiomeColor.java new file mode 100644 index 0000000..a354625 --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/biome/BiomeColor.java @@ -0,0 +1,34 @@ +package com.terraforged.app.biome; + +import me.dags.noise.util.NoiseUtil; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +public class BiomeColor { + + private static final BufferedImage image = load(); + private static final int width = image.getWidth() - 1; + private static final int height = image.getHeight() - 1; + + public static int getRGB(float temp, float moist) { + float humidity = temp * moist; + temp = 1 - temp; + humidity = 1 - humidity; + int x = NoiseUtil.round(temp * width); + int y = NoiseUtil.round(humidity * height); + return image.getRGB(x, y); + } + + + private static BufferedImage load() { + try (InputStream inputStream = BiomeColor.class.getResourceAsStream("/grass.png")) { + return ImageIO.read(inputStream); + } catch (IOException e) { + e.printStackTrace(); + return new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + } + } +} diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/biome/BiomeProvider.java b/TerraForgedApp/src/main/java/com/terraforged/app/biome/BiomeProvider.java new file mode 100644 index 0000000..777b257 --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/biome/BiomeProvider.java @@ -0,0 +1,109 @@ +package com.terraforged.app.biome; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.util.grid.FixedGrid; +import com.terraforged.core.world.biome.BiomeData; +import com.terraforged.core.world.biome.BiomeType; +import com.terraforged.core.world.terrain.Terrain; +import processing.data.JSONArray; +import processing.data.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.*; + +public class BiomeProvider { + + private final List> biomes; + + public BiomeProvider() { + biomes = getBiomes(5); + } + + public BiomeData getBiome(Cell cell) { + FixedGrid grid = biomes.get(cell.biomeType.ordinal()); + if (grid == null) { + return null; + } + return grid.get(cell.moisture, cell.temperature, cell.biome); + } + + private static List> getBiomes(int gridSize) { + List> data = new ArrayList<>(); + for (BiomeType type : BiomeType.values()) { + data.add(type.ordinal(), null); + } + + Map biomes = loadBiomes(); + Map> types = loadBiomeTypes(); + for (Map.Entry> e : types.entrySet()) { + List list = new LinkedList<>(); + for (String id : e.getValue()) { + BiomeData biome = biomes.get(id); + if (biome != null) { + list.add(biome); + } + } + FixedGrid grid = FixedGrid.generate(gridSize, list, b -> b.rainfall, b -> b.temperature); + data.set(e.getKey().ordinal(), grid); + } + + return data; + } + + private static Map loadBiomes() { + try (InputStream inputStream = BiomeProvider.class.getResourceAsStream("/biome_data.json")) { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder sb = new StringBuilder(); + reader.lines().forEach(sb::append); + JSONArray array = JSONArray.parse(sb.toString()); + Map biomes = new HashMap<>(); + for (int i = 0; i < array.size(); i++) { + JSONObject object = array.getJSONObject(i); + String name = object.getString("id"); + float moisture = object.getFloat("moisture"); + float temperature = object.getFloat("temperature"); + int color = BiomeColor.getRGB(temperature, moisture); + BiomeData biome = new BiomeData(name, null, color, moisture, temperature); + biomes.put(name, biome); + } + return biomes; + } catch (IOException e) { + e.printStackTrace(); + return Collections.emptyMap(); + } + } + + private static Map> loadBiomeTypes() { + try (InputStream inputStream = BiomeProvider.class.getResourceAsStream("/biome_groups.json")) { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder sb = new StringBuilder(); + reader.lines().forEach(sb::append); + JSONObject object = JSONObject.parse(sb.toString()); + Iterator iterator = object.keyIterator(); + Map> biomes = new HashMap<>(); + while (iterator.hasNext()) { + String key = "" + iterator.next(); + if (key.contains("rivers")) { + continue; + } + if (key.contains("oceans")) { + continue; + } + BiomeType type = BiomeType.valueOf(key); + List group = new LinkedList<>(); + JSONArray array = object.getJSONArray(key); + for (int i = 0; i < array.size(); i++) { + group.add(array.getString(i)); + } + biomes.put(type, group); + } + return biomes; + } catch (IOException e) { + e.printStackTrace(); + return Collections.emptyMap(); + } + } +} diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/mesh/Mesh.java b/TerraForgedApp/src/main/java/com/terraforged/app/mesh/Mesh.java new file mode 100644 index 0000000..e0e9690 --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/mesh/Mesh.java @@ -0,0 +1,116 @@ +package com.terraforged.app.mesh; + +public class Mesh { + + private final Mesh inner; + private final float x0; + private final float y0; + private final float x1; + private final float y1; + private final float quality; + + public Mesh(Mesh inner, float x0, float y0, float x1, float y1, float quality) { + this.inner = inner; + this.x0 = x0; + this.y0 = y0; + this.x1 = x1; + this.y1 = y1; + this.quality = quality; + } + + public Mesh expand(float scale) { + return expand(scale, scale); + } + + public Mesh expand(float scale, float quality) { + float width0 = getWidth(); + float height0 = getHeight(); + float width1 = width0 * scale; + float height1 = height0 * scale; + float deltaX = (width1 - width0) / 2F; + float deltaY = (height1 - height0) / 2F; + float newQuality = this.quality * quality; + return new Mesh(this, x0 - deltaX, y0 - deltaY, x1 + deltaX, y1 + deltaY, newQuality); + } + + public float getWidth() { + return x1 - x0; + } + + public float getHeight() { + return y1 - y0; + } + + public void start(float width, float height) { + if (inner != null) { + inner.start(width, height); + } + } + + public void render() { + if (inner == null) { + renderNormal(); + } else { + renderCutout(); + inner.render(); + } + } + + public void beginStrip() { + if (inner != null) { + inner.beginStrip(); + } + } + + public void endStrip() { + if (inner != null) { + inner.endStrip(); + } + } + + public void visit(float x, float y) { + if (inner != null) { + inner.visit(x, y); + } + } + + private void renderNormal() { + beginStrip(); + iterate(x0, y0, x1, y1); + endStrip(); + } + + private void renderCutout() { + beginStrip(); + iterate(x0, y0, inner.x1, inner.y0); + endStrip(); + + beginStrip(); + iterate(inner.x1, y0, x1, inner.y1); + endStrip(); + + beginStrip(); + iterate(inner.x0, inner.y1, x1, y1); + endStrip(); + + beginStrip(); + iterate(x0, inner.y0, inner.x0, y1); + endStrip(); + } + + private void iterate(float minX, float minY, float maxX, float maxY) { + float x = minX - quality; + float y = minY - quality; + while (y < maxY) { + y = Math.min(y + quality, maxY); + beginStrip(); + while (x < maxX) { + x = Math.min(x + quality, maxX); + visit(x, y); + visit(x, y + quality); + } + x = minX - quality; + endStrip(); + } + } +} diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/mesh/NoiseMesh.java b/TerraForgedApp/src/main/java/com/terraforged/app/mesh/NoiseMesh.java new file mode 100644 index 0000000..6beae8d --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/mesh/NoiseMesh.java @@ -0,0 +1,42 @@ +package com.terraforged.app.mesh; + +import com.terraforged.app.renderer.Renderer; +import com.terraforged.app.Applet; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.terrain.Terrain; +import processing.core.PApplet; + +public class NoiseMesh extends Mesh { + + private final Applet applet; + private final Renderer renderer; + private final Cell cell = new Cell<>(); + + public NoiseMesh(Applet applet, Renderer renderer, float x0, float y0, float x1, float y1) { + super(null, x0, y0, x1, y1, 1F); + this.applet = applet; + this.renderer = renderer; + } + + @Override + public void start(float width, float height) { + applet.noStroke(); + } + + @Override + public void beginStrip() { + applet.beginShape(PApplet.TRIANGLE_STRIP); + } + + @Override + public void endStrip() { + applet.endShape(); + } + + @Override + public void visit(float x, float y) { + float height = cell.value * 255; + float surface = renderer.getSurface(cell, height, 63, 10F); + applet.vertex(x * 10F, y * 10F, (int) surface); + } +} diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/mesh/TestRenderer.java b/TerraForgedApp/src/main/java/com/terraforged/app/mesh/TestRenderer.java new file mode 100644 index 0000000..169a0ac --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/mesh/TestRenderer.java @@ -0,0 +1,33 @@ +package com.terraforged.app.mesh; + +import com.terraforged.app.renderer.Renderer; +import com.terraforged.app.Applet; + +public class TestRenderer extends Renderer { + + private static boolean printed = false; + + public TestRenderer(Applet visualizer) { + super(visualizer); + } + + @Override + public void render(float zoom) { + Mesh mesh = new NoiseMesh(applet, this, -64, -64, 64, 64); + for (int i = 0; i < 1; i++) { +// mesh = mesh.expand(4); + } + + float width = mesh.getWidth(); + float height = mesh.getHeight(); + if (!printed) { + printed = true; + System.out.println(width + "x" + height); + } + + applet.pushMatrix(); + mesh.start(mesh.getWidth(), mesh.getHeight()); + mesh.render(); + applet.popMatrix(); + } +} diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/renderer/MeshRenderer.java b/TerraForgedApp/src/main/java/com/terraforged/app/renderer/MeshRenderer.java new file mode 100644 index 0000000..23540bf --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/renderer/MeshRenderer.java @@ -0,0 +1,50 @@ +package com.terraforged.app.renderer; + +import com.terraforged.app.Applet; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.heightmap.Levels; +import com.terraforged.core.world.terrain.Terrain; +import processing.core.PApplet; + +public class MeshRenderer extends Renderer { + + public MeshRenderer(Applet visualizer) { + super(visualizer); + } + + @Override + public void render(float zoom) { + float seaLevel = new Levels(applet.getCache().getSettings().generator).water; + int worldHeight = applet.getCache().getSettings().generator.world.worldHeight; + int waterLevel = (int) (seaLevel * worldHeight); + int seabedLevel = (int) ((seaLevel - 0.04) * worldHeight); + int resolution = applet.getCache().getRegion().getBlockSize().size; + + float w = applet.width / (float) (resolution - 1); + float h = applet.width / (float) (resolution - 1); + + applet.noStroke(); + applet.pushMatrix(); + applet.translate(-applet.width / 2F, -applet.width / 2F); + + for (int dy = 0; dy < resolution - 1; dy++) { + applet.beginShape(PApplet.TRIANGLE_STRIP); + for (int dx = 0; dx < resolution; dx++) { + draw(dx, dy, w, h, zoom, worldHeight, waterLevel, seabedLevel); + draw(dx, dy + 1, w, h, zoom, worldHeight, waterLevel, resolution / 2); + } + applet.endShape(); + } + + applet.popMatrix(); + } + + private void draw(int dx, int dz, float w, float h, float zoom, int worldHeight, int waterLevel, int center) { + Cell cell = applet.getCache().getRegion().getCell(dx, dz); + float height = (cell.value * worldHeight); + float x = dx * w; + float z = dz * h; + float y = (int) getSurface(cell, height, waterLevel, 1); + applet.vertex(x, z, y / (zoom * 0.2F)); + } +} diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/renderer/Renderer.java b/TerraForgedApp/src/main/java/com/terraforged/app/renderer/Renderer.java new file mode 100644 index 0000000..e9eebbc --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/renderer/Renderer.java @@ -0,0 +1,105 @@ +package com.terraforged.app.renderer; + +import com.terraforged.app.Applet; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.terrain.Terrain; + +import java.awt.*; + +public abstract class Renderer { + + protected final Applet applet; + + protected Renderer(Applet visualizer) { + this.applet = visualizer; + } + + public float getSurface(Cell cell, float height, int waterLevel, float el) { + if (cell.tag == applet.getCache().getTerrain().volcanoPipe) { + applet.fill(2, 80, 64); + return height * 0.95F * el; + } + if (height < waterLevel) { + float temp = cell.temperature; + float tempDelta = temp > 0.5 ? temp - 0.5F : -(0.5F - temp); + float tempAlpha = (tempDelta / 0.5F); + float hueMod = 4 * tempAlpha; + + float depth = (waterLevel - height) / (float) (90); + float darkness = (1 - depth); + float darknessMod = 0.5F + (darkness * 0.5F); + + applet.fill(60 - hueMod, 65, 90 * darknessMod); + return height * el; + } else if (applet.controller.getColorMode() == Applet.ELEVATION) { + float hei = Math.min(1, Math.max(0, height - waterLevel) / (255F - waterLevel)); + float temp = cell.temperature; + float moist = Math.min(temp, cell.moisture); + + float hue = 35 - (temp * (1 - moist)) * 25; + float sat = 75 * (1 - hei); + float bri = 50 + 40 * hei; + applet.fill(hue, sat, bri); + return height * el; + } else if (applet.controller.getColorMode() == Applet.BIOME_TYPE) { + Color c = cell.biomeType.getColor(); + float[] hsb = Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), null); + float bri = 90 + cell.biomeTypeMask * 10; + applet.fill(hsb[0] * 100, hsb[1] * 100, hsb[2] * bri); + return height * el; + } else if (applet.controller.getColorMode() == Applet.BIOME) { + float hue = applet.color(cell); + float sat = 70; + float bright = 50 + 50 * cell.riverMask; + applet.fill(hue, sat, bright); + return height * el; + } else if(applet.controller.getColorMode() == Applet.TERRAIN_TYPE) { + float hue = applet.color(cell); + if (cell.tag == applet.getCache().getTerrain().coast) { + hue = 15; + } + float modifier = cell.mask; + float modAlpha = 0.1F; + float mod = (1 - modAlpha) + (modifier * modAlpha); + float sat = 70; + float bri = 70; + applet.fill(hue, 65, 70); + return height * el; + } else if(applet.controller.getColorMode() == Applet.EROSION) { + float change = cell.sediment + cell.erosion; + float value = Math.abs(cell.sediment * 250); + value = Math.max(0, Math.min(1, value)); + float hue = value * 70; + float sat = 70; + float bri = 70; + applet.fill(hue, sat, bri); + return height * el; + } else { + float hue = applet.color(cell); + float sat = 70; + float bri = 70; + applet.fill(hue, sat, bri); + return height * el; + } + } + + public abstract void render(float zoom); + + private void renderGradLine(int steps, float x1, float y1, float x2, float y2, float hue1, float hue2, float sat, float bright) { + float dx = x2 - x1; + float dy = y2 - y1; + float fx = dx / steps; + float fy = dy / steps; + float dhue = hue2 - hue1; + float fhue = dhue / steps; + for (int i = 0; i < steps; i++) { + float px1 = x1 + (i * fx); + float py1 = y1 + (i * fy); + float px2 = x2 + ((i + 1) * fx); + float py2 = y2 + ((i + 1) * fy); + float hue = (i + 1) * fhue; + applet.stroke(hue1 + hue, sat, bright); + applet.line(px1, py1, px2, py2); + } + } +} diff --git a/TerraForgedApp/src/main/java/com/terraforged/app/renderer/VoxelRenderer.java b/TerraForgedApp/src/main/java/com/terraforged/app/renderer/VoxelRenderer.java new file mode 100644 index 0000000..795b6f9 --- /dev/null +++ b/TerraForgedApp/src/main/java/com/terraforged/app/renderer/VoxelRenderer.java @@ -0,0 +1,130 @@ +package com.terraforged.app.renderer; + +import com.terraforged.app.Applet; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.heightmap.Levels; +import com.terraforged.core.world.terrain.Terrain; +import processing.core.PApplet; + +public class VoxelRenderer extends Renderer { + + public VoxelRenderer(Applet visualizer) { + super(visualizer); + } + + @Override + public void render(float zoom) { + Levels levels = new Levels(applet.getCache().getSettings().generator); + int worldHeight = applet.getCache().getSettings().generator.world.worldHeight; + int waterLevel = levels.waterY; + int resolution = applet.getCache().getRegion().getBlockSize().size; + int center = resolution / 2; + + float w = applet.width / (float) resolution; + float h = applet.width / (float) resolution; + float el = w / zoom; + + applet.pushMatrix(); + applet.translate(-applet.width / 2F, -applet.width / 2F); + + int difX = Math.max(0, applet.getCache().getRegion().getBlockSize().size - resolution); + int difY = Math.max(0, applet.getCache().getRegion().getBlockSize().size - resolution); + int offsetX = difX / 2; + int offsetZ = difY / 2; + + for (int dy = 0; dy < resolution; dy++) { + for (int dx = 0; dx < resolution; dx++) { + Cell cell = applet.getCache().getRegion().getCell(dx + offsetX, dy + offsetZ); + + float cellHeight = cell.value * worldHeight; + int height = Math.min(worldHeight, Math.max(0, (int) cellHeight)); + + if (height < 0) { + continue; + } + + float x0 = dx * w; + float x1 = (dx + 1) * w; + float z0 = dy * h; + float z1 = (dy + 1) * h; + float y = getSurface(cell, height, waterLevel, el); + + if ((dx == center && (dy == center || dy - 1 == center || dy + 1 == center)) + || (dy == center && (dx - 1 == center || dx + 1 == center))) { + applet.fill(100F, 100F, 100F); + } + + drawColumn(x0, z0, 0, x1, z1, y); + } + } + + drawRulers(16, worldHeight, resolution, h, el); + + applet.popMatrix(); + } + + private void drawRulers(int step, int max, int resolution, float unit, float height) { + float width = (resolution + 1) * unit; + int doubleStep = step * 2; + + for (int dz = 0; dz <= 1; dz++) { + for (int dx = 0; dx <= 1; dx++) { + float x0 = dx * width; + float x1 = x0 - unit; + float z0 = dz * width; + float z1 = z0 - unit; + for (int dy = 0; dy < max; dy += step) { + float y0 = dy * height; + float y1 = y0 + (step * height); + float h = 100, s = 100, b = 100; + if ((dy % doubleStep) != step) { + s = 0; + } + applet.fill(h, s, b); + drawColumn(x0, z0, y0, x1, z1, y1); + } + } + } + } + + private void drawColumn(float x0, float y0, float z0, float x1, float y1, float z1) { + applet.beginShape(PApplet.QUADS); + // +Z "front" face + applet.vertex(x0, y0, z1); + applet.vertex(x1, y0, z1); + applet.vertex(x1, y1, z1); + applet.vertex(x0, y1, z1); + + // -Z "back" face + applet.vertex(x1, y0, z0); + applet.vertex(x0, y0, z0); + applet.vertex(x0, y1, z0); + applet.vertex(x1, y1, z0); + + // +Y "bottom" face + applet.vertex(x0, y1, z1); + applet.vertex(x1, y1, z1); + applet.vertex(x1, y1, z0); + applet.vertex(x0, y1, z0); + + // -Y "top" face + applet.vertex(x0, y0, z0); + applet.vertex(x1, y0, z0); + applet.vertex(x1, y0, z1); + applet.vertex(x0, y0, z1); + + // +X "right" face + applet.vertex(x1, y0, z1); + applet.vertex(x1, y0, z0); + applet.vertex(x1, y1, z0); + applet.vertex(x1, y1, z1); + + // -X "left" face + applet.vertex(x0, y0, z0); + applet.vertex(x0, y0, z1); + applet.vertex(x0, y1, z1); + applet.vertex(x0, y1, z0); + + applet.endShape(); + } +} diff --git a/TerraForgedApp/src/main/resources/biome_data.json b/TerraForgedApp/src/main/resources/biome_data.json new file mode 100644 index 0000000..1de2bf8 --- /dev/null +++ b/TerraForgedApp/src/main/resources/biome_data.json @@ -0,0 +1,764 @@ +[ + { + "id": "biomesoplenty:alps", + "moisture": 0.2, + "temperature": 0.1, + "color": -2302756 + }, + { + "id": "biomesoplenty:alps_foothills", + "moisture": 0.2, + "temperature": 0.1, + "color": -10461088 + }, + { + "id": "biomesoplenty:bayou", + "moisture": 0.59999996, + "temperature": 0.58000004, + "color": -13592211 + }, + { + "id": "biomesoplenty:bog", + "moisture": 0.59999996, + "temperature": 0.42, + "color": -13592211 + }, + { + "id": "biomesoplenty:boreal_forest", + "moisture": 0.4, + "temperature": 0.32, + "color": -13592211 + }, + { + "id": "biomesoplenty:brushland", + "moisture": 0.06666667, + "temperature": 0.8, + "color": -13592211 + }, + { + "id": "biomesoplenty:chaparral", + "moisture": 0.29999998, + "temperature": 0.52, + "color": -13592211 + }, + { + "id": "biomesoplenty:cherry_blossom_grove", + "moisture": 0.59999996, + "temperature": 0.44, + "color": -13592211 + }, + { + "id": "biomesoplenty:cold_desert", + "moisture": 0.0, + "temperature": 0.3, + "color": -6034953 + }, + { + "id": "biomesoplenty:coniferous_forest", + "moisture": 0.33333334, + "temperature": 0.38, + "color": -13592211 + }, + { + "id": "biomesoplenty:dead_forest", + "moisture": 0.2, + "temperature": 0.32, + "color": -13592211 + }, + { + "id": "biomesoplenty:fir_clearing", + "moisture": 0.33333334, + "temperature": 0.38, + "color": -13592211 + }, + { + "id": "biomesoplenty:floodplain", + "moisture": 0.8, + "temperature": 0.56, + "color": -13592211 + }, + { + "id": "biomesoplenty:flower_meadow", + "moisture": 0.46666667, + "temperature": 0.35999998, + "color": -13592211 + }, + { + "id": "biomesoplenty:grassland", + "moisture": 0.46666667, + "temperature": 0.44, + "color": -13592211 + }, + { + "id": "biomesoplenty:gravel_beach", + "moisture": 0.33333334, + "temperature": 0.44, + "color": -6034953 + }, + { + "id": "biomesoplenty:grove", + "moisture": 0.18333334, + "temperature": 0.52, + "color": -13592211 + }, + { + "id": "biomesoplenty:highland", + "moisture": 0.4, + "temperature": 0.44, + "color": -13592211 + }, + { + "id": "biomesoplenty:highland_moor", + "moisture": 0.4, + "temperature": 0.44, + "color": -13592211 + }, + { + "id": "biomesoplenty:lavender_field", + "moisture": 0.46666667, + "temperature": 0.52, + "color": -13592211 + }, + { + "id": "biomesoplenty:lush_grassland", + "moisture": 0.59999996, + "temperature": 0.58000004, + "color": -13592211 + }, + { + "id": "biomesoplenty:lush_swamp", + "moisture": 0.6666667, + "temperature": 0.48000002, + "color": -13592211 + }, + { + "id": "biomesoplenty:mangrove", + "moisture": 0.46666667, + "temperature": 0.524, + "color": -13592211 + }, + { + "id": "biomesoplenty:maple_woods", + "moisture": 0.53333336, + "temperature": 0.3, + "color": -13592211 + }, + { + "id": "biomesoplenty:marsh", + "moisture": 0.46666667, + "temperature": 0.45999998, + "color": -13592211 + }, + { + "id": "biomesoplenty:meadow", + "moisture": 0.46666667, + "temperature": 0.35999998, + "color": -13592211 + }, + { + "id": "biomesoplenty:mire", + "moisture": 0.59999996, + "temperature": 0.42, + "color": -13592211 + }, + { + "id": "biomesoplenty:mystic_grove", + "moisture": 0.53333336, + "temperature": 0.48000002, + "color": -13592211 + }, + { + "id": "biomesoplenty:oasis", + "moisture": 0.33333334, + "temperature": 1.0, + "color": -6034953 + }, + { + "id": "biomesoplenty:ominous_woods", + "moisture": 0.4, + "temperature": 0.44, + "color": -13592211 + }, + { + "id": "biomesoplenty:orchard", + "moisture": 0.26666668, + "temperature": 0.52, + "color": -13592211 + }, + { + "id": "biomesoplenty:outback", + "moisture": 0.033333335, + "temperature": 1.0, + "color": -6034953 + }, + { + "id": "biomesoplenty:overgrown_cliffs", + "moisture": 0.53333336, + "temperature": 0.58000004, + "color": -13592211 + }, + { + "id": "biomesoplenty:pasture", + "moisture": 0.2, + "temperature": 0.52, + "color": -13592211 + }, + { + "id": "biomesoplenty:prairie", + "moisture": 0.2, + "temperature": 0.52, + "color": -13592211 + }, + { + "id": "biomesoplenty:pumpkin_patch", + "moisture": 0.53333336, + "temperature": 0.35999998, + "color": -13592211 + }, + { + "id": "biomesoplenty:rainforest", + "moisture": 1.0, + "temperature": 0.54, + "color": -13592211 + }, + { + "id": "biomesoplenty:redwood_forest", + "moisture": 0.4, + "temperature": 0.52, + "color": -12427646 + }, + { + "id": "biomesoplenty:redwood_forest_edge", + "moisture": 0.4, + "temperature": 0.52, + "color": -12427646 + }, + { + "id": "biomesoplenty:scrubland", + "moisture": 0.06666667, + "temperature": 0.64, + "color": -13592211 + }, + { + "id": "biomesoplenty:seasonal_forest", + "moisture": 0.53333336, + "temperature": 0.35999998, + "color": -13592211 + }, + { + "id": "biomesoplenty:shield", + "moisture": 0.53333336, + "temperature": 0.35999998, + "color": -13592211 + }, + { + "id": "biomesoplenty:shrubland", + "moisture": 0.033333335, + "temperature": 0.44, + "color": -13592211 + }, + { + "id": "biomesoplenty:silkglade", + "moisture": 0.13333334, + "temperature": 0.5, + "color": -13592211 + }, + { + "id": "biomesoplenty:snowy_coniferous_forest", + "moisture": 0.33333334, + "temperature": 0.1, + "color": -13592211 + }, + { + "id": "biomesoplenty:snowy_fir_clearing", + "moisture": 0.33333334, + "temperature": 0.1, + "color": -13592211 + }, + { + "id": "biomesoplenty:snowy_forest", + "moisture": 0.33333334, + "temperature": 0.1, + "color": -13592211 + }, + { + "id": "biomesoplenty:steppe", + "moisture": 0.033333335, + "temperature": 0.51, + "color": -13592211 + }, + { + "id": "biomesoplenty:temperate_rainforest", + "moisture": 0.8, + "temperature": 0.45999998, + "color": -13592211 + }, + { + "id": "biomesoplenty:temperate_rainforest_hills", + "moisture": 0.8, + "temperature": 0.45999998, + "color": -13592211 + }, + { + "id": "biomesoplenty:tropical_rainforest", + "moisture": 0.6666667, + "temperature": 0.6, + "color": -13592211 + }, + { + "id": "biomesoplenty:tundra", + "moisture": 0.33333334, + "temperature": 0.28, + "color": -13592211 + }, + { + "id": "biomesoplenty:wasteland", + "moisture": 0.0, + "temperature": 1.0, + "color": -12427646 + }, + { + "id": "biomesoplenty:wetland", + "moisture": 0.46666667, + "temperature": 0.44, + "color": -13592211 + }, + { + "id": "biomesoplenty:white_beach", + "moisture": 0.6666667, + "temperature": 0.58000004, + "color": -6034953 + }, + { + "id": "biomesoplenty:woodland", + "moisture": 0.33333334, + "temperature": 0.52, + "color": -13592211 + }, + { + "id": "biomesoplenty:xeric_shrubland", + "moisture": 0.06666667, + "temperature": 0.9, + "color": -6034953 + }, + { + "id": "minecraft:badlands", + "moisture": 0.0, + "temperature": 1.0, + "color": -6034953 + }, + { + "id": "minecraft:badlands_plateau", + "moisture": 0.0, + "temperature": 1.0, + "color": -6034953 + }, + { + "id": "minecraft:bamboo_jungle", + "moisture": 0.59999996, + "temperature": 0.58000004, + "color": -13592211 + }, + { + "id": "minecraft:bamboo_jungle_hills", + "moisture": 0.59999996, + "temperature": 0.58000004, + "color": -13592211 + }, + { + "id": "minecraft:beach", + "moisture": 0.26666668, + "temperature": 0.52, + "color": -6034953 + }, + { + "id": "minecraft:birch_forest", + "moisture": 0.4, + "temperature": 0.44, + "color": -13592211 + }, + { + "id": "minecraft:birch_forest_hills", + "moisture": 0.4, + "temperature": 0.44, + "color": -13592211 + }, + { + "id": "minecraft:cold_ocean", + "moisture": 0.33333334, + "temperature": 0.4, + "color": -13592211 + }, + { + "id": "minecraft:dark_forest", + "moisture": 0.53333336, + "temperature": 0.48000002, + "color": -13592211 + }, + { + "id": "minecraft:dark_forest_hills", + "moisture": 0.53333336, + "temperature": 0.48000002, + "color": -13592211 + }, + { + "id": "minecraft:deep_cold_ocean", + "moisture": 0.33333334, + "temperature": 0.4, + "color": -13592211 + }, + { + "id": "minecraft:deep_frozen_ocean", + "moisture": 0.33333334, + "temperature": 0.4, + "color": -13592211 + }, + { + "id": "minecraft:deep_lukewarm_ocean", + "moisture": 0.33333334, + "temperature": 0.4, + "color": -13592211 + }, + { + "id": "minecraft:deep_ocean", + "moisture": 0.33333334, + "temperature": 0.4, + "color": -13592211 + }, + { + "id": "minecraft:deep_warm_ocean", + "moisture": 0.33333334, + "temperature": 0.4, + "color": -6034953 + }, + { + "id": "minecraft:desert", + "moisture": 0.0, + "temperature": 1.0, + "color": -6034953 + }, + { + "id": "minecraft:desert_hills", + "moisture": 0.0, + "temperature": 1.0, + "color": -6034953 + }, + { + "id": "minecraft:desert_lakes", + "moisture": 0.0, + "temperature": 1.0, + "color": -6034953 + }, + { + "id": "minecraft:eroded_badlands", + "moisture": 0.0, + "temperature": 1.0, + "color": -6034953 + }, + { + "id": "minecraft:flower_forest", + "moisture": 0.53333336, + "temperature": 0.48000002, + "color": -13592211 + }, + { + "id": "minecraft:forest", + "moisture": 0.53333336, + "temperature": 0.48000002, + "color": -13592211 + }, + { + "id": "minecraft:frozen_ocean", + "moisture": 0.33333334, + "temperature": 0.2, + "color": -13592211 + }, + { + "id": "minecraft:frozen_river", + "moisture": 0.33333334, + "temperature": 0.2, + "color": -13592211 + }, + { + "id": "minecraft:giant_spruce_taiga", + "moisture": 0.53333336, + "temperature": 0.3, + "color": -13592211 + }, + { + "id": "minecraft:giant_spruce_taiga_hills", + "moisture": 0.53333336, + "temperature": 0.3, + "color": -13592211 + }, + { + "id": "minecraft:giant_tree_taiga", + "moisture": 0.53333336, + "temperature": 0.32, + "color": -13592211 + }, + { + "id": "minecraft:giant_tree_taiga_hills", + "moisture": 0.53333336, + "temperature": 0.32, + "color": -13592211 + }, + { + "id": "minecraft:gravelly_mountains", + "moisture": 0.2, + "temperature": 0.28, + "color": -13592211 + }, + { + "id": "minecraft:ice_spikes", + "moisture": 0.33333334, + "temperature": 0.2, + "color": -2302756 + }, + { + "id": "minecraft:jungle", + "moisture": 0.59999996, + "temperature": 0.58000004, + "color": -13592211 + }, + { + "id": "minecraft:jungle_hills", + "moisture": 0.59999996, + "temperature": 0.58000004, + "color": -13592211 + }, + { + "id": "minecraft:lukewarm_ocean", + "moisture": 0.33333334, + "temperature": 0.4, + "color": -13592211 + }, + { + "id": "minecraft:modified_badlands_plateau", + "moisture": 0.0, + "temperature": 1.0, + "color": -6034953 + }, + { + "id": "minecraft:modified_gravelly_mountains", + "moisture": 0.2, + "temperature": 0.28, + "color": -13592211 + }, + { + "id": "minecraft:modified_jungle", + "moisture": 0.59999996, + "temperature": 0.58000004, + "color": -13592211 + }, + { + "id": "minecraft:modified_wooded_badlands_plateau", + "moisture": 0.0, + "temperature": 1.0, + "color": -6034953 + }, + { + "id": "minecraft:mountains", + "moisture": 0.2, + "temperature": 0.28, + "color": -13592211 + }, + { + "id": "minecraft:mushroom_field_shore", + "moisture": 0.6666667, + "temperature": 0.56, + "color": -13592211 + }, + { + "id": "minecraft:mushroom_fields", + "moisture": 0.6666667, + "temperature": 0.56, + "color": -13592211 + }, + { + "id": "minecraft:ocean", + "moisture": 0.33333334, + "temperature": 0.4, + "color": -13592211 + }, + { + "id": "minecraft:plains", + "moisture": 0.26666668, + "temperature": 0.52, + "color": -13592211 + }, + { + "id": "minecraft:river", + "moisture": 0.33333334, + "temperature": 0.4, + "color": -13592211 + }, + { + "id": "minecraft:savanna", + "moisture": 0.0, + "temperature": 0.68, + "color": -13592211 + }, + { + "id": "minecraft:savanna_plateau", + "moisture": 0.0, + "temperature": 0.6, + "color": -13592211 + }, + { + "id": "minecraft:shattered_savanna", + "moisture": 0.0, + "temperature": 0.64, + "color": -13592211 + }, + { + "id": "minecraft:shattered_savanna_plateau", + "moisture": 0.0, + "temperature": 0.6, + "color": -13592211 + }, + { + "id": "minecraft:snowy_beach", + "moisture": 0.2, + "temperature": 0.22, + "color": -6034953 + }, + { + "id": "minecraft:snowy_mountains", + "moisture": 0.33333334, + "temperature": 0.2, + "color": -13592211 + }, + { + "id": "minecraft:snowy_taiga", + "moisture": 0.26666668, + "temperature": 0.0, + "color": -13592211 + }, + { + "id": "minecraft:snowy_taiga_hills", + "moisture": 0.26666668, + "temperature": 0.0, + "color": -13592211 + }, + { + "id": "minecraft:snowy_taiga_mountains", + "moisture": 0.26666668, + "temperature": 0.0, + "color": -13592211 + }, + { + "id": "minecraft:snowy_tundra", + "moisture": 0.33333334, + "temperature": 0.2, + "color": -13592211 + }, + { + "id": "minecraft:sunflower_plains", + "moisture": 0.26666668, + "temperature": 0.52, + "color": -13592211 + }, + { + "id": "minecraft:swamp", + "moisture": 0.59999996, + "temperature": 0.52, + "color": -13592211 + }, + { + "id": "minecraft:swamp_hills", + "moisture": 0.59999996, + "temperature": 0.52, + "color": -13592211 + }, + { + "id": "minecraft:taiga", + "moisture": 0.53333336, + "temperature": 0.3, + "color": -13592211 + }, + { + "id": "minecraft:taiga_hills", + "moisture": 0.53333336, + "temperature": 0.3, + "color": -13592211 + }, + { + "id": "minecraft:taiga_mountains", + "moisture": 0.53333336, + "temperature": 0.3, + "color": -13592211 + }, + { + "id": "minecraft:tall_birch_forest", + "moisture": 0.4, + "temperature": 0.44, + "color": -13592211 + }, + { + "id": "minecraft:tall_birch_hills", + "moisture": 0.4, + "temperature": 0.44, + "color": -13592211 + }, + { + "id": "minecraft:warm_ocean", + "moisture": 0.33333334, + "temperature": 0.4, + "color": -6034953 + }, + { + "id": "minecraft:wooded_badlands_plateau", + "moisture": 0.0, + "temperature": 1.0, + "color": -6034953 + }, + { + "id": "minecraft:wooded_hills", + "moisture": 0.53333336, + "temperature": 0.48000002, + "color": -13592211 + }, + { + "id": "minecraft:wooded_mountains", + "moisture": 0.2, + "temperature": 0.28, + "color": -13592211 + }, + { + "id": "terraforged:cold_steppe", + "moisture": 0.06666667, + "temperature": 0.28, + "color": -13592211 + }, + { + "id": "terraforged:savanna_scrub", + "moisture": 0.0, + "temperature": 0.68, + "color": -13592211 + }, + { + "id": "terraforged:shattered_savanna_scrub", + "moisture": 0.0, + "temperature": 0.64, + "color": -13592211 + }, + { + "id": "terraforged:snowy_taiga_scrub", + "moisture": 0.26666668, + "temperature": 0.0, + "color": -13592211 + }, + { + "id": "terraforged:steppe", + "moisture": 0.06666667, + "temperature": 0.48000002, + "color": -13592211 + }, + { + "id": "terraforged:taiga_scrub", + "moisture": 0.53333336, + "temperature": 0.3, + "color": -13592211 + } +] \ No newline at end of file diff --git a/TerraForgedApp/src/main/resources/biome_groups.json b/TerraForgedApp/src/main/resources/biome_groups.json new file mode 100644 index 0000000..5eed0dd --- /dev/null +++ b/TerraForgedApp/src/main/resources/biome_groups.json @@ -0,0 +1,168 @@ +{ + "TROPICAL_RAINFOREST": [ + "biomesoplenty:overgrown_cliffs", + "biomesoplenty:tropical_rainforest", + "minecraft:bamboo_jungle", + "minecraft:bamboo_jungle_hills", + "minecraft:jungle", + "minecraft:jungle_hills", + "minecraft:modified_jungle" + ], + "SAVANNA": [ + "biomesoplenty:brushland", + "biomesoplenty:scrubland", + "biomesoplenty:xeric_shrubland", + "minecraft:savanna", + "minecraft:savanna_plateau", + "minecraft:shattered_savanna", + "minecraft:shattered_savanna_plateau", + "terraforged:savanna_scrub", + "terraforged:shattered_savanna_scrub" + ], + "DESERT": [ + "biomesoplenty:oasis", + "biomesoplenty:outback", + "biomesoplenty:wasteland", + "minecraft:badlands", + "minecraft:badlands_plateau", + "minecraft:desert", + "minecraft:desert_hills", + "minecraft:desert_lakes", + "minecraft:eroded_badlands", + "minecraft:modified_badlands_plateau", + "minecraft:modified_wooded_badlands_plateau", + "minecraft:wooded_badlands_plateau" + ], + "TEMPERATE_RAINFOREST": [ + "biomesoplenty:rainforest", + "biomesoplenty:temperate_rainforest", + "biomesoplenty:temperate_rainforest_hills", + "minecraft:plains" + ], + "TEMPERATE_FOREST": [ + "biomesoplenty:cherry_blossom_grove", + "biomesoplenty:grove", + "biomesoplenty:mystic_grove", + "biomesoplenty:orchard", + "biomesoplenty:pumpkin_patch", + "biomesoplenty:redwood_forest", + "biomesoplenty:redwood_forest_edge", + "biomesoplenty:seasonal_forest", + "biomesoplenty:silkglade", + "biomesoplenty:temperate_rainforest", + "biomesoplenty:temperate_rainforest_hills", + "biomesoplenty:woodland", + "minecraft:birch_forest", + "minecraft:birch_forest_hills", + "minecraft:dark_forest", + "minecraft:flower_forest", + "minecraft:forest", + "minecraft:plains", + "minecraft:tall_birch_forest", + "minecraft:wooded_hills" + ], + "GRASSLAND": [ + "biomesoplenty:bayou", + "biomesoplenty:bog", + "biomesoplenty:chaparral", + "biomesoplenty:floodplain", + "biomesoplenty:grassland", + "biomesoplenty:highland_moor", + "biomesoplenty:lavender_field", + "biomesoplenty:lush_grassland", + "biomesoplenty:lush_swamp", + "biomesoplenty:mangrove", + "biomesoplenty:marsh", + "biomesoplenty:mire", + "biomesoplenty:pasture", + "biomesoplenty:prairie", + "biomesoplenty:shrubland", + "biomesoplenty:steppe", + "biomesoplenty:wetland", + "minecraft:plains", + "minecraft:sunflower_plains", + "minecraft:swamp" + ], + "COLD_STEPPE": [ + "terraforged:cold_steppe" + ], + "STEPPE": [ + "biomesoplenty:steppe", + "terraforged:steppe" + ], + "TAIGA": [ + "biomesoplenty:boreal_forest", + "biomesoplenty:coniferous_forest", + "biomesoplenty:dead_forest", + "biomesoplenty:fir_clearing", + "biomesoplenty:flower_meadow", + "biomesoplenty:maple_woods", + "biomesoplenty:meadow", + "biomesoplenty:ominous_woods", + "biomesoplenty:shield", + "biomesoplenty:tundra", + "minecraft:giant_spruce_taiga", + "minecraft:giant_tree_taiga", + "minecraft:giant_tree_taiga_hills", + "minecraft:taiga", + "minecraft:taiga_hills", + "terraforged:taiga_scrub" + ], + "TUNDRA": [ + "biomesoplenty:alps", + "biomesoplenty:alps_foothills", + "biomesoplenty:snowy_coniferous_forest", + "biomesoplenty:snowy_fir_clearing", + "biomesoplenty:snowy_forest", + "minecraft:ice_spikes", + "minecraft:snowy_taiga", + "minecraft:snowy_taiga_hills", + "minecraft:snowy_tundra", + "terraforged:snowy_taiga_scrub" + ], + "ALPINE": [ + "biomesoplenty:highland", + "minecraft:gravelly_mountains", + "minecraft:modified_gravelly_mountains", + "minecraft:mountains", + "minecraft:snowy_mountains", + "minecraft:snowy_taiga_mountains", + "minecraft:taiga_mountains", + "minecraft:wooded_mountains" + ], + "rivers": { + "COLD": [ + "minecraft:frozen_river" + ], + "MEDIUM": [ + "minecraft:river" + ], + "WARM": [] + }, + "oceans": { + "COLD": [ + "minecraft:cold_ocean", + "minecraft:frozen_ocean" + ], + "MEDIUM": [ + "minecraft:ocean" + ], + "WARM": [ + "minecraft:lukewarm_ocean", + "minecraft:warm_ocean" + ] + }, + "deep_oceans": { + "COLD": [ + "minecraft:deep_cold_ocean", + "minecraft:deep_frozen_ocean" + ], + "MEDIUM": [ + "minecraft:deep_ocean" + ], + "WARM": [ + "minecraft:deep_lukewarm_ocean", + "minecraft:deep_warm_ocean" + ] + } +} \ No newline at end of file diff --git a/TerraForgedApp/src/main/resources/grass.png b/TerraForgedApp/src/main/resources/grass.png new file mode 100644 index 0000000000000000000000000000000000000000..f59dd38b3942f844f952cd1fbd63176df4beba65 GIT binary patch literal 7146 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

GMq3D5LC4$OZ952(rH$x}`Qe54X6n-JY1*tu`jG zAoYI20q1?+MOQffUNcvS&?b%W{`;)d!72ZVsfg<70H3VZZ<;+#8XW3=ylSO=u4yV@ zCQv{W1+sq>x+Fk++uLvW?3nweYC?+H3f~^BOxOOXE?Cm$vphcr?Pqymmb!(EGSqaJ z+r;YGJ)kM$YMRl8$eoWqq;&g?x^5$#r>S-Rn5LPy2sgM<>jA68hSEueS{d=#tHsja*pZY0su)tGubc)>Ud)4quBF}Fv{b)3DwrEZByLTBUl}LZickM|yE{|Eux$u}L ztSU(5g{{fS6R$B!IEI=UKm@plVX$_psS^Xw-UM4VUQWs;@Pu6vdc`3$V)0|mSp|SG zr{0VY#4#wIpAy0!-A8XGj-hB@@M>78@y(=ae5y7lggx^1pMG*rvKw)D9`7V+%i3-N zjL|gtzTj@^&YVXu@i}ZNqLrIlRRtGLP+7zBA0Aa!cYEx7t9q3!5!bms*cRM#-WC3S zq8SnDoLyosq9))v-N!2-Bc6n#g=+wO2!JWW%m*5z{}l8H0tz3&yq_{KpFYNEf!qq>{ zn(!aI?SjzYp+NPMczw#G3kL)pPu6}zT~oM(URkt>;otyNuT@~OAB!%Z@Euss z<`8)0><4&oAcPvJk z{H>C>8wf#w!CX`t7=TjD0$e#Ba#8 z?K}21txacwY3Vh|%6#aXQ$rpd*7auNXVwEJp7Ax3V9C3g=bL-IqRIQI)I`(PUGA|z zBE_Lo{nt=MI70*5oTP(WgW`qmIw&xYEI=z5Cs1}xbqVUGy;8W;YVh}33LcgxQ@^m+ z%iQB<58$iiKr!U&C{doK{+lzLxP2!X`8!tgj2~xTxOl#(dv-NGOg+BdQ_HWORZTrm z-;Vhx>@&=D=m_s{PHKREc3;$^ynmmS^a-Dwd?i+RXZZ%HqP(5%eZ5A}NS{5JOEBOW zpH*Jglx^`2r+-61mYIQnFza8~nq$ZVJ@eb8C)kkEIQ%az)GV#(RUiBZxy^Js!y*9) zZYhcGmkVSsfnawjavEWO*?Q(K|d$pt0mSUKmBdQ zA-@xS5!|{)4FJ_TVE;-6@Y1(&yTkmXJ{ZusbJEZuZm%?(_2nusvK<$GMTWCnjsYn|HBYV`Ns-z57V|2d?Y z|7t+SWBVs{YW;tgO}-CrL=lU_-NU@P1WnuHZ~h1=gs~67V(}8SAC86(_qq!p-OJ6j zOLYn1WwT?O;ha_K)UMz-$(>i;h+2ULBeu>#)wA9Fk)5acn-}eLA*QFkMJv>`A zX1x_1Mc8-DR0V^}%fFkZ%P6Ii+^wun3fFt?yN7A;PMzjO|AK{`ncp8@gq`KM2TXWc znoHC4kncdZ8oWoE-twcf$Eiv5VlVUgy!U$W`R%fS zNw5O@=qB8{Z@jEX>WI;a1tj-CqRGfohUP^oM*R!sxmIyL`KJ^feHwGHp%uxEREwTJ zfDm-={NnZEI}DNmBbBR79mRz3I`a=+W}!5>k+}-VGT`*HoMY>1vs z>al8&`!UF>1zG-{3E(e_B2e-{+VbtG^K;CWv6r)TjC;0BC-YRZmfW3n?;68t)=cx3 z?;)wx2$tUe`Dm0p+SB^Qh5n@gYz9j#R~oVJ9mH~4uJW|R7hlw026trr740#3IJ@I%iVps08WO$EcCc#(W!ompI!*rm7%$sCW=?MF+4KMgIk z=UV0Xl-^#?%5-9b3Gj8iFG+wM$fgC2i@KB|%Xd@z<@8!pgHWcaTLS3HYJo1S2=?jy zuCL}1Rju@@#@qu7KI?-H;ZPUXeanBouMTNG*05sZX)s(VJ|tYepF8Gw9r+0(E&L5} ze~e3H2D|eo>8VKqr9!U`_&YM)S}(Qa*Y%TAX`l@#cD z{*P(vGoI>KFy_a#U-Nu)ov5I6AMzc#dTYT#R1RTi{N!$=psIQ!cVf{yJQeS@+JC4{lSYKJX6b&!$X)N-8&}ntD8E9mO06y~| z3MJV#A>-j$@sV$uD|m1iz`f@?4aVjq*>saUS(Y%9!&B7(k4Vth`tlNF#?^2a~R=FNCTm4JSbi8BK!Wnr#sd>4xe68V7LCGP@yTzaHGyO3w zVK;tBO1xfZmm`jj+xI%brlLqeisNBuxlNxH34s8fb>;yjhGF;kqdK%W!?0-W>P}Xc zrJEs+bX;E2!u2tnDt+%ck@e9nf zJM8!~!4Zi|F^=<%eiQ?c`B309^N7QjJ>8ZZ#C;urU4opA zeID0f*Zg*kV7QHWO!wCdqYTRaI|=+Py$dWq9m6m;9jFDE3#dRQFUt);WvZix)K*0? z#wCqD!O!$%ikyPdFNuS=XlnRC4SLh|71gG!iK}Wm zx@vnAy`ZB(Q2?0SVTj*L0Ze}Id7@vFY~C>Q7su^Xw>~kg!iSoLPA~O1wF-+1l-}#i zE^9dmSF}^$h|v7L{lu$3rYbIV6EyGw;A#T&Ho;bI$P_EuWgZ5ndh%WR2Cay@&wccH z`T~r@O@HNDLPXGm^$Jt)tYxx@p@U7ha}}lvT||0t3xW`B3%~XQ|f=b^0*A*_n+=d{`(twJR-72V+RRD znVP%pyJ@ckKTXzpw&^`0aY$tSY>m1d+XxaFdA8n|_d-NuH*(k~pSK|sNB!%wu*(OIX9UyTTZ_sE9=2g!fj&9l0KVaVezW(cO-eZv)Xvtvz4;@g bP!ew$x|6b$Cp?o}+5k;8UDdKXR)PNq_#Qy4 literal 0 HcmV?d00001 diff --git a/TerraForgedCore/build.gradle b/TerraForgedCore/build.gradle new file mode 100644 index 0000000..664feb8 --- /dev/null +++ b/TerraForgedCore/build.gradle @@ -0,0 +1,5 @@ +apply plugin: "java" + +dependencies { + compile project(":Noise2D") +} \ No newline at end of file diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/cell/Cell.java b/TerraForgedCore/src/main/java/com/terraforged/core/cell/Cell.java new file mode 100644 index 0000000..8ee2815 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/cell/Cell.java @@ -0,0 +1,98 @@ +package com.terraforged.core.cell; + +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.util.concurrent.ObjectPool; +import com.terraforged.core.world.biome.BiomeType; +import com.terraforged.core.world.terrain.Terrain; + +public class Cell { + + private static final Cell EMPTY = new Cell() { + @Override + public boolean isAbsent() { + return true; + } + }; + + private static final ObjectPool> POOL = new ObjectPool<>(100, Cell::new); + + public float continent; + public float continentEdge; + + public float value; + public float biome; + public float biomeMoisture; + public float biomeTemperature; + public float moisture; + public float temperature; + public float steepness; + public float erosion; + public float sediment; + + public float mask = 1F; + public float biomeMask = 1F; + public float biomeTypeMask = 1F; + public float regionMask = 1F; + public float riverMask = 1F; + public BiomeType biomeType = BiomeType.GRASSLAND; + + public T tag = null; + + public void copy(Cell other) { + continent = other.continent; + continentEdge = other.continentEdge; + + value = other.value; + biome = other.biome; + + biomeMask = other.biomeMask; + riverMask = other.riverMask; + biomeMoisture = other.biomeMoisture; + biomeTemperature = other.biomeTemperature; + mask = other.mask; + regionMask = other.regionMask; + moisture = other.moisture; + temperature = other.temperature; + steepness = other.steepness; + erosion = other.erosion; + sediment = other.sediment; + biomeType = other.biomeType; + biomeTypeMask = other.biomeTypeMask; + tag = other.tag; + } + + public float combinedMask(float clamp) { + return NoiseUtil.map(biomeMask * regionMask, 0, clamp, clamp); + } + + public float biomeMask(float clamp) { + return NoiseUtil.map(biomeMask, 0, clamp, clamp); + } + + public float regionMask(float clamp) { + return NoiseUtil.map(regionMask, 0, clamp, clamp); + } + + public boolean isAbsent() { + return false; + } + + @SuppressWarnings("unchecked") + public static Cell empty() { + return EMPTY; + } + + public static ObjectPool.Item> pooled() { + return POOL.get(); + } + + public interface Visitor { + + void visit(Cell cell, int dx, int dz); + } + + public interface ZoomVisitor { + + void visit(Cell cell, float x, float z); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/cell/Extent.java b/TerraForgedCore/src/main/java/com/terraforged/core/cell/Extent.java new file mode 100644 index 0000000..ab7e3ba --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/cell/Extent.java @@ -0,0 +1,10 @@ +package com.terraforged.core.cell; + +import com.terraforged.core.world.terrain.Terrain; + +public interface Extent { + + Cell getCell(int x, int z); + + void visit(int minX, int minZ, int maxX, int maxZ, Cell.Visitor visitor); +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/cell/Populator.java b/TerraForgedCore/src/main/java/com/terraforged/core/cell/Populator.java new file mode 100644 index 0000000..47564c8 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/cell/Populator.java @@ -0,0 +1,23 @@ +package com.terraforged.core.cell; + +import me.dags.noise.Module; +import com.terraforged.core.util.concurrent.ObjectPool; +import com.terraforged.core.world.terrain.Terrain; + +public interface Populator extends Module { + + void apply(Cell cell, float x, float y); + + void tag(Cell cell, float x, float y); + + default float getValue(float x, float z) { + try (ObjectPool.Item> cell = Cell.pooled()) { + return getValue(cell.getValue(), x, z); + } + } + + default float getValue(Cell cell, float x, float z) { + apply(cell, x, z); + return cell.value; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/cell/Tag.java b/TerraForgedCore/src/main/java/com/terraforged/core/cell/Tag.java new file mode 100644 index 0000000..250c93e --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/cell/Tag.java @@ -0,0 +1,6 @@ +package com.terraforged.core.cell; + +public interface Tag { + + String getName(); +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/decorator/Decorator.java b/TerraForgedCore/src/main/java/com/terraforged/core/decorator/Decorator.java new file mode 100644 index 0000000..bbdb2c1 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/decorator/Decorator.java @@ -0,0 +1,9 @@ +package com.terraforged.core.decorator; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.terrain.Terrain; + +public interface Decorator { + + void apply(Cell cell, float x, float y); +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/decorator/DesertStacks.java b/TerraForgedCore/src/main/java/com/terraforged/core/decorator/DesertStacks.java new file mode 100644 index 0000000..70eeadd --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/decorator/DesertStacks.java @@ -0,0 +1,55 @@ +package com.terraforged.core.decorator; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.util.Seed; +import com.terraforged.core.world.biome.BiomeType; +import com.terraforged.core.world.heightmap.Levels; +import com.terraforged.core.world.terrain.Terrain; + +public class DesertStacks implements Decorator { + + private final float minY; + private final float maxY; + private final Module module; + + public DesertStacks(Seed seed, Levels levels) { + Module mask = Source.perlin(seed.next(), 500, 1).clamp(0.7, 1).map(0, 1); + + Module shape = Source.perlin(seed.next(), 25, 1).clamp(0.6, 1).map(0, 1) + .mult(Source.perlin(seed.next(), 8, 1).alpha(0.1)); + + Module top = Source.perlin(seed.next(), 4, 1).alpha(0.25); + + Module scale = Source.perlin(seed.next(), 400, 1) + .clamp(20F / 255F, 25F / 255F); + + Module stack = (x, y) -> { + float value = shape.getValue(x, y); + if (value > 0.3) { + return top.getValue(x, y); + } + return value * 0.95F; + }; + + this.minY = levels.water(0); + this.maxY = levels.water(50); + this.module = stack.scale(scale).mult(mask); + } + + @Override + public void apply(Cell cell, float x, float y) { + if (BiomeType.DESERT != cell.biomeType) { + return; + } + + if (cell.value <= minY || cell.value > maxY) { + return; + } + + float value = module.getValue(x, y); + value *= cell.biomeMask; + cell.value += value; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/decorator/SwampPools.java b/TerraForgedCore/src/main/java/com/terraforged/core/decorator/SwampPools.java new file mode 100644 index 0000000..ce92739 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/decorator/SwampPools.java @@ -0,0 +1,59 @@ +package com.terraforged.core.decorator; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.util.Seed; +import com.terraforged.core.world.heightmap.Levels; +import com.terraforged.core.world.terrain.Terrain; +import com.terraforged.core.world.terrain.Terrains; + +public class SwampPools implements Decorator { + + private final Module module; + private final Levels levels; + private final Terrains terrains; + private final float minY; + private final float maxY; + private final float blendY; + private final float blendRange; + + public SwampPools(Seed seed, Terrains terrains, Levels levels) { + this.levels = levels; + this.terrains = terrains; + this.minY = levels.water(-3); + this.maxY = levels.water(1); + this.blendY = levels.water(4); + this.blendRange = blendY - maxY; + this.module = Source.perlin(seed.next(), 14, 1).clamp(0.45, 0.8).map(0, 1); + } + + @Override + public void apply(Cell cell, float x, float y) { + if (cell.tag == terrains.ocean) { + return; + } + + if (cell.moisture < 0.7 || cell.temperature < 0.3) { + return; + } + + if (cell.value <= levels.water) { + return; + } + + if (cell.value > blendY) { + return; + } + + float alpha = module.getValue(x, y); + if (cell.value > maxY) { + float delta = blendY - cell.value; + float alpha2 = delta / blendRange; + alpha *= alpha2; + } + + cell.value = NoiseUtil.lerp(cell.value, minY, alpha); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/filter/Erosion.java b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Erosion.java new file mode 100644 index 0000000..a55511a --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Erosion.java @@ -0,0 +1,234 @@ +package com.terraforged.core.filter; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.settings.Settings; +import com.terraforged.core.world.heightmap.Levels; + +import java.util.Random; + +public class Erosion implements Filter { + + private int erosionRadius = 3; + private float inertia = 0.05f; + private float sedimentCapacityFactor = 4; + private float minSedimentCapacity = 0.01f; + private float erodeSpeed = 0.3f; + private float depositSpeed = 0.3f; + private float evaporateSpeed = 0.01f; + private float gravity = 8; + private int maxDropletLifetime = 30; + private float initialWaterVolume = 1; + private float initialSpeed = 1; + private final Random random = new Random(); + private final TerrainPos gradient = new TerrainPos(); + private int[][] erosionBrushIndices = new int[0][]; + private float[][] erosionBrushWeights = new float[0][]; + + private final Modifier modifier; + + public Erosion(Settings settings, Levels levels) { + erodeSpeed = settings.filters.erosion.erosionRate; + depositSpeed = settings.filters.erosion.depositeRate; + modifier = Modifier.range(levels.ground, levels.ground(15)); + } + + @Override + public void setSeed(long seed) { + random.setSeed(seed); + } + + @Override + public void apply(Filterable cellMap) { + int size = cellMap.getRawWidth(); + Cell[] cells = cellMap.getBacking(); + + // Create water droplet at random point on map + float posX = random.nextInt(size - 1); + float posY = random.nextInt(size - 1); + float dirX = 0; + float dirY = 0; + float speed = initialSpeed; + float water = initialWaterVolume; + float sediment = 0; + + for (int lifetime = 0; lifetime < maxDropletLifetime; lifetime++) { + int nodeX = (int) posX; + int nodeY = (int) posY; + int dropletIndex = nodeY * size + nodeX; + // Calculate droplet's offset inside the cell (0,0) = at NW node, (1,1) = at SE node + float cellOffsetX = posX - nodeX; + float cellOffsetY = posY - nodeY; + + // Calculate droplet's height and direction of flow with bilinear interpolation of surrounding heights + gradient.update(cells, size, posX, posY); + + // Update the droplet's direction and position (move position 1 unit regardless of speed) + dirX = (dirX * inertia - gradient.gradientX * (1 - inertia)); + dirY = (dirY * inertia - gradient.gradientY * (1 - inertia)); + + // Normalize direction + float len = (float) Math.sqrt(dirX * dirX + dirY * dirY); + if (Float.isNaN(len)) { + len = 0; + } + + if (len != 0) { + dirX /= len; + dirY /= len; + } + + posX += dirX; + posY += dirY; + + // Stop simulating droplet if it's not moving or has flowed over edge of map + if ((dirX == 0 && dirY == 0) || posX < 0 || posX >= size - 1 || posY < 0 || posY >= size - 1) { + break; + } + + // Find the droplet's new height and calculate the deltaHeight + float oldHeight = gradient.height; + float newHeight = gradient.update(cells, size, posX, posY).height; + float deltaHeight = newHeight - oldHeight; + + // Calculate the droplet's sediment capacity (higher when moving fast down a slope and contains lots of water) + float sedimentCapacity = Math.max(-deltaHeight * speed * water * sedimentCapacityFactor, minSedimentCapacity); + + // If carrying more sediment than capacity, or if flowing uphill: + if (sediment > sedimentCapacity || deltaHeight > 0) { + // If moving uphill (deltaHeight > 0) try fill up to the current height, otherwise deposit a fraction of the excess sediment + float amountToDeposit = (deltaHeight > 0) ? Math.min(deltaHeight, sediment) : (sediment - sedimentCapacity) * depositSpeed; + sediment -= amountToDeposit; + + // Add the sediment to the four nodes of the current cell using bilinear interpolation + // Deposition is not distributed over a radius (like erosion) so that it can fill small pits + deposit(cells[dropletIndex], amountToDeposit * (1 - cellOffsetX) * (1 - cellOffsetY)); + deposit(cells[dropletIndex + 1], amountToDeposit * cellOffsetX * (1 - cellOffsetY)); + deposit(cells[dropletIndex + size], amountToDeposit * (1 - cellOffsetX) * cellOffsetY); + deposit(cells[dropletIndex + size + 1], amountToDeposit * cellOffsetX * cellOffsetY); + } else { + // Erode a fraction of the droplet's current carry capacity. + // Clamp the erosion to the change in height so that it doesn't dig a hole in the terrain behind the droplet + float amountToErode = Math.min((sedimentCapacity - sediment) * erodeSpeed, -deltaHeight); + + // Use erosion brush to erode from all nodes inside the droplet's erosion radius + for (int brushPointIndex = 0; brushPointIndex < erosionBrushIndices[dropletIndex].length; brushPointIndex++) { + int nodeIndex = erosionBrushIndices[dropletIndex][brushPointIndex]; + Cell cell = cells[nodeIndex]; + float brushWeight = erosionBrushWeights[dropletIndex][brushPointIndex]; + float weighedErodeAmount = amountToErode * brushWeight; + float deltaSediment = Math.min(cell.value, weighedErodeAmount);//cell.value < weighedErodeAmount) ? cell.value : weighedErodeAmount; + erode(cell, deltaSediment); + sediment += deltaSediment; + } + } + + // Update droplet's speed and water content + speed = (float) Math.sqrt(speed * speed + deltaHeight * gravity); + water *= (1 - evaporateSpeed); + + if (Float.isNaN(speed)) { + speed = 0; + } + } + } + + @Override + public void apply(Filterable map, int iterations) { + if (erosionBrushIndices.length != map.getRawWidth()) { + init(map.getRawWidth(), erosionRadius); + } + + while (iterations-- > 0) { + apply(map); + } + } + + private void init(int size, int radius) { + erosionBrushIndices = new int[size * size][]; + erosionBrushWeights = new float[size * size][]; + + int[] xOffsets = new int[radius * radius * 4]; + int[] yOffsets = new int[radius * radius * 4]; + float[] weights = new float[radius * radius * 4]; + float weightSum = 0; + int addIndex = 0; + + for (int i = 0; i < erosionBrushIndices.length; i++) { + int centreX = i % size; + int centreY = i / size; + + if (centreY <= radius || centreY >= size - radius || centreX <= radius + 1 || centreX >= size - radius) { + weightSum = 0; + addIndex = 0; + for (int y = -radius; y <= radius; y++) { + for (int x = -radius; x <= radius; x++) { + float sqrDst = x * x + y * y; + if (sqrDst < radius * radius) { + int coordX = centreX + x; + int coordY = centreY + y; + + if (coordX >= 0 && coordX < size && coordY >= 0 && coordY < size) { + float weight = 1 - (float) Math.sqrt(sqrDst) / radius; + weightSum += weight; + weights[addIndex] = weight; + xOffsets[addIndex] = x; + yOffsets[addIndex] = y; + addIndex++; + } + } + } + } + } + + int numEntries = addIndex; + erosionBrushIndices[i] = new int[numEntries]; + erosionBrushWeights[i] = new float[numEntries]; + + for (int j = 0; j < numEntries; j++) { + erosionBrushIndices[i][j] = (yOffsets[j] + centreY) * size + xOffsets[j] + centreX; + erosionBrushWeights[i][j] = weights[j] / weightSum; + } + } + } + + private void deposit(Cell cell, float amount) { + float change = modifier.modify(cell, amount); + cell.value += change; + cell.sediment += change; + } + + private void erode(Cell cell, float amount) { + float change = modifier.modify(cell, amount); + cell.value -= change; + cell.erosion -= change; + } + + private static class TerrainPos { + private float height; + private float gradientX; + private float gradientY; + + private TerrainPos update(Cell[] nodes, int mapSize, float posX, float posY) { + int coordX = (int) posX; + int coordY = (int) posY; + + // Calculate droplet's offset inside the cell (0,0) = at NW node, (1,1) = at SE node + float x = posX - coordX; + float y = posY - coordY; + + // Calculate heights of the four nodes of the droplet's cell + int nodeIndexNW = coordY * mapSize + coordX; + float heightNW = nodes[nodeIndexNW].value; + float heightNE = nodes[nodeIndexNW + 1].value; + float heightSW = nodes[nodeIndexNW + mapSize].value; + float heightSE = nodes[nodeIndexNW + mapSize + 1].value; + + // Calculate droplet's direction of flow with bilinear interpolation of height difference along the edges + this.gradientX = (heightNE - heightNW) * (1 - y) + (heightSE - heightSW) * y; + this.gradientY = (heightSW - heightNW) * (1 - x) + (heightSE - heightNE) * x; + // Calculate height with bilinear interpolation of the heights of the nodes of the cell + this.height = heightNW * (1 - x) * (1 - y) + heightNE * x * (1 - y) + heightSW * (1 - x) * y + heightSE * x * y; + return this; + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/filter/Filter.java b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Filter.java new file mode 100644 index 0000000..772931c --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Filter.java @@ -0,0 +1,32 @@ +package com.terraforged.core.filter; + +import com.terraforged.core.cell.Cell; + +public interface Filter { + + void apply(Filterable cellMap); + + default void setSeed(long seed) { + + } + + default void apply(Filterable cellMap, int iterations) { + while (iterations-- > 0) { + apply(cellMap); + } + } + + default void iterate(Filterable cellMap, Visitor visitor) { + for (int dz = 0; dz < cellMap.getRawHeight(); dz++) { + for (int dx = 0; dx < cellMap.getRawWidth(); dx++) { + Cell cell = cellMap.getCellRaw(dx, dz); + visitor.visit(cellMap, cell, dx, dz); + } + } + } + + interface Visitor { + + void visit(Filterable cellMap, Cell cell, int dx, int dz); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/filter/Filterable.java b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Filterable.java new file mode 100644 index 0000000..3731cf1 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Filterable.java @@ -0,0 +1,15 @@ +package com.terraforged.core.filter; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Tag; + +public interface Filterable { + + int getRawWidth(); + + int getRawHeight(); + + Cell getCellRaw(int x, int z); + + Cell[] getBacking(); +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/filter/Modifier.java b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Modifier.java new file mode 100644 index 0000000..a21c370 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Modifier.java @@ -0,0 +1,36 @@ +package com.terraforged.core.filter; + +import com.terraforged.core.cell.Cell; + +public interface Modifier { + + float getModifier(float value); + + default float modify(Cell cell, float value) { + return value * getModifier(cell.value); + } + + default Modifier invert() { + return v -> 1 - getModifier(v); + } + + static Modifier range(float minValue, float maxValue) { + return new Modifier() { + + private final float min = minValue; + private final float max = maxValue; + private final float range = maxValue - minValue; + + @Override + public float getModifier(float value) { + if (value > max) { + return 1F; + } + if (value < min) { + return 0F; + } + return (value - min) / range; + } + }; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/filter/Smoothing.java b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Smoothing.java new file mode 100644 index 0000000..fd53893 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Smoothing.java @@ -0,0 +1,60 @@ +package com.terraforged.core.filter; + +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.settings.Settings; +import com.terraforged.core.world.heightmap.Levels; + +public class Smoothing implements Filter { + + private final int radius; + private final float rad2; + private final float strength; + private final Modifier modifier; + + public Smoothing(Settings settings, Levels levels) { + this.radius = NoiseUtil.round(settings.filters.smoothing.smoothingRadius + 0.5F); + this.rad2 = settings.filters.smoothing.smoothingRadius * settings.filters.smoothing.smoothingRadius; + this.strength = settings.filters.smoothing.smoothingRate; + this.modifier = Modifier.range(levels.ground(10), levels.ground(150)).invert(); + } + + @Override + public void apply(Filterable cellMap) { + int maxZ = cellMap.getRawHeight() - radius; + int maxX = cellMap.getRawWidth() - radius; + for (int z = radius; z < maxZ; z++) { + for (int x = radius; x < maxX; x++) { + Cell cell = cellMap.getCellRaw(x, z); + + float total = 0; + float weights = 0; + + for (int dz = -radius; dz <= radius; dz++) { + for (int dx = -radius; dx <= radius; dx++) { + float dist2 = dx * dx + dz * dz; + if (dist2 > rad2) { + continue; + } + int px = x + dx; + int pz = z + dz; + Cell neighbour = cellMap.getCellRaw(px, pz); + if (neighbour.isAbsent()) { + continue; + } + float value = neighbour.value; + float weight = 1F - (dist2 / rad2); + total += (value * weight); + weights += weight; + } + } + + if (weights > 0) { + float dif = cell.value - (total / weights); +// cell.value -= dif * strength; + cell.value -= modifier.modify(cell, dif * strength); + } + } + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/filter/Steepness.java b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Steepness.java new file mode 100644 index 0000000..5aa696f --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/filter/Steepness.java @@ -0,0 +1,53 @@ +package com.terraforged.core.filter; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.terrain.Terrains; + +public class Steepness implements Filter, Filter.Visitor { + + private final int radius; + private final float scaler; + private final Terrains terrains; + + public Steepness(Terrains terrains) { + this(1, 16F, terrains); + } + + public Steepness(int radius, float scaler, Terrains terrains) { + this.radius = radius; + this.scaler = scaler; + this.terrains = terrains; + } + + @Override + public void apply(Filterable cellMap) { + iterate(cellMap, this); + } + + @Override + public void visit(Filterable cellMap, Cell cell, int cx, int cz) { + float totalHeightDif = 0F; + for (int dz = -1; dz <= 2; dz++) { + for (int dx = -1; dx <= 2; dx++) { + if (dx == 0 && dz == 0) { + continue; + } + + int x = cx + dx * radius; + int z = cz + dz * radius; + Cell neighbour = cellMap.getCellRaw(x, z); + if (neighbour.isAbsent()) { + continue; + } + + float height = Math.max(neighbour.value, 62 / 256F); + + totalHeightDif += (Math.abs(cell.value - height) / radius); + } + } + cell.steepness = Math.min(1, totalHeightDif * scaler); + if (cell.tag == terrains.coast && cell.steepness < 0.2F) { + cell.tag = terrains.beach; + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/module/Blender.java b/TerraForgedCore/src/main/java/com/terraforged/core/module/Blender.java new file mode 100644 index 0000000..fdb03c2 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/module/Blender.java @@ -0,0 +1,102 @@ +package com.terraforged.core.module; + +import me.dags.noise.Module; +import me.dags.noise.func.Interpolation; +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.world.terrain.Terrain; + +public class Blender extends Select implements Populator { + + private final Populator lower; + private final Populator upper; + + private final float blendLower; + private final float blendUpper; + private final float blendRange; + private final float midpoint; + private final float tagThreshold; + + private boolean mask = false; + + public Blender(Module control, Populator lower, Populator upper, float min, float max, float split) { + super(control); + this.lower = lower; + this.upper = upper; + this.blendLower = min; + this.blendUpper = max; + this.blendRange = blendUpper - blendLower; + this.midpoint = blendLower + (blendRange * split); + this.tagThreshold = midpoint; + } + + public Blender(Populator control, Populator lower, Populator upper, float min, float max, float split, float tagThreshold) { + super(control); + this.lower = lower; + this.upper = upper; + this.blendLower = min; + this.blendUpper = max; + this.blendRange = blendUpper - blendLower; + this.midpoint = blendLower + (blendRange * split); + this.tagThreshold = tagThreshold; + } + + public Blender mask() { + mask = true; + return this; + } + + @Override + public void apply(Cell cell, float x, float y) { + float select = getSelect(cell, x, y); + + if (select < blendLower) { + lower.apply(cell, x, y); + return; + } + + if (select > blendUpper) { + upper.apply(cell, x, y); + return; + } + + float alpha = Interpolation.LINEAR.apply((select - blendLower) / blendRange); + lower.apply(cell, x, y); + + float lowerVal = cell.value; + Terrain lowerType = cell.tag; + + upper.apply(cell, x, y); + float upperVal = cell.value; + + cell.value = NoiseUtil.lerp(lowerVal, upperVal, alpha); + if (select < midpoint) { + cell.tag = lowerType; + } + + if (mask) { + cell.mask *= alpha; + } + } + + @Override + public void tag(Cell cell, float x, float y) { + float select = getSelect(cell, x, y); + if (select < blendLower) { + lower.tag(cell, x, y); + return; + } + + if (select > blendUpper) { + upper.tag(cell, x, y); + return; + } + + if (select < tagThreshold) { + lower.tag(cell, x, y); + } else { + upper.tag(cell, x, y); + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/module/CellLookup.java b/TerraForgedCore/src/main/java/com/terraforged/core/module/CellLookup.java new file mode 100644 index 0000000..f045f4f --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/module/CellLookup.java @@ -0,0 +1,21 @@ +package com.terraforged.core.module; + +import me.dags.noise.Module; + +public class CellLookup implements Module { + + private final int scale; + private final Module module; + + public CellLookup(Module module, int scale) { + this.module = module; + this.scale = scale; + } + + @Override + public float getValue(float x, float y) { + float px = x * scale; + float pz = y * scale; + return module.getValue(px, pz); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/module/CellLookupOffset.java b/TerraForgedCore/src/main/java/com/terraforged/core/module/CellLookupOffset.java new file mode 100644 index 0000000..91a6fe4 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/module/CellLookupOffset.java @@ -0,0 +1,30 @@ +package com.terraforged.core.module; + +import me.dags.noise.Module; +import me.dags.noise.util.NoiseUtil; + +public class CellLookupOffset implements Module { + + private final int scale; + private final Module lookup; + private final Module direction; + private final Module strength; + + public CellLookupOffset(Module lookup, Module direction, Module strength, int scale) { + this.scale = scale; + this.lookup = lookup; + this.direction = direction; + this.strength = strength; + } + + @Override + public float getValue(float x, float y) { + float px = x * scale; + float pz = y * scale; + float str = strength.getValue(x, y); + float dir = direction.getValue(x, y) * NoiseUtil.PI2; + float dx = NoiseUtil.sin(dir) * str; + float dz = NoiseUtil.cos(dir) * str; + return lookup.getValue(px + dx, pz + dz); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/module/Lerp.java b/TerraForgedCore/src/main/java/com/terraforged/core/module/Lerp.java new file mode 100644 index 0000000..1b37e34 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/module/Lerp.java @@ -0,0 +1,54 @@ +package com.terraforged.core.module; + +import me.dags.noise.Module; +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.world.terrain.Terrain; + +public class Lerp implements Populator { + + private final Module control; + private final Populator lower; + private final Populator upper; + + public Lerp(Module control, Populator lower, Populator upper) { + this.control = control; + this.lower = lower; + this.upper = upper; + } + + @Override + public void apply(Cell cell, float x, float y) { + float alpha = control.getValue(x, y); + cell.regionMask = alpha; + + if (alpha == 0) { + lower.apply(cell, x, y); + return; + } + + if (alpha == 1) { + upper.apply(cell, x, y); + return; + } + + lower.apply(cell, x, y); + float lowerValue = cell.value; + + upper.apply(cell, x, y); + float upperValue = cell.value; + + cell.value = NoiseUtil.lerp(lowerValue, upperValue, alpha); + } + + @Override + public void tag(Cell cell, float x, float y) { + float alpha = control.getValue(x, y); + if (alpha == 0) { + lower.tag(cell, x, y); + return; + } + upper.tag(cell, x, y); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/module/MultiBlender.java b/TerraForgedCore/src/main/java/com/terraforged/core/module/MultiBlender.java new file mode 100644 index 0000000..2260cbf --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/module/MultiBlender.java @@ -0,0 +1,108 @@ +package com.terraforged.core.module; + +import me.dags.noise.func.Interpolation; +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.world.climate.Climate; +import com.terraforged.core.world.terrain.Terrain; + +public class MultiBlender extends Select implements Populator { + + private final Climate climate; + private final Populator lower; + private final Populator middle; + private final Populator upper; + private final float midpoint; + + private final float blendLower; + private final float blendUpper; + + private final float lowerRange; + private final float upperRange; + + private boolean mask = false; + + public MultiBlender(Climate climate, Populator control, Populator lower, Populator middle, Populator upper, float min, float mid, float max) { + super(control); + this.climate = climate; + this.lower = lower; + this.upper = upper; + this.middle = middle; + + this.midpoint = mid; + this.blendLower = min; + this.blendUpper = max; + + this.lowerRange = midpoint - blendLower; + this.upperRange = blendUpper - midpoint; + } + + @Override + public void apply(Cell cell, float x, float y) { + float select = getSelect(cell, x, y); + if (select < blendLower) { + lower.apply(cell, x, y); + return; + } + + if (select > blendUpper) { + upper.apply(cell, x, y); + return; + } + + if (select < midpoint) { + float alpha = Interpolation.CURVE3.apply((select - blendLower) / lowerRange); + + lower.apply(cell, x, y); + float lowerVal = cell.value; + Terrain lowerType = cell.tag; + + middle.apply(cell, x, y); + float upperVal = cell.value; + + cell.value = NoiseUtil.lerp(lowerVal, upperVal, alpha); +// cell.tag = lowerType; + + if (mask) { + cell.mask *= alpha; + } + } else { + float alpha = Interpolation.CURVE3.apply((select - midpoint) / upperRange); + + middle.apply(cell, x, y); + float lowerVal = cell.value; + + upper.apply(cell, x, y); + cell.value = NoiseUtil.lerp(lowerVal, cell.value, alpha); + + if (mask) { + cell.mask *= alpha; + } + } + } + + @Override + public void tag(Cell cell, float x, float y) { + float select = getSelect(cell, x, y); + if (select < blendLower) { + lower.tag(cell, x, y); + return; + } + + if (select > blendUpper) { + upper.tag(cell, x, y); + return; + } + + if (select < midpoint) { + lower.tag(cell, x, y); +// upper.tag(cell, x, y); + if (cell.value > cell.tag.getMax(climate.getRand().getValue(x, y))) { + upper.tag(cell, x, y); + } + } else { + upper.tag(cell, x, y); + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/module/Select.java b/TerraForgedCore/src/main/java/com/terraforged/core/module/Select.java new file mode 100644 index 0000000..0e16c79 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/module/Select.java @@ -0,0 +1,18 @@ +package com.terraforged.core.module; + +import me.dags.noise.Module; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.terrain.Terrain; + +public class Select { + + private final Module control; + + public Select(Module control) { + this.control = control; + } + + public float getSelect(Cell cell, float x, float y) { + return control.getValue(x, y); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/module/Selector.java b/TerraForgedCore/src/main/java/com/terraforged/core/module/Selector.java new file mode 100644 index 0000000..5f8f6cd --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/module/Selector.java @@ -0,0 +1,66 @@ +package com.terraforged.core.module; + +import me.dags.noise.Module; +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.world.terrain.Terrain; +import com.terraforged.core.world.terrain.TerrainPopulator; + +import java.util.LinkedList; +import java.util.List; + +public class Selector implements Populator { + + private final int maxIndex; + private final Module control; + private final Populator[] nodes; + + public Selector(Module control, List populators) { + this.control = control; + this.nodes = getWeightedArray(populators); + this.maxIndex = nodes.length - 1; + } + + @Override + public void apply(Cell cell, float x, float y) { + get(x, y).apply(cell, x, y); + } + + @Override + public void tag(Cell cell, float x, float y) { + get(x, y).tag(cell, x, y); + } + + public Populator get(float x, float y) { + float selector = control.getValue(x, y); + int index = NoiseUtil.round(selector * maxIndex); + return nodes[index]; + } + + private static Populator[] getWeightedArray(List modules) { + float smallest = Float.MAX_VALUE; + for (Populator p : modules) { + if (p instanceof TerrainPopulator) { + smallest = Math.min(smallest, ((TerrainPopulator) p).getType().getWeight()); + } else { + smallest = Math.min(smallest, 1); + } + } + + List result = new LinkedList<>(); + for (Populator p : modules) { + int count; + if (p instanceof TerrainPopulator) { + count = Math.round(((TerrainPopulator) p).getType().getWeight() / smallest); + } else { + count = Math.round(1 / smallest); + } + while (count-- > 0) { + result.add(p); + } + } + + return result.toArray(new Populator[0]); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/region/Region.java b/TerraForgedCore/src/main/java/com/terraforged/core/region/Region.java new file mode 100644 index 0000000..dd9de3c --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/region/Region.java @@ -0,0 +1,311 @@ +package com.terraforged.core.region; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Extent; +import com.terraforged.core.decorator.Decorator; +import com.terraforged.core.filter.Filterable; +import com.terraforged.core.region.chunk.ChunkGenTask; +import com.terraforged.core.region.chunk.ChunkReader; +import com.terraforged.core.region.chunk.ChunkWriter; +import com.terraforged.core.region.chunk.ChunkZoomTask; +import com.terraforged.core.util.concurrent.ThreadPool; +import com.terraforged.core.world.heightmap.Heightmap; +import com.terraforged.core.world.terrain.Terrain; + +import java.util.Collection; +import java.util.function.Consumer; + +public class Region implements Extent { + + private final int regionX; + private final int regionZ; + private final int chunkX; + private final int chunkZ; + private final int blockX; + private final int blockZ; + private final Size blockSize; + private final Size chunkSize; + private final GenCell[] blocks; + private final GenChunk[] chunks; + + public Region(int regionX, int regionZ, int size, int borderChunks) { + this.regionX = regionX; + this.regionZ = regionZ; + this.chunkX = regionX << size; + this.chunkZ = regionZ << size; + this.blockX = Size.chunkToBlock(chunkX); + this.blockZ = Size.chunkToBlock(chunkZ); + this.chunkSize = Size.chunks(size, borderChunks); + this.blockSize = Size.blocks(size, borderChunks); + this.blocks = new GenCell[blockSize.total * blockSize.total]; + this.chunks = new GenChunk[chunkSize.total * chunkSize.total]; + } + + public int getRegionX() { + return regionX; + } + + public int getRegionZ() { + return regionZ; + } + + public int getBlockX() { + return blockX; + } + + public int getBlockZ() { + return blockZ; + } + + public int getChunkCount() { + return chunks.length; + } + + public int getBlockCount() { + return blocks.length; + } + + public Size getChunkSize() { + return chunkSize; + } + + public Size getBlockSize() { + return blockSize; + } + + public Filterable filterable() { + return new FilterRegion(); + } + + @Override + public Cell getCell(int blockX, int blockZ) { + int relBlockX = blockSize.border + blockSize.mask(blockX); + int relBlockZ = blockSize.border + blockSize.mask(blockZ); + int index = blockSize.indexOf(relBlockX, relBlockZ); + return blocks[index]; + } + + public Cell getRawCell(int blockX, int blockZ) { + int index = blockSize.indexOf(blockX, blockZ); + return blocks[index]; + } + + public ChunkReader getChunk(int chunkX, int chunkZ) { + int relChunkX = chunkSize.border + chunkSize.mask(chunkX); + int relChunkZ = chunkSize.border + chunkSize.mask(chunkZ); + int index = chunkSize.indexOf(relChunkX, relChunkZ); + return chunks[index]; + } + + public void generate(Consumer consumer) { + for (int cz = 0; cz < chunkSize.total; cz++) { + for (int cx = 0; cx < chunkSize.total; cx++) { + int index = chunkSize.indexOf(cx, cz); + GenChunk chunk = computeChunk(index, cx, cz); + consumer.accept(chunk); + } + } + } + + public void generate(Heightmap heightmap, ThreadPool.Batcher batcher) { + for (int cz = 0; cz < chunkSize.total; cz++) { + for (int cx = 0; cx < chunkSize.total; cx++) { + int index = chunkSize.indexOf(cx, cz); + GenChunk chunk = computeChunk(index, cx, cz); + Runnable task = new ChunkGenTask(chunk, heightmap); + batcher.submit(task); + } + } + } + + public void generateZoom(Heightmap heightmap, float offsetX, float offsetZ, float zoom, ThreadPool.Batcher batcher) { + float translateX = offsetX - ((blockSize.total * zoom) / 2F); + float translateZ = offsetZ - ((blockSize.total * zoom) / 2F); + for (int cz = 0; cz < chunkSize.total; cz++) { + for (int cx = 0; cx < chunkSize.total; cx++) { + int index = chunkSize.indexOf(cx, cz); + GenChunk chunk = computeChunk(index, cx, cz); + Runnable task = new ChunkZoomTask(chunk, heightmap, translateX, translateZ, zoom); + batcher.submit(task); + } + } + } + + public void decorate(Collection decorators) { + for (int dz = 0; dz < blockSize.total; dz++) { + for (int dx = 0; dx < blockSize.total; dx++) { + int index = blockSize.indexOf(dx, dz); + GenCell cell = blocks[index]; + for (Decorator decorator : decorators) { + decorator.apply(cell, getBlockX() + dx, getBlockZ() + dz); + } + } + } + } + + public void decorateZoom(Collection decorators, float offsetX, float offsetZ, float zoom) { + float translateX = offsetX - ((blockSize.total * zoom) / 2F); + float translateZ = offsetZ - ((blockSize.total * zoom) / 2F); + for (int dz = 0; dz < blockSize.total; dz++) { + for (int dx = 0; dx < blockSize.total; dx++) { + int index = blockSize.indexOf(dx, dz); + GenCell cell = blocks[index]; + for (Decorator decorator : decorators) { + decorator.apply(cell, getBlockX() + translateX + dx, getBlockZ() + translateZ + dz); + } + } + } + } + + public void iterate(Consumer consumer) { + for (int cz = 0; cz < chunkSize.size; cz++) { + int chunkZ = chunkSize.border + cz; + for (int cx = 0; cx < chunkSize.size; cx++) { + int chunkX = chunkSize.border + cx; + int index = chunkSize.indexOf(chunkX, chunkZ); + GenChunk chunk = chunks[index]; + consumer.accept(chunk); + } + } + } + + public void iterate(Cell.Visitor visitor) { + for (int dz = 0; dz < blockSize.size; dz++) { + int z = blockSize.border + dz; + for (int dx = 0; dx < blockSize.size; dx++) { + int x = blockSize.border + dx; + int index = blockSize.indexOf(x, z); + GenCell cell = blocks[index]; + visitor.visit(cell, dx, dz); + } + } + } + + @Override + public void visit(int minX, int minZ, int maxX, int maxZ, Cell.Visitor visitor) { + int regionMinX = getBlockX(); + int regionMinZ = getBlockZ(); + if (maxX < regionMinX || maxZ < regionMinZ) { + return; + } + + int regionMaxX = getBlockX() + getBlockSize().size - 1; + int regionMaxZ = getBlockZ() + getBlockSize().size - 1; + if (minX > regionMaxX || maxZ > regionMaxZ) { + return; + } + + minX = Math.max(minX, regionMinX); + minZ = Math.max(minZ, regionMinZ); + maxX = Math.min(maxX, regionMaxX); + maxZ = Math.min(maxZ, regionMaxZ); + + for (int z = minZ; z <= maxX; z++) { + for (int x = minX; x <= maxZ; x++) { + visitor.visit(getCell(x, z), x, z); + } + } + } + + private GenChunk computeChunk(int index, int chunkX, int chunkZ) { + GenChunk chunk = chunks[index]; + if (chunk == null) { + chunk = new GenChunk(chunkX, chunkZ); + chunks[index] = chunk; + } + return chunk; + } + + private GenCell computeCell(int index) { + GenCell cell = blocks[index]; + if (cell == null) { + cell = new GenCell(); + blocks[index] = cell; + } + return cell; + } + + private static class GenCell extends Cell {} + + private class GenChunk implements ChunkReader, ChunkWriter { + + private final int chunkX; + private final int chunkZ; + private final int blockX; + private final int blockZ; + private final int relBlockX; + private final int relBlockZ; + + private GenChunk(int relChunkX, int relChunkZ) { + this.relBlockX = relChunkX << 4; + this.relBlockZ = relChunkZ << 4; + this.chunkX = Region.this.chunkX + relChunkX; + this.chunkZ = Region.this.chunkZ + relChunkZ; + this.blockX = chunkX << 4; + this.blockZ = chunkZ << 4; + } + + @Override + public int getChunkX() { + return chunkX; + } + + @Override + public int getChunkZ() { + return chunkZ; + } + + @Override + public int getBlockX() { + return blockX; + } + + @Override + public int getBlockZ() { + return blockZ; + } + + @Override + public Cell getCell(int blockX, int blockZ) { + int relX = relBlockX + (blockX & 15); + int relZ = relBlockZ + (blockZ & 15); + int index = blockSize.indexOf(relX, relZ); + return blocks[index]; + } + + @Override + public Cell genCell(int blockX, int blockZ) { + int relX = relBlockX + (blockX & 15); + int relZ = relBlockZ + (blockZ & 15); + int index = blockSize.indexOf(relX, relZ); + return computeCell(index); + } + } + + private class FilterRegion implements Filterable { + + @Override + public int getRawWidth() { + return blockSize.total; + } + + @Override + public int getRawHeight() { + return blockSize.total; + } + + @Override + public Cell[] getBacking() { + return blocks; + } + + @Override + public Cell getCellRaw(int x, int z) { + int index = blockSize.indexOf(x, z); + if (index < 0 || index >= blocks.length) { + return Cell.empty(); + } + return blocks[index]; + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/region/RegionCache.java b/TerraForgedCore/src/main/java/com/terraforged/core/region/RegionCache.java new file mode 100644 index 0000000..e6173f1 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/region/RegionCache.java @@ -0,0 +1,88 @@ +package com.terraforged.core.region; + +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.region.chunk.ChunkReader; +import com.terraforged.core.util.Cache; +import com.terraforged.core.util.FutureValue; +import com.terraforged.core.world.heightmap.RegionExtent; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class RegionCache implements RegionExtent { + + private final boolean queuing; + private final RegionGenerator renderer; + private final Cache> cache; + + private Region cachedRegion = null; + + public RegionCache(boolean queueNeighbours, RegionGenerator renderer) { + this.renderer = renderer; + this.queuing = queueNeighbours; + this.cache = new com.terraforged.core.util.Cache<>(30, 30, TimeUnit.SECONDS); + } + + @Override + public int chunkToRegion(int coord) { + return renderer.chunkToRegion(coord); + } + + @Override + public Future getRegionAsync(int regionX, int regionZ) { + long id = NoiseUtil.seed(regionX, regionZ); + Future future = cache.get(id); + if (future == null) { + future = renderer.getRegionAsync(regionX, regionZ); + cache.put(id, future); + } + return future; + } + + @Override + public ChunkReader getChunk(int chunkX, int chunkZ) { + int regionX = renderer.chunkToRegion(chunkX); + int regionZ = renderer.chunkToRegion(chunkZ); + Region region = getRegion(regionX, regionZ); + return region.getChunk(chunkX, chunkZ); + } + + @Override + public Region getRegion(int regionX, int regionZ) { + if (cachedRegion != null && regionX == cachedRegion.getRegionX() && regionZ == cachedRegion.getRegionZ()) { + return cachedRegion; + } + + long id = NoiseUtil.seed(regionX, regionZ); + Future futureRegion = cache.get(id); + + if (futureRegion == null) { + cachedRegion = renderer.generateRegion(regionX, regionZ); + cache.put(id, new FutureValue<>(cachedRegion)); + } else { + try { + cachedRegion = futureRegion.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + if (queuing) { + queueNeighbours(regionX, regionZ); + } + + return cachedRegion; + } + + private void queueNeighbours(int regionX, int regionZ) { + for (int z = -1; z <= 1; z++) { + for (int x = -1; x <= 1; x++){ + if (x == 0 && z == 0) { + continue; + } + getRegionAsync(regionX + x, regionZ + z); + } + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/region/RegionCacheFactory.java b/TerraForgedCore/src/main/java/com/terraforged/core/region/RegionCacheFactory.java new file mode 100644 index 0000000..296be63 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/region/RegionCacheFactory.java @@ -0,0 +1,8 @@ +package com.terraforged.core.region; + +import com.terraforged.core.world.WorldGeneratorFactory; + +public interface RegionCacheFactory { + + RegionCache create(WorldGeneratorFactory factory); +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/region/RegionGenerator.java b/TerraForgedCore/src/main/java/com/terraforged/core/region/RegionGenerator.java new file mode 100644 index 0000000..c5dc74c --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/region/RegionGenerator.java @@ -0,0 +1,145 @@ +package com.terraforged.core.region; + +import com.terraforged.core.filter.Filterable; +import com.terraforged.core.util.concurrent.ObjectPool; +import com.terraforged.core.util.concurrent.ThreadPool; +import com.terraforged.core.world.WorldGenerator; +import com.terraforged.core.world.WorldGeneratorFactory; +import com.terraforged.core.world.heightmap.RegionExtent; +import com.terraforged.core.world.terrain.Terrain; + +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Future; + +public class RegionGenerator implements RegionExtent { + + private final int factor; + private final int border; + private final ThreadPool threadPool; + private final ObjectPool genPool; + + private RegionGenerator(Builder builder) { + this.factor = builder.factor; + this.border = builder.border; + this.threadPool = builder.threadPool; + this.genPool = new ObjectPool<>(50, builder.factory); + } + + public RegionCache toCache() { + return toCache(true); + } + + public RegionCache toCache(boolean queueNeighbours) { + return new RegionCache(queueNeighbours, this); + } + + @Override + public int chunkToRegion(int i) { + return i >> factor; + } + + @Override + public Region getRegion(int regionX, int regionZ) { + return generateRegion(regionX, regionZ); + } + + @Override + public Future getRegionAsync(int regionX, int regionZ) { + return generate(regionX, regionZ); + } + + public Future generate(int regionX, int regionZ) { + return ForkJoinPool.commonPool().submit(() -> generateRegion(regionX, regionZ)); + } + + public Future generate(float centerX, float centerZ, float zoom, boolean filter) { + return ForkJoinPool.commonPool().submit(() -> generateRegion(centerX, centerZ, zoom, filter)); + } + + public Region generateRegion(int regionX, int regionZ) { + try (ObjectPool.Item item = genPool.get()) { + WorldGenerator generator = item.getValue(); + Region region = new Region(regionX, regionZ, factor, border); + try (ThreadPool.Batcher batcher = threadPool.batcher(region.getChunkCount())) { + region.generate(generator.getHeightmap(), batcher); + } + postProcess(region, generator); + return region; + } + } + + private void postProcess(Region region, WorldGenerator generator) { + Filterable filterable = region.filterable(); + generator.getFilters().setRegion(region.getRegionX(), region.getRegionZ()); + generator.getFilters().getErosion().apply(filterable, generator.getFilters().getSettings().erosion.iterations); + generator.getFilters().getSmoothing().apply(filterable, generator.getFilters().getSettings().smoothing.iterations); + generator.getFilters().getSteepness().apply(filterable); + region.decorate(generator.getDecorators().getDecorators()); + } + + public Region generateRegion(float centerX, float centerZ, float zoom, boolean filter) { + try (ObjectPool.Item item = genPool.get()) { + WorldGenerator generator = item.getValue(); + Region region = new Region(0, 0, factor, border); + try (ThreadPool.Batcher batcher = threadPool.batcher(region.getChunkCount())) { + region.generateZoom(generator.getHeightmap(), centerX, centerZ, zoom, batcher); + } + postProcess(region, generator, centerX, centerZ, zoom, filter); + return region; + } + } + + private void postProcess(Region region, WorldGenerator generator, float centerX, float centerZ, float zoom, + boolean filter) { + Filterable filterable = region.filterable(); + if (filter) { + generator.getFilters().setRegion(region.getRegionX(), region.getRegionZ()); + generator.getFilters().getErosion().apply(filterable, generator.getFilters().getSettings().erosion.iterations); + generator.getFilters().getSmoothing().apply(filterable, generator.getFilters().getSettings().smoothing.iterations); + } + + generator.getFilters().getSteepness().apply(filterable); + +// region.decorateZoom(generator.getDecorators().getDecorators(), centerX, centerZ, zoom); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private int factor = 0; + private int border = 0; + private ThreadPool threadPool; + private WorldGeneratorFactory factory; + + public Builder size(int factor, int border) { + return factor(factor).border(border); + } + + public Builder factor(int factor) { + this.factor = factor; + return this; + } + + public Builder border(int border) { + this.border = border; + return this; + } + + public Builder pool(ThreadPool threadPool) { + this.threadPool = threadPool; + return this; + } + + public Builder factory(WorldGeneratorFactory factory) { + this.factory = factory; + return this; + } + + public RegionGenerator build() { + return new RegionGenerator(this); + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/region/Size.java b/TerraForgedCore/src/main/java/com/terraforged/core/region/Size.java new file mode 100644 index 0000000..e9ec60d --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/region/Size.java @@ -0,0 +1,50 @@ +package com.terraforged.core.region; + +public class Size { + + public final int size; + public final int total; + public final int border; + private final int mask; + + public Size(int size, int border) { + this.size = size; + this.mask = size - 1; + this.border = border; + this.total = size + (2 * border); + } + + public int mask(int i) { + return i & mask; + } + + public int indexOf(int x, int z) { + return z * total + x; + } + + public static int chunkToBlock(int i) { + return i << 4; + } + + public static int blockToChunk(int i) { + return i >> 4; + } + + public static int count(int minX, int minZ, int maxX, int maxZ) { + int dx = maxX - minX; + int dz = maxZ - minZ; + return dx * dz; + } + + public static Size chunks(int factor, int borderChunks) { + int chunks = 1 << factor; + return new Size(chunks, borderChunks); + } + + public static Size blocks(int factor, int borderChunks) { + int chunks = 1 << factor; + int blocks = chunks << 4; + int borderBlocks = borderChunks << 4; + return new Size(blocks, borderBlocks); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkGenTask.java b/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkGenTask.java new file mode 100644 index 0000000..7f66f78 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkGenTask.java @@ -0,0 +1,23 @@ +package com.terraforged.core.region.chunk; + +import com.terraforged.core.world.heightmap.Heightmap; + +public class ChunkGenTask implements Runnable { + + protected final ChunkWriter chunk; + protected final Heightmap heightmap; + + public ChunkGenTask(ChunkWriter chunk, Heightmap heightmap) { + this.chunk = chunk; + this.heightmap = heightmap; + } + + @Override + public void run() { + chunk.generate((cell, dx, dz) -> { + float x = chunk.getBlockX() + dx; + float z = chunk.getBlockZ() + dz; + heightmap.apply(cell, x, z); + }); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkHolder.java b/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkHolder.java new file mode 100644 index 0000000..c988c15 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkHolder.java @@ -0,0 +1,14 @@ +package com.terraforged.core.region.chunk; + +import com.terraforged.core.cell.Extent; + +public interface ChunkHolder extends Extent { + + int getChunkX(); + + int getChunkZ(); + + int getBlockX(); + + int getBlockZ(); +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkReader.java b/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkReader.java new file mode 100644 index 0000000..9115a9a --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkReader.java @@ -0,0 +1,44 @@ +package com.terraforged.core.region.chunk; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.terrain.Terrain; + +public interface ChunkReader extends ChunkHolder { + + @Override + Cell getCell(int dx, int dz); + + @Override + default void visit(int minX, int minZ, int maxX, int maxZ, Cell.Visitor visitor) { + int regionMinX = getBlockX(); + int regionMinZ = getBlockZ(); + if (maxX < regionMinX || maxZ < regionMinZ) { + return; + } + + int regionMaxX = getBlockX() + 15; + int regionMaxZ = getBlockZ() + 15; + if (minX > regionMaxX || maxZ > regionMaxZ) { + return; + } + + minX = Math.max(minX, regionMinX); + minZ = Math.max(minZ, regionMinZ); + maxX = Math.min(maxX, regionMaxX); + maxZ = Math.min(maxZ, regionMaxZ); + + for (int z = minZ; z <= maxX; z++) { + for (int x = minX; x <= maxZ; x++) { + visitor.visit(getCell(x, z), x, z); + } + } + } + + default void iterate(Cell.Visitor visitor) { + for (int dz = 0; dz < 16; dz++) { + for (int dx = 0; dx < 16; dx++) { + visitor.visit(getCell(dx, dz), dx, dz); + } + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkWriter.java b/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkWriter.java new file mode 100644 index 0000000..0297814 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkWriter.java @@ -0,0 +1,17 @@ +package com.terraforged.core.region.chunk; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.terrain.Terrain; + +public interface ChunkWriter extends ChunkHolder { + + Cell genCell(int dx, int dz); + + default void generate(Cell.Visitor visitor) { + for (int dz = 0; dz < 16; dz++) { + for (int dx = 0; dx < 16; dx++) { + visitor.visit(genCell(dx, dz), dx, dz); + } + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkZoomTask.java b/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkZoomTask.java new file mode 100644 index 0000000..41bba4e --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/region/chunk/ChunkZoomTask.java @@ -0,0 +1,26 @@ +package com.terraforged.core.region.chunk; + +import com.terraforged.core.world.heightmap.Heightmap; + +public class ChunkZoomTask extends ChunkGenTask { + + private final float translateX; + private final float translateZ; + private final float zoom; + + public ChunkZoomTask(ChunkWriter chunk, Heightmap heightmap, float translateX, float translateZ, float zoom) { + super(chunk, heightmap); + this.translateX = translateX; + this.translateZ = translateZ; + this.zoom = zoom; + } + + @Override + public void run() { + chunk.generate((cell, dx, dz) -> { + float x = ((chunk.getBlockX() + dx) * zoom) + translateX; + float z = ((chunk.getBlockZ() + dz) * zoom) + translateZ; + heightmap.apply(cell, x, z); + }); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/settings/BiomeSettings.java b/TerraForgedCore/src/main/java/com/terraforged/core/settings/BiomeSettings.java new file mode 100644 index 0000000..76bedde --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/settings/BiomeSettings.java @@ -0,0 +1,79 @@ +package com.terraforged.core.settings; + +import com.terraforged.core.util.serialization.annotation.Comment; +import com.terraforged.core.util.serialization.annotation.Range; +import com.terraforged.core.util.serialization.annotation.Serializable; +import com.terraforged.core.world.biome.BiomeType; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class BiomeSettings { + + public BiomeGroup desert = new BiomeGroup(BiomeType.DESERT); + public BiomeGroup steppe = new BiomeGroup(BiomeType.STEPPE); + public BiomeGroup coldSteppe = new BiomeGroup(BiomeType.COLD_STEPPE); + public BiomeGroup grassland = new BiomeGroup(BiomeType.GRASSLAND); + public BiomeGroup savanna = new BiomeGroup(BiomeType.SAVANNA); + public BiomeGroup taiga = new BiomeGroup(BiomeType.TAIGA); + public BiomeGroup temperateForest = new BiomeGroup(BiomeType.TEMPERATE_FOREST); + public BiomeGroup temperateRainForest = new BiomeGroup(BiomeType.TEMPERATE_RAINFOREST); + public BiomeGroup tropicalRainForest = new BiomeGroup(BiomeType.TROPICAL_RAINFOREST); + public BiomeGroup tundra = new BiomeGroup(BiomeType.TUNDRA); + + public List asList() { + return Arrays.asList( + desert, + steppe, + coldSteppe, + grassland, + savanna, + taiga, + temperateForest, + temperateRainForest, + tropicalRainForest, + tundra + ); + } + + public Map asMap() { + return asList().stream().collect(Collectors.toMap(g -> g.type, g -> g)); + } + + @Serializable + public static class BiomeGroup { + + public BiomeType type; + + public BiomeWeight[] biomes = new BiomeWeight[0]; + + public BiomeGroup() { + + } + + public BiomeGroup(BiomeType biomeType) { + this.type = biomeType; + } + } + + @Serializable + public static class BiomeWeight { + + public String id; + + @Range(min = 0, max = 50) + @Comment("Controls how common this biome type is") + public int weight; + + public BiomeWeight() { + + } + + public BiomeWeight(String id, int weight) { + this.id = id; + this.weight = weight; + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/settings/FilterSettings.java b/TerraForgedCore/src/main/java/com/terraforged/core/settings/FilterSettings.java new file mode 100644 index 0000000..e84b4c4 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/settings/FilterSettings.java @@ -0,0 +1,45 @@ +package com.terraforged.core.settings; + +import com.terraforged.core.util.serialization.annotation.Comment; +import com.terraforged.core.util.serialization.annotation.Range; +import com.terraforged.core.util.serialization.annotation.Serializable; + +@Serializable +public class FilterSettings { + + public Erosion erosion = new Erosion(); + + public Smoothing smoothing = new Smoothing(); + + @Serializable + public static class Erosion { + + @Range(min = 0, max = 30000) + @Comment("Controls the number of erosion iterations") + public int iterations = 15000; + + @Range(min = 0F, max = 1F) + @Comment("Controls how quickly material dissolves (during erosion)") + public float erosionRate = 0.35F; + + @Range(min = 0F, max = 1F) + @Comment("Controls how quickly material is deposited (during erosion)") + public float depositeRate = 0.5F; + } + + @Serializable + public static class Smoothing { + + @Range(min = 0, max = 5) + @Comment("Controls the number of smoothing iterations") + public int iterations = 1; + + @Range(min = 0, max = 5) + @Comment("Controls the smoothing radius") + public float smoothingRadius = 1.75F; + + @Range(min = 0, max = 1) + @Comment("Controls how strongly smoothing is applied") + public float smoothingRate = 0.85F; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/settings/GeneratorSettings.java b/TerraForgedCore/src/main/java/com/terraforged/core/settings/GeneratorSettings.java new file mode 100644 index 0000000..e2b7452 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/settings/GeneratorSettings.java @@ -0,0 +1,200 @@ +package com.terraforged.core.settings; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import com.terraforged.core.util.serialization.annotation.Comment; +import com.terraforged.core.util.serialization.annotation.Range; +import com.terraforged.core.util.serialization.annotation.Serializable; + +@Serializable +public class GeneratorSettings { + + public transient long seed = 0L; + + /** + * WORLD PROPERTIES + */ + public World world = new World(); + + /** + * TERRAIN PROPERTIES + */ + public Land land = new Land(); + + /** + * BIOME PROPERTIES + */ + public Biome biome = new Biome(); + + public BiomeNoise biomeEdgeNoise = new BiomeNoise(); + + /** + * RIVER PROPERTIES + */ + public River primaryRivers = new River(5, 2, 8, 25, 8, 0.75F); + + public River secondaryRiver = new River(4, 1, 6, 15, 5, 0.75F); + + public River tertiaryRivers = new River(3, 0, 4, 10, 4, 0.75F); + + public Lake lake = new Lake(); + + @Serializable + public static class World { + + @Range(min = 0, max = 256) + @Comment("Controls the world height") + public int worldHeight = 256; + + @Range(min = 0, max = 255) + @Comment("Controls the sea level") + public int seaLevel = 63; + + @Range(min = 0F, max = 1F) + @Comment("Controls the amount of ocean between continents") + public float oceanSize = 0.25F; + } + + @Serializable + public static class Land { + + @Range(min = 500, max = 10000) + @Comment("Controls the size of continents") + public int continentScale = 4000; + + @Range(min = 250, max = 5000) + @Comment("Controls the size of mountain ranges") + public int mountainScale = 950; + + @Range(min = 125, max = 2500) + @Comment("Controls the size of terrain regions") + public int regionSize = 1000; + } + + @Serializable + public static class Biome { + + @Range(min = 50, max = 500) + @Comment("Controls the size of individual biomes") + public int biomeSize = 200; + + @Range(min = 1, max = 200) + @Comment("Controls the scale of shape distortion for biomes") + public int biomeWarpScale = 30; + + @Range(min = 1, max = 200) + @Comment("Controls the strength of shape distortion for biomes") + public int biomeWarpStrength = 30; + } + + @Serializable + public static class BiomeNoise { + + @Comment("The noise type") + public Source type = Source.PERLIN; + + @Range(min = 1, max = 100) + @Comment("Controls the scale of the noise") + public int scale = 4; + + @Range(min = 1, max = 5) + @Comment("Controls the number of noise octaves") + public int octaves = 1; + + @Range(min = 0F, max = 5.5F) + @Comment("Controls the gain subsequent noise octaves") + public float gain = 0.5F; + + @Range(min = 0F, max = 10.5F) + @Comment("Controls the lacunarity of subsequent noise octaves") + public float lacunarity = 2F; + + @Range(min = 1, max = 100) + @Comment("Controls the strength of the noise") + public int strength = 12; + + public Module build(int seed) { + return Source.build(seed, scale, octaves).gain(gain).lacunarity(lacunarity).build(type).bias(-0.5); + } + } + + @Serializable + public static class River { + + @Range(min = 1, max = 10) + @Comment("Controls the depth of the river") + public int bedDepth; + + @Range(min = 1, max = 10) + @Comment("Controls the height of river banks") + public int minBankHeight; + + @Range(min = 1, max = 10) + @Comment("Controls the height of river banks") + public int maxBankHeight; + + @Range(min = 1, max = 20) + @Comment("Controls the river-bed width") + public int bedWidth; + + @Range(min = 1, max = 50) + @Comment("Controls the river-banks width") + public int bankWidth; + + @Range(min = 0.0F, max = 1.0F) + @Comment("Controls how much rivers taper") + public float fade; + + public River() { + } + + public River(int depth, int minBank, int maxBank, int outer, int inner, float fade) { + this.minBankHeight = minBank; + this.maxBankHeight = maxBank; + this.bankWidth = outer; + this.bedWidth = inner; + this.bedDepth = depth; + this.fade = fade; + } + } + + public static class Lake { + + @Range(min = 0.0F, max = 1.0F) + @Comment("Controls the chance of a lake spawning") + public float chance = 0.2F; + + @Range(min = 0F, max = 1F) + @Comment("The minimum distance along a river that a lake will spawn") + public float minStartDistance = 0.03F; + + @Range(min = 0F, max = 1F) + @Comment("The maximum distance along a river that a lake will spawn") + public float maxStartDistance = 0.07F; + + @Range(min = 1, max = 20) + @Comment("The max depth of the lake") + public int depth = 10; + + @Range(min = 10, max = 50) + @Comment("The minimum size of the lake") + public int sizeMin = 50; + + @Range(min = 50, max = 150) + @Comment("The maximum size of the lake") + public int sizeMax = 100; + + @Range(min = 1, max = 10) + @Comment("The minimum bank height") + public int minBankHeight = 2; + + @Range(min = 1, max = 10) + @Comment("The maximum bank height") + public int maxBankHeight = 10; + + public Lake() { + + } + + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/settings/Settings.java b/TerraForgedCore/src/main/java/com/terraforged/core/settings/Settings.java new file mode 100644 index 0000000..a09fbbd --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/settings/Settings.java @@ -0,0 +1,15 @@ +package com.terraforged.core.settings; + +import com.terraforged.core.util.serialization.annotation.Serializable; + +@Serializable +public class Settings { + + public GeneratorSettings generator = new GeneratorSettings(); + + public FilterSettings filters = new FilterSettings(); + + public TerrainSettings terrain = new TerrainSettings(); + + public BiomeSettings biomes = new BiomeSettings(); +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/settings/TerrainSettings.java b/TerraForgedCore/src/main/java/com/terraforged/core/settings/TerrainSettings.java new file mode 100644 index 0000000..79dc2d9 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/settings/TerrainSettings.java @@ -0,0 +1,50 @@ +package com.terraforged.core.settings; + +import com.terraforged.core.util.serialization.annotation.Comment; +import com.terraforged.core.util.serialization.annotation.Range; +import com.terraforged.core.util.serialization.annotation.Serializable; + +@Serializable +public class TerrainSettings { + + public transient float deepOcean = 1F; + public transient float ocean = 1F; + public transient float coast = 1F; + public transient float river = 1F; + + @Range(min = 0, max = 10) + @Comment("Controls how common this terrain type is") + public float steppe = 5F; + + @Range(min = 0, max = 10) + @Comment("Controls how common this terrain type is") + public float plains = 5F; + + @Range(min = 0, max = 10) + @Comment("Controls how common this terrain type is") + public float hills = 2F; + + @Range(min = 0, max = 10) + @Comment("Controls how common this terrain type is") + public float dales = 2F; + + @Range(min = 0, max = 10) + @Comment("Controls how common this terrain type is") + public float plateau = 2F; + + @Range(min = 0, max = 10) + @Comment("Controls how common this terrain type is") + public float badlands = 2F; + + @Range(min = 0, max = 10) + @Comment("Controls how common this terrain type is") + public float torridonian = 0.5F; + + @Range(min = 0, max = 10) + @Comment("Controls how common this terrain type is") + public float mountains = 0.5F; + + @Range(min = 0, max = 10) + @Comment("Controls how common this terrain type is") + public float volcano = 1F; +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/Cache.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/Cache.java new file mode 100644 index 0000000..c8659d4 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/Cache.java @@ -0,0 +1,72 @@ +package com.terraforged.core.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public class Cache { + + private final long lifespan; + private final long interval; + private final Map> cache; + + private long lastUpdate = 0L; + + public Cache(long lifespan, long interval, TimeUnit unit) { + this(lifespan, interval, unit, () -> new HashMap<>(100)); + } + + public Cache(long lifespan, long interval, TimeUnit unit, Supplier>> backing) { + this.lifespan = unit.toMillis(lifespan); + this.interval = unit.toMillis(interval); + this.cache = backing.get(); + } + + public V get(K id) { + update(); + CachedValue value = cache.get(id); + if (value != null) { + return value.getValue(); + } + return null; + } + + public void put(K id, V region) { + update(); + cache.put(id, new CachedValue<>(region)); + } + + public void drop(K id) { + update(); + } + + public boolean contains(K id) { + return cache.containsKey(id); + } + + private void update() { + long time = System.currentTimeMillis(); + if (time - lastUpdate < interval) { + return; + } + cache.values().removeIf(value -> time - value.time >= lifespan); + lastUpdate = time; + } + + public static class CachedValue { + + private final T value; + private long time; + + private CachedValue(T value) { + this.value = value; + this.time = System.currentTimeMillis(); + } + + private T getValue() { + time = System.currentTimeMillis(); + return value; + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/FutureValue.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/FutureValue.java new file mode 100644 index 0000000..624f9eb --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/FutureValue.java @@ -0,0 +1,38 @@ +package com.terraforged.core.util; + +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class FutureValue implements Future { + + private final T value; + + public FutureValue(T value) { + this.value = value; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public T get() { + return value; + } + + @Override + public T get(long timeout, TimeUnit unit) { + return value; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/PosIterator.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/PosIterator.java new file mode 100644 index 0000000..39dafb2 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/PosIterator.java @@ -0,0 +1,131 @@ +package com.terraforged.core.util; + +public class PosIterator { + + private final int minX; + private final int minZ; + private final int maxX; + private final int maxY; + private final int maxZ; + private final int size; + + private int x; + private int y; + private int z; + private int index = -1; + + public PosIterator(int x, int y, int z, int width, int height, int length) { + this.x = x - 1; + this.y = y; + this.z = z; + this.minX = x; + this.minZ = z; + this.maxX = x + width; + this.maxY = y + height; + this.maxZ = z + length; + this.size = (width * height * length) - 1; + } + + /** + * Steps the iterator forward one position if there there is a new position within it's x/y/z bounds. + * + * @return true if the an increment was made, false if the maximum x,y,z coord has been reached + */ + public boolean next() { + if (x + 1 < maxX) { + x += 1; + index++; + return true; + } + + if (z + 1 < maxZ) { + x = minX; + z += 1; + index++; + return true; + } + + if (y + 1 < maxY) { + x = minX - 1; + z = minZ; + y += 1; + return true; + } + + return false; + } + + public int size() { + return size; + } + + public int index() { + return index; + } + + public int x() { + return x; + } + + public int y() { + return y; + } + + public int z() { + return z; + } + + /** + * Iterates over a 2D area in the x-z planes, centered on x:z, with the given radius + * + * Iteration Order: + * 1. Increments the x-axis from x - radius to x + radius (inclusive), then: + * 2. Increments the z-axis once, resets the x-axis to x - radius, then: + * 3. Repeats steps 1 & 2 until z reaches z + radius + */ + public static PosIterator radius2D(int x, int z, int radius) { + int startX = x - radius; + int startZ = z - radius; + int size = radius * 2 + 1; + return new PosIterator(startX, 0, startZ, size, 0, size); + } + + /** + * Iterates over a 3D volume, centered on x:y:z, with the given radius + * + * Iteration Order: + * 1. Increments the x-axis (starting from x - radius) up to x + radius (inclusive), then: + * 2. Increments the z-axis once (starting from z - radius) and resets the x-axis to x - radius, then: + * 3. Increments the y-axis once (starting from y - radius), resets the x & z axes to x - radius & z - radius + * respectively, then: + * 4. Repeats steps 1-3 until y reaches y + radius + */ + public static PosIterator radius3D(int x, int y, int z, int radius) { + int startX = x - radius; + int startY = y - radius; + int startZ = z - radius; + int size = radius * 2 + 1; + return new PosIterator(startX, startY, startZ, size, size, size); + } + + public static PosIterator area(int x, int z, int width, int length) { + return new PosIterator(x, 0, z, width, 0, length); + } + + public static PosIterator volume3D(int x, int y, int z, int width, int height, int length) { + return new PosIterator(x, y, z, width, height, length); + } + + public static PosIterator range2D(int minX, int minZ, int maxX, int maxZ) { + int width = maxX - minX; + int length = maxZ - minZ; + return new PosIterator(minX, 0, minZ, width, 0, length); + } + + public static PosIterator range2D(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + int width = 1 + maxX - minX; + int height = 1 + maxY - minY; + int length = 1 + maxZ - minZ; + return new PosIterator(minX, minY, minZ, width, height, length); + } +} \ No newline at end of file diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/Seed.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/Seed.java new file mode 100644 index 0000000..74f94a7 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/Seed.java @@ -0,0 +1,42 @@ +package com.terraforged.core.util; + +public class Seed { + + private final Seed root; + + private final int value; + private int seed; + + public Seed(long seed) { + this((int) seed); + } + + public Seed(int seed) { + this.value = seed; + this.seed = seed; + this.root = this; + } + + private Seed(Seed root) { + this.root = root; + this.seed = root.next(); + this.value = seed; + } + + public int next() { + return ++root.seed; + } + + public int get() { + return value; + } + + public Seed nextSeed() { + return new Seed(root); + } + + public Seed reset() { + this.seed = value; + return this; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/VariablePredicate.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/VariablePredicate.java new file mode 100644 index 0000000..e67e18c --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/VariablePredicate.java @@ -0,0 +1,31 @@ +package com.terraforged.core.util; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.heightmap.Levels; +import com.terraforged.core.world.terrain.Terrain; + +import java.util.function.BiPredicate; + +public class VariablePredicate { + + private final Module module; + private final BiPredicate, Float> predicate; + + public VariablePredicate(Module module, BiPredicate, Float> predicate) { + this.module = module; + this.predicate = predicate; + } + + public boolean test(Cell cell, float x, float z) { + return predicate.test(cell, module.getValue(x, z)); + } + + public static VariablePredicate height(Seed seed, Levels levels, int min, int max, int size, int octaves) { + float bias = levels.scale(min); + float scale = levels.scale(max - min); + Module source = Source.perlin(seed.next(), size, 1).scale(scale).bias(bias); + return new VariablePredicate(source, (cell, height) -> cell.value < height); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/concurrent/ObjectPool.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/concurrent/ObjectPool.java new file mode 100644 index 0000000..eb0ec0f --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/concurrent/ObjectPool.java @@ -0,0 +1,78 @@ +package com.terraforged.core.util.concurrent; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public class ObjectPool { + + private final int capacity; + private final List> pool; + private final Supplier supplier; + + public ObjectPool(int size, Supplier supplier) { + this.capacity = size; + this.pool = new ArrayList<>(size); + this.supplier = supplier; + } + + public Item get() { + synchronized (pool) { + if (pool.size() > 0) { + return pool.remove(pool.size() - 1).retain(); + } + } + return new Item<>(supplier.get(), this); + } + + public int size() { + synchronized (pool) { + return pool.size(); + } + } + + private boolean restore(Item item) { + synchronized (pool) { + int size = pool.size(); + if (size < capacity) { + pool.add(item); + return true; + } + } + return false; + } + + public static class Item implements AutoCloseable { + + private final T value; + private final ObjectPool pool; + + private boolean released = false; + + private Item(T value, ObjectPool pool) { + this.value = value; + this.pool = pool; + } + + public T getValue() { + return value; + } + + public void release() { + if (!released) { + released = true; + released = pool.restore(this); + } + } + + private Item retain() { + released = false; + return this; + } + + @Override + public void close() { + release(); + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/concurrent/ThreadPool.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/concurrent/ThreadPool.java new file mode 100644 index 0000000..507cdbd --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/concurrent/ThreadPool.java @@ -0,0 +1,110 @@ +package com.terraforged.core.util.concurrent; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +public class ThreadPool { + + public static final int DEFAULT_POOL_SIZE = Math.max(2, Runtime.getRuntime().availableProcessors() / 2); + + private static final Object lock = new Object(); + + private static ThreadPool instance = new ThreadPool(); + + private final int size; + private final ExecutorService service; + + private ThreadPool() { + this.service = ForkJoinPool.commonPool(); + this.size = -1; + } + + public ThreadPool(int size) { + this.service = Executors.newFixedThreadPool(size); + this.size = size; + } + + public void shutdown() { + if (size > 0) { + service.shutdown(); + } + } + + public Future submit(Runnable runnable) { + return service.submit(runnable); + } + + public Future submit(Callable callable) { + return service.submit(callable); + } + + public Batcher batcher(int size) { + return new Batcher(size); + } + + public class Batcher implements AutoCloseable { + + private final List> tasks; + + public Batcher(int size) { + tasks = new ArrayList<>(size); + } + + public void submit(Runnable task) { + tasks.add(ThreadPool.this.submit(task)); + } + + public void submit(Callable task) { + tasks.add(ThreadPool.this.submit(task)); + } + + public void await() { + boolean hasMore = true; + while (hasMore) { + hasMore = false; + for (Future future : tasks) { + if (!future.isDone()) { + hasMore = true; + break; + } + } + } + tasks.clear(); + } + + @Override + public void close() { + await(); + } + } + + public static ThreadPool getFixed(int size) { + synchronized (lock) { + if (instance.size != size) { + instance.shutdown(); + instance = new ThreadPool(size); + } + return instance; + } + } + + public static ThreadPool getFixed() { + synchronized (lock) { + if (instance.size == -1) { + instance = new ThreadPool(ThreadPool.DEFAULT_POOL_SIZE); + } + return instance; + } + } + + public static ThreadPool getCommon() { + synchronized (lock) { + if (instance.size != -1) { + instance.shutdown(); + instance = new ThreadPool(); + } + return instance; + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/grid/FixedGrid.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/grid/FixedGrid.java new file mode 100644 index 0000000..b7a93d3 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/grid/FixedGrid.java @@ -0,0 +1,134 @@ +package com.terraforged.core.util.grid; + +import me.dags.noise.util.NoiseUtil; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; + +public class FixedGrid implements Iterable>> { + + private final MappedList>> grid; + + private FixedGrid(MappedList>> grid) { + this.grid = grid; + } + + public int size() { + return grid.size(); + } + + public FixedList get(float x, float y) { + return grid.get(y).get(x); + } + + public T get(float x, float y, float z) { + MappedList> row = grid.get(y); + FixedList cell = row.get(x); + return cell.get(z); + } + + @Override + public Iterator>> iterator() { + return grid.iterator(); + } + + public static FixedGrid create(List>> grid, float minX, float minY, float rangeX, float rangeY) { + List>> list = new ArrayList<>(); + for (List> src : grid) { + List> row = new ArrayList<>(src.size()); + for (List cell : src) { + row.add(FixedList.of(cell)); + } + list.add(MappedList.of(row, minX, rangeX)); + } + return new FixedGrid<>(MappedList.of(list, minY, rangeY)); + } + + public static FixedGrid generate(int size, List values, Function xFunc, Function yFunc) { + List>> src = createList(size, () -> createList(size, ArrayList::new)); + List>> dest = createList(size, () -> createList(size, ArrayList::new)); + + float minX = 1F; + float maxX = 0F; + float minY = 1F; + float maxY = 0F; + for (T value : values) { + float x = xFunc.apply(value); + float y = yFunc.apply(value); + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + + int maxIndex = size - 1; + float rangeX = maxX - minX; + float rangeY = maxY - minY; + for (T value : values) { + float colVal = (xFunc.apply(value) - minX) / rangeX; + float rowVal = (yFunc.apply(value) - minY) / rangeY; + int colIndex = NoiseUtil.round(maxIndex * colVal); + int rowIndex = NoiseUtil.round(maxIndex * rowVal); + List> row = src.get(rowIndex); + List group = row.get(colIndex); + group.add(value); + } + + for (int y = 0; y < size; y++) { + List> srcRow = src.get(y); + List> destRow = dest.get(y); + for (int x = 0; x < size; x++) { + List srcGroup = srcRow.get(x); + List destGroup = destRow.get(x); + if (srcGroup.isEmpty()) { + float fx = minX + (x / (float) maxIndex) * rangeX; + float fy = minY + (x / (float) maxIndex) * rangeY; + addClosest(values, destGroup, fx, fy, xFunc, yFunc); + } else { + destGroup.addAll(srcGroup); + } + } + } + + return create(dest, minX, minY, rangeX, rangeY); + } + + private static void addClosest(List source, List dest, float fx, float fy, Function xFunc, Function yFunc) { + float dist2 = Float.MAX_VALUE; + Map distances = new HashMap<>(); + for (T t : source) { + if (!distances.containsKey(t)) { + float dx = fx - xFunc.apply(t); + float dy = fy - yFunc.apply(t); + float d2 = dx * dx + dy * dy; + distances.put(t, d2); + if (d2 < dist2) { + dist2 = d2; + } + } + } + + if (dist2 <= 0) { + dist2 = 1F; + } + + List sorted = new ArrayList<>(distances.keySet()); + sorted.sort((o1, o2) -> Float.compare(distances.getOrDefault(o1, Float.MAX_VALUE), distances.getOrDefault(o2, Float.MAX_VALUE))); + + for (T t : sorted) { + float d2 = distances.get(t); + if (d2 / dist2 < 1.025F) { + dest.add(t); + } + } + } + + private static List createList(int size, Supplier supplier) { + List list = new ArrayList<>(); + while (list.size() < size) { + list.add(supplier.get()); + } + return list; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/grid/FixedList.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/grid/FixedList.java new file mode 100644 index 0000000..4a4ff73 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/grid/FixedList.java @@ -0,0 +1,67 @@ +package com.terraforged.core.util.grid; + +import me.dags.noise.util.NoiseUtil; + +import java.util.*; + +public class FixedList implements Iterable { + + private final int maxIndex; + private final T[] elements; + + FixedList(T[] elements) { + this.maxIndex = elements.length - 1; + this.elements = elements; + } + + public T get(int index) { + if (index < 0) { + return elements[0]; + } + if (index > maxIndex) { + return elements[maxIndex]; + } + return elements[index]; + } + + public T get(float value) { + return get(indexOf(value)); + } + + public int size() { + return elements.length; + } + + public int indexOf(float value) { + return NoiseUtil.round(value * maxIndex); + } + + public Set uniqueValues() { + Set set = new HashSet<>(); + Collections.addAll(set, elements); + return set; + } + + @Override + public Iterator iterator() { + return new Iterator() { + + private int index = 0; + + @Override + public boolean hasNext() { + return index < elements.length; + } + + @Override + public T next() { + return elements[index++]; + } + }; + } + + @SuppressWarnings("unchecked") + public static FixedList of(List list) { + return new FixedList<>((T[]) list.toArray()); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/grid/MappedList.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/grid/MappedList.java new file mode 100644 index 0000000..554c28d --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/grid/MappedList.java @@ -0,0 +1,25 @@ +package com.terraforged.core.util.grid; + +import java.util.List; + +public class MappedList extends FixedList { + + private final float min; + private final float range; + + public MappedList(T[] elements, float min, float range) { + super(elements); + this.min = min; + this.range = range; + } + + @Override + public int indexOf(float value) { + return super.indexOf((value - min) / range); + } + + @SuppressWarnings("unchecked") + public static MappedList of(List list, float min, float range) { + return new MappedList<>((T[]) list.toArray(), min, range); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Comment.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Comment.java new file mode 100644 index 0000000..5a2a195 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Comment.java @@ -0,0 +1,13 @@ +package com.terraforged.core.util.serialization.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Comment { + + String[] value(); +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Option.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Option.java new file mode 100644 index 0000000..a28eda3 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Option.java @@ -0,0 +1,12 @@ +package com.terraforged.core.util.serialization.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Option { + +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Range.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Range.java new file mode 100644 index 0000000..5b35e01 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Range.java @@ -0,0 +1,15 @@ +package com.terraforged.core.util.serialization.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Range { + + float min(); + + float max(); +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Serializable.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Serializable.java new file mode 100644 index 0000000..b699ae5 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/annotation/Serializable.java @@ -0,0 +1,11 @@ +package com.terraforged.core.util.serialization.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Serializable { +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Deserializer.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Deserializer.java new file mode 100644 index 0000000..f75eec2 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Deserializer.java @@ -0,0 +1,85 @@ +package com.terraforged.core.util.serialization.serializer; + +import com.terraforged.core.util.serialization.annotation.Serializable; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; + +public class Deserializer { + + public void deserialize(Reader reader, Object object) throws Throwable { + Class type = object.getClass(); + for (String name : reader.getKeys()) { + if (name.charAt(0) == '_') { + continue; + } + Reader child = reader.getChild(name); + Field field = type.getField(getName(name)); + if (Serializer.isSerializable(field)) { + field.setAccessible(true); + fromValue(child, object, field); + } + } + } + + private void fromValue(Reader reader, Object object, Field field) throws Throwable { + if (field.getType() == int.class) { + field.set(object, reader.getInt("value")); + return; + } + if (field.getType() == float.class) { + field.set(object, reader.getFloat("value")); + return; + } + if (field.getType() == boolean.class) { + field.set(object, reader.getBool("value")); + return; + } + if (field.getType() == String.class) { + field.set(object, reader.getString("value")); + return; + } + if (field.getType().isEnum()) { + String name = reader.getString("value"); + for (Enum e : field.getType().asSubclass(Enum.class).getEnumConstants()) { + if (e.name().equals(name)) { + field.set(object, e); + return; + } + } + } + if (field.getType().isAnnotationPresent(Serializable.class)) { + Reader child = reader.getChild("value"); + Object value = field.getType().newInstance(); + deserialize(child, value); + field.set(object, value); + return; + } + if (field.getType().isArray()) { + Class type = field.getType().getComponentType(); + if (type.isAnnotationPresent(Serializable.class)) { + Reader child = reader.getChild("value"); + Object array = Array.newInstance(type, child.getSize()); + for (int i = 0; i < child.getSize(); i++) { + Object value = type.newInstance(); + deserialize(child.getChild(i), value); + Array.set(array, i, value); + } + field.set(object, array); + } + } + } + + private static String getName(String name) { + StringBuilder sb = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (c == '_' && i + 1 < name.length()) { + sb.append(Character.toUpperCase(name.charAt(++i))); + } else { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Reader.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Reader.java new file mode 100644 index 0000000..291044b --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Reader.java @@ -0,0 +1,58 @@ +package com.terraforged.core.util.serialization.serializer; + +import java.util.Collection; + +public interface Reader { + + int getSize(); + + Reader getChild(String key); + + Reader getChild(int index); + + Collection getKeys(); + + String getString(); + + boolean getBool(); + + float getFloat(); + + int getInt(); + + default String getString(String key) { + return getChild(key).getString(); + } + + default boolean getBool(String key) { + return getChild(key).getBool(); + } + + default float getFloat(String key) { + return getChild(key).getFloat(); + } + + default int getInt(String key) { + return getChild(key).getInt(); + } + + default String getString(int index) { + return getChild(index).getString(); + } + + default boolean getBool(int index) { + return getChild(index).getBool(); + } + + default float getFloat(int index) { + return getChild(index).getFloat(); + } + + default int getInt(int index) { + return getChild(index).getInt(); + } + + default void writeTo(Object object) throws Throwable { + new Deserializer().deserialize(this, object); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Serializer.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Serializer.java new file mode 100644 index 0000000..557d5ea --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Serializer.java @@ -0,0 +1,146 @@ +package com.terraforged.core.util.serialization.serializer; + +import com.terraforged.core.util.serialization.annotation.Comment; +import com.terraforged.core.util.serialization.annotation.Range; +import com.terraforged.core.util.serialization.annotation.Serializable; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public class Serializer { + + public void serialize(Object object, Writer writer) throws IllegalAccessException { + if (object.getClass().isArray()) { + writer.beginArray(); + int length = Array.getLength(object); + for (int i = 0; i < length; i++) { + Object element = Array.get(object, i); + serialize(element, writer); + } + writer.endArray(); + } else if (!object.getClass().isPrimitive()) { + int order = 0; + writer.beginObject(); + for (Field field : object.getClass().getFields()) { + if (Serializer.isSerializable(field)) { + field.setAccessible(true); + write(object, field, order, writer); + order++; + } + } + writer.endObject(); + } + } + + private void write(Object object, Field field, int order, Writer writer) throws IllegalAccessException { + if (field.getType() == int.class) { + writer.name(getName(field)); + writer.beginObject(); + writer.name("value").value((int) field.get(object)); + writeMeta(field, order, writer); + writer.endObject(); + return; + } + if (field.getType() == float.class) { + writer.name(getName(field)); + writer.beginObject(); + writer.name("value").value((float) field.get(object)); + writeMeta(field, order, writer); + writer.endObject(); + return; + } + if (field.getType() == String.class) { + writer.name(getName(field)); + writer.beginObject(); + writer.name("value").value((String) field.get(object)); + writeMeta(field, order, writer); + writer.endObject(); + return; + } + if (field.getType().isEnum()) { + writer.name(getName(field)); + writer.beginObject(); + writer.name("value").value(((Enum) field.get(object)).name()); + writeMeta(field, order, writer); + writer.endObject(); + return; + } + if (field.getType().isArray()) { + if (field.getType().getComponentType().isAnnotationPresent(Serializable.class)) { + writer.name(getName(field)); + writer.beginObject(); + writer.name("value"); + serialize(field.get(object), writer); + writeMeta(field, order, writer); + writer.endObject(); + } + return; + } + if (field.getType().isAnnotationPresent(Serializable.class)) { + writer.name(getName(field)); + writer.beginObject(); + writer.name("value"); + serialize(field.get(object), writer); + writeMeta(field, order, writer); + writer.endObject(); + } + } + + private void writeMeta(Field field, int order, Writer writer) { + writer.name("_name").value(getName(field)); + writer.name("_order").value(order); + + Range range = field.getAnnotation(Range.class); + if (range != null) { + if (field.getType() == int.class) { + writer.name("_min").value((int) range.min()); + writer.name("_max").value((int) range.max()); + } else { + writer.name("_min").value(range.min()); + writer.name("_max").value(range.max()); + } + } + + Comment comment = field.getAnnotation(Comment.class); + if (comment != null) { + writer.name("_comment"); + writer.beginArray(); + for (String line : comment.value()) { + writer.value(line); + } + writer.endArray(); + } + + if (field.getType().isEnum()) { + writer.name("_options"); + writer.beginArray(); + for (Enum o : field.getType().asSubclass(Enum.class).getEnumConstants()) { + writer.value(o.name()); + } + writer.endArray(); + } + } + + private static String getName(Field field) { + String name = field.getName(); + StringBuilder sb = new StringBuilder(name.length() * 2); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (Character.isUpperCase(c)) { + sb.append('_').append(Character.toLowerCase(c)); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + protected static boolean isSerializable(Field field) { + int modifiers = field.getModifiers(); + return Modifier.isPublic(modifiers) + && !Modifier.isFinal(modifiers) + && !Modifier.isStatic(modifiers) + && !Modifier.isTransient(modifiers); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Writer.java b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Writer.java new file mode 100644 index 0000000..658a18d --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/util/serialization/serializer/Writer.java @@ -0,0 +1,24 @@ +package com.terraforged.core.util.serialization.serializer; + +public interface Writer { + + Writer name(String name); + + Writer beginObject(); + + Writer endObject(); + + Writer beginArray(); + + Writer endArray(); + + Writer value(String value); + + Writer value(float value); + + Writer value(int value); + + default void readFrom(Object value) throws IllegalAccessException { + new Serializer().serialize(value, this); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/GeneratorContext.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/GeneratorContext.java new file mode 100644 index 0000000..14b7c49 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/GeneratorContext.java @@ -0,0 +1,41 @@ +package com.terraforged.core.world; + +import com.terraforged.core.settings.Settings; +import com.terraforged.core.util.Seed; +import com.terraforged.core.world.heightmap.Levels; +import com.terraforged.core.world.terrain.Terrains; +import com.terraforged.core.world.terrain.provider.StandardTerrainProvider; +import com.terraforged.core.world.terrain.provider.TerrainProviderFactory; + +public class GeneratorContext { + + public final Seed seed; + public final Levels levels; + public final Terrains terrain; + public final Settings settings; + public final TerrainProviderFactory terrainFactory; + + public GeneratorContext(Terrains terrain, Settings settings) { + this(terrain, settings, StandardTerrainProvider::new); + } + + public GeneratorContext(Terrains terrain, Settings settings, TerrainProviderFactory terrainFactory) { + this.terrain = terrain; + this.settings = settings; + this.seed = new Seed(settings.generator.seed); + this.levels = new Levels(settings.generator); + this.terrainFactory = terrainFactory; + } + + private GeneratorContext(GeneratorContext src) { + seed = new Seed(src.seed.get()); + levels = src.levels; + terrain = src.terrain; + settings = src.settings; + terrainFactory = src.terrainFactory; + } + + public GeneratorContext copy() { + return new GeneratorContext(this); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldDecorators.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldDecorators.java new file mode 100644 index 0000000..f9ea02a --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldDecorators.java @@ -0,0 +1,26 @@ +package com.terraforged.core.world; + +import com.terraforged.core.decorator.Decorator; +import com.terraforged.core.decorator.DesertStacks; +import com.terraforged.core.decorator.SwampPools; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class WorldDecorators { + + private final List decorators; + + public WorldDecorators(GeneratorContext context) { + context = context.copy(); + List list = new ArrayList<>(); + list.add(new DesertStacks(context.seed, context.levels)); + list.add(new SwampPools(context.seed, context.terrain, context.levels)); + decorators = Collections.unmodifiableList(list); + } + + public List getDecorators() { + return decorators; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldFilters.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldFilters.java new file mode 100644 index 0000000..5902871 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldFilters.java @@ -0,0 +1,46 @@ +package com.terraforged.core.world; + +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.filter.Erosion; +import com.terraforged.core.filter.Smoothing; +import com.terraforged.core.filter.Steepness; +import com.terraforged.core.settings.FilterSettings; + +public class WorldFilters { + + private final Erosion erosion; + private final Smoothing smoothing; + private final Steepness steepness; + private final FilterSettings settings; + + public WorldFilters(GeneratorContext context) { + context = context.copy(); + this.settings = context.settings.filters; + this.erosion = new Erosion(context.settings, context.levels); + this.smoothing = new Smoothing(context.settings, context.levels); + this.steepness = new Steepness(1, 10F, context.terrain); + } + + public void setRegion(int regionX, int regionZ) { + long seed = NoiseUtil.seed(regionX, regionZ); + getErosion().setSeed(seed); + getSmoothing().setSeed(seed); + getSteepness().setSeed(seed); + } + + public FilterSettings getSettings() { + return settings; + } + + public Erosion getErosion() { + return erosion; + } + + public Smoothing getSmoothing() { + return smoothing; + } + + public Steepness getSteepness() { + return steepness; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldGenerator.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldGenerator.java new file mode 100644 index 0000000..1caf93f --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldGenerator.java @@ -0,0 +1,28 @@ +package com.terraforged.core.world; + +import com.terraforged.core.world.heightmap.Heightmap; + +public class WorldGenerator { + + private final Heightmap heightmap; + private final WorldFilters filters; + private final WorldDecorators decorators; + + public WorldGenerator(Heightmap heightmap, WorldDecorators decorators, WorldFilters filters) { + this.filters = filters; + this.heightmap = heightmap; + this.decorators = decorators; + } + + public Heightmap getHeightmap() { + return heightmap; + } + + public WorldFilters getFilters() { + return filters; + } + + public WorldDecorators getDecorators() { + return decorators; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldGeneratorFactory.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldGeneratorFactory.java new file mode 100644 index 0000000..1896286 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/WorldGeneratorFactory.java @@ -0,0 +1,44 @@ +package com.terraforged.core.world; + +import com.terraforged.core.world.climate.Climate; +import com.terraforged.core.world.heightmap.Heightmap; +import com.terraforged.core.world.heightmap.WorldHeightmap; + +import java.util.function.Supplier; + +public class WorldGeneratorFactory implements Supplier { + + private final GeneratorContext context; + + private final Heightmap heightmap; + private final WorldDecorators decorators; + + public WorldGeneratorFactory(GeneratorContext context) { + this.context = context; + this.heightmap = new WorldHeightmap(context); + this.decorators = new WorldDecorators(context); + } + + public WorldGeneratorFactory(GeneratorContext context, Heightmap heightmap) { + this.context = context; + this.heightmap = heightmap; + this.decorators = new WorldDecorators(context); + } + + public Heightmap getHeightmap() { + return heightmap; + } + + public Climate getClimate() { + return getHeightmap().getClimate(); + } + + public WorldDecorators getDecorators() { + return decorators; + } + + @Override + public WorldGenerator get() { + return new WorldGenerator(heightmap, decorators, new WorldFilters(context)); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeData.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeData.java new file mode 100644 index 0000000..e63c24a --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeData.java @@ -0,0 +1,134 @@ +package com.terraforged.core.world.biome; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class BiomeData implements Comparable { + + private static float[] bounds = {0, 1 / 3F, 2 / 3F, 1F}; + + public static final List BIOMES = new ArrayList<>(); + public static BiomeData DEFAULT = new BiomeData("none", "", 1, 0.5F, 0.5F); + + public final String name; + + public final Object reference; + public final float color; + public final float rainfall; + public final float temperature; + + public BiomeData(String name, Object reference, float color, float rainfall, float temperature) { + this.reference = reference; + this.name = name; + this.rainfall = rainfall; + this.temperature = temperature; + this.color = color; + } + + public BiomeData(String name, Object reference, int color, float rainfall, float temperature) { + Color c = new Color(color); + this.reference = reference; + this.name = name; + this.rainfall = rainfall; + this.temperature = temperature; + this.color = getHue(c.getRed(), c.getGreen(), c.getBlue()); + } + + @Override + public int compareTo(BiomeData o) { + return name.compareTo(o.name); + } + + public static Collection getBiomes(float temperature, float rainfall) { + int temp = Math.min(3, (int) (bounds.length * temperature)); + int rain = Math.min(3, (int) (bounds.length * rainfall)); + return getBiomes(temp, rain); + } + + public static Collection getBiomes(int tempLower, int rainLower) { + int temp0 = tempLower; + int temp1 = temp0 + 1; + int rain0 = rainLower; + int rain1 = rain0 + 1; + + float tempMin = bounds[temp0]; + float tempMax = bounds[temp1]; + float rainMin = bounds[rain0]; + float rainMax = bounds[rain1]; + + List biomes = new ArrayList<>(); + for (BiomeData biome : BIOMES) { + if (biome.temperature >= tempMin && biome.temperature <= tempMax + && biome.rainfall >= rainMin && biome.rainfall <= rainMax) { + biomes.add(biome); + } + } + + if (biomes.isEmpty()) { + biomes.add(DEFAULT); + } + + return biomes; + } + + public static Collection getTempBiomes(float temperature) { + int lower = Math.min(3, (int) (bounds.length * temperature)); + int upper = lower + 1; + + float min = bounds[lower]; + float max = bounds[upper]; + + List biomes = new ArrayList<>(); + for (BiomeData data : BIOMES) { + if (data.temperature >= min && data.temperature <= max) { + biomes.add(data); + } + } + + return biomes; + } + + public static Collection getRainBiomes(float rainfall) { + int lower = Math.min(3, (int) (bounds.length * rainfall)); + int upper = lower + 1; + + float min = bounds[lower]; + float max = bounds[upper]; + + List biomes = new ArrayList<>(); + for (BiomeData data : BIOMES) { + if (data.rainfall >= min && data.rainfall <= max) { + biomes.add(data); + } + } + + return biomes; + } + + private static float getHue(int red, int green, int blue) { + float min = Math.min(Math.min(red, green), blue); + float max = Math.max(Math.max(red, green), blue); + + if (min == max) { + return 0; + } + + float hue; + if (max == red) { + hue = (green - blue) / (max - min); + + } else if (max == green) { + hue = 2f + (blue - red) / (max - min); + + } else { + hue = 4f + (red - green) / (max - min); + } + + hue = hue * 60; + if (hue < 0) hue = hue + 360; + + return (Math.round(hue) / 360F) * 100F; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeManager.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeManager.java new file mode 100644 index 0000000..3eaec8c --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeManager.java @@ -0,0 +1,10 @@ +package com.terraforged.core.world.biome; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BiomeManager { + + private final Map> biomes = new HashMap<>(); +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeType.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeType.java new file mode 100644 index 0000000..c67a817 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeType.java @@ -0,0 +1,111 @@ +package com.terraforged.core.world.biome; + +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.cell.Cell; + +import java.awt.*; + +public enum BiomeType { + + TROPICAL_RAINFOREST(7, 83, 48, new Color(7, 83, 48)), + SAVANNA(151, 165, 39, new Color(151, 165, 39)), + DESERT(200, 113, 55, new Color(200, 113, 55)), + TEMPERATE_RAINFOREST(10, 84, 109, new Color(10, 160, 65)), + TEMPERATE_FOREST(44, 137, 160, new Color(50, 200, 80)), + GRASSLAND(179, 124, 6, new Color(100, 220, 60)), + COLD_STEPPE(131, 112, 71, new Color(175, 180, 150)), + STEPPE(199, 155, 60, new Color(200, 200, 120)), + TAIGA(91, 143, 82, new Color(91, 143, 82)), + TUNDRA(147, 167, 172, new Color(147, 167, 172)), + ALPINE(0, 0, 0, new Color(160, 120, 170)); + + public static final int RESOLUTION = 256; + public static final int MAX = RESOLUTION - 1; + + private final Color lookup; + private final Color color; + + BiomeType(int r, int g, int b, Color color) { + this(new Color(r, g, b), color); + } + + BiomeType(Color lookup, Color color) { + this.lookup = lookup; + this.color = BiomeTypeColors.getInstance().getColor(name(), color); + } + + Color getLookup() { + return lookup; + } + + public Color getColor() { + return color; + } + + public static BiomeType get(float temperature, float moisture) { + return getCurve(temperature, moisture); + } + + public static BiomeType getLinear(float temperature, float moisture) { + int x = NoiseUtil.round(MAX * temperature); + int y = getYLinear(x, temperature, moisture); + return getType(x, y); + } + + public static BiomeType getCurve(float temperature, float moisture) { + int x = NoiseUtil.round(MAX * temperature); + int y = getYCurve(x, temperature, moisture); + return getType(x, y); + } + + public static float getEdge(float temperature, float moisture) { + return getEdgeCurve(temperature, moisture); + } + + public static float getEdgeLinear(float temperature, float moisture) { + int x = NoiseUtil.round(MAX * temperature); + int y = getYLinear(x, temperature, moisture); + return getEdge(x, y); + } + + public static float getEdgeCurve(float temperature, float moisture) { + int x = NoiseUtil.round(MAX * temperature); + int y = getYCurve(x, temperature, moisture); + return getEdge(x, y); + } + + public static void apply(Cell cell) { + applyCurve(cell); + } + + public static void applyLinear(Cell cell) { + cell.biomeType = get(cell.biomeTemperature, cell.biomeMoisture); + cell.biomeTypeMask = getEdge(cell.temperature, cell.moisture); + } + + public static void applyCurve(Cell cell) { + cell.biomeType = get(cell.biomeTemperature, cell.biomeMoisture); + cell.biomeTypeMask = getEdge(cell.temperature, cell.moisture); + } + + private static BiomeType getType(int x, int y) { + return BiomeTypeLoader.getInstance().getTypeMap()[y][x]; + } + + private static float getEdge(int x, int y) { + return BiomeTypeLoader.getInstance().getEdgeMap()[y][x]; + } + + private static int getYLinear(int x, float temperature, float moisture) { + if (moisture > temperature) { + return x; + } + return NoiseUtil.round(MAX * moisture); + } + + private static int getYCurve(int x, float temperature, float moisture) { + int max = x + ((MAX - x) / 2); + int y = NoiseUtil.round(max * moisture); + return Math.min(x, y); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeTypeColors.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeTypeColors.java new file mode 100644 index 0000000..a47747a --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeTypeColors.java @@ -0,0 +1,50 @@ +package com.terraforged.core.world.biome; + +import java.awt.*; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class BiomeTypeColors { + + private static BiomeTypeColors instance = new BiomeTypeColors(); + + private final Map colors = new HashMap<>(); + + private BiomeTypeColors() { + try (InputStream inputStream = BiomeType.class.getResourceAsStream("/biomes.txt")) { + Properties properties = new Properties(); + properties.load(inputStream); + for (Map.Entry entry : properties.entrySet()) { + Color color = Color.decode("#" + entry.getValue().toString()); + colors.put(entry.getKey().toString(), color); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public Color getColor(String name, Color defaultColor) { + return colors.getOrDefault(name, defaultColor); + } + + public static BiomeTypeColors getInstance() { + return instance; + } + + public static void main(String[] args) throws Throwable { + try (FileWriter writer = new FileWriter("biome_colors.properties")) { + Properties properties = new Properties(); + for (BiomeType type : BiomeType.values()) { + int r = type.getColor().getRed(); + int g = type.getColor().getGreen(); + int b = type.getColor().getBlue(); + properties.setProperty(type.name(), String.format("%02x%02x%02x", r, g, b)); + } + properties.store(writer, "TerraForged BiomeType Hex Colors (do not include hash/pound character)"); + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeTypeLoader.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeTypeLoader.java new file mode 100644 index 0000000..94466db --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/biome/BiomeTypeLoader.java @@ -0,0 +1,179 @@ +package com.terraforged.core.world.biome; + +import me.dags.noise.util.NoiseUtil; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +public class BiomeTypeLoader { + + private static BiomeTypeLoader instance; + + private final float[][] edges = new float[BiomeType.RESOLUTION][BiomeType.RESOLUTION]; + private final BiomeType[][] map = new BiomeType[BiomeType.RESOLUTION][BiomeType.RESOLUTION]; + + public BiomeTypeLoader() { + generateTypeMap(); + generateEdgeMap(); + } + + public BiomeType[][] getTypeMap() { + return map; + } + + public float[][] getEdgeMap() { + return edges; + } + + private BiomeType getType(int x, int y) { + return map[y][x]; + } + + private void generateTypeMap() { + try { + BufferedImage image = ImageIO.read(BiomeType.class.getResourceAsStream("/biomes.png")); + float xf = image.getWidth() / (float) BiomeType.RESOLUTION; + float yf = image.getHeight() / (float) BiomeType.RESOLUTION; + for (int y = 0; y < BiomeType.RESOLUTION; y++) { + for (int x = 0; x < BiomeType.RESOLUTION; x++) { + if (BiomeType.MAX - y > x) { + map[BiomeType.MAX - y][x] = BiomeType.ALPINE; + continue; + } + int ix = NoiseUtil.round(x * xf); + int iy = NoiseUtil.round(y * yf); + int argb = image.getRGB(ix, iy); + Color color = fromARGB(argb); + map[BiomeType.MAX - y][x] = forColor(color); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void generateEdgeMap() { + int[] distances = new int[BiomeType.values().length]; + for (int y = 0; y < BiomeType.RESOLUTION; y++) { + for (int x = 0; x < BiomeType.RESOLUTION; x++) { + if (y > x) continue; + BiomeType type = getType(x, y); + if (type == BiomeType.ALPINE) { + continue; + } + int distance2 = getEdge(x, y, type); + edges[y][x] = distance2; + distances[type.ordinal()] = Math.max(distances[type.ordinal()], distance2); + } + } + + for (int y = 0; y < BiomeType.RESOLUTION; y++) { + for (int x = 0; x < BiomeType.RESOLUTION; x++) { + BiomeType type = getType(x, y); + int max = distances[type.ordinal()]; + float distance = edges[y][x]; + float value = NoiseUtil.pow(distance / max, 0.33F); + edges[y][x] = NoiseUtil.clamp(value, 0, 1); + } + } + } + + private int getEdge(int cx, int cy, BiomeType type) { + int radius = BiomeType.RESOLUTION / 4; + int distance2 = Integer.MAX_VALUE; + int x0 = Math.max(0, cx - radius); + int x1 = Math.min(BiomeType.MAX, cx + radius); + int y0 = Math.max(0, cy - radius); + int y1 = Math.min(BiomeType.MAX, cy + radius); + for (int y = y0; y <= y1; y++) { + for (int x = x0; x <= x1; x++) { + BiomeType neighbour = getType(x, y); + + if (neighbour == BiomeType.ALPINE) { + continue; + } + + if (neighbour != type) { + int dist2 = dist2(cx, cy, x, y); + if (dist2 < distance2) { + distance2 = dist2; + } + } + } + } + return distance2; + } + + private static BiomeType forColor(Color color) { + BiomeType type = null; + int closest = Integer.MAX_VALUE; + for (BiomeType t : BiomeType.values()) { + int distance2 = getDistance2(color, t.getLookup()); + if (distance2 < closest) { + closest = distance2; + type = t; + } + } + if (type == null) { + return BiomeType.GRASSLAND; + } + return type; + } + + private static int getDistance2(Color a, Color b) { + int dr = a.getRed() - b.getRed(); + int dg = a.getGreen() - b.getGreen(); + int db = a.getBlue() - b.getBlue(); + return dr * dr + dg * dg + db * db; + } + + private static Color fromARGB(int argb) { + int b = (argb) & 0xFF; + int g = (argb >> 8) & 0xFF; + int r = (argb >> 16) & 0xFF; + return new Color(r, g, b); + } + + private static int dist2(int x1, int y1, int x2, int y2) { + int dx = x1 - x2; + int dy = y1 - y2; + return dx * dx + dy * dy; + } + + private static BufferedImage generateEdgeMapImage() { + BufferedImage image = new BufferedImage(BiomeType.RESOLUTION, BiomeType.RESOLUTION, BufferedImage.TYPE_INT_RGB); + for (int y = 0; y < BiomeType.RESOLUTION; y++) { + for (int x = 0; x < BiomeType.RESOLUTION; x++) { + float temperature = x / (float) BiomeType.RESOLUTION; + float moisture = y / (float) BiomeType.RESOLUTION; + float value = BiomeType.getEdge(temperature, moisture); + int color = Color.HSBtoRGB(0, 0, value); + image.setRGB(x, image.getHeight() - 1 - y, color); + } + } + return image; + } + + public static BiomeTypeLoader getInstance() { + if (instance == null) { + instance = new BiomeTypeLoader(); + } + return instance; + } + + public static void main(String[] args) throws Throwable { + BufferedImage img = generateEdgeMapImage(); + ImageIO.write(img, "png", new File("biomes_dist.png")); + JLabel label = new JLabel(new ImageIcon(img)); + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.add(label); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/climate/Climate.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/climate/Climate.java new file mode 100644 index 0000000..a1f9529 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/climate/Climate.java @@ -0,0 +1,141 @@ +package com.terraforged.core.world.climate; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import me.dags.noise.func.CellFunc; +import me.dags.noise.func.DistanceFunc; +import me.dags.noise.source.Rand; +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.module.CellLookup; +import com.terraforged.core.module.CellLookupOffset; +import com.terraforged.core.world.GeneratorContext; +import com.terraforged.core.world.heightmap.WorldHeightmap; +import com.terraforged.core.world.terrain.Terrain; + +public class Climate { + + private final float seaLevel; + private final float lowerHeight; + private final float midHeight = 0.425F; + private final float upperHeight = 0.75F; + + private final float moistureModifier = 0.05F; + private final float temperatureModifier = 0.175F; + + private final Rand rand; + private final Module treeLine; + private final Module heightMap; + private final Module offsetHeightMap; + private final Module offsetX; + private final Module offsetY; + + private final ClimateModule biomeNoise; + + public Climate(GeneratorContext context, WorldHeightmap heightmap) { + final int cellSeed = context.seed.next(); + final int cellSize = context.settings.generator.biome.biomeSize; + final int warpScale = context.settings.generator.biome.biomeWarpScale; + final int warpStrength = context.settings.generator.biome.biomeWarpStrength; + + this.biomeNoise = new ClimateModule(context.seed, context.settings.generator); + + Module warpX = Source.perlin(context.seed.next(), warpScale, 2); + Module warpZ = Source.perlin(context.seed.next(), warpScale, 2); + + Module windDirection = Source.cubic(context.seed.next(), cellSize, 1); + Module windStrength = Source.perlin(context.seed.next(), cellSize, 1) + .scale(cellSize * 1.5) + .bias(cellSize * 0.5); + + this.treeLine = Source.perlin(context.seed.next(), context.settings.generator.biome.biomeSize * 2, 1) + .scale(0.1).bias(0.4); + + this.heightMap = Source.build(cellSeed, cellSize, 1) + .distFunc(DistanceFunc.NATURAL) + .cellFunc(CellFunc.NOISE_LOOKUP) + .source(new CellLookup(heightmap::getValue, cellSize)) + .cell() + .warp(warpX, warpZ, warpStrength); + + this.offsetHeightMap = Source.build(cellSeed, cellSize, 1) + .distFunc(DistanceFunc.NATURAL) + .cellFunc(CellFunc.NOISE_LOOKUP) + .source(new CellLookupOffset(heightmap::getValue, windDirection, windStrength, cellSize)) + .cell() + .warp(warpX, warpZ, warpStrength); + + this.rand = new Rand(Source.builder().seed(context.seed.next())); + this.offsetX = context.settings.generator.biomeEdgeNoise.build(context.seed.next()); + this.offsetY = context.settings.generator.biomeEdgeNoise.build(context.seed.next()); + this.seaLevel = context.levels.water; + this.lowerHeight = context.levels.ground; + } + + public Rand getRand() { + return rand; + } + + public float getCellHeight(float x, float z) { + return heightMap.getValue(x, z); + } + + public float getOffsetCellHeight(float x, float z) { + return offsetHeightMap.getValue(x, z); + } + + public float getOffsetX(float x, float z, int distance) { + return offsetX.getValue(x, z) * distance; + } + + public float getOffsetZ(float x, float z, int distance) { + return offsetY.getValue(x, z) * distance; + } + + public float getTreeLine(float x, float z) { + return treeLine.getValue(x, z); + } + + public void apply(Cell cell, float x, float z, boolean mask) { + biomeNoise.apply(cell, x, z, mask); + modify(cell, x, z); + } + + private void modify(Cell cell, float x, float z) { + float height = getCellHeight(x, z); + float height1 = getOffsetCellHeight(x, z); + float moistDelta = 0F; + if (height > seaLevel) { + if (height1 < seaLevel) { + moistDelta = 0.7F; + } else if (height - height1 > 0.2) { + moistDelta = height - height1; + } else if (height1 - height > 0.1) { + moistDelta = Math.max(-0.5F, height - height1) * 2F; + } + } + + float moistChange = moistureModifier * moistDelta; + cell.moisture = NoiseUtil.clamp(cell.moisture + moistChange, 0, 1); + + if (height > upperHeight) { + cell.temperature = Math.max(0, cell.temperature - temperatureModifier); + return; + } + + // temperature decreases away from 'midHeight' towards 'upperHeight' + if (height > midHeight) { + float delta = (height - midHeight) / (upperHeight - midHeight); + cell.temperature = Math.max(0, cell.temperature - (delta * temperatureModifier)); + return; + } + + height = Math.max(lowerHeight, height); + + // temperature increases away from 'midHeight' towards 'lowerHeight' + if (height >= lowerHeight) { + float delta = 1 - ((height - lowerHeight) / (midHeight - lowerHeight)); + cell.temperature = Math.min(1, cell.temperature + (delta * temperatureModifier)); + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/climate/ClimateModule.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/climate/ClimateModule.java new file mode 100644 index 0000000..5828e4b --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/climate/ClimateModule.java @@ -0,0 +1,134 @@ +package com.terraforged.core.world.climate; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import me.dags.noise.func.DistanceFunc; +import me.dags.noise.func.EdgeFunc; +import me.dags.noise.util.NoiseUtil; +import me.dags.noise.util.Vec2f; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.settings.GeneratorSettings; +import com.terraforged.core.util.Seed; +import com.terraforged.core.world.biome.BiomeType; +import com.terraforged.core.world.terrain.Terrain; + +public class ClimateModule { + + private final int seed; + + private final float edgeClamp; + private final float edgeScale; + private final float biomeFreq; + private final float warpStrength; + + private final Module warpX; + private final Module warpZ; + private final Module moisture; + private final Module temperature; + + public ClimateModule(Seed seed, GeneratorSettings settings) { + int biomeSize = settings.biome.biomeSize; + float biomeFreq = 1F / biomeSize; + int moistureSize = 40 * biomeSize; + int temperatureSize = 10 * biomeSize; + int moistScale = NoiseUtil.round(moistureSize * biomeFreq); + int tempScale = NoiseUtil.round(temperatureSize * biomeFreq); + int warpScale = settings.biome.biomeWarpScale; + + this.seed = seed.next(); + this.edgeClamp = 0.85F; + this.edgeScale = 1 / edgeClamp; + this.biomeFreq = 1F / biomeSize; + this.warpStrength = settings.biome.biomeWarpStrength; + this.warpX = Source.perlin(seed.next(), warpScale, 2).bias(-0.5); + this.warpZ = Source.perlin(seed.next(), warpScale, 2).bias(-0.5); + + this.moisture = Source.simplex(seed.next(), moistScale, 2) + .clamp(0.15, 0.85).map(0, 1) + .warp(seed.next(), moistScale / 2, 1, moistScale / 4D) + .warp(seed.next(), moistScale / 6, 2, moistScale / 12D); + + Module temperature = Source.sin(tempScale, Source.constant(0.9)).clamp(0.05, 0.95).map(0, 1); + this.temperature = new Compressor(temperature, 0.1F, 0.2F) + .warp(seed.next(), tempScale * 4, 2, tempScale * 4) + .warp(seed.next(), tempScale, 1, tempScale) + .warp(seed.next(), tempScale / 8, 1, tempScale / 8D); + } + + public void apply(Cell cell, float x, float y, boolean mask) { + float ox = warpX.getValue(x, y) * warpStrength; + float oz = warpZ.getValue(x, y) * warpStrength; + + x += ox; + y += oz; + + x *= biomeFreq; + y *= biomeFreq; + + int cellX = 0; + int cellY = 0; + + Vec2f vec2f = null; + int xr = NoiseUtil.round(x); + int yr = NoiseUtil.round(y); + float edgeDistance = 999999.0F; + float edgeDistance2 = 999999.0F; + float valueDistance = 3.4028235E38F; + DistanceFunc dist = DistanceFunc.NATURAL; + + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + int xi = xr + dx; + int yi = yr + dy; + Vec2f vec = NoiseUtil.CELL_2D[NoiseUtil.hash2D(seed, xi, yi) & 255]; + + float vecX = xi - x + vec.x; + float vecY = yi - y + vec.y; + float distance = dist.apply(vecX, vecY); + + if (distance < valueDistance) { + valueDistance = distance; + vec2f = vec; + cellX = xi; + cellY = yi; + } + + if (distance < edgeDistance2) { + edgeDistance2 = Math.max(edgeDistance, distance); + } else { + edgeDistance2 = Math.max(edgeDistance, edgeDistance2); + } + + edgeDistance = Math.min(edgeDistance, distance); + } + } + + if (mask) { + cell.biomeMask = edgeValue(edgeDistance, edgeDistance2); + } else { + cell.biome = cellValue(seed, cellX, cellY); + cell.biomeMask = edgeValue(edgeDistance, edgeDistance2); + cell.biomeMoisture = moisture.getValue(cellX + vec2f.x, cellY + vec2f.y); + cell.biomeTemperature = temperature.getValue(cellX + vec2f.x, cellY + vec2f.y); + cell.moisture = moisture.getValue(x, y); + cell.temperature = temperature.getValue(x, y); + + BiomeType.apply(cell); + } + } + + private float cellValue(int seed, int cellX, int cellY) { + float value = NoiseUtil.valCoord2D(seed, cellX, cellY); + return NoiseUtil.map(value, -1, 1, 2); + } + + private float edgeValue(float distance, float distance2) { + EdgeFunc edge = EdgeFunc.DISTANCE_2_DIV; + float value = edge.apply(distance, distance2); + value = 1 - NoiseUtil.map(value, edge.min(), edge.max(), edge.range()); + if (value > edgeClamp) { + return 1F; + } + return value * edgeScale; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/climate/Compressor.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/climate/Compressor.java new file mode 100644 index 0000000..e4fa8e8 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/climate/Compressor.java @@ -0,0 +1,56 @@ +package com.terraforged.core.world.climate; + +import me.dags.noise.Module; + +public class Compressor implements Module { + + private final float lowerStart; + private final float lowerEnd; + private final float lowerRange; + private final float lowerExpandRange; + + private final float upperStart; + private final float upperEnd; + private final float upperRange; + private final float upperExpandedRange; + + private final float compression; + private final float compressionRange; + + private final Module module; + + public Compressor(Module module, float inset, float amount) { + this(module, inset, inset + amount, 1 - inset - amount, 1 - inset); + } + + public Compressor(Module module, float lowerStart, float lowerEnd, float upperStart, float upperEnd) { + this.module = module; + this.lowerStart = lowerStart; + this.lowerEnd = lowerEnd; + this.lowerRange = lowerStart; + this.lowerExpandRange = lowerEnd; + this.upperStart = upperStart; + this.upperEnd = upperEnd; + this.upperRange = 1 - upperEnd; + this.upperExpandedRange = 1 - upperStart; + this.compression = upperStart - lowerEnd; + this.compressionRange = upperEnd - lowerStart; + } + + @Override + public float getValue(float x, float y) { + float value = module.getValue(x, y); + if (value <= lowerStart) { + float alpha = value / lowerRange; + return alpha * lowerExpandRange; + } else if (value >= upperEnd) { + float delta = value - upperEnd; + float alpha = delta / upperRange; + return upperStart + alpha * upperExpandedRange; + } else { + float delta = value - lowerStart; + float alpha = delta / compressionRange; + return lowerEnd + alpha * compression; + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentBlender.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentBlender.java new file mode 100644 index 0000000..ecc5134 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentBlender.java @@ -0,0 +1,21 @@ +package com.terraforged.core.world.continent; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.module.Blender; +import com.terraforged.core.world.terrain.Terrain; + +public class ContinentBlender extends Blender { + + private final Populator control; + + public ContinentBlender(Populator continent, Populator lower, Populator upper, float min, float max, float split, float tagThreshold) { + super(continent, lower, upper, min, max, split, tagThreshold); + this.control = continent; + } + + @Override + public float getValue(Cell cell, float x, float z) { + return cell.continentEdge; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentModule.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentModule.java new file mode 100644 index 0000000..6d19e67 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentModule.java @@ -0,0 +1,7 @@ +package com.terraforged.core.world.continent; + +import com.terraforged.core.cell.Populator; + +public interface ContinentModule extends Populator { + +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentMultiBlender.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentMultiBlender.java new file mode 100644 index 0000000..564534a --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/ContinentMultiBlender.java @@ -0,0 +1,22 @@ +package com.terraforged.core.world.continent; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.module.MultiBlender; +import com.terraforged.core.world.climate.Climate; +import com.terraforged.core.world.terrain.Terrain; + +public class ContinentMultiBlender extends MultiBlender { + + private final Populator control; + + public ContinentMultiBlender(Climate climate, Populator continent, Populator lower, Populator middle, Populator upper, float min, float mid, float max) { + super(climate, continent, lower, middle, upper, min, mid, max); + this.control = continent; + } + + @Override + public float getValue(Cell cell, float x, float z) { + return cell.continentEdge; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/VoronoiContinentModule.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/VoronoiContinentModule.java new file mode 100644 index 0000000..c59145e --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/continent/VoronoiContinentModule.java @@ -0,0 +1,138 @@ +package com.terraforged.core.world.continent; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import me.dags.noise.domain.Domain; +import me.dags.noise.func.DistanceFunc; +import me.dags.noise.func.EdgeFunc; +import me.dags.noise.util.NoiseUtil; +import me.dags.noise.util.Vec2f; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.settings.GeneratorSettings; +import com.terraforged.core.util.Seed; +import com.terraforged.core.world.terrain.Terrain; + +public class VoronoiContinentModule implements Populator { + + private static final float edgeClampMin = 0.05F; + private static final float edgeClampMax = 0.50F; + private static final float edgeClampRange = edgeClampMax - edgeClampMin; + + private final int seed; + private final float frequency; + + private final float edgeMin; + private final float edgeMax; + private final float edgeRange; + + private final Domain warp; + private final Module shape; + + public VoronoiContinentModule(Seed seed, GeneratorSettings settings) { + int tectonicScale = settings.land.continentScale * 4; + int continentScale = settings.land.continentScale / 2; + double oceans = Math.min(Math.max(settings.world.oceanSize, 0.01), 0.99); + double shapeMin = 0.15 + (oceans * 0.35); + this.seed = seed.next(); + + this.frequency = 1F / tectonicScale; + this.edgeMin = edgeClampMin; + this.edgeMax = (float) oceans; + this.edgeRange = edgeMax - edgeMin; + + this.warp = Domain.warp(Source.SIMPLEX, seed.next(), continentScale, 3, continentScale); + + this.shape = Source.perlin(seed.next(), settings.land.continentScale, 2) + .clamp(shapeMin, 0.7) + .map(0, 1) + .warp(Source.SIMPLEX, seed.next(), continentScale / 2, 1, continentScale / 4D) + .warp(seed.next(), 50, 1, 20D); + } + + @Override + public float getValue(float x, float y) { + Cell cell = new Cell<>(); + apply(cell, x, y); + return cell.continentEdge; + } + + + @Override + public void apply(Cell cell, final float x, final float y) { + float ox = warp.getOffsetX(x, y); + float oz = warp.getOffsetY(x, y); + + float px = x + ox; + float py = y + oz; + + px *= frequency; + py *= frequency; + + int cellX = 0; + int cellY = 0; + + int xr = NoiseUtil.round(px); + int yr = NoiseUtil.round(py); + float edgeDistance = 999999.0F; + float edgeDistance2 = 999999.0F; + float valueDistance = 3.4028235E38F; + DistanceFunc dist = DistanceFunc.NATURAL; + + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + int xi = xr + dx; + int yi = yr + dy; + Vec2f vec = NoiseUtil.CELL_2D[NoiseUtil.hash2D(seed, xi, yi) & 255]; + + float vecX = xi - px + vec.x; + float vecY = yi - py + vec.y; + float distance = dist.apply(vecX, vecY); + + if (distance < valueDistance) { + valueDistance = distance; + cellX = xi; + cellY = yi; + } + + if (distance < edgeDistance2) { + edgeDistance2 = Math.max(edgeDistance, distance); + } else { + edgeDistance2 = Math.max(edgeDistance, edgeDistance2); + } + + edgeDistance = Math.min(edgeDistance, distance); + } + } + + + float shapeNoise = shape.getValue(x, y); + float continentNoise = edgeValue(edgeDistance, edgeDistance2); + + cell.continent = cellValue(seed, cellX, cellY); + cell.continentEdge = shapeNoise * continentNoise; + } + + @Override + public void tag(Cell cell, float x, float y) { + + } + + private float cellValue(int seed, int cellX, int cellY) { + float value = NoiseUtil.valCoord2D(seed, cellX, cellY); + return NoiseUtil.map(value, -1, 1, 2); + } + + private float edgeValue(float distance, float distance2) { + EdgeFunc edge = EdgeFunc.DISTANCE_2_DIV; + float value = edge.apply(distance, distance2); + float edgeValue = 1 - NoiseUtil.map(value, edge.min(), edge.max(), edge.range()); + if (edgeValue < edgeMin) { + return 0F; + } + if (edgeValue > edgeMax) { + return 1F; + } + return (edgeValue - edgeMin) / edgeRange; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Geology.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Geology.java new file mode 100644 index 0000000..3bff4c1 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Geology.java @@ -0,0 +1,37 @@ +package com.terraforged.core.world.geology; + +import me.dags.noise.Module; + +import java.util.ArrayList; +import java.util.List; + +public class Geology { + + private final Module selector; + private final List> backing = new ArrayList<>(); + + public Geology(Module selector) { + this.selector = selector; + } + + public Geology add(Geology geology) { + backing.addAll(geology.backing); + return this; + } + + public Geology add(Strata strata) { + backing.add(strata); + return this; + } + + public Strata getStrata(float x, int y) { + float noise = selector.getValue(x, y); + return getStrata(noise); + } + + public Strata getStrata(float value) { + int index = (int) (value * backing.size()); + index = Math.min(backing.size() - 1, index); + return backing.get(index); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Strata.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Strata.java new file mode 100644 index 0000000..ed775f9 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Strata.java @@ -0,0 +1,120 @@ +package com.terraforged.core.world.geology; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.util.Seed; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +public class Strata { + + private final float[] depthBuffer; + private final Module heightMod; + private final List> strata; + + private Strata(Module heightMod, List> strata) { + this.strata = strata; + this.heightMod = heightMod; + this.depthBuffer = new float[strata.size()]; + } + + public boolean downwards(final int x, final int y, final int z, Stratum.Visitor visitor) { + int py = y; + T last = null; + float sum = getDepth(x, z); + for (int i = 0; i < strata.size(); i++) { + float depth = depthBuffer[i] / sum; + int height = NoiseUtil.round(depth * y); + T value = strata.get(i).getValue(); + last = value; + for (int dy = 0; dy < height; dy++) { + if (py <= y) { + boolean cont = visitor.visit(py, value); + if (!cont) { + return false; + } + } + if (--py < 0) { + return false; + } + } + } + if (last != null) { + while (py > 0) { + visitor.visit(py, last); + py--; + } + } + return true; + } + + public boolean upwards(int x, int y, int z, Stratum.Visitor visitor) { + int py = 0; + float sum = getDepth(x, z); + for (int i = strata.size() - 1; i > 0; i--) { + float depth = depthBuffer[i] / sum; + int height = NoiseUtil.round(depth * y); + T value = strata.get(i).getValue(); + for (int dy = 0; dy < height; dy++) { + boolean cont = visitor.visit(py, value); + if (!cont) { + return false; + } + if (++py > y) { + return false; + } + } + } + return true; + } + + private int getYOffset(int x, int z) { + return (int) (64 * heightMod.getValue(x, z)); + } + + private float getDepth(int x, int z) { + float sum = 0F; + for (int i = 0; i < strata.size(); i++) { + float depth = strata.get(i).getDepth(x, z); + sum += depth; + depthBuffer[i] = depth; + } + return sum; + } + + public static Builder builder(int seed, me.dags.noise.source.Builder noise) { + return new Builder<>(seed, noise); + } + + public static class Builder { + + private final Seed seed; + private final me.dags.noise.source.Builder noise; + private final List> strata = new LinkedList<>(); + + public Builder(int seed, me.dags.noise.source.Builder noise) { + this.seed = new Seed(seed); + this.noise = noise; + } + + public Builder add(T material, double depth) { + Module module = noise.seed(seed.next()).perlin().scale(depth); + strata.add(Stratum.of(material, module)); + return this; + } + + public Builder add(Source type, T material, double depth) { + Module module = noise.seed(seed.next()).build(type).scale(depth); + strata.add(Stratum.of(material, module)); + return this; + } + + public Strata build() { + Module height = Source.cell(seed.next(), 100); + return new Strata<>(height, new ArrayList<>(strata)); + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Stratum.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Stratum.java new file mode 100644 index 0000000..541fe91 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/geology/Stratum.java @@ -0,0 +1,40 @@ +package com.terraforged.core.world.geology; + +import me.dags.noise.Module; +import me.dags.noise.Source; + +public class Stratum { + + private final T value; + private final Module depth; + + public Stratum(T value, double depth) { + this(value, Source.constant(depth)); + } + + public Stratum(T value, Module depth) { + this.depth = depth; + this.value = value; + } + + public T getValue() { + return value; + } + + public float getDepth(float x, float z) { + return depth.getValue(x, z); + } + + public static Stratum of(T t, double depth) { + return new Stratum<>(t, depth); + } + + public static Stratum of(T t, Module depth) { + return new Stratum<>(t, depth); + } + + public interface Visitor { + + boolean visit(int y, T value); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/Heightmap.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/Heightmap.java new file mode 100644 index 0000000..d5ec58a --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/Heightmap.java @@ -0,0 +1,55 @@ +package com.terraforged.core.world.heightmap; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.cell.Extent; +import com.terraforged.core.region.Size; +import com.terraforged.core.util.concurrent.ObjectPool; +import com.terraforged.core.world.climate.Climate; +import com.terraforged.core.world.river.RiverManager; +import com.terraforged.core.world.terrain.Terrain; + +import java.rmi.UnexpectedException; + +public interface Heightmap extends Populator, Extent { + + Climate getClimate(); + + RiverManager getRiverManager(); + + @Override + default Cell getCell(int x, int z) { + throw new RuntimeException("Don't use this pls"); + } + + @Override + default void visit(int minX, int minZ, int maxX, int maxZ, Cell.Visitor visitor) { + int chunkSize = Size.chunkToBlock(1); + + int chunkMinX = Size.blockToChunk(minX); + int chunkMinZ = Size.blockToChunk(minZ); + int chunkMaxX = Size.blockToChunk(maxX); + int chunkMaxZ = Size.blockToChunk(maxZ); + + try (ObjectPool.Item> cell = Cell.pooled()) { + for (int chunkZ = chunkMinZ; chunkZ <= chunkMaxZ; chunkZ++) { + for (int chunkX = chunkMinX; chunkX <= chunkMaxX; chunkX++) { + int chunkStartX = Size.chunkToBlock(chunkX); + int chunkStartZ = Size.chunkToBlock(chunkZ); + for (int dz = 0; dz < chunkSize; dz++) { + for (int dx = 0; dx < chunkSize; dx++) { + int x = chunkStartX + dx; + int z = chunkStartZ + dz; + apply(cell.getValue(), x, z); + if (x >= minX && x < maxX && z >= minZ && z < maxZ) { + int relX = x - minX; + int relZ = z - minZ; + visitor.visit(cell.getValue(), relX, relZ); + } + } + } + } + } + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/Levels.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/Levels.java new file mode 100644 index 0000000..c09155f --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/Levels.java @@ -0,0 +1,54 @@ +package com.terraforged.core.world.heightmap; + +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.settings.GeneratorSettings; + +public class Levels { + + public final int worldHeight; + + // y index of the top-most water block + public final int waterY; + private final int groundY; + + // top of the first ground block (ie 1 above ground index) + public final int groundLevel; + // top of the top-most water block (ie 1 above water index) + public final int waterLevel; + + // ground index mapped between 0-1 (default 63 / 256) + public final float ground; + // water index mapped between 0-1 (default 62 / 256) + public final float water; + + public Levels(GeneratorSettings settings) { + worldHeight = Math.max(1, settings.world.worldHeight); + + waterLevel = settings.world.seaLevel; + groundLevel = waterLevel + 1; + + waterY = Math.min(waterLevel - 1, worldHeight); + groundY = Math.min(groundLevel - 1, worldHeight); + + ground = NoiseUtil.div(groundY, worldHeight); + water = NoiseUtil.div(waterY, worldHeight); + } + + public float scale(int level) { + return NoiseUtil.div(level, worldHeight); + } + + public float water(int amount) { + return NoiseUtil.div(waterY + amount, worldHeight); + } + + public float ground(int amount) { + return NoiseUtil.div(groundY + amount, worldHeight); + } + + public static float getSeaLevel(GeneratorSettings settings) { + int worldHeight = Math.max(1, settings.world.worldHeight); + int waterLevel = Math.min(settings.world.seaLevel, worldHeight); + return NoiseUtil.div(waterLevel, worldHeight); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/RegionConfig.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/RegionConfig.java new file mode 100644 index 0000000..625611d --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/RegionConfig.java @@ -0,0 +1,20 @@ +package com.terraforged.core.world.heightmap; + +import me.dags.noise.Module; + +public class RegionConfig { + + public final int seed; + public final int scale; + public final Module warpX; + public final Module warpZ; + public final double warpStrength; + + public RegionConfig(int seed, int scale, Module warpX, Module warpZ, double warpStrength) { + this.seed = seed; + this.scale = scale; + this.warpX = warpX; + this.warpZ = warpZ; + this.warpStrength = warpStrength; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/RegionExtent.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/RegionExtent.java new file mode 100644 index 0000000..6e7cc99 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/RegionExtent.java @@ -0,0 +1,70 @@ +package com.terraforged.core.world.heightmap; + +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Extent; +import com.terraforged.core.region.Region; +import com.terraforged.core.region.Size; +import com.terraforged.core.region.chunk.ChunkReader; +import com.terraforged.core.world.terrain.Terrain; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +public interface RegionExtent extends Extent { + + int chunkToRegion(int coord); + + Region getRegion(int regionX, int regionZ); + + Future getRegionAsync(int regionX, int regionZ); + + default ChunkReader getChunk(int chunkX, int chunkZ) { + int regionX = chunkToRegion(chunkX); + int regionZ = chunkToRegion(chunkZ); + Region region = getRegion(regionX, regionZ); + return region.getChunk(chunkX, chunkZ); + } + + default List> getRegions(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + List> regions = new LinkedList<>(); + for (int rz = minRegionZ; rz <= maxRegionZ; rz++) { + for (int rx = minRegionX; rx <= maxRegionX; rx++) { + regions.add(getRegionAsync(rx, rz)); + } + } + return regions; + } + + @Override + default Cell getCell(int x, int z) { + int regionX = chunkToRegion(Size.blockToChunk(x)); + int regionZ = chunkToRegion(Size.blockToChunk(z)); + Region region = getRegion(regionX, regionZ); + return region.getCell(x, z); + } + + @Override + default void visit(int minX, int minZ, int maxX, int maxZ, Cell.Visitor visitor) { + int minRegionX = chunkToRegion(Size.blockToChunk(minX)); + int minRegionZ = chunkToRegion(Size.blockToChunk(minZ)); + int maxRegionX = chunkToRegion(Size.blockToChunk(maxX)); + int maxRegionZ = chunkToRegion(Size.blockToChunk(maxZ)); + List> regions = getRegions(minRegionX, minRegionZ, maxRegionX, maxRegionZ); + while (!regions.isEmpty()) { + regions.removeIf(future -> { + if (!future.isDone()) { + return false; + } + try { + Region region = future.get(); + region.visit(minX, minZ, maxX, maxZ, visitor); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + return true; + }); + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/WorldHeightmap.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/WorldHeightmap.java new file mode 100644 index 0000000..1befc2a --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/heightmap/WorldHeightmap.java @@ -0,0 +1,206 @@ +package com.terraforged.core.world.heightmap; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import me.dags.noise.func.EdgeFunc; +import me.dags.noise.func.Interpolation; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.module.Blender; +import com.terraforged.core.module.Lerp; +import com.terraforged.core.module.MultiBlender; +import com.terraforged.core.module.Selector; +import com.terraforged.core.settings.GeneratorSettings; +import com.terraforged.core.settings.Settings; +import com.terraforged.core.util.Seed; +import com.terraforged.core.world.GeneratorContext; +import com.terraforged.core.world.climate.Climate; +import com.terraforged.core.world.continent.ContinentBlender; +import com.terraforged.core.world.continent.ContinentMultiBlender; +import com.terraforged.core.world.continent.VoronoiContinentModule; +import com.terraforged.core.world.river.RiverManager; +import com.terraforged.core.world.terrain.Terrain; +import com.terraforged.core.world.terrain.TerrainPopulator; +import com.terraforged.core.world.terrain.Terrains; +import com.terraforged.core.world.terrain.provider.TerrainProvider; + +public class WorldHeightmap implements Heightmap { + + private static final float DEEP_OCEAN_VALUE = 0.2F; + private static final float OCEAN_VALUE = 0.3F; + private static final float BEACH_VALUE = 0.34F; + private static final float COAST_VALUE = 0.4F; + private static final float INLAND_VALUE = 0.6F; + + private final Levels levels; + private final Terrains terrain; + private final Settings settings; + + private final Climate climate; + private final Populator root; + private final Populator continent; + private final RiverManager riverManager; + private final TerrainProvider terrainProvider; + + public WorldHeightmap(GeneratorContext context) { + context = context.copy(); + + this.levels = context.levels; + this.terrain = context.terrain; + this.settings = context.settings; + this.climate = new Climate(context, this); + + Seed seed = context.seed; + Levels levels = context.levels; + GeneratorSettings genSettings = context.settings.generator; + + Seed regionSeed = seed.nextSeed(); + Seed regionWarp = seed.nextSeed(); + + int regionWarpScale = 400; + int regionWarpStrength = 200; + RegionConfig regionConfig = new RegionConfig( + regionSeed.get(), + context.settings.generator.land.regionSize, + Source.simplex(regionWarp.next(), regionWarpScale, 2), + Source.simplex(regionWarp.next(), regionWarpScale, 2), + regionWarpStrength + ); + + // controls where mountain chains form in the world + Module mountainShapeBase = Source.cellEdge(seed.next(), genSettings.land.mountainScale, EdgeFunc.DISTANCE_2_ADD) + .add(Source.cubic(seed.next(), genSettings.land.mountainScale, 1).scale(-0.05)); + + // sharpens the transition to create steeper mountains + Module mountainShape = mountainShapeBase + .curve(Interpolation.CURVE3) + .clamp(0, 0.9) + .map(0, 1); + + // controls the shape of terrain regions + Module regionShape = Source.cell(regionConfig.seed, regionConfig.scale) + .warp(regionConfig.warpX, regionConfig.warpZ, regionConfig.warpStrength); + + // the corresponding edges of terrain regions so we can fade out towards borders + Module regionEdge = Source.cellEdge(regionConfig.seed, regionConfig.scale, EdgeFunc.DISTANCE_2_DIV).invert() + .warp(regionConfig.warpX, regionConfig.warpZ, regionConfig.warpStrength) + .pow(1.5) + .clamp(0, 0.75) + .map(0, 1); + + this.terrainProvider = context.terrainFactory.create(context, regionConfig, this); + + // the voronoi controlled terrain regions + Populator terrainRegions = new Selector(regionShape, terrainProvider.getPopulators()); + // the terrain type at region edges + Populator terrainRegionBorders = new TerrainPopulator(terrainProvider.getLandforms().steppe(seed), context.terrain.steppe); + + // transitions between the unique terrain regions and the common border terrain + Populator terrain = new Lerp( + regionEdge, + terrainRegionBorders, + terrainRegions + ); + + // mountain populator + Populator mountains = register(terrainProvider.getLandforms().mountains(seed), context.terrain.mountains); + + // controls what's ocean and what's land + this.continent = createContinent(context); + + // blends between normal terrain and mountain chains + Populator land = new Blender( + mountainShape, + terrain, + mountains, + 0.1F, + 0.9F, + 0.6F + ); + + // uses the continent noise to blend between deep ocean, to ocean, to coast + MultiBlender oceans = new ContinentMultiBlender( + climate, + continent, + register(terrainProvider.getLandforms().deepOcean(seed.next()), context.terrain.deepOcean), + register(Source.constant(levels.water(-7)), context.terrain.ocean), + register(Source.constant(levels.water), context.terrain.coast), + DEEP_OCEAN_VALUE, // below == deep, above == transition to shallow + OCEAN_VALUE, // below == transition to deep, above == transition to coast + COAST_VALUE // below == transition to shallow, above == coast + ); + + // blends between the ocean/coast terrain and land terrains + root = new ContinentBlender( + continent, + oceans, + land, + OCEAN_VALUE, // below == pure ocean + INLAND_VALUE, // above == pure land + COAST_VALUE, // split point + COAST_VALUE - 0.05F + ).mask(); + + this.riverManager = new RiverManager(this, context); + } + + public RiverManager getRiverManager() { + return riverManager; + } + + @Override + public void apply(Cell cell, float x, float z) { + // initial type + cell.tag = terrain.steppe; + + // apply continent value/edge noise + continent.apply(cell, x, z); + + // apply actuall heightmap + root.apply(cell, x, z); + + // apply rivers + riverManager.apply(cell, x, z); + + // apply climate data + if (cell.value <= levels.water) { + climate.apply(cell, x, z, false); + if (cell.tag == terrain.coast) { + cell.tag = terrain.ocean; + } + } else { + int range = settings.generator.biomeEdgeNoise.strength; + float dx = climate.getOffsetX(x, z, range); + float dz = climate.getOffsetZ(x, z, range); + float px = x + dx; + float pz = z + dz; + tag(cell, px, pz); + climate.apply(cell, px, pz, false); + climate.apply(cell, x, z, true); + } + } + + @Override + public void tag(Cell cell, float x, float z) { + continent.apply(cell, x, z); + root.tag(cell, x, z); + } + + public Climate getClimate() { + return climate; + } + + public Populator getPopulator(Terrain terrain) { + return terrainProvider.getPopulator(terrain); + } + + private TerrainPopulator register(Module module, Terrain terrain) { + TerrainPopulator populator = new TerrainPopulator(module, terrain); + terrainProvider.registerMixable(populator); + return populator; + } + + private Populator createContinent(GeneratorContext context) { + return new VoronoiContinentModule(context.seed, context.settings.generator); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/river/Lake.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/Lake.java new file mode 100644 index 0000000..2a9b977 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/Lake.java @@ -0,0 +1,80 @@ +package com.terraforged.core.world.river; + +import me.dags.noise.Source; +import me.dags.noise.util.NoiseUtil; +import me.dags.noise.util.Vec2f; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.terrain.Terrain; +import com.terraforged.core.world.terrain.TerrainPopulator; +import com.terraforged.core.world.terrain.Terrains; + +public class Lake extends TerrainPopulator { + + private static final int VALLEY_2 = River.VALLEY_WIDTH * River.VALLEY_WIDTH; + + private final float lakeDistance2; + private final float valleyDistance2; + private final float bankAlphaMin; + private final float bankAlphaMax; + private final float bankAlphaRange; + private final Vec2f center; + private final LakeConfig config; + private final Terrains terrains; + + public Lake(Vec2f center, float radius, LakeConfig config, Terrains terrains) { + super(Source.ZERO, terrains.lake); + this.center = center; + this.config = config; + this.bankAlphaMin = config.bankMin; + this.bankAlphaMax = Math.min(1, bankAlphaMin + 0.275F); + this.bankAlphaRange = bankAlphaMax - bankAlphaMin; + this.lakeDistance2 = radius * radius; + this.valleyDistance2 = VALLEY_2 - lakeDistance2; + this.terrains = terrains; + } + + @Override + public void apply(Cell cell, float x, float z) { + float distance2 = getDistance2(x, z); + if (distance2 > VALLEY_2) { + return; + } + + float bankHeight = getBankHeight(cell); + if (distance2 > lakeDistance2) { + if (cell.value < bankHeight) { + return; + } + float valleyAlpha = 1F - ((distance2 - lakeDistance2) / valleyDistance2); + cell.value = NoiseUtil.lerp(cell.value, bankHeight, valleyAlpha); + return; + } + + cell.value = Math.min(bankHeight, cell.value); + + if (distance2 < lakeDistance2) { + float depthAlpha = 1F - (distance2 / lakeDistance2); + float lakeDepth = Math.min(cell.value, config.depth); + cell.value = NoiseUtil.lerp(cell.value, lakeDepth, depthAlpha); + cell.tag = terrains.lake; + } + } + + @Override + public void tag(Cell cell, float x, float z) { + cell.tag = terrains.lake; + } + + private float getDistance2(float x, float z) { + float dx = center.x - x; + float dz = center.y - z; + return (dx * dx + dz * dz); + } + + private float getBankHeight(Cell cell) { + // scale bank height based on elevation of the terrain (higher terrain == taller banks) + float bankHeightAlpha = NoiseUtil.map(cell.value, bankAlphaMin, bankAlphaMax, bankAlphaRange); + // lerp between the min and max heights + return NoiseUtil.lerp(config.bankMin, config.bankMax, bankHeightAlpha); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/river/LakeConfig.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/LakeConfig.java new file mode 100644 index 0000000..d432051 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/LakeConfig.java @@ -0,0 +1,51 @@ +package com.terraforged.core.world.river; + +import com.terraforged.core.settings.GeneratorSettings; +import com.terraforged.core.world.heightmap.Levels; + +public class LakeConfig { + + public final float depth; + public final float chance; + public final float sizeMin; + public final float sizeMax; + public final float bankMin; + public final float bankMax; + public final float distanceMin; + public final float distanceMax; + + private LakeConfig(Builder builder) { + depth = builder.depth; + chance = builder.chance; + sizeMin = builder.sizeMin; + sizeMax = builder.sizeMax; + bankMin = builder.bankMin; + bankMax = builder.bankMax; + distanceMin = builder.distanceMin; + distanceMax = builder.distanceMax; + } + + public static LakeConfig of(GeneratorSettings.Lake settings, Levels levels) { + Builder builder = new Builder(); + builder.chance = settings.chance; + builder.sizeMin = settings.sizeMin; + builder.sizeMax = settings.sizeMax; + builder.depth = levels.water(-settings.depth); + builder.distanceMin = settings.minStartDistance; + builder.distanceMax = settings.maxStartDistance; + builder.bankMin = levels.water(settings.minBankHeight); + builder.bankMax = levels.water(settings.maxBankHeight); + return new LakeConfig(builder); + } + + public static class Builder { + public float chance; + public float depth = 10; + public float sizeMin = 30; + public float sizeMax = 100; + public float bankMin = 1; + public float bankMax = 8; + public float distanceMin = 0.025F; + public float distanceMax = 0.05F; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/river/PosGenerator.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/PosGenerator.java new file mode 100644 index 0000000..0d9cba2 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/PosGenerator.java @@ -0,0 +1,151 @@ +package com.terraforged.core.world.river; + +import me.dags.noise.domain.Domain; +import me.dags.noise.util.Vec2i; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.heightmap.WorldHeightmap; +import com.terraforged.core.world.terrain.Terrain; + +import java.util.Random; + +/** + * Generates random positions within (length / 4) of one of the 4 corners of a region. + * The first position generated will be near any of the four corners + * The second position generated will be near any of the 3 remaining corners to ensure a reasonable distance to the first + */ +public class PosGenerator { + + private final int quadSize; + private final Vec2i[] quads = new Vec2i[4]; + + private final int padding; + private final Domain domain; + private final Cell lookup; + private final WorldHeightmap heightmap; + + private int i; + private int dx; + private int dz; + + public PosGenerator(WorldHeightmap heightmap, Domain domain, Cell lookup, int size, int padding) { + this.domain = domain; + this.lookup = lookup; + this.padding = padding; + this.heightmap = heightmap; + this.quadSize = (size - (padding * 2)) / 4; + int x1 = 0; + int y1 = 0; + int x2 = 3 * quadSize; + int y2 = 3 * quadSize; + quads[index(0, 0)] = new Vec2i(x1, y1); + quads[index(1, 0)] = new Vec2i(x2, y1); + quads[index(0, 1)] = new Vec2i(x1, y2); + quads[index(1, 1)] = new Vec2i(x2, y2); + } + + private void nextSeed(Random random) { + int index = random.nextInt(4); + Vec2i vec = quads[index]; + i = index; + dx = padding + vec.x + random.nextInt(quadSize); + dz = padding + vec.y + random.nextInt(quadSize); + } + + private void nextPos(Random random) { + int steps = 1 + random.nextInt(3); + int index = (i + steps) & 3; + Vec2i vec = quads[index]; + i = index; + dx = padding + vec.x + random.nextInt(quadSize); + dz = padding + vec.y + random.nextInt(quadSize); + } + + public RiverNode next(int x, int z, Random random, int attempts) { + for (int i = 0; i < attempts; i++) { + nextSeed(random); + int px = x + dx; + int pz = z + dz; + int wx = (int) domain.getX(px, pz); + int wz = (int) domain.getY(px, pz); + float value1 = heightmap.getValue(lookup, px, pz); + float value2 = heightmap.getValue(lookup, wx, wz); + RiverNode.Type type1 = RiverNode.getType(value1); + RiverNode.Type type2 = RiverNode.getType(value2); + if (type1 == type2 && type1 != RiverNode.Type.NONE) { + if (type1 == RiverNode.Type.END) { + return new RiverNode(wx, wz, type1); + } + return new RiverNode(px, pz, type1); + } + } + return null; + } + + public RiverNode nextFrom(int x, int z, Random random, int attempts, RiverNode point, int mindDist2) { + for (int i = 0; i < attempts; i++) { + nextPos(random); + int px = x + dx; + int pz = z + dz; + if (dist2(px, pz, point.x, point.z) < mindDist2) { + continue; + } + int wx = (int) domain.getX(px, pz); + int wz = (int) domain.getY(px, pz); + float value1 = heightmap.getValue(lookup, px, pz); + float value2 = heightmap.getValue(lookup, wx, wz); + RiverNode.Type type1 = RiverNode.getType(value1); + RiverNode.Type type2 = RiverNode.getType(value2); + if (type1 == type2 && type1 == point.type.opposite()) { + if (type1 == RiverNode.Type.END) { + return new RiverNode(wx, wz, type1); + } + return new RiverNode(px, pz, type1); + } + } + return null; + } + + public RiverNode nextType(int x, int z, Random random, int attempts, RiverNode.Type match) { + for (int i = 0; i < attempts; i++) { + nextSeed(random); + int px = x + dx; + int pz = z + dz; + int wx = (int) domain.getX(px, pz); + int wz = (int) domain.getY(px, pz); + float value1 = heightmap.getValue(lookup, px, pz); + float value2 = heightmap.getValue(lookup, wx, wz); + RiverNode.Type type1 = RiverNode.getType(value1); + RiverNode.Type type2 = RiverNode.getType(value2); + if (type1 == type2 && type1 == match) { + return new RiverNode(px, pz, type1); + } + } + return null; + } + + public RiverNode nextMinHeight(int x, int z, Random random, int attempts, float minHeight) { + for (int i = 0; i < attempts; i++) { + nextPos(random); + int px = x + dx; + int pz = z + dz; + int wx = (int) domain.getX(px, pz); + int wz = (int) domain.getY(px, pz); + float value1 = heightmap.getValue(lookup, px, pz); + float value2 = heightmap.getValue(lookup, wx, wz); + if (value1 > minHeight && value2 > minHeight) { + return new RiverNode(px, pz, RiverNode.Type.START); + } + } + return null; + } + + private static int index(int x, int z) { + return z * 2 + x; + } + + private static float dist2(int x1, int y1, int x2, int y2) { + float dx = x2 - x1; + float dy = y2 - y1; + return dx * dx + dy * dy; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/river/River.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/River.java new file mode 100644 index 0000000..179f259 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/River.java @@ -0,0 +1,172 @@ +package com.terraforged.core.world.river; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import me.dags.noise.source.Line; +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.world.terrain.Terrain; +import com.terraforged.core.world.terrain.TerrainPopulator; +import com.terraforged.core.world.terrain.Terrains; + +public class River extends TerrainPopulator { + + public static final int VALLEY_WIDTH = 275; + protected static final float DEPTH_FADE_STRENGTH = 0.5F; + + public final boolean main; + private final boolean connecting; + + private final float bedHeight; + + private final float minBankHeight; + private final float maxBankHeight; + private final float bankAlphaMin; + private final float bankAlphaMax; + private final float bankAlphaRange; + private final Module bankVariance; + + private final Line bed; + private final Line banks; + private final Line valley; + public final RiverBounds bounds; + + private final Terrains terrains; + + private final float depthFadeBias; + + public River(RiverBounds bounds, RiverConfig config, Terrains terrains, double fadeIn, double fadeOut) { + this(bounds, config, terrains, fadeIn, fadeOut, false); + } + + public River(RiverBounds bounds, RiverConfig config, Terrains terrains, double fadeIn, double fadeOut, boolean connecting) { + super(Source.ZERO, terrains.river); + Module in = Source.constant(fadeIn); + Module out = Source.constant(fadeOut); + Module bedWidth = Source.constant(config.bedWidth * config.bedWidth); + Module bankWidth = Source.constant(config.bankWidth * config.bankWidth); + Module valleyWidth = Source.constant(VALLEY_WIDTH * VALLEY_WIDTH); + this.bounds = bounds; + this.main = config.main; + this.terrains = terrains; + this.connecting = connecting; + this.bedHeight = config.bedHeight; + this.minBankHeight = config.minBankHeight; + this.maxBankHeight = config.maxBankHeight; + this.bankAlphaMin = minBankHeight; + this.bankAlphaMax = Math.min(1, minBankHeight + 0.35F); + this.bankAlphaRange = bankAlphaMax - bankAlphaMin; + this.bankVariance = Source.perlin(1234, 150, 1); + this.depthFadeBias = 1 - DEPTH_FADE_STRENGTH; + this.bed = Source.line(bounds.x1(), bounds.y1(), bounds.x2(), bounds.y2(), bedWidth, in, out, 0.1F); + this.banks = Source.line(bounds.x1(), bounds.y1(), bounds.x2(), bounds.y2(), bankWidth, in, out, 0.1F); + this.valley = Source.line(bounds.x1(), bounds.y1(), bounds.x2(), bounds.y2(), valleyWidth, Source.ZERO, Source.ZERO, 0.33F); + } + + @Override + public void apply(Cell cell, float x, float z) { + if (cell.value <= bedHeight) { + return; + } + carve(cell, x, z); + } + + @Override + public void tag(Cell cell, float x, float z) { + if (!terrains.overridesRiver(cell.tag)) { + cell.tag = terrains.river; + } + } + + private void carve(Cell cell, float x, float z) { + float valleyAlpha = valley.getValue(x, z); + if (valleyAlpha == 0) { + return; + } + + // riverMask decreases the closer to the river the position gets + cell.riverMask *= (1 - valleyAlpha); + + float bankHeight = getBankHeight(cell, x, z); + if (!carveValley(cell, valleyAlpha, bankHeight)) { + return; + } + + // is a branching river and x,z is past the connecting point + if (connecting && banks.clipEnd(x, z)) { + return; + } + + float widthModifier = banks.getWidthModifier(x, z); + float banksAlpha = banks.getValue(x, z, widthModifier); + if (banksAlpha == 0) { + return; + } + + float bedHeight = getBedHeight(bankHeight, widthModifier); + if (!carveBanks(cell, banksAlpha, bedHeight)) { + return; + } + + float bedAlpha = bed.getValue(x, z); + if (bedAlpha == 0) { + return; + } + + carveBed(cell, bedAlpha, bedHeight); + } + + private float getBankHeight(Cell cell, float x, float z) { + // scale bank height based on elevation of the terrain (higher terrain == taller banks) + float bankHeightAlpha = NoiseUtil.map(cell.value, bankAlphaMin, bankAlphaMax, bankAlphaRange); + // use perlin noise to add a little extra variance to the bank height + float bankHeightVariance = bankVariance.getValue(x, z); + // lerp between the min and max heights + return NoiseUtil.lerp(minBankHeight, maxBankHeight, bankHeightAlpha * bankHeightVariance); + } + + private float getBedHeight(float bankHeight, float widthModifier) { + // scale depth of river by with it's width (wider == deeper) + // depthAlpha changes the river depth up ${DEPTH_FADE_STRENGTH} % + float depthAlpha = depthFadeBias + (DEPTH_FADE_STRENGTH * widthModifier); + return NoiseUtil.lerp(bankHeight, this.bedHeight, depthAlpha); + } + + private boolean carveValley(Cell cell, float valleyAlpha, float bankHeight) { + // lerp the position's height to the riverbank height + if (cell.value > bankHeight) { + cell.value = NoiseUtil.lerp(cell.value, bankHeight, valleyAlpha); + } + return true; + } + + private boolean carveBanks(Cell cell, float banksAlpha, float bedHeight) { + // lerp the position's height to the riverbed height (ie the riverbank slopes) + if (cell.value > bedHeight) { + cell.value = NoiseUtil.lerp(cell.value, bedHeight, banksAlpha); + tag(cell, terrains.riverBanks); + } + return true; + } + + private void carveBed(Cell cell, float bedAlpha, float bedHeight) { + // lerp the height down to the riverbed height + cell.value = NoiseUtil.lerp(cell.value, bedHeight, bedAlpha); + tag(cell, terrains.river); + } + + + private void tag(Cell cell, Terrain tag) { + if (!terrains.overridesRiver(cell.tag)) { + cell.tag = tag; + } + } + + public static boolean validStart(float value) { + return value > (70F / 256F); + } + + public static boolean validEnd(float value) { + return value < (60F / 256F); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverBounds.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverBounds.java new file mode 100644 index 0000000..dc53b8e --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverBounds.java @@ -0,0 +1,66 @@ +package com.terraforged.core.world.river; + +import me.dags.noise.util.Vec2f; + +public class RiverBounds { + + private final int x1; + private final int y1; + private final int x2; + private final int y2; + private final int minX; + private final int minY; + private final int maxX; + private final int maxY; + + public RiverBounds(int x1, int y1, int x2, int y2, int radius) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.minX = Math.min(x1, x2) - radius; + this.minY = Math.min(y1, y2) - radius; + this.maxX = Math.max(x1, x2) + radius; + this.maxY = Math.max(y1, y2) + radius; + } + + public int x1() { + return x1; + } + + public int y1() { + return y1; + } + + public int x2() { + return x2; + } + + public int y2() { + return y2; + } + + public boolean overlaps(RiverBounds other) { + if (minX > other.maxX || maxX < other.minX) { + return false; + } + if (minY > other.maxY || maxY < other.minY) { + return false; + } + return true; + } + + public Vec2f pos(float distance) { + int dx = x2() - x1(); + int dy = y2() - y1(); + return new Vec2f(x1() + dx * distance, y1() + dy * distance); + } + + public static RiverBounds fromNodes(RiverNode p1, RiverNode p2) { + if (p1.type == RiverNode.Type.START) { + return new RiverBounds(p1.x, p1.z, p2.x, p2.z, 300); + } else { + return new RiverBounds(p2.x, p2.z, p1.x, p1.z, 300); + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverConfig.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverConfig.java new file mode 100644 index 0000000..04726f8 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverConfig.java @@ -0,0 +1,87 @@ +package com.terraforged.core.world.river; + +import com.terraforged.core.world.heightmap.Levels; + +public class RiverConfig { + + public final boolean main; + public final int bedWidth; + public final int bankWidth; + public final float bedHeight; + public final float minBankHeight; + public final float maxBankHeight; + public final int length2; + public final double fade; + + private RiverConfig(Builder builder) { + main = builder.main; + bedWidth = builder.bedWidth; + bankWidth = builder.bankWidth; + bedHeight = builder.levels.water(-builder.bedDepth); + minBankHeight = builder.levels.water(builder.minBankHeight); + maxBankHeight = builder.levels.water(builder.maxBankHeight); + length2 = builder.length * builder.length; + fade = builder.fade; + } + + public static Builder builder(Levels levels) { + return new Builder(levels); + } + + public static class Builder { + + private boolean main = false; + private int bedWidth = 4; + private int bankWidth = 15; + private int bedDepth = 5; + private int maxBankHeight = 1; + private int minBankHeight = 1; + private int length = 1000; + private double fade = 0.2; + private final Levels levels; + + private Builder(Levels levels) { + this.levels = levels; + } + + public Builder main(boolean value) { + this.main = value; + return this; + } + + public Builder bedWidth(int value) { + this.bedWidth = value; + return this; + } + + public Builder bankWidth(int value) { + this.bankWidth = value; + return this; + } + + public Builder bedDepth(int depth) { + this.bedDepth = depth; + return this; + } + + public Builder bankHeight(int min, int max) { + this.minBankHeight = Math.min(min, max); + this.maxBankHeight = Math.max(min, max); + return this; + } + + public Builder length(int value) { + this.length = value; + return this; + } + + public Builder fade(double value) { + this.fade = value; + return this; + } + + public RiverConfig build() { + return new RiverConfig(this); + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverManager.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverManager.java new file mode 100644 index 0000000..da3cf79 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverManager.java @@ -0,0 +1,86 @@ +package com.terraforged.core.world.river; + +import me.dags.noise.util.NoiseUtil; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.util.Cache; +import com.terraforged.core.world.GeneratorContext; +import com.terraforged.core.world.heightmap.WorldHeightmap; +import com.terraforged.core.world.terrain.Terrain; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +public class RiverManager { + + private static final int QUAD_SIZE = (1 << RiverRegion.SCALE) / 2; + + private final LakeConfig lakes; + private final RiverConfig primary; + private final RiverConfig secondary; + private final RiverConfig tertiary; + private final WorldHeightmap heightmap; + private final GeneratorContext context; + private final Cache cache = new Cache<>(60, 60, TimeUnit.SECONDS, () -> new ConcurrentHashMap<>()); + + public RiverManager(WorldHeightmap heightmap, GeneratorContext context) { + this.heightmap = heightmap; + this.context = context; + this.primary = RiverConfig.builder(context.levels) + .bankHeight(context.settings.generator.primaryRivers.minBankHeight, context.settings.generator.primaryRivers.maxBankHeight) + .bankWidth(context.settings.generator.primaryRivers.bankWidth) + .bedWidth(context.settings.generator.primaryRivers.bedWidth) + .bedDepth(context.settings.generator.primaryRivers.bedDepth) + .fade(context.settings.generator.primaryRivers.fade) + .length(2500) + .main(true) + .build(); + this.secondary = RiverConfig.builder(context.levels) + .bankHeight(context.settings.generator.secondaryRiver.minBankHeight, context.settings.generator.secondaryRiver.maxBankHeight) + .bankWidth(context.settings.generator.secondaryRiver.bankWidth) + .bedWidth(context.settings.generator.secondaryRiver.bedWidth) + .bedDepth(context.settings.generator.secondaryRiver.bedDepth) + .fade(context.settings.generator.secondaryRiver.fade) + .length(1000) + .build(); + this.tertiary = RiverConfig.builder(context.levels) + .bankHeight(context.settings.generator.tertiaryRivers.minBankHeight, context.settings.generator.tertiaryRivers.maxBankHeight) + .bankWidth(context.settings.generator.tertiaryRivers.bankWidth) + .bedWidth(context.settings.generator.tertiaryRivers.bedWidth) + .bedDepth(context.settings.generator.tertiaryRivers.bedDepth) + .fade(context.settings.generator.tertiaryRivers.fade) + .length(500) + .build(); + this.lakes = LakeConfig.of(context.settings.generator.lake, context.levels); + } + + public void apply(Cell cell, float x, float z) { + int rx = RiverRegion.blockToRegion((int) x); + int rz = RiverRegion.blockToRegion((int) z); + + // check which quarter of the region pos (x,y) is in & get the neighbouring regions' relative coords + int qx = x < RiverRegion.regionToBlock(rx) + QUAD_SIZE ? -1 : 1; + int qz = z < RiverRegion.regionToBlock(rz) + QUAD_SIZE ? -1 : 1; + + // relative positions of neighbouring regions + int minX = Math.min(0, qx); + int minZ = Math.min(0, qz); + int maxX = Math.max(0, qx); + int maxZ = Math.max(0, qz); + + for (int dz = minZ; dz <= maxZ; dz++) { + for (int dx = minX; dx <= maxX; dx++) { + getRegion(rx + dx, rz + dz).apply(cell, x, z); + } + } + } + + private RiverRegion getRegion(int rx, int rz) { + long id = NoiseUtil.seed(rx, rz); + RiverRegion region = cache.get(id); + if (region == null) { + region = new RiverRegion(rx, rz, heightmap, context, primary, secondary, tertiary, lakes); + cache.put(id, region); + } + return region; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverNode.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverNode.java new file mode 100644 index 0000000..99b4700 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverNode.java @@ -0,0 +1,48 @@ +package com.terraforged.core.world.river; + +public class RiverNode { + + public final int x; + public final int z; + public final Type type; + + public RiverNode(int x, int z, Type type) { + this.x = x; + this.z = z; + this.type = type; + } + + public static Type getType(float value) { + if (River.validStart(value)) { + return Type.START; + } + if (River.validEnd(value)) { + return Type.END; + } + return Type.NONE; + } + + public enum Type { + NONE { + @Override + public Type opposite() { + return this; + } + }, + START { + @Override + public Type opposite() { + return END; + } + }, + END { + @Override + public Type opposite() { + return START; + } + }, + ; + + public abstract Type opposite(); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverRegion.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverRegion.java new file mode 100644 index 0000000..44913e8 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/river/RiverRegion.java @@ -0,0 +1,207 @@ +package com.terraforged.core.world.river; + +import me.dags.noise.domain.Domain; +import me.dags.noise.util.NoiseUtil; +import me.dags.noise.util.Vec2f; +import me.dags.noise.util.Vec2i; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.util.concurrent.ObjectPool; +import com.terraforged.core.world.GeneratorContext; +import com.terraforged.core.world.heightmap.WorldHeightmap; +import com.terraforged.core.world.terrain.Terrain; +import com.terraforged.core.world.terrain.Terrains; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + +public class RiverRegion { + + public static final int SCALE = 12; +// private static final float LAKE_CHANCE = 0.05F; +// private static final float LAKE_MIN_DIST = 0.025F; +// private static final float LAKE_MAX_DIST = 0.05F; +// private static final float LAKE_MIN_SIZE = 50; +// private static final float LAKE_MAX_SIZE = 100; + + private final int seed; + private final Domain domain; + private final Terrains terrains; + private final LakeConfig lake; + private final RiverConfig primary; + private final RiverConfig secondary; + private final RiverConfig tertiary; + + private final List rivers; + private final List lakes = new LinkedList<>(); + + public RiverRegion(int regionX, int regionZ, WorldHeightmap heightmap, GeneratorContext context, RiverConfig primary, RiverConfig secondary, RiverConfig tertiary, LakeConfig lake) { + this.seed = new Random(NoiseUtil.seed(regionX, regionZ)).nextInt(); + this.lake = lake; + this.primary = primary; + this.secondary = secondary; + this.tertiary = tertiary; + this.terrains = context.terrain; + this.domain = Domain.warp(seed, 400, 1, 400).add(Domain.warp(seed + 1, 50, 1, 25)); + try (ObjectPool.Item> cell = Cell.pooled()) { + PosGenerator pos = new PosGenerator(heightmap, domain, cell.getValue(),1 << SCALE, River.VALLEY_WIDTH); + this.rivers = generate(regionX, regionZ, pos); + } + } + + public void apply(Cell cell, float x, float z) { + float px = domain.getX(x, z); + float pz = domain.getY(x, z); + for (River river : rivers) { + river.apply(cell, px, pz); + } + for (Lake lake : lakes) { + lake.apply(cell, px, pz); + } + } + + private List generate(int regionX, int regionZ, PosGenerator pos) { + int x = regionToBlock(regionX); + int z = regionToBlock(regionZ); + long regionSeed = NoiseUtil.seed(regionX, regionZ); + + Random random = new Random(regionSeed); + List rivers = new LinkedList<>(); + + // generates main rivers until either 10 attempts have passed or 2 rivers generate + for (int i = 0; rivers.size() < 1 && i < 20; i++) { + generateRiver(x, z, pos, primary, random, rivers); + } + + for (int i = 0; rivers.size() < 10 && i < 15; i++) { + generateRiver(x, z, pos, secondary, random, rivers); + } + + for (int i = 0; rivers.size() < 10 && i < 15; i++) { + generateConnectingRiver(x, z, pos, tertiary, random, rivers); + } + + for (int i = 0; rivers.size() < 20 && i < 40; i++) { + generateRiver(x, z, pos, tertiary, random, rivers); + } + + + Collections.reverse(rivers); + + return rivers; + } + + /** + * Attempts to generate a line that starts inland and reaches the ocean + */ + private boolean generateRiver(int x, int z, PosGenerator pos, RiverConfig config, Random random, List rivers) { + // generate either a river start or end node + RiverNode p1 = pos.next(x, z, random, 50); + if (p1 == null) { + return false; + } + + // generate a node with a min distance from p1 and that has the opposite node type to p1 + RiverNode p2 = pos.nextFrom(x, z, random,50, p1, config.length2); + if (p2 == null) { + return false; + } + + // avoid collisions with existing rivers + RiverBounds bounds = RiverBounds.fromNodes(p1, p2); + for (River river : rivers) { + if (bounds.overlaps(river.bounds)) { + return false; + } + } + + generateLake(bounds, random); + + return rivers.add(new River(bounds, config, terrains, config.fade, 0)); + } + + /** + * Attempts to generate an inland position (p1), finds the nearest river (AB) to it, and tries to connect + * a line from p1 to some position along AB + */ + private boolean generateConnectingRiver(int x, int z, PosGenerator pos, RiverConfig config, Random random, List rivers) { + // generate a river start node + RiverNode p1 = pos.nextType(x, z, random, 50, RiverNode.Type.START); + if (p1 == null) { + return false; + } + + // find closest river + River closest = null; + float startDist2 = Integer.MAX_VALUE; + for (River river : rivers) { + float d2 = dist2(p1.x, p1.z, river.bounds.x1(), river.bounds.y1()); + if (d2 < startDist2) { + startDist2 = d2; + closest = river; + } + } + + // no close rivers + if (closest == null) { + return false; + } + + // p1 is too close to start of the closest river + if (startDist2 < 640000) { + return false; + } + + // p1 should be closer to the main river start than it's end, otherwise we're likely to get forks + // in the wrong direction (ie one river splitting into two, rather two rivers joining) + float endDist2 = dist2(p1.x, p1.z, closest.bounds.x2(), closest.bounds.y2()); + if (endDist2 < startDist2) { + return false; + } + + // pick a point some random distance along the main river, between 30%-70% along the main river length + // 0.4 offset should ensure the main river has become sufficiently wide for it not to look weird having + // a second river connect to it + float dist = 0.4F + (random.nextFloat() * 0.3F); + Vec2i p2 = closest.bounds.pos(dist).toInt(); + RiverBounds bounds = new RiverBounds(p1.x, p1.z, p2.x, p2.y, 300); + + // check that the new river does not clash/overlap any other nearby rivers + for (River river : rivers) { + if (river == closest) { + continue; + } + if (river.bounds.overlaps(bounds)) { + return false; + } + } + + generateLake(bounds, random); + + return rivers.add(new River(bounds, config, terrains, config.fade, 0, true)); + } + + private void generateLake(RiverBounds bounds, Random random) { + if (random.nextFloat() < lake.chance) { + float size = lake.sizeMin + (random.nextFloat() * lake.sizeMax); + float distance = lake.distanceMin + (random.nextFloat() * lake.distanceMax); + Vec2f center = bounds.pos(distance); + lakes.add(new Lake(center, size, lake, terrains)); + } + } + + private static float dist2(float x1, float y1, float x2, float y2) { + float dx = x2 - x1; + float dy = y2 - y1; + return dx * dx + dy * dy; + } + + public static int regionToBlock(int value) { + return value << SCALE; + } + + public static int blockToRegion(int value) { + return value >> SCALE; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/LandForms.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/LandForms.java new file mode 100644 index 0000000..fd25347 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/LandForms.java @@ -0,0 +1,254 @@ +package com.terraforged.core.world.terrain; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import me.dags.noise.func.EdgeFunc; +import com.terraforged.core.util.Seed; +import com.terraforged.core.world.heightmap.Levels; + +public class LandForms { + + private static final int PLAINS_H = 250; + private static final double PLAINS_V = 0.24; + + public static final int PLATEAU_H = 500; + public static final double PLATEAU_V = 0.45; + + public static final int LOESS_H0 = 100; + public static final int LOESS_H1 = 200; + public static final double LOESS_V = 0.5; + + private static final int HILLS_H = 500; + private static final double HILLS1_V = 0.6; + private static final double HILLS2_V = 0.55; + + private static final int MOUNTAINS_H = 410; + private static final double MOUNTAINS_V = 0.7; + + private static final int MOUNTAINS2_H = 400; + private static final double MOUNTAINS2_V = 0.645; + + private final float terrainHorizontalScale; + private final float terrainVerticalScale; + private final float groundLevel; + private final float seaLevel; + + public LandForms(Levels levels) { + terrainHorizontalScale = 1F; + terrainVerticalScale = 0.975F; + groundLevel = levels.ground; + seaLevel = levels.water; + } + + public Module deepOcean(int seed) { + Module hills = Source.perlin(++seed, 150, 3) + .scale(seaLevel * 0.7) + .bias(Source.perlin(++seed, 200, 1).scale(seaLevel * 0.2F)); + + Module canyons = Source.perlin(++seed, 150, 4) + .powCurve(0.2) + .invert() + .scale(seaLevel * 0.7) + .bias(Source.perlin(++seed, 170, 1).scale(seaLevel * 0.15F)); + + return Source.perlin(++seed, 500, 1) + .blend(hills, canyons, 0.6, 0.65) + .warp(++seed, 50, 2, 50); + } + + public Module steppe(Seed seed) { + int scaleH = Math.round(PLAINS_H * terrainHorizontalScale); + + double erosionAmount = 0.45; + + Module erosion = Source.build(seed.next(), scaleH * 2, 3).lacunarity(3.75).perlin().alpha(erosionAmount); + Module warpX = Source.build(seed.next(), scaleH / 4, 3).lacunarity(3).perlin(); + Module warpY = Source.build(seed.next(), scaleH / 4, 3).lacunarity(3).perlin(); + + return Source.perlin(seed.next(), scaleH, 1) + .mult(erosion) + .warp(warpX, warpY, Source.constant(scaleH / 4F)) + .warp(seed.next(), 256, 1, 200) + .scale(0.125 * terrainVerticalScale) + .bias(groundLevel); + } + + public Module plains(Seed seed) { + int scaleH = Math.round(PLAINS_H * terrainHorizontalScale); + + double erosionAmount = 0.45; + + Module erosion = Source.build(seed.next(), scaleH * 2, 3).lacunarity(3.75).perlin().alpha(erosionAmount); + Module warpX = Source.build(seed.next(), scaleH / 4, 3).lacunarity(3.5).perlin(); + Module warpY = Source.build(seed.next(), scaleH / 4, 3).lacunarity(3.5).perlin(); + + return Source.perlin(seed.next(), scaleH, 1) + .mult(erosion) + .warp(warpX, warpY, Source.constant(scaleH / 4F)) + .warp(seed.next(), 256, 1, 256) + .scale(PLAINS_V * terrainVerticalScale) + .bias(groundLevel); + } + + public Module plateau(Seed seed) { + Module valley = Source.ridge(seed.next(), 500, 1).invert() + .warp(seed.next(), 100, 1, 150) + .warp(seed.next(), 20, 1, 15); + + Module top = Source.build(seed.next(), 150, 3).lacunarity(2.45).ridge() + .warp(seed.next(), 300, 1, 150) + .warp(seed.next(), 40, 2, 20) + .scale(0.15) + .mult(valley.clamp(0.02, 0.1).map(0, 1)); + + Module surface = Source.perlin(seed.next(), 20, 3).scale(0.05) + .warp(seed.next(), 40, 2, 20); + + return valley + .mult(Source.cubic(seed.next(), 500, 1).scale(0.6).bias(0.3)) + .add(top) + .terrace( + Source.perlin(seed.next(), 20, 1).scale(0.3).bias(0.2), + Source.perlin(seed.next(), 20, 2).scale(0.1).bias(0.2), + 4, + 0.4 + ) + .add(surface) + .scale(PLATEAU_V * terrainVerticalScale) + .bias(groundLevel); + } + + public Module badlands(Seed seed) { + Module ridge = Source.build(seed.next(), LOESS_H0, 4).ridge().scale(0.25); + Module ridge2 = Source.build(seed.next(), LOESS_H1, 3).gain(1.5).ridge().scale(0.75); + Module mask = Source.perlin(seed.next(), LOESS_H1 * 2, 2).clamp(0.2, 0.9).map(0, 1); + return ridge.add(ridge2).mult(mask) + .warp(seed.next(), 360, 3, 200) + .scale(LOESS_V * terrainVerticalScale) + .bias(groundLevel); + } + + public Module hills1(Seed seed) { + return Source.perlin(seed.next(), 200, 3) + .mult(Source.billow(seed.next(), 400, 3).alpha(0.5)) + .warp(seed.next(), 30, 3, 20) + .warp(seed.next(), 400, 3, 200) + .scale(HILLS1_V * terrainVerticalScale) + .bias(groundLevel); + } + + public Module hills2(Seed seed) { + return Source.cubic(seed.next(), 128, 2) + .mult(Source.perlin(seed.next(), 32, 4).alpha(0.075)) + .warp(seed.next(), 30, 3, 20) + .warp(seed.next(), 400, 3, 200) + .mult(Source.ridge(seed.next(), 512, 2).alpha(0.8)) + .scale(HILLS2_V * terrainVerticalScale) + .bias(groundLevel); + } + + public Module dales(Seed seed) { + Module hills1 = Source.build(seed.next(), 300, 4).gain(0.8).lacunarity(4).billow().powCurve(0.5).scale(0.75); + Module hills2 = Source.build(seed.next(), 350, 3).gain(0.8).lacunarity(4).billow().pow(1.25); + Module combined = Source.perlin(seed.next(), 400, 1).clamp(0.3, 0.6).map(0, 1).blend( + hills1, + hills2, + 0.4, + 0.75 + ); + Module hills = combined + .pow(1.125) + .warp(seed.next(), 300, 1, 100); + return hills.scale(0.225).bias(groundLevel); + } + + public Module mountains(Seed seed) { + int scaleH = Math.round(MOUNTAINS_H * terrainHorizontalScale); + return Source.build(seed.next(), scaleH, 4).gain(1.15).lacunarity(2.35).ridge() + .mult(Source.perlin(seed.next(), 24, 4).alpha(0.075)) + .warp(seed.next(), 350, 1, 150) + .scale(MOUNTAINS_V * terrainVerticalScale) + .bias(groundLevel); + } + + public Module mountains2(Seed seed) { + double scale = MOUNTAINS2_V * terrainVerticalScale; + Module cell = Source.cellEdge(seed.next(), 360, EdgeFunc.DISTANCE_2).scale(1.2).clamp(0, 1) + .warp(seed.next(), 200, 2, 100); + Module blur = Source.perlin(seed.next(), 10, 1).alpha(0.025); + Module surface = Source.ridge(seed.next(), 125, 4).alpha(0.37); + Module mountains = cell.clamp(0, 1).mult(blur).mult(surface).pow(1.1); + return mountains.scale(scale).bias(groundLevel); + } + + public Module mountains3(Seed seed) { + double scale = MOUNTAINS2_V * terrainVerticalScale; + + Module cell = Source.cellEdge(seed.next(), MOUNTAINS2_H, EdgeFunc.DISTANCE_2).scale(1.2).clamp(0, 1) + .warp(seed.next(), 200, 2, 100); + Module blur = Source.perlin(seed.next(), 10, 1).alpha(0.025); + Module surface = Source.ridge(seed.next(), 125, 4).alpha(0.37); + Module mountains = cell.clamp(0, 1).mult(blur).mult(surface).pow(1.1); + + Module terraced = mountains.terrace( + Source.perlin(seed.next(), 50, 1).scale(0.5), + Source.perlin(seed.next(), 100, 1).clamp(0.5, 0.95).map(0, 1), + Source.constant(0.45), + 0.2F, + 0.45F, + 24, + 1 + ); + + return terraced.scale(scale).bias(groundLevel); + } + + public Module badlands2(Seed seed) { + Module mask = Source.perlin(seed.next(), 800, 1).clamp(0.35, 0.65).map(0, 1); + Module hills = Source.ridge(seed.next(), 500, 4) + .warp(seed.next(), 400, 2, 100) + .mult(mask); + + double modulation = 0.4; + double alpha = 1 - modulation; + Module mod1 = hills.warp(seed.next(), 100, 1, 50).scale(modulation); + + Module lowFreq = hills.steps(4).scale(alpha).add(mod1); + Module highFreq = hills.steps(10).scale(alpha).add(mod1); + Module detail = lowFreq.add(highFreq); + + Module mod2 = hills.mult(Source.perlin(seed.next(), 400, 3).scale(modulation)); + Module shape = hills.terrace(0.1, 1, 4, 0.01) + .scale(alpha) + .add(mod2) + .scale(alpha); + + return shape.mult(detail.alpha(0.5)).scale(0.7).bias(groundLevel); + } + + public Module torridonian(Seed seed) { + Module plains = Source.perlin(seed.next(), 100, 3) + .warp(seed.next(), 300, 1, 150) + .warp(seed.next(), 20, 1, 40) + .scale(0.15); + + Module hills = Source.perlin(seed.next(), 150, 4) + .warp(seed.next(), 300, 1, 200) + .warp(seed.next(), 20, 2, 20) + .boost(); + + Module test = Source.perlin(seed.next(), 200, 3) + .blend(plains, hills, 0.6, 0.6) + .terrace( + Source.perlin(seed.next(), 120, 1).scale(0.25), + Source.perlin(seed.next(), 200, 1).scale(0.5).bias(0.5), + Source.constant(0.5), + 0, + 0.3, + 6, + 1 + ).boost(); + + return test.scale(0.5).bias(groundLevel); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/Terrain.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/Terrain.java new file mode 100644 index 0000000..1c8f998 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/Terrain.java @@ -0,0 +1,151 @@ +package com.terraforged.core.world.terrain; + +import com.terraforged.core.cell.Tag; +import com.terraforged.core.settings.Settings; +import com.terraforged.core.world.heightmap.Levels; + +import java.util.concurrent.atomic.AtomicInteger; + +public class Terrain implements Tag { + + public static final int ID_START = 9; + public static final AtomicInteger MAX_ID = new AtomicInteger(0); + public static final Terrain NONE = new Terrain("none", -1); + + private final String name; + private final int id; + private float weight; + + public Terrain(String name, int id) { + this(name, id, 1F); + } + + public Terrain(String name, int id, double weight) { + this.name = name; + this.id = id; + this.weight = (float) weight; + MAX_ID.set(Math.max(MAX_ID.get(), id)); + } + + public int getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + public float getMax(float noise) { + return 1F; + } + + public float getWeight() { + return weight; + } + + @Override + public String toString() { + return getName(); + } + + public static Terrain ocean(Settings settings) { + return new Terrain("ocean", 0) { + + private final float max = new Levels(settings.generator).water; + + @Override + public float getMax(float noise) { + return max; + } + }; + } + + public static Terrain deepOcean(Settings settings) { + return new Terrain("deep_ocean", 1) { + + private final float max = new Levels(settings.generator).water / 2F; + + @Override + public float getMax(float noise) { + return max; + } + }; + } + + public static Terrain coast(Settings settings) { + return new Terrain("coast", 2) { + + private final float max = new Levels(settings.generator).ground(1); + + @Override + public float getMax(float noise) { + return max + (noise * (1 / 255F)); + } + }; + } + + public static Terrain beach(Settings settings) { + return new Terrain("beach", 2) { + + private final float max = new Levels(settings.generator).ground(1); + + @Override + public float getMax(float noise) { + return max + (noise * (1 / 255F)); + } + }; + } + + public static Terrain lake(Settings settings) { + return new Terrain("lake", 3); + } + + public static Terrain river(Settings settings) { + return new Terrain("river", 3); + } + + public static Terrain riverBank(Settings settings) { + return new Terrain("river_banks", 4); + } + + public static Terrain steppe(Settings settings) { + return new Terrain("steppe", 5, settings.terrain.steppe); + } + + public static Terrain plains(Settings settings) { + return new Terrain("plains", 5, settings.terrain.plains); + } + + public static Terrain plateau(Settings settings) { + return new Terrain("plateau", 6, settings.terrain.plateau); + } + + public static Terrain badlands(Settings settings) { + return new Terrain("badlands", 7, settings.terrain.badlands); + } + + public static Terrain hills(Settings settings) { + return new Terrain("hills", 8, settings.terrain.hills); + } + + public static Terrain dales(Settings settings) { + return new Terrain("dales", 9, settings.terrain.hills); + } + + public static Terrain torridonian(Settings settings) { + return new Terrain("torridonian_fells", 10, settings.terrain.torridonian); + } + + public static Terrain mountains(Settings settings) { + return new Terrain("mountains", 11, settings.terrain.mountains); + } + + public static Terrain volcano(Settings settings) { + return new Terrain("volcano", 12, settings.terrain.volcano); + } + + public static Terrain volcanoPipe(Settings settings) { + return new Terrain("volcano_pipe", 13, settings.terrain.volcano); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/TerrainPopulator.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/TerrainPopulator.java new file mode 100644 index 0000000..1185802 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/TerrainPopulator.java @@ -0,0 +1,77 @@ +package com.terraforged.core.world.terrain; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.util.Seed; + +import java.util.Arrays; +import java.util.function.BiFunction; + +public class TerrainPopulator implements Populator { + + private final Terrain type; + private final Module source; + + public TerrainPopulator(Module source, Terrain type) { + this.type = type; + this.source = source; + } + + public Module getSource() { + return source; + } + + public Terrain getType() { + return type; + } + + @Override + public void apply(Cell cell, float x, float z) { + cell.value = source.getValue(x, z); + cell.tag = type; + } + + @Override + public void tag(Cell cell, float x, float y) { + cell.tag = type; + } + + public static TerrainPopulator[] combine(TerrainPopulator[] input, Seed seed, int scale) { + return combine(input, (tp1, tp2) -> TerrainPopulator.combine(tp1, tp2, seed, scale)); + } + + public static TerrainPopulator combine(TerrainPopulator tp1, TerrainPopulator tp2, Seed seed, int scale) { + Module combined = Source.perlin(seed.next(), scale, 1) + .warp(seed.next(), scale / 2, 2, scale / 2) + .blend(tp1.getSource(), tp2.getSource(), 0.5, 0.25); + + String name = tp1.getType().getName() + "-" + tp2.getType().getName(); + int id = Terrain.ID_START + 1 + tp1.getType().getId() * tp2.getType().getId(); + float weight = Math.min(tp1.getType().getWeight(), tp2.getType().getWeight()); + Terrain type = new Terrain(name, id, weight); + + return new TerrainPopulator(combined, type); + } + + public static T[] combine(T[] input, BiFunction operator) { + int length = input.length; + for (int i = 1; i < input.length; i++) { + length += (input.length - i); + } + + T[] result = Arrays.copyOf(input, length); + for (int i = 0, k = input.length; i < input.length; i++) { + T t1 = input[i]; + result[i] = t1; + for (int j = i + 1; j < input.length; j++, k++) { + T t2 = input[j]; + T t3 = operator.apply(t1, t2); + result[k] = t3; + } + } + + return result; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/Terrains.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/Terrains.java new file mode 100644 index 0000000..406b77e --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/Terrains.java @@ -0,0 +1,115 @@ +package com.terraforged.core.world.terrain; + +import com.terraforged.core.settings.Settings; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Terrains { + + public final Terrain ocean; + public final Terrain deepOcean; + public final Terrain coast; + public final Terrain beach; + public final Terrain lake; + public final Terrain river; + public final Terrain riverBanks; + public final Terrain badlands; + public final Terrain steppe; + public final Terrain plains; + public final Terrain plateau; + public final Terrain hills; + public final Terrain dales; + public final Terrain torridonian; + public final Terrain mountains; + public final Terrain volcano; + public final Terrain volcanoPipe; + public final List index; + + private Terrains(Mutable mutable) { + List index = new ArrayList<>(); + Collections.addAll( + index, + ocean = mutable.ocean, + deepOcean = mutable.deepOcean, + coast = mutable.coast, + beach = mutable.beach, + lake = mutable.lake, + river = mutable.river, + torridonian = mutable.torridonian, + riverBanks = mutable.riverbanks, + badlands = mutable.badlands, + plateau = mutable.plateau, + steppe = mutable.steppe, + plains = mutable.plains, + hills = mutable.hills, + dales = mutable.dales, + mountains = mutable.mountains, + volcano = mutable.volcanoes, + volcanoPipe = mutable.volcanoPipe + ); + this.index = Collections.unmodifiableList(index); + } + + public int getId(Terrain landForm) { + return landForm.getId(); + } + + public boolean overridesRiver(Terrain terrain) { + return isOcean(terrain) || terrain == coast; + } + + public boolean isOcean(Terrain terrain) { + return terrain == ocean || terrain == deepOcean; + } + + public boolean isRiver(Terrain terrain) { + return terrain == river || terrain == riverBanks; + } + + public static Terrains create(Settings settings) { + Mutable terrain = new Mutable(); + terrain.ocean = Terrain.ocean(settings); + terrain.deepOcean = Terrain.deepOcean(settings); + terrain.coast = Terrain.coast(settings); + terrain.beach = Terrain.beach(settings); + terrain.lake = Terrain.lake(settings); + terrain.river = Terrain.river(settings); + terrain.riverbanks = Terrain.riverBank(settings); + terrain.badlands = Terrain.badlands(settings); + terrain.plateau = Terrain.plateau(settings); + terrain.steppe = Terrain.steppe(settings); + terrain.plains = Terrain.plains(settings); + terrain.hills = Terrain.hills(settings); + terrain.dales = Terrain.dales(settings); + terrain.torridonian = Terrain.torridonian(settings); + terrain.mountains = Terrain.mountains(settings); + terrain.volcanoes = Terrain.volcano(settings); + terrain.volcanoPipe = Terrain.volcanoPipe(settings); + return terrain.create(); + } + + public static final class Mutable { + public Terrain ocean = Terrain.NONE; + public Terrain deepOcean = Terrain.NONE; + public Terrain coast = Terrain.NONE; + public Terrain beach = Terrain.NONE; + public Terrain lake = Terrain.NONE; + public Terrain river = Terrain.NONE; + public Terrain riverbanks = Terrain.NONE; + public Terrain badlands = Terrain.NONE; + public Terrain plateau = Terrain.NONE; + public Terrain steppe = Terrain.NONE; + public Terrain plains = Terrain.NONE; + public Terrain hills = Terrain.NONE; + public Terrain dales = Terrain.NONE; + public Terrain torridonian = Terrain.NONE; + public Terrain mountains = Terrain.NONE; + public Terrain volcanoes = Terrain.NONE; + public Terrain volcanoPipe = Terrain.NONE; + public Terrains create() { + return new Terrains(this); + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/VolcanoModule.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/VolcanoModule.java new file mode 100644 index 0000000..31e3946 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/VolcanoModule.java @@ -0,0 +1,80 @@ +package com.terraforged.core.world.terrain; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import me.dags.noise.func.EdgeFunc; +import com.terraforged.core.util.Seed; +import com.terraforged.core.world.heightmap.Levels; +import com.terraforged.core.world.heightmap.RegionConfig; + +public class VolcanoModule implements Module { + + private final Module cone; + private final Module height; + private final Module lowlands; + private final float inversionPoint; + private final float blendLower; + private final float blendUpper; + private final float blendRange; + private final float bias; + + public VolcanoModule(Seed seed, RegionConfig region, Levels levels) { + float midpoint = 0.3F; + float range = 0.3F; + + Module heightNoise = Source.perlin(seed.next(), 2, 1).map(0.45, 0.6); + + this.height = Source.cellNoise(region.seed, region.scale, heightNoise) + .warp(region.warpX, region.warpZ, region.warpStrength); + + this.cone = Source.cellEdge(region.seed, region.scale, EdgeFunc.DISTANCE_2_DIV).invert() + .powCurve(14) + .clamp(0.475, 1) + .map(0, 1) + .grad(0, 0.5, 0.5) + .warp(seed.next(), 15, 2, 10) + .scale(height); + + this.lowlands = Source.ridge(seed.next(), 150, 3) + .warp(seed.next(), 30, 1, 30) + .scale(0.1); + + this.inversionPoint = 0.95F; + this.blendLower = midpoint - (range / 2F); + this.blendUpper = blendLower + range; + this.blendRange = blendUpper - blendLower; + this.bias = levels.ground; + } + + @Override + public float getValue(float x, float z) { + float value = cone.getValue(x, z); + float limit = height.getValue(x, z); + float maxHeight = limit * inversionPoint; + + // as value passes the inversion point we start calculating the inner-cone of the volcano + if (value > maxHeight) { + // modifies the steepness of the volcano inner-cone (larger == steeper) + float steepnessModifier = 1; + + // as alpha approaches 1.0, position is closer to center of volcano + float delta = (value - maxHeight) * steepnessModifier; + float range = (limit - maxHeight); + float alpha = delta / range; + + // calculate height inside volcano + if (alpha > 0.99) { + value = -bias + (1F / 255F); + } else { + value = maxHeight - ((maxHeight / 10F) * alpha); + } + } else if (value < blendLower) { + value += lowlands.getValue(x, z); + } else if (value < blendUpper) { + float alpha = 1 - ((value - blendLower) / blendRange); + value += (lowlands.getValue(x, z) * alpha); + } + + return value + bias; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/VolcanoPopulator.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/VolcanoPopulator.java new file mode 100644 index 0000000..ecf9752 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/VolcanoPopulator.java @@ -0,0 +1,116 @@ +package com.terraforged.core.world.terrain; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import me.dags.noise.func.EdgeFunc; +import com.terraforged.core.cell.Cell; +import com.terraforged.core.util.Seed; +import com.terraforged.core.world.heightmap.Levels; +import com.terraforged.core.world.heightmap.RegionConfig; + +public class VolcanoPopulator extends TerrainPopulator { + + private static final float throat_value = 0.925F; + + private final Module cone; + private final Module height; + private final Module lowlands; + private final float inversionPoint; + private final float blendLower; + private final float blendUpper; + private final float blendRange; + private final float bias; + + private final Terrain inner; + private final Terrain outer; + + public VolcanoPopulator(Seed seed, RegionConfig region, Levels levels, Terrains terrains) { + super(Source.ZERO, terrains.volcano); + float midpoint = 0.3F; + float range = 0.3F; + + Module heightNoise = Source.perlin(seed.next(), 2, 1).map(0.45, 0.6); + + this.height = Source.cellNoise(region.seed, region.scale, heightNoise) + .warp(region.warpX, region.warpZ, region.warpStrength); + + this.cone = Source.cellEdge(region.seed, region.scale, EdgeFunc.DISTANCE_2_DIV).invert() + .warp(region.warpX, region.warpZ, region.warpStrength) + .powCurve(14) + .clamp(0.475, 1) + .map(0, 1) + .grad(0, 0.5, 0.5) + .warp(seed.next(), 15, 2, 10) + .scale(height); + + this.lowlands = Source.ridge(seed.next(), 150, 3) + .warp(seed.next(), 30, 1, 30) + .scale(0.1); + + this.inversionPoint = 0.95F; + this.blendLower = midpoint - (range / 2F); + this.blendUpper = blendLower + range; + this.blendRange = blendUpper - blendLower; + this.outer = terrains.volcano; + this.inner = terrains.volcanoPipe; + this.bias = levels.ground; + } + + @Override + public void apply(Cell cell, float x, float z) { + float value = cone.getValue(x, z); + float limit = height.getValue(x, z); + float maxHeight = limit * inversionPoint; + + // as value passes the inversion point we start calculating the inner-cone of the volcano + if (value > maxHeight) { + // modifies the steepness of the volcano inner-cone (larger == steeper) + float steepnessModifier = 1F; + + // as alpha approaches 1.0, position is closer to center of volcano + float delta = (value - maxHeight) * steepnessModifier; + float range = (limit - maxHeight); + float alpha = delta / range; + + // calculate height inside volcano + if (alpha > throat_value) { + cell.tag = inner; + } + + value = maxHeight - ((maxHeight / 5F) * alpha); + } else if (value < blendLower) { + value += lowlands.getValue(x, z); + cell.tag = outer; + } else if (value < blendUpper) { + float alpha = 1 - ((value - blendLower) / blendRange); + value += (lowlands.getValue(x, z) * alpha); + cell.tag = outer; + } + + cell.value = bias + value; + } + + @Override + public void tag(Cell cell, float x, float z) { + float value = cone.getValue(x, z); + float limit = height.getValue(x, z); + float maxHeight = limit * inversionPoint; + if (value > maxHeight) { + float steepnessModifier = 1; + + // as alpha approaches 1.0, position is closer to center of volcano + float delta = (value - maxHeight) * steepnessModifier; + float range = (limit - maxHeight); + float alpha = delta / range; + + // calculate height inside volcano + if (alpha > throat_value) { + cell.tag = inner; + } else { + cell.tag = outer; + } + } else { + cell.tag = outer; + } + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/StandardTerrainProvider.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/StandardTerrainProvider.java new file mode 100644 index 0000000..fe446d8 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/StandardTerrainProvider.java @@ -0,0 +1,126 @@ +package com.terraforged.core.world.terrain.provider; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.util.Seed; +import com.terraforged.core.world.GeneratorContext; +import com.terraforged.core.world.heightmap.RegionConfig; +import com.terraforged.core.world.terrain.LandForms; +import com.terraforged.core.world.terrain.Terrain; +import com.terraforged.core.world.terrain.TerrainPopulator; +import com.terraforged.core.world.terrain.VolcanoPopulator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +public class StandardTerrainProvider implements TerrainProvider { + + private final List mixable = new ArrayList<>(); + private final List unmixable = new ArrayList<>(); + private final Map populators = new HashMap<>(); + + private final LandForms landForms; + private final RegionConfig config; + private final GeneratorContext context; + private final Populator defaultPopulator; + + public StandardTerrainProvider(GeneratorContext context, RegionConfig config, Populator defaultPopulator) { + this.config = config; + this.context = context; + this.landForms = new LandForms(context.levels); + this.defaultPopulator = defaultPopulator; + init(); + } + + public void init() { + registerMixable(context.terrain.steppe, landForms.steppe(context.seed)); + registerMixable(context.terrain.plains, landForms.plains(context.seed)); + registerMixable(context.terrain.hills, landForms.hills1(context.seed)); + registerMixable(context.terrain.hills, landForms.hills2(context.seed)); + registerMixable(context.terrain.dales, landForms.dales(context.seed)); + registerMixable(context.terrain.badlands, landForms.badlands2(context.seed)); + registerMixable(context.terrain.plateau, landForms.plateau(context.seed)); + registerMixable(context.terrain.torridonian, landForms.torridonian(context.seed)); + + registerUnMixable(new VolcanoPopulator(context.seed, config, context.levels, context.terrain)); + registerUnMixable(context.terrain.badlands, landForms.badlands2(context.seed)); + registerUnMixable(context.terrain.mountains, landForms.mountains2(context.seed)); + registerUnMixable(context.terrain.mountains, landForms.mountains3(context.seed)); + } + + @Override + public void registerMixable(TerrainPopulator populator) { + populators.putIfAbsent(populator.getType(), populator); + mixable.add(populator); + } + + @Override + public void registerUnMixable(TerrainPopulator populator) { + populators.putIfAbsent(populator.getType(), populator); + unmixable.add(populator); + } + + @Override + public Populator getPopulator(Terrain terrain) { + return populators.getOrDefault(terrain, defaultPopulator); + } + + @Override + public LandForms getLandforms() { + return landForms; + } + + @Override + public List getPopulators() { + List mixed = combine(mixable, this::combine); + List result = new ArrayList<>(mixed.size() + unmixable.size()); + result.addAll(mixed); + result.addAll(unmixable); + return result; + } + + private TerrainPopulator combine(TerrainPopulator tp1, TerrainPopulator tp2) { + return combine(tp1, tp2, context.seed, config.scale / 2); + } + + private static TerrainPopulator combine(TerrainPopulator tp1, TerrainPopulator tp2, Seed seed, int scale) { + Module combined = Source.perlin(seed.next(), scale, 1) + .warp(seed.next(), scale / 2, 2, scale / 2) + .blend(tp1.getSource(), tp2.getSource(), 0.5, 0.25); + + String name = tp1.getType().getName() + "-" + tp2.getType().getName(); + int id = Terrain.ID_START + 1 + tp1.getType().getId() * tp2.getType().getId(); + float weight = Math.min(tp1.getType().getWeight(), tp2.getType().getWeight()); + Terrain type = new Terrain(name, id, weight); + + return new TerrainPopulator(combined, type); + } + + private static List combine(List input, BiFunction operator) { + int length = input.size(); + for (int i = 1; i < input.size(); i++) { + length += (input.size() - i); + } + + List result = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + result.add(null); + } + + for (int i = 0, k = input.size(); i < input.size(); i++) { + T t1 = input.get(i); + result.set(i, t1); + for (int j = i + 1; j < input.size(); j++, k++) { + T t2 = input.get(j); + T t3 = operator.apply(t1, t2); + result.set(k, t3); + } + } + + return result; + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/TerrainProvider.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/TerrainProvider.java new file mode 100644 index 0000000..137b18b --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/TerrainProvider.java @@ -0,0 +1,61 @@ +package com.terraforged.core.world.terrain.provider; + +import me.dags.noise.Module; +import com.terraforged.core.cell.Populator; +import com.terraforged.core.world.terrain.LandForms; +import com.terraforged.core.world.terrain.Terrain; +import com.terraforged.core.world.terrain.TerrainPopulator; + +import java.util.List; + +/** + * Provides the heightmap generator with terrain specific noise generation modules (TerrainPopulators) + */ +public interface TerrainProvider { + + LandForms getLandforms(); + + /** + * Returns the resulting set of TerrainPopulators + */ + List getPopulators(); + + /** + * Returns a populator for the provided Terrain, or the default if not registered + */ + Populator getPopulator(Terrain terrain); + + /** + * Add a TerrainPopulator to world generation. + * + * 'Mixable' TerrainPopulators are used to create additional terrain types, created by blending two + * different mixable TerrainPopulators together (this is in addition to the unmixed version of the populator) + */ + void registerMixable(TerrainPopulator populator); + + /** + * Add a TerrainPopulator to world generation + * + * 'UnMixable' TerrainPopulators are NOT blended together to create additional terrain types + */ + void registerUnMixable(TerrainPopulator populator); + + /** + * Add a TerrainPopulator to world generation. + * + * 'Mixable' TerrainPopulators are used to create additional terrain types, created by blending two + * different mixable TerrainPopulators together (this is in addition to the unmixed version of the populator) + */ + default void registerMixable(Terrain type, Module source) { + registerMixable(new TerrainPopulator(source, type)); + } + + /** + * Add a TerrainPopulator to world generation + * + * 'UnMixable' TerrainPopulators are NOT blended together to create additional terrain types + */ + default void registerUnMixable(Terrain type, Module source) { + registerUnMixable(new TerrainPopulator(source, type)); + } +} diff --git a/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/TerrainProviderFactory.java b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/TerrainProviderFactory.java new file mode 100644 index 0000000..ba69f19 --- /dev/null +++ b/TerraForgedCore/src/main/java/com/terraforged/core/world/terrain/provider/TerrainProviderFactory.java @@ -0,0 +1,10 @@ +package com.terraforged.core.world.terrain.provider; + +import com.terraforged.core.cell.Populator; +import com.terraforged.core.world.GeneratorContext; +import com.terraforged.core.world.heightmap.RegionConfig; + +public interface TerrainProviderFactory { + + TerrainProvider create(GeneratorContext context, RegionConfig config, Populator defaultPopulator); +} diff --git a/TerraForgedCore/src/main/resources/biomes.png b/TerraForgedCore/src/main/resources/biomes.png new file mode 100644 index 0000000000000000000000000000000000000000..131a9bf6c6a2ae4cbb474a76c6ca804c315bf120 GIT binary patch literal 14615 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 Czu5Kw literal 0 HcmV?d00001 diff --git a/TerraForgedCore/src/main/resources/biomes.txt b/TerraForgedCore/src/main/resources/biomes.txt new file mode 100644 index 0000000..334339e --- /dev/null +++ b/TerraForgedCore/src/main/resources/biomes.txt @@ -0,0 +1,13 @@ +#TerraForged BiomeType Hex Colors (do not include hash/pound character) +#Fri Jan 10 23:15:10 GMT 2020 +ALPINE=a078aa +TAIGA=5b8f52 +TEMPERATE_RAINFOREST=0aa041 +TUNDRA=93a7ac +TROPICAL_RAINFOREST=075330 +SAVANNA=97a527 +GRASSLAND=64dc3c +TEMPERATE_FOREST=32c850 +STEPPE=c8c878 +DESERT=c87137 +COLD_STEPPE=afb496 diff --git a/TerraForgedCore/src/main/resources/terraforged.png b/TerraForgedCore/src/main/resources/terraforged.png new file mode 100644 index 0000000000000000000000000000000000000000..ecedcf8df9c4e8d900159e97cc12c17f7b4c74d2 GIT binary patch literal 27685 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