start on poisson disk sampling for feature placement
This commit is contained in:
parent
2375a2b7fc
commit
66c2a318cd
@ -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
|
||||
|
@ -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);
|
||||
|
@ -62,6 +62,10 @@ public class RegionDelegate extends WorldGenRegion {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public WorldGenRegion getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMainChunkX() {
|
||||
return region.getMainChunkX();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user