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 1014d1b93..b020c699c 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 @@ -23,12 +23,16 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.bukkit.block.data.BlockData; +import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.zip.ZipFile; @@ -103,7 +107,7 @@ final class BlockTextures { if (face == null) { return Optional.empty(); } - BufferedImage image = readImage(assetPath(face.texture(), "textures", ".png")); + BufferedImage image = readLayeredImage(face.textures()); if (image == null) { return Optional.empty(); } @@ -149,13 +153,34 @@ final class BlockTextures { } JsonObject face = elements == null ? null : topmostFace(elements, faceName); - if (face == null) { - String particle = dereference("#particle", textures); - boolean isSideOnSprite = elements != null; - return particle == null ? null : new ModelFace(particle, isSideOnSprite ? firstTintIndex(elements) : NO_TINT_INDEX, isSideOnSprite); + if (face != null) { + String texture = dereference(face.get("texture").getAsString(), textures); + return texture == null ? null : new ModelFace(List.of(texture), tintIndexOf(face), false); } - String texture = dereference(face.get("texture").getAsString(), textures); - return texture == null ? null : new ModelFace(texture, tintIndexOf(face), false); + List spriteLayers = elements == null ? List.of() : spriteLayers(elements, textures); + if (!spriteLayers.isEmpty()) { + return new ModelFace(spriteLayers, firstTintIndex(elements), true); + } + String particle = dereference("#particle", textures); + return particle == null ? null : new ModelFace(List.of(particle), NO_TINT_INDEX, false); + } + + // Some plants draw a second sprite over the first, like the glowing fireflies on a firefly bush. + private static List spriteLayers(JsonArray elements, Map textures) { + Set layers = new LinkedHashSet<>(); + for (JsonElement element : elements) { + JsonObject faces = element.getAsJsonObject().getAsJsonObject("faces"); + if (faces == null) { + continue; + } + for (Map.Entry face : faces.entrySet()) { + String texture = dereference(face.getValue().getAsJsonObject().get("texture").getAsString(), textures); + if (texture != null) { + layers.add(texture); + } + } + } + return List.copyOf(layers); } private static int firstTintIndex(JsonArray elements) { @@ -291,12 +316,35 @@ final class BlockTextures { return ClientJarFiles.readJson(clientJar, path); } + // Each layer is drawn over the ones before it, using only its first frame when it is animated. + private BufferedImage readLayeredImage(List layers) throws IOException { + BufferedImage bottom = readImage(assetPath(layers.get(0), "textures", ".png")); + if (bottom == null || layers.size() == 1) { + return bottom; + } + int size = bottom.getWidth(); + BufferedImage combined = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + Graphics2D canvas = combined.createGraphics(); + try { + for (String layer : layers) { + BufferedImage image = readImage(assetPath(layer, "textures", ".png")); + if (image != null) { + int frameSize = image.getWidth(); + canvas.drawImage(image, 0, 0, size, size, 0, 0, frameSize, frameSize, null); + } + } + } finally { + canvas.dispose(); + } + return combined; + } + private BufferedImage readImage(String path) throws IOException { return ClientJarFiles.readImage(clientJar, path); } // Plants and other blocks without a top face only have a side-on sprite, which would read as stripes from above. - private record ModelFace(String texture, int tintIndex, boolean isSideOnSprite) { + private record ModelFace(List textures, int tintIndex, boolean isSideOnSprite) { } private record ResolvedTexture(BlockTexture texture, boolean isSideOnSprite) { 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 242bbe053..1a61daa55 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 @@ -40,7 +40,6 @@ 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 double BARE_GROUND_SHADE = 0.6D; private final BiomePalette palette; private final KList blocks; @@ -167,13 +166,10 @@ final class PaletteLayerPreview { } /** - * The colour where the palette places nothing: the dimmed ground under a decorator, or plain dark otherwise. + * The colour where the palette places nothing: the ground under a decorator, or plain dark otherwise. */ int bareColorAt(double worldX, double worldZ, boolean showTexturePixels) { - if (ground == null) { - return PaletteLayerScale.ABSENT_COLOR; - } - return BlockPreviewColors.shade(groundColorAt(worldX, worldZ, showTexturePixels), BARE_GROUND_SHADE); + return ground == null ? PaletteLayerScale.ABSENT_COLOR : groundColorAt(worldX, worldZ, showTexturePixels); } private int groundColorAt(double worldX, double worldZ, boolean showTexturePixels) {