From bfeb01703719e34039ddf8b687cb2de625e33b89 Mon Sep 17 00:00:00 2001 From: piexel Date: Mon, 19 Oct 2026 18:16:06 +0200 Subject: [PATCH] Let two block tall plants stand up on the decorator map Squeezing both halves of tall grass into one block made it look like short grass. The bottom half now fills its own block and the top half covers the block above it on the map, so tall plants are two blocks tall there too. The legend shows them by their top half, like Minecraft's inventory icon. Model used: Claude Opus 5.5 --- .../iris/core/gui/BiomePalettePlacement.java | 2 +- .../volmit/iris/core/gui/BlockTextures.java | 45 +++--------- .../iris/core/gui/PaletteLayerPreview.java | 73 +++++++++++++++++-- 3 files changed, 78 insertions(+), 42 deletions(-) diff --git a/core/src/main/java/com/volmit/iris/core/gui/BiomePalettePlacement.java b/core/src/main/java/com/volmit/iris/core/gui/BiomePalettePlacement.java index 8110c8b51..8b61d812c 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/BiomePalettePlacement.java +++ b/core/src/main/java/com/volmit/iris/core/gui/BiomePalettePlacement.java @@ -295,7 +295,7 @@ abstract class BiomePalettePlacement { notes.add("Where other decorators of this biome could place too, the world picks one of them at random."); } if (placesTwoBlockTallPlants()) { - notes.add("Two block tall plants show both halves squeezed into one block."); + notes.add("Two block tall plants stand up on the map: their top half covers the block above them."); } if (decorator.isStacking()) { notes.add("It stacks " + decorator.getStackMin() + "–" + decorator.getStackMax() diff --git a/core/src/main/java/com/volmit/iris/core/gui/BlockTextures.java b/core/src/main/java/com/volmit/iris/core/gui/BlockTextures.java index 79dfdd80c..9bf7212c7 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/BlockTextures.java +++ b/core/src/main/java/com/volmit/iris/core/gui/BlockTextures.java @@ -86,18 +86,21 @@ final class BlockTextures { /** * Like {@link #topTexture}, but plants keep their sprite, so single decorations on the ground stay recognisable. - * Two block tall plants show both halves squeezed into one block. + * Two block tall plants give their bottom half, the part standing on the ground. */ Optional iconTexture(BlockData block) { + BlockData standing = block instanceof Bisected ? withHalf(block, Bisected.Half.BOTTOM) : block; + return resolve(standing).map(ResolvedTexture::texture); + } + + /** + * The sprite of a two block tall plant's top half, or nothing for any other block. + */ + Optional upperHalfIcon(BlockData block) { if (!(block instanceof Bisected)) { - return resolve(block).map(ResolvedTexture::texture); + return Optional.empty(); } - Optional bottom = resolve(withHalf(block, Bisected.Half.BOTTOM)); - Optional top = resolve(withHalf(block, Bisected.Half.TOP)); - if (bottom.isEmpty() || top.isEmpty() || !bottom.get().isSideOnSprite() || !top.get().isSideOnSprite()) { - return resolve(block).map(ResolvedTexture::texture); - } - return Optional.of(BlockTexture.stacked(top.get().texture(), bottom.get().texture())); + return resolve(withHalf(block, Bisected.Half.TOP)).filter(ResolvedTexture::isSideOnSprite).map(ResolvedTexture::texture); } private static BlockData withHalf(BlockData block, Bisected.Half half) { @@ -431,32 +434,6 @@ final class BlockTextures { return new BlockTexture(size, pixels, opaque, average, biomeTint); } - /** - * The top texture above the bottom one, each at half height, or just the bottom when their sizes differ. - */ - static BlockTexture stacked(BlockTexture top, BlockTexture bottom) { - if (top.size != bottom.size) { - return bottom; - } - int size = bottom.size; - int[] pixels = new int[size * size]; - boolean[] opaque = new boolean[size * size]; - for (int z = 0; z < size; z++) { - BlockTexture half = z < size / 2 ? top : bottom; - int sourceRow = (z * 2) % size; - System.arraycopy(half.pixels, sourceRow * size, pixels, z * size, size); - System.arraycopy(half.opaque, sourceRow * size, opaque, z * size, size); - } - return new BlockTexture(size, pixels, opaque, averageOf(top.averageColor, bottom.averageColor), bottom.biomeTint); - } - - private static int averageOf(int first, int second) { - int red = (((first >>> 16) & 0xFF) + ((second >>> 16) & 0xFF)) / 2; - int green = (((first >>> 8) & 0xFF) + ((second >>> 8) & 0xFF)) / 2; - int blue = ((first & 0xFF) + (second & 0xFF)) / 2; - return red << 16 | green << 8 | blue; - } - BlockTexture flattened() { int[] flat = new int[pixels.length]; Arrays.fill(flat, averageColor); diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java index 1a61daa55..e3e9763e1 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java @@ -29,8 +29,10 @@ import com.volmit.iris.util.math.RNG; import org.bukkit.block.data.BlockData; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; final class PaletteLayerPreview { @@ -40,6 +42,7 @@ final class PaletteLayerPreview { private static final int MISSING_BLOCK_COLOR = 0x5A5F6B; private static final double FAR_FROM_WEIGHT_FACTOR = 2D; private static final int BLOCK_CACHE_SIZE = 1 << 16; + private static final int NO_OVERHANG = -1; private final BiomePalette palette; private final KList blocks; @@ -51,6 +54,8 @@ final class PaletteLayerPreview { private final int[] entryBySlot; private final BiomeTintField biomeTints; private final BlockTexture[] textureByEntry; + private final BlockTexture[] upperHalfByEntry; + private final boolean hasTallPlants; private final BlockTexture[] legendTextureByEntry; private final int[] legendColorByEntry; private final List entries; @@ -74,7 +79,9 @@ final class PaletteLayerPreview { this.biomeTints = biomeTints; this.showsGroundByEntry = resolveGroundShowingEntries(); this.textureByEntry = resolveEntryTextures(textures); - this.legendTextureByEntry = tintForLegend(textureByEntry); + this.upperHalfByEntry = resolveUpperHalves(textures); + this.hasTallPlants = Arrays.stream(upperHalfByEntry).anyMatch(Objects::nonNull); + this.legendTextureByEntry = tintForLegend(legendTextures()); this.legendColorByEntry = resolveLegendColors(); Statistics statistics = measure(); @@ -146,6 +153,10 @@ final class PaletteLayerPreview { } int blockColorAt(double noise, double worldX, double worldZ, boolean showTexturePixels) { + int overhang = showTexturePixels ? overhangColorAt(worldX, worldZ) : NO_OVERHANG; + if (overhang != NO_OVERHANG) { + return overhang; + } int entry = entryIndexForNoise(noise); if (showsGroundByEntry[entry]) { return bareColorAt(worldX, worldZ, showTexturePixels); @@ -158,18 +169,43 @@ final class PaletteLayerPreview { return groundColorAt(worldX, worldZ, true); } int untintedColor = showTexturePixels ? texture.texelAt(worldX, worldZ) : texture.averageColor(); - if (texture.biomeTint() == null) { - return untintedColor; - } - int tint = biomeTints.colorAt(texture.biomeTint(), (int) Math.floor(worldX), (int) Math.floor(worldZ)); - return BlockTexture.multiply(untintedColor, tint); + return tinted(texture, untintedColor, worldX, worldZ); } /** * The colour where the palette places nothing: the ground under a decorator, or plain dark otherwise. */ int bareColorAt(double worldX, double worldZ, boolean showTexturePixels) { - return ground == null ? PaletteLayerScale.ABSENT_COLOR : groundColorAt(worldX, worldZ, showTexturePixels); + if (ground == null) { + return PaletteLayerScale.ABSENT_COLOR; + } + int overhang = showTexturePixels ? overhangColorAt(worldX, worldZ) : NO_OVERHANG; + return overhang != NO_OVERHANG ? overhang : groundColorAt(worldX, worldZ, showTexturePixels); + } + + // A two block tall plant stands up on the map: its top half covers the block above it, which is one block north. + private int overhangColorAt(double worldX, double worldZ) { + if (!hasTallPlants) { + return NO_OVERHANG; + } + double plantZ = worldZ + 1D; + double noise = sample(worldX, plantZ); + if (Double.isNaN(noise)) { + return NO_OVERHANG; + } + BlockTexture upperHalf = upperHalfByEntry[entryIndexForNoise(noise)]; + if (upperHalf == null || !upperHalf.isOpaqueAt(worldX, worldZ)) { + return NO_OVERHANG; + } + return tinted(upperHalf, upperHalf.texelAt(worldX, worldZ), worldX, plantZ); + } + + private int tinted(BlockTexture texture, int untintedColor, double worldX, double worldZ) { + if (texture.biomeTint() == null) { + return untintedColor; + } + int tint = biomeTints.colorAt(texture.biomeTint(), (int) Math.floor(worldX), (int) Math.floor(worldZ)); + return BlockTexture.multiply(untintedColor, tint); } private int groundColorAt(double worldX, double worldZ, boolean showTexturePixels) { @@ -259,6 +295,29 @@ final class PaletteLayerPreview { return resolved; } + private BlockTexture[] resolveUpperHalves(BlockTextures textures) { + BlockTexture[] resolved = new BlockTexture[blocks.size()]; + if (textures == null || palette.kind() != BiomePalette.Kind.DECORATOR) { + return resolved; + } + for (int entry = 0; entry < blocks.size(); entry++) { + BlockData block = blocks.get(entry).getBlockData(data); + resolved[entry] = block == null ? null : textures.upperHalfIcon(block).orElse(null); + } + return resolved; + } + + // Tall plants are known by their top half, the way Minecraft draws them in the inventory. + private BlockTexture[] legendTextures() { + BlockTexture[] legend = textureByEntry.clone(); + for (int entry = 0; entry < legend.length; entry++) { + if (upperHalfByEntry[entry] != null) { + legend[entry] = upperHalfByEntry[entry]; + } + } + return legend; + } + private Optional textureOf(BlockData block, BlockTextures textures) { return palette.kind() == BiomePalette.Kind.DECORATOR ? textures.iconTexture(block) : textures.topTexture(block); }