start on poisson disk sampling for feature placement

This commit is contained in:
dags- 2020-04-02 19:02:25 +01:00
parent 2375a2b7fc
commit 66c2a318cd
10 changed files with 407 additions and 0 deletions

View File

@ -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<Feature<?>> event) {
Log.info("Registering features");
FeatureManager.registerTemplates(event);
event.getRegistry().register(DiskFeature.INSTANCE);
}
@SubscribeEvent
public static void registerDecorators(RegistryEvent.Register<Placement<?>> event) {
Log.info("Registering decorators");
event.getRegistry().register(new PoissonAtSurface());
}
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.FORGE)
public static class ForgeEvents {
@SubscribeEvent

View File

@ -193,6 +193,7 @@ public class TerraChunkGenerator extends ObfHelperChunkGenerator<GenerationSetti
@Override
public final void func_225550_a_(BiomeManager biomeManager, IChunk chunk, GenerationStage.Carving carving) {
// World carvers have hardcoded 'carvable' blocks which can be problematic with modded blocks
// AirCarverFix shims the actual blockstates to an equivalent carvable type
super.func_225550_a_(biomeManager, new ChunkCarverFix(chunk, context.materials), carving);

View File

@ -62,6 +62,10 @@ public class RegionDelegate extends WorldGenRegion {
this.region = region;
}
public WorldGenRegion getRegion() {
return region;
}
@Override
public int getMainChunkX() {
return region.getMainChunkX();

View File

@ -0,0 +1,48 @@
package com.terraforged.mod.feature.decorator.poisson;
import com.terraforged.core.cell.Cell;
import com.terraforged.core.region.chunk.ChunkReader;
import com.terraforged.mod.chunk.TerraContainer;
import com.terraforged.mod.chunk.fix.RegionDelegate;
import me.dags.noise.Module;
import me.dags.noise.Source;
import me.dags.noise.util.NoiseUtil;
import net.minecraft.world.IWorld;
import net.minecraft.world.biome.BiomeContainer;
import net.minecraft.world.chunk.IChunk;
import net.minecraft.world.gen.WorldGenRegion;
public class BiomeVariance implements Module {
private final ChunkReader chunk;
public BiomeVariance(ChunkReader chunk) {
this.chunk = chunk;
}
@Override
public float getValue(float x, float y) {
int dx = ((int) x) & 15;
int dz = ((int) y) & 15;
Cell<?> 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;
}
}

View File

@ -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<Vec2f[][]> 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<Integer, Integer> consumer) {
try (ObjectPool.Item<Vec2f[][]> 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<Integer, Integer> 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<Integer, Integer> 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);
}
}

View File

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

View File

@ -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 <T> Dynamic<T> serialize(DynamicOps<T> 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);
}
}

View File

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

View File

@ -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<PoissonConfig> {
private Poisson instance = null;
private final Object lock = new Object();
public PoissonDecorator() {
super(PoissonConfig::deserialize);
}
@Override
public final <FC extends IFeatureConfig, F extends Feature<FC>> boolean place(IWorld world, ChunkGenerator<?> generator, Random random, BlockPos pos, PoissonConfig config, ConfiguredFeature<FC, F> 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<BlockPos> getPositions(IWorld worldIn, ChunkGenerator<? extends GenerationSettings> 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;
}
}
}

View File

@ -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<Integer, Integer> {
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;
}
}
}