From 66c2a318cd812eb3f30af28395b130c5df0eaeb3 Mon Sep 17 00:00:00 2001 From: dags- Date: Thu, 2 Apr 2020 19:02:25 +0100 Subject: [PATCH] start on poisson disk sampling for feature placement --- .../com/terraforged/mod/TerraForgedMod.java | 9 + .../mod/chunk/TerraChunkGenerator.java | 1 + .../mod/chunk/fix/RegionDelegate.java | 4 + .../decorator/poisson/BiomeVariance.java | 48 ++++++ .../feature/decorator/poisson/Poisson.java | 161 ++++++++++++++++++ .../decorator/poisson/PoissonAtSurface.java | 19 +++ .../decorator/poisson/PoissonConfig.java | 33 ++++ .../decorator/poisson/PoissonContext.java | 25 +++ .../decorator/poisson/PoissonDecorator.java | 64 +++++++ .../decorator/poisson/PoissonVisitor.java | 43 +++++ 10 files changed, 407 insertions(+) create mode 100644 TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/BiomeVariance.java create mode 100644 TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/Poisson.java create mode 100644 TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonAtSurface.java create mode 100644 TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonConfig.java create mode 100644 TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonContext.java create mode 100644 TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonDecorator.java create mode 100644 TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonVisitor.java diff --git a/TerraForgedMod/src/main/java/com/terraforged/mod/TerraForgedMod.java b/TerraForgedMod/src/main/java/com/terraforged/mod/TerraForgedMod.java index 5e24a1e..bcac106 100644 --- a/TerraForgedMod/src/main/java/com/terraforged/mod/TerraForgedMod.java +++ b/TerraForgedMod/src/main/java/com/terraforged/mod/TerraForgedMod.java @@ -29,12 +29,14 @@ import com.terraforged.api.material.WGTags; import com.terraforged.feature.FeatureManager; import com.terraforged.mod.command.TerraCommand; import com.terraforged.mod.data.DataGen; +import com.terraforged.mod.feature.decorator.poisson.PoissonAtSurface; import com.terraforged.mod.feature.feature.DiskFeature; import com.terraforged.mod.feature.tree.SaplingManager; import com.terraforged.mod.util.DataPackFinder; import com.terraforged.mod.util.Environment; import net.minecraft.world.biome.Biomes; import net.minecraft.world.gen.feature.Feature; +import net.minecraft.world.gen.placement.Placement; import net.minecraftforge.common.BiomeDictionary; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; @@ -74,10 +76,17 @@ public class TerraForgedMod { @SubscribeEvent public static void registerFeatures(RegistryEvent.Register> event) { + Log.info("Registering features"); FeatureManager.registerTemplates(event); event.getRegistry().register(DiskFeature.INSTANCE); } + @SubscribeEvent + public static void registerDecorators(RegistryEvent.Register> event) { + Log.info("Registering decorators"); + event.getRegistry().register(new PoissonAtSurface()); + } + @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.FORGE) public static class ForgeEvents { @SubscribeEvent diff --git a/TerraForgedMod/src/main/java/com/terraforged/mod/chunk/TerraChunkGenerator.java b/TerraForgedMod/src/main/java/com/terraforged/mod/chunk/TerraChunkGenerator.java index 188e0a3..3721d04 100644 --- a/TerraForgedMod/src/main/java/com/terraforged/mod/chunk/TerraChunkGenerator.java +++ b/TerraForgedMod/src/main/java/com/terraforged/mod/chunk/TerraChunkGenerator.java @@ -193,6 +193,7 @@ public class TerraChunkGenerator extends ObfHelperChunkGenerator cell = chunk.getCell(dx, dz); + return NoiseUtil.map(1 - cell.biomeEdge, 0.05F, 0.25F, 0.2F); + } + + public static Module of(IWorld world) { + if (world instanceof RegionDelegate) { + WorldGenRegion region = ((RegionDelegate) world).getRegion(); + IChunk chunk = region.getChunk(region.getMainChunkX(), region.getMainChunkZ()); + return of(chunk); + } + return Source.ONE; + } + + public static Module of(IChunk chunk) { + BiomeContainer container = chunk.getBiomes(); + if (container instanceof TerraContainer) { + ChunkReader reader = ((TerraContainer) container).getChunkReader(); + return new BiomeVariance(reader); + } + return Source.ONE; + } +} diff --git a/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/Poisson.java b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/Poisson.java new file mode 100644 index 0000000..b7785a3 --- /dev/null +++ b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/Poisson.java @@ -0,0 +1,161 @@ +package com.terraforged.mod.feature.decorator.poisson; + +import com.terraforged.core.util.concurrent.ObjectPool; +import me.dags.noise.util.NoiseUtil; +import me.dags.noise.util.Vec2f; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.Arrays; +import java.util.Random; +import java.util.function.BiConsumer; + +public class Poisson { + + private static final int SAMPLES = 50; + + private final int radius; + private final int radius2; + private final int maxDistance; + private final int regionSize; + private final int gridSize; + private final float cellSize; + private final ObjectPool pool; + + public Poisson(int radius) { + int size = 48; + this.radius = radius; + this.radius2 = radius * radius; + int halfRadius = radius / 2; + this.maxDistance = radius * 2; + this.regionSize = size - halfRadius; + this.cellSize = radius / NoiseUtil.SQRT2; + this.gridSize = (int) Math.ceil(regionSize / cellSize); + this.pool = new ObjectPool<>(3, () -> new Vec2f[gridSize][gridSize]); + } + + public void visit(int chunkX, int chunkZ, PoissonContext context, BiConsumer consumer) { + try (ObjectPool.Item grid = pool.get()) { + clear(grid.getValue()); + context.startX = (chunkX << 4); + context.startZ = (chunkZ << 4); + context.endX = context.startX + 16; + context.endZ = context.startZ + 16; + int regionX = (context.startX >> 5); + int regionZ = (context.startZ >> 5); + context.offsetX = regionX << 5; + context.offsetZ = regionZ << 5; + context.random.setSeed(NoiseUtil.hash2D(context.seed, regionX, regionZ)); + int x = context.random.nextInt(regionSize); + int z = context.random.nextInt(regionSize); + visit(x, z, grid.getValue(), SAMPLES, context, consumer); + } + } + + private void visit(float px, float pz, Vec2f[][] grid, int samples, PoissonContext context, BiConsumer consumer) { + for (int i = 0; i < samples; i++) { + float angle = context.random.nextFloat() * NoiseUtil.PI2; + float distance = radius + (context.random.nextFloat() * maxDistance); + float x = px + NoiseUtil.sin(angle) * distance; + float z = pz + NoiseUtil.cos(angle) * distance; + if (valid(x, z, grid, context)) { + Vec2f vec = new Vec2f(x, z); + visit(vec, context, consumer); + int cellX = (int) (x / cellSize); + int cellZ = (int) (z / cellSize); + grid[cellZ][cellX] = vec; + visit(vec.x, vec.y, grid, samples, context, consumer); + } + } + } + + private void visit(Vec2f pos, PoissonContext context, BiConsumer consumer) { + int dx = context.offsetX + (int) pos.x; + int dz = context.offsetZ + (int) pos.y; + if (dx >= context.startX && dx < context.endX && dz >= context.startZ && dz < context.endZ) { + consumer.accept(dx, dz); + } + } + + private boolean valid(float x, float z, Vec2f[][] grid, PoissonContext context) { + if (x < 0 || x >= regionSize || z < 0 || z >= regionSize) { + return false; + } + + int cellX = (int) (x / cellSize); + int cellZ = (int) (z / cellSize); + if (grid[cellZ][cellX] != null) { + return false; + } + + float noise = context.density.getValue(context.offsetX + x, context.offsetZ + z); + float radius2 = noise * this.radius2; + + int searchRadius = 2; + int minX = Math.max(0, cellX - searchRadius); + int maxX = Math.min(grid[0].length - 1, cellX + searchRadius); + int minZ = Math.max(0, cellZ - searchRadius); + int maxZ = Math.min(grid.length - 1, cellZ + searchRadius); + + for (int dz = minZ; dz <= maxZ; dz++) { + for (int dx = minX; dx <= maxX; dx++) { + Vec2f vec = grid[dz][dx]; + if (vec == null) { + continue; + } + + float dist2 = vec.dist2(x, z); + if (dist2 < radius2) { + return false; + } + } + } + return true; + } + + private static void clear(Vec2f[][] grid) { + for (Vec2f[] row : grid) { + Arrays.fill(row, null); + } + } + + public static void main(String[] args) { + int size = 512; + int radius = 10; + + int chunkSize = 16; + int chunks = size / chunkSize; + + BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB); + Poisson poisson = new Poisson(radius); + PoissonContext context = new PoissonContext(213, new Random()); + + long time = 0L; + long count = 0L; + for (int cz = 0; cz < chunks; cz++) { + for (int cx = 0; cx < chunks; cx++) { + long start = System.nanoTime(); + poisson.visit(cx, cz, context, (x, z) -> { + if (x < 0 || x >= image.getWidth() || z < 0 || z >= image.getHeight()) { + return; + } + image.setRGB(x, z, Color.WHITE.getRGB()); + }); + time += (System.nanoTime() - start); + count++; + } + } + + double avg = (double) time / count; + System.out.println(avg + "ns"); + + JFrame frame = new JFrame(); + frame.add(new JLabel(new ImageIcon(image))); + frame.setVisible(true); + frame.pack(); + frame.setResizable(false); + frame.setLocationRelativeTo(null); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } +} diff --git a/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonAtSurface.java b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonAtSurface.java new file mode 100644 index 0000000..9f7b3a1 --- /dev/null +++ b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonAtSurface.java @@ -0,0 +1,19 @@ +package com.terraforged.mod.feature.decorator.poisson; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IWorld; +import net.minecraft.world.gen.Heightmap; + +import java.util.Random; + +public class PoissonAtSurface extends PoissonDecorator { + + public PoissonAtSurface() { + setRegistryName("terraforged", "poisson_surface"); + } + + @Override + public int getYAt(IWorld world, BlockPos pos, Random random) { + return world.getHeight(Heightmap.Type.WORLD_SURFACE_WG, pos.getX(), pos.getZ()); + } +} diff --git a/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonConfig.java b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonConfig.java new file mode 100644 index 0000000..3de3577 --- /dev/null +++ b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonConfig.java @@ -0,0 +1,33 @@ +package com.terraforged.mod.feature.decorator.poisson; + +import com.google.common.collect.ImmutableMap; +import com.mojang.datafixers.Dynamic; +import com.mojang.datafixers.types.DynamicOps; +import net.minecraft.world.gen.placement.IPlacementConfig; + +public class PoissonConfig implements IPlacementConfig { + + public final int radius; + public final int variance; + + public PoissonConfig(int radius, int variance) { + this.radius = radius; + this.variance = variance; + } + + @Override + public Dynamic serialize(DynamicOps ops) { + return new Dynamic<>(ops, ops.createMap( + ImmutableMap.of( + ops.createString("radius"), ops.createInt(radius), + ops.createString("variance_scale"), ops.createInt(variance) + )) + ); + } + + public static PoissonConfig deserialize(Dynamic dynamic) { + int radius = dynamic.get("radius").asInt(4); + int variance = dynamic.get("variance_scale").asInt(0); + return new PoissonConfig(radius, variance); + } +} diff --git a/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonContext.java b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonContext.java new file mode 100644 index 0000000..7c6c512 --- /dev/null +++ b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonContext.java @@ -0,0 +1,25 @@ +package com.terraforged.mod.feature.decorator.poisson; + +import me.dags.noise.Module; +import me.dags.noise.Source; + +import java.util.Random; + +public class PoissonContext { + + public int offsetX; + public int offsetZ; + public int startX; + public int startZ; + public int endX; + public int endZ; + public Module density = Source.ONE; + + public final int seed; + public final Random random; + + public PoissonContext(long seed, Random random) { + this.seed = (int) seed; + this.random = random; + } +} diff --git a/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonDecorator.java b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonDecorator.java new file mode 100644 index 0000000..61ed89c --- /dev/null +++ b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonDecorator.java @@ -0,0 +1,64 @@ +package com.terraforged.mod.feature.decorator.poisson; + +import me.dags.noise.Module; +import me.dags.noise.Source; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IWorld; +import net.minecraft.world.gen.ChunkGenerator; +import net.minecraft.world.gen.GenerationSettings; +import net.minecraft.world.gen.feature.ConfiguredFeature; +import net.minecraft.world.gen.feature.Feature; +import net.minecraft.world.gen.feature.IFeatureConfig; +import net.minecraft.world.gen.placement.Placement; + +import java.util.Random; +import java.util.stream.Stream; + +public abstract class PoissonDecorator extends Placement { + + private Poisson instance = null; + private final Object lock = new Object(); + + public PoissonDecorator() { + super(PoissonConfig::deserialize); + } + + @Override + public final > boolean place(IWorld world, ChunkGenerator generator, Random random, BlockPos pos, PoissonConfig config, ConfiguredFeature feature) { + int radius = Math.max(2, Math.min(30, config.radius)); + Poisson poisson = getInstance(radius); + PoissonVisitor visitor = new PoissonVisitor(this, feature, world, generator, random, pos); + setVariance(world, visitor, config); + int chunkX = pos.getX() >> 4; + int chunkZ = pos.getZ() >> 4; + poisson.visit(chunkX, chunkZ, visitor, visitor); + return visitor.hasPlacedOne(); + } + + @Override + public final Stream getPositions(IWorld worldIn, ChunkGenerator generatorIn, Random random, PoissonConfig configIn, BlockPos pos) { + return Stream.empty(); + } + + public abstract int getYAt(IWorld world, BlockPos pos, Random random); + + private Poisson getInstance(int radius) { + synchronized (lock) { + if (instance == null) { + instance = new Poisson(radius); + } + return instance; + } + } + + private void setVariance(IWorld world, PoissonContext context, PoissonConfig config) { + Module module = BiomeVariance.of(world); + if (module != Source.ONE) { + if (config.variance > 0) { + Module variance = Source.simplex((int) world.getSeed(), config.variance, 1).scale(1.5); + module = module.mult(variance); + } + context.density = module; + } + } +} diff --git a/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonVisitor.java b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonVisitor.java new file mode 100644 index 0000000..b39b6ad --- /dev/null +++ b/TerraForgedMod/src/main/java/com/terraforged/mod/feature/decorator/poisson/PoissonVisitor.java @@ -0,0 +1,43 @@ +package com.terraforged.mod.feature.decorator.poisson; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IWorld; +import net.minecraft.world.gen.ChunkGenerator; +import net.minecraft.world.gen.feature.ConfiguredFeature; + +import java.util.Random; +import java.util.function.BiConsumer; + +public class PoissonVisitor extends PoissonContext implements BiConsumer { + + private final BlockPos pos; + private final IWorld world; + private final ConfiguredFeature feature; + private final PoissonDecorator decorator; + private final ChunkGenerator generator; + private final BlockPos.Mutable mutable = new BlockPos.Mutable(); + + private boolean placedOne = false; + + public PoissonVisitor(PoissonDecorator decorator, ConfiguredFeature feature, IWorld world, ChunkGenerator generator, Random random, BlockPos pos) { + super(world.getSeed(), random); + this.pos = pos; + this.world = world; + this.feature = feature; + this.decorator = decorator; + this.generator = generator; + } + + public boolean hasPlacedOne() { + return placedOne; + } + + @Override + public void accept(Integer x, Integer z) { + mutable.setPos(x, pos.getY(), z); + mutable.setY(decorator.getYAt(world, mutable, random)); + if (feature.place(world, generator, random, mutable)) { + placedOne = true; + } + } +}