Draw the biome's ground under decorator previews

Decorator previews used the dark "nothing here" colour around and
between their sprites. They now show the biome's top surface layer
there: through the see-through parts of each sprite, and dimmed where
the decorator places nothing or places air. Hovering a bare spot says
the decorator places nothing there.

Model used: Claude Opus 5.5
This commit is contained in:
piexel committed 2026-10-19 17:26:46 +02:00
1 parent 84ffce263a
commit 3c773e3e57
6 files changed
+83 -10

No files matched your search

@@ -324,7 +324,8 @@ final class BlockTextures {
}
// Biome tinted textures keep their grey pixels, because the biome colour changes from column to column.
record BlockTexture(int size, int[] pixels, int averageColor, BiomeTint biomeTint) {
// See-through pixels get a darker shade of the average colour, and the opaque mask remembers which they were.
record BlockTexture(int size, int[] pixels, boolean[] opaque, int averageColor, BiomeTint biomeTint) {
private static final int OPAQUE_ALPHA = 128;
private static final double GAP_SHADE = 0.55D;
@@ -363,13 +364,15 @@ final class BlockTextures {
pixels[index] = gapColor;
}
}
return new BlockTexture(size, pixels, average, biomeTint);
return new BlockTexture(size, pixels, opaque, average, biomeTint);
}
BlockTexture flattened() {
int[] flat = new int[pixels.length];
Arrays.fill(flat, averageColor);
return new BlockTexture(size, flat, averageColor, biomeTint);
boolean[] allOpaque = new boolean[pixels.length];
Arrays.fill(allOpaque, true);
return new BlockTexture(size, flat, allOpaque, averageColor, biomeTint);
}
BlockTexture tinted(int tint) {
@@ -377,13 +380,21 @@ final class BlockTextures {
for (int index = 0; index < pixels.length; index++) {
tintedPixels[index] = multiply(pixels[index], tint);
}
return new BlockTexture(size, tintedPixels, multiply(averageColor, tint), null);
return new BlockTexture(size, tintedPixels, opaque, multiply(averageColor, tint), null);
}
int texelAt(double worldX, double worldZ) {
return pixels[texelIndex(worldX, worldZ)];
}
boolean isOpaqueAt(double worldX, double worldZ) {
return opaque[texelIndex(worldX, worldZ)];
}
private int texelIndex(double worldX, double worldZ) {
int x = Math.min(size - 1, (int) ((worldX - Math.floor(worldX)) * size));
int z = Math.min(size - 1, (int) ((worldZ - Math.floor(worldZ)) * size));
return pixels[z * size + x];
return z * size + x;
}
BufferedImage toImage() {
@@ -29,6 +29,10 @@ interface NoiseColorScale {
int colorInvalid();
default int colorInvalidAt(double worldX, double worldZ) {
return colorInvalid();
}
Color displayColorNormalized(double normalized);
String label();
@@ -1305,7 +1305,9 @@ public final class NoiseExplorerGUI extends JPanel {
private static String describePaletteAt(PaletteLayerPreview preview, double worldX, double worldZ) {
double noise = preview.sample(worldX, worldZ);
if (Double.isNaN(noise)) {
return "The layer doesn't reach this depth here";
return preview.palette().kind() == BiomePalette.Kind.DECORATOR
? "The decorator places nothing here"
: "The layer doesn't reach this depth here";
}
return preview.entryForNoise(noise).block() + " noise " + formatNumber(noise);
}
@@ -200,7 +200,7 @@ final class NoiseRenderCoordinator implements AutoCloseable {
pixels[pixelIndex++] = colorScale.colorAt(value, worldX, worldZ);
stats.acceptFinite(value, center, paletteMinimum, paletteMaximum);
} else {
pixels[pixelIndex++] = colorScale.colorInvalid();
pixels[pixelIndex++] = colorScale.colorInvalidAt(worldX, worldZ);
stats.acceptInvalid(value, center);
}
worldX += worldStep;
@@ -40,10 +40,13 @@ 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<IrisBlockData> blocks;
private final BiomePalettePlacement placement;
private final PaletteLayerPreview ground;
private final boolean[] showsGroundByEntry;
private final int depth;
private final IrisData data;
private final int[] entryBySlot;
@@ -57,11 +60,12 @@ final class PaletteLayerPreview {
private final double absentShare;
private final double outOfRangeShare;
private PaletteLayerPreview(BiomePalette palette, BiomePalettePlacement placement, int depth, IrisData data,
BlockTextures textures, BiomeTintField biomeTints) {
private PaletteLayerPreview(BiomePalette palette, BiomePalettePlacement placement, PaletteLayerPreview ground, int depth,
IrisData data, BlockTextures textures, BiomeTintField biomeTints) {
this.palette = palette;
this.blocks = placement.blocks();
this.placement = placement;
this.ground = ground;
this.depth = depth;
this.data = data;
this.entryBySlot = IrisBiomePaletteLayer.paletteEntryPerSlot(blocks, data).stream().mapToInt(Integer::intValue).toArray();
@@ -69,6 +73,7 @@ final class PaletteLayerPreview {
throw new IllegalStateException("No block of " + palette.biomeKey() + " " + palette.lowercaseLabel() + " could be found");
}
this.biomeTints = biomeTints;
this.showsGroundByEntry = resolveGroundShowingEntries();
this.textureByEntry = resolveEntryTextures(textures);
this.legendTextureByEntry = tintForLegend(textureByEntry);
this.legendColorByEntry = resolveLegendColors();
@@ -93,7 +98,22 @@ final class PaletteLayerPreview {
BiomeTintField biomeTints = textures == null
? null
: new BiomeTintField(biome, new RNG(engine.getSeedManager().getBiome()), textures.biomeColors());
return new PaletteLayerPreview(palette, placement, depth, data, textures, biomeTints);
PaletteLayerPreview ground = palette.kind() == BiomePalette.Kind.DECORATOR
? groundUnder(engine, palette.biomeKey(), biome, data, textures, biomeTints)
: null;
return new PaletteLayerPreview(palette, placement, ground, depth, data, textures, biomeTints);
}
// Decorations stand on the biome's top surface layer, so that is what shows around and between them.
private static PaletteLayerPreview groundUnder(Engine engine, String biomeKey, IrisBiome biome, IrisData data,
BlockTextures textures, BiomeTintField biomeTints) {
BiomePalette topLayer = new BiomePalette(biomeKey, BiomePalette.Kind.LAYER, 0);
KList<IrisBlockData> blocks = topLayer.blocksIn(biome);
if (blocks == null || blocks.isEmpty()) {
return null;
}
return new PaletteLayerPreview(topLayer, BiomePalettePlacement.of(engine, biome, topLayer), null, 0, data, textures,
biomeTints);
}
// The world picks one block per whole coordinate, and zoomed in, many pixels fall on the same block.
@@ -128,10 +148,16 @@ final class PaletteLayerPreview {
int blockColorAt(double noise, double worldX, double worldZ, boolean showTexturePixels) {
int entry = entryIndexForNoise(noise);
if (showsGroundByEntry[entry]) {
return bareColorAt(worldX, worldZ, showTexturePixels);
}
BlockTexture texture = textureByEntry[entry];
if (texture == null) {
return legendColorByEntry[entry];
}
if (ground != null && showTexturePixels && !texture.isOpaqueAt(worldX, worldZ)) {
return groundColorAt(worldX, worldZ, true);
}
int untintedColor = showTexturePixels ? texture.texelAt(worldX, worldZ) : texture.averageColor();
if (texture.biomeTint() == null) {
return untintedColor;
@@ -140,6 +166,31 @@ final class PaletteLayerPreview {
return BlockTexture.multiply(untintedColor, tint);
}
/**
* The colour where the palette places nothing: the dimmed 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);
}
private int groundColorAt(double worldX, double worldZ, boolean showTexturePixels) {
double noise = ground.sample(worldX, worldZ);
return Double.isNaN(noise) ? PaletteLayerScale.ABSENT_COLOR : ground.blockColorAt(noise, worldX, worldZ, showTexturePixels);
}
// A decorator places air to leave gaps, which looks the same as placing nothing.
private boolean[] resolveGroundShowingEntries() {
boolean[] showsGround = new boolean[blocks.size()];
for (int entry = 0; entry < blocks.size(); entry++) {
BlockData block = blocks.get(entry).getBlockData(data);
showsGround[entry] = ground != null && block != null && block.getMaterial().isAir();
}
return showsGround;
}
Entry entryForNoise(double noise) {
return entries.get(entryIndexForNoise(noise));
}
@@ -59,6 +59,11 @@ final class PaletteLayerScale implements NoiseColorScale {
return ABSENT_COLOR;
}
@Override
public int colorInvalidAt(double worldX, double worldZ) {
return view == View.NOISE ? ABSENT_COLOR : preview.bareColorAt(worldX, worldZ, showTexturePixels);
}
@Override
public Color displayColorNormalized(double normalized) {
return new Color(colorFinite(minimum() + normalized * (maximum() - minimum())));