mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Refined featurette edge behavior
- Keep coral tips attached to the fan silhouette - Preserve large leaf weights and disable zero threshold ores - Reuse formation palette sources across each object
This commit is contained in:
@@ -58,7 +58,7 @@ public class IrisOreGenerator {
|
||||
}
|
||||
|
||||
public PlatformBlockState generate(int x, int y, int z, RNG rng, IrisData data) {
|
||||
if (!range.contains(y)) {
|
||||
if (threshold <= 0 || !range.contains(y)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -88,16 +88,18 @@ public final class CoralGenerator {
|
||||
|
||||
private static void buildFan(CoralCanvas canvas, IrisCoral coral, int height, long seed) {
|
||||
int half = Math.max(1, coral.getFanWidth());
|
||||
for (int y = 0; y < height; y++) {
|
||||
int tippedWidth = -1;
|
||||
for (int y = height - 1; y >= 0; y--) {
|
||||
double t = y / (double) Math.max(1, height - 1);
|
||||
double profile = Math.sin(Math.PI * t);
|
||||
int w = (int) Math.round(half * profile);
|
||||
for (int dx = -w; dx <= w; dx++) {
|
||||
canvas.set(dx, y, 0, CoralCanvas.Role.STRUCTURE);
|
||||
if (Math.abs(dx) > tippedWidth) {
|
||||
placeTip(canvas, coral, dx, y, 0, seed + dx);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = -half; dx <= half; dx++) {
|
||||
placeTip(canvas, coral, dx, height - 1, 0, seed + dx);
|
||||
tippedWidth = Math.max(tippedWidth, w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-10
@@ -25,35 +25,48 @@ import art.arcane.iris.engine.object.IrisProceduralBlocks;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.math.Vector3i;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
public final class FormationBlockResolver {
|
||||
private FormationBlockResolver() {
|
||||
private final IrisFormation formation;
|
||||
private final IrisData data;
|
||||
private final RNG paletteRng;
|
||||
private final Int2ObjectOpenHashMap<RNG> strataRngs = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
public FormationBlockResolver(IrisFormation formation, IrisData data) {
|
||||
this.formation = formation;
|
||||
this.data = data;
|
||||
paletteRng = new RNG(formation.getSeed());
|
||||
}
|
||||
|
||||
public static PlatformBlockState resolve(IrisFormation f, IrisData data, FormationCanvas.Role role, Vector3i raw) {
|
||||
public PlatformBlockState resolve(FormationCanvas.Role role, Vector3i raw) {
|
||||
int x = raw.getBlockX();
|
||||
int y = raw.getBlockY();
|
||||
int z = raw.getBlockZ();
|
||||
RNG paletteRng = new RNG(f.getSeed());
|
||||
|
||||
if (role == FormationCanvas.Role.CAP && capDefined(f)) {
|
||||
PlatformBlockState cap = IrisProceduralBlocks.resolve(f.getCapBlock(), f.getCapPalette(), data, x, y, z, paletteRng);
|
||||
if (role == FormationCanvas.Role.CAP && capDefined(formation)) {
|
||||
PlatformBlockState cap = IrisProceduralBlocks.resolve(formation.getCapBlock(), formation.getCapPalette(), data, x, y, z, paletteRng);
|
||||
if (cap != null) {
|
||||
return cap;
|
||||
}
|
||||
}
|
||||
|
||||
if (strataDefined(f)) {
|
||||
IrisMaterialPalette strata = f.getStrataPalette();
|
||||
int thickness = Math.max(1, f.getStrataThickness());
|
||||
if (strataDefined(formation)) {
|
||||
IrisMaterialPalette strata = formation.getStrataPalette();
|
||||
int thickness = Math.max(1, formation.getStrataThickness());
|
||||
int band = Math.floorDiv(y, thickness);
|
||||
PlatformBlockState strataState = strata.get(new RNG(f.getSeed() + (band * 31L)), x, band, z, data);
|
||||
RNG strataRng = strataRngs.get(band);
|
||||
if (strataRng == null) {
|
||||
strataRng = new RNG(formation.getSeed() + band * 31L);
|
||||
strataRngs.put(band, strataRng);
|
||||
}
|
||||
PlatformBlockState strataState = strata.get(strataRng, x, band, z, data);
|
||||
if (strataState != null) {
|
||||
return strataState;
|
||||
}
|
||||
}
|
||||
|
||||
return IrisProceduralBlocks.resolve(f.getBlock(), f.getBlockPalette(), data, x, y, z, paletteRng);
|
||||
return IrisProceduralBlocks.resolve(formation.getBlock(), formation.getBlockPalette(), data, x, y, z, paletteRng);
|
||||
}
|
||||
|
||||
private static boolean capDefined(IrisFormation f) {
|
||||
|
||||
@@ -62,8 +62,9 @@ public final class FormationGenerator {
|
||||
}
|
||||
|
||||
Map<Vector3i, PlatformBlockState> resolved = new HashMap<>();
|
||||
FormationBlockResolver resolver = new FormationBlockResolver(f, data);
|
||||
for (Map.Entry<Vector3i, FormationCanvas.Role> entry : canvas.getCells().entrySet()) {
|
||||
PlatformBlockState bd = FormationBlockResolver.resolve(f, data, entry.getValue(), entry.getKey());
|
||||
PlatformBlockState bd = resolver.resolve(entry.getValue(), entry.getKey());
|
||||
if (bd == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisProceduralBlocks;
|
||||
import art.arcane.iris.engine.object.IrisProceduralTree;
|
||||
import art.arcane.iris.engine.object.IrisTreeDecorator;
|
||||
import art.arcane.iris.engine.object.IrisTreeSecondaryLeaf;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
@@ -106,16 +107,16 @@ public final class TreeBlockResolver {
|
||||
}
|
||||
|
||||
private static String pickWeighted(IrisProceduralTree tree, RNG rng) {
|
||||
int total = 0;
|
||||
for (art.arcane.iris.engine.object.IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) {
|
||||
long total = 0;
|
||||
for (IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) {
|
||||
total += Math.max(0, s.getWeight());
|
||||
}
|
||||
if (total <= 0) {
|
||||
return null;
|
||||
}
|
||||
double r = rng.nextDouble() * total;
|
||||
double cumulative = 0;
|
||||
for (art.arcane.iris.engine.object.IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) {
|
||||
long cumulative = 0;
|
||||
for (IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) {
|
||||
cumulative += Math.max(0, s.getWeight());
|
||||
if (r < cumulative) {
|
||||
return s.getBlock();
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisOreGeneratorTest {
|
||||
@Test
|
||||
public void zeroThresholdDisablesEvenZeroNoiseSamples() {
|
||||
Fixture fixture = new Fixture(0.0);
|
||||
fixture.generator.setThreshold(0.0);
|
||||
|
||||
assertNull(fixture.generator.generate(2, 40, 3, fixture.rng, null));
|
||||
verifyNoInteractions(fixture.noise, fixture.style);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void positiveThresholdStillIncludesExactCutoffAndVerticalBounds() {
|
||||
Fixture fixture = new Fixture(0.5);
|
||||
fixture.generator.setThreshold(0.5);
|
||||
|
||||
assertSame(fixture.ore, fixture.generator.generate(2, 30, 3, fixture.rng, null));
|
||||
assertSame(fixture.ore, fixture.generator.generate(2, 80, 3, fixture.rng, null));
|
||||
assertNull(fixture.generator.generate(2, 29, 3, fixture.rng, null));
|
||||
assertNull(fixture.generator.generate(2, 81, 3, fixture.rng, null));
|
||||
fixture.generator.setThreshold(0.49);
|
||||
assertNull(fixture.generator.generate(2, 40, 3, fixture.rng, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullThresholdIncludesMaximumNoise() {
|
||||
Fixture fixture = new Fixture(1.0);
|
||||
fixture.generator.setThreshold(1.0);
|
||||
|
||||
assertSame(fixture.ore, fixture.generator.generate(2, 40, 3, fixture.rng, null));
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final RNG rng = new RNG(34L);
|
||||
private final PlatformBlockState ore = mock(PlatformBlockState.class);
|
||||
private final CNG noise = mock(CNG.class);
|
||||
private final IrisGeneratorStyle style = mock(IrisGeneratorStyle.class);
|
||||
private final IrisOreGenerator generator = new IrisOreGenerator();
|
||||
|
||||
private Fixture(double sample) {
|
||||
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
|
||||
when(palette.getPalette()).thenReturn(new KList<>(mock(IrisBlockData.class)));
|
||||
when(palette.get(any(), anyDouble(), anyDouble(), anyDouble(), isNull())).thenReturn(ore);
|
||||
when(noise.noise(anyDouble(), anyDouble(), anyDouble())).thenReturn(sample);
|
||||
when(style.create(any(), isNull())).thenReturn(noise);
|
||||
generator.setPalette(palette);
|
||||
generator.setChanceStyle(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package art.arcane.iris.engine.object.coral;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisCoral;
|
||||
import art.arcane.iris.engine.object.IrisCoralForm;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CoralGeneratorTest {
|
||||
@Test
|
||||
public void fanTipsFollowTheTopOfEachOccupiedColumn() {
|
||||
PlatformBlockState structure = mock(PlatformBlockState.class);
|
||||
PlatformBlockState tip = mock(PlatformBlockState.class);
|
||||
for (int height : new int[]{2, 7, 14}) {
|
||||
for (int width : new int[]{1, 3, 11}) {
|
||||
IrisCoral coral = new IrisCoral().setForm(IrisCoralForm.FAN)
|
||||
.setHeightMin(height).setHeightMax(height).setFanWidth(width)
|
||||
.setWaterlogged(false).setBlockPalette(palette(structure));
|
||||
IrisObject bare = CoralGenerator.generate(coral, 0, new RNG(0L), null);
|
||||
Map<Integer, Integer> tops = new HashMap<>();
|
||||
Set<IrisBlockVector> barePositions = new HashSet<>();
|
||||
for (IrisBlockVector position : bare.getBlocks().keys()) {
|
||||
barePositions.add(position);
|
||||
tops.merge(position.getBlockX(), position.getBlockY(), Math::max);
|
||||
}
|
||||
coral.setTipPalette(palette(tip));
|
||||
for (double chance : new double[]{0.0, 0.35, 1.0}) {
|
||||
coral.setTipChance(chance);
|
||||
for (long seed = 0; seed < 16; seed++) {
|
||||
IrisObject decorated = CoralGenerator.generate(coral, 0, new RNG(seed), null);
|
||||
Set<IrisBlockVector> positions = new HashSet<>();
|
||||
int tipCount = 0;
|
||||
for (IrisBlockVector position : decorated.getBlocks().keys()) {
|
||||
positions.add(position);
|
||||
if (decorated.getBlocks().get(position) == tip) {
|
||||
assertTrue("Fan tip must occupy an existing column", tops.containsKey(position.getBlockX()));
|
||||
assertEquals(tops.get(position.getBlockX()).intValue(), position.getBlockY());
|
||||
tipCount++;
|
||||
}
|
||||
}
|
||||
assertEquals(barePositions, positions);
|
||||
if (chance == 1.0) {
|
||||
assertEquals(tops.size(), tipCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisMaterialPalette palette(PlatformBlockState state) {
|
||||
IrisBlockData block = mock(IrisBlockData.class);
|
||||
when(block.getWeight()).thenReturn(1);
|
||||
when(block.getBlockData(null)).thenReturn(state);
|
||||
return new IrisMaterialPalette().setPalette(new KList<>(block));
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package art.arcane.iris.engine.object.formation;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisFormation;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.math.Vector3i;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class FormationBlockResolverTest {
|
||||
@Test
|
||||
public void sharedResolverMatchesFreshPaletteSeedsAcrossBands() {
|
||||
IrisFormation formation = new IrisFormation().setSeed(43191L).setStrataThickness(3);
|
||||
IrisMaterialPalette body = palette("stone", "andesite");
|
||||
IrisMaterialPalette cap = palette("granite", "diorite");
|
||||
IrisMaterialPalette strata = palette("terracotta", "red_terracotta");
|
||||
formation.setBlockPalette(body);
|
||||
List<Vector3i> positions = new ArrayList<>();
|
||||
for (int x = -8; x <= 8; x++) {
|
||||
for (int y = -12; y <= 24; y++) {
|
||||
positions.add(new Vector3i(x, y, x - y));
|
||||
}
|
||||
}
|
||||
Set<String> materials = new HashSet<>();
|
||||
for (boolean withStrata : new boolean[]{false, true}) {
|
||||
formation.setStrataPalette(withStrata ? strata : null);
|
||||
for (boolean withCap : new boolean[]{false, true}) {
|
||||
formation.setCapPalette(withCap ? cap : null);
|
||||
FormationBlockResolver resolver = new FormationBlockResolver(formation, null);
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
for (Vector3i position : positions) {
|
||||
FormationCanvas.Role role = position.getBlockX() % 2 == 0
|
||||
? FormationCanvas.Role.CAP : FormationCanvas.Role.BODY;
|
||||
IrisMaterialPalette expectedPalette = withCap && role == FormationCanvas.Role.CAP
|
||||
? cap : withStrata ? strata : body;
|
||||
int sampleY = expectedPalette == strata
|
||||
? Math.floorDiv(position.getBlockY(), formation.getStrataThickness()) : position.getBlockY();
|
||||
long sampleSeed = formation.getSeed() + (expectedPalette == strata ? sampleY * 31L : 0L);
|
||||
RNG fresh = new RNG(sampleSeed);
|
||||
PlatformBlockState expected = expectedPalette.get(fresh,
|
||||
position.getBlockX(), sampleY, position.getBlockZ(), null);
|
||||
assertSame(expected, resolver.resolve(role, position));
|
||||
assertEquals(new RNG(sampleSeed).nextLong(), fresh.nextLong());
|
||||
materials.add(expected.key());
|
||||
}
|
||||
Collections.reverse(positions);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(materials.size() == 6);
|
||||
}
|
||||
|
||||
private static IrisMaterialPalette palette(String first, String second) {
|
||||
return new IrisMaterialPalette().setPalette(new KList<>(block(first), block(second)));
|
||||
}
|
||||
|
||||
private static IrisBlockData block(String material) {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
when(state.key()).thenReturn("minecraft:" + material);
|
||||
IrisBlockData block = mock(IrisBlockData.class);
|
||||
when(block.getWeight()).thenReturn(1);
|
||||
when(block.getBlockData(null)).thenReturn(state);
|
||||
return block;
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,13 @@ package art.arcane.iris.engine.object.tree;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisProceduralTree;
|
||||
import art.arcane.iris.engine.object.IrisTreeSecondaryLeaf;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
@@ -16,9 +19,42 @@ import java.util.Set;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class TreeBlockResolverTest {
|
||||
@Test
|
||||
public void largeSecondaryWeightsKeepTheirRelativeDistribution() {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
tree.setSeed(91874L);
|
||||
tree.setLeavesPalette(palette("oak_leaves", "birch_leaves"));
|
||||
PlatformBlockState blossom = mock(PlatformBlockState.class);
|
||||
PlatformBlockState light = mock(PlatformBlockState.class);
|
||||
List<PlatformBlockState> normal = new ArrayList<>();
|
||||
List<PlatformBlockState> large = new ArrayList<>();
|
||||
TreeBlockCanvas.Cell cell = new TreeBlockCanvas.Cell(
|
||||
TreeBlockCanvas.Role.SECONDARY_LEAF, TreeBlockCanvas.Axis.NONE, false, -1, null);
|
||||
try (MockedStatic<B> blocks = mockStatic(B.class)) {
|
||||
blocks.when(() -> B.getStateOrNull("minecraft:cherry_leaves", false)).thenReturn(blossom);
|
||||
blocks.when(() -> B.getStateOrNull("minecraft:shroomlight", false)).thenReturn(light);
|
||||
for (int weight : new int[]{1, Integer.MAX_VALUE}) {
|
||||
tree.setWeightedSecondaryLeaves(new KList<>(
|
||||
new IrisTreeSecondaryLeaf("minecraft:cherry_leaves", weight),
|
||||
new IrisTreeSecondaryLeaf("minecraft:shroomlight", weight)));
|
||||
List<PlatformBlockState> resolved = weight == 1 ? normal : large;
|
||||
for (int x = -16; x <= 16; x++) {
|
||||
for (int y = 0; y < 16; y++) {
|
||||
resolved.add(TreeBlockResolver.resolve(tree, null, cell,
|
||||
new TreeBlockCanvas.Vec(x, y, x + y), new RNG(tree.getSeed())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(normal.contains(blossom));
|
||||
assertTrue(normal.contains(light));
|
||||
assertEquals(normal, large);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sharedPaletteRandomSourceMatchesFreshPerVoxelSources() {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
|
||||
Reference in New Issue
Block a user