Follow the layer slider live and keep a ghost of what it cuts away

Cutting layers no longer rebuilds the mesh: it is built once with the top
faces hidden by the block above kept aside, so a cut is a quick filter on
the UI thread and the view follows the slider while it is dragged. The
cut-away part stays as a faint ghost that only fills pixels the shown part
leaves empty, so the object's shape stays readable without hazing over
the floor being looked into.

Model used: Claude Opus 5.5
This commit is contained in:
piexel committed 2026-10-19 21:46:52 +02:00
1 parent 99258bfd98
commit 51c9d01bf7
3 files changed
+90 -34

No files matched your search

@@ -54,7 +54,7 @@ final class ObjectExplorer {
private final UUID worldId;
private final ObjectBrowser browser = new ObjectBrowser(this::showObject);
private final ObjectViewport viewport = new ObjectViewport();
private final ObjectLayerCut layerCut = new ObjectLayerCut(shownLayers -> rebuildShownMesh());
private final ObjectLayerCut layerCut = new ObjectLayerCut(shownLayers -> showLayerCut());
private final JComponent viewWithLayerCut = layerCut.floatingOver(viewport);
private final ObjectOverviewPanel overview = new ObjectOverviewPanel(this::showSeeThrough);
private final StudioStatusBar statusBar = new StudioStatusBar();
@@ -72,6 +72,7 @@ final class ObjectExplorer {
private volatile BiomeTintColors tintColors;
private String shownKey;
private LoadedObject shown;
private ObjectMesh wholeMesh;
private Set<String> seeThroughTypes = Set.of();
ObjectExplorer(Engine engine, UUID worldId) {
@@ -159,6 +160,7 @@ final class ObjectExplorer {
private void showObject(String key) {
shownKey = key;
shown = null;
wholeMesh = null;
meshRevision.incrementAndGet();
long revision = objectRevision.incrementAndGet();
viewport.showMessage("Loading " + key + "...");
@@ -197,6 +199,7 @@ final class ObjectExplorer {
return;
}
shown = loaded;
wholeMesh = loaded.mesh();
seeThroughTypes = Set.of();
statusBar.showActivity("");
viewport.show(loaded.mesh());
@@ -216,46 +219,42 @@ final class ObjectExplorer {
private void showSeeThrough(Set<String> blockTypes) {
seeThroughTypes = blockTypes;
rebuildShownMesh();
rebuildWholeMesh();
}
// Blocks under the cut that were covered by the ones above get their hidden faces back, so each floor shows, and
// blocks behind see-through ones get theirs back too.
private void rebuildShownMesh() {
// Blocks behind see-through ones get their hidden faces back, so the mesh is built again.
private void rebuildWholeMesh() {
LoadedObject object = shown;
if (object == null) {
return;
}
long revision = meshRevision.incrementAndGet();
ObjectMesh.Bounds bounds = object.mesh().bounds();
boolean cutting = layerCut.isCutting();
int highestShownY = bounds.minY() + layerCut.shownLayerCount() - 1;
Set<String> seeThrough = seeThroughTypes;
loader.execute(() -> {
if (revision != meshRevision.get()) {
return;
}
ObjectMesh mesh = !cutting && seeThrough.isEmpty()
ObjectMesh mesh = seeThrough.isEmpty()
? object.mesh()
: ObjectMesh.build(cutting ? blocksUpTo(object.blocks(), highestShownY) : object.blocks(), this::modelOf, bounds,
: ObjectMesh.build(object.blocks(), this::modelOf, object.mesh().bounds(),
block -> seeThrough.contains(block.getMaterial().getKey().getKey()));
EventQueue.invokeLater(() -> {
if (revision == meshRevision.get() && shown == object) {
viewport.replaceMesh(mesh);
showMeshSummary(mesh);
wholeMesh = mesh;
showLayerCut();
}
});
});
}
private static Map<BlockVector, BlockData> blocksUpTo(Map<BlockVector, BlockData> blocks, int highestY) {
Map<BlockVector, BlockData> below = new HashMap<>();
blocks.forEach((position, block) -> {
if (position.getBlockY() <= highestY) {
below.put(position, block);
}
});
return below;
private void showLayerCut() {
if (wholeMesh == null) {
return;
}
int highestShownY = wholeMesh.bounds().minY() + layerCut.shownLayerCount() - 1;
ObjectMesh mesh = layerCut.isCutting() ? wholeMesh.cutAbove(highestShownY) : wholeMesh;
viewport.replaceMesh(mesh);
showMeshSummary(mesh);
}
private void showFailure(long revision, String key, Throwable error) {
@@ -32,11 +32,15 @@ import java.util.function.Predicate;
/**
* The faces of an object's blocks that can be seen from outside. A face is left out when the neighbour on its side
* covers that side, which is most of them in a large object. See-through faces, like water and stained glass, are
* kept apart because they are drawn last, blended over everything behind them.
* kept apart because they are drawn last, blended over everything behind them. Top faces covered only by the block
* above are kept aside too, so the top layers can be cut away without building the mesh again; what is cut away stays
* as a faint ghost of the object's shape.
*/
record ObjectMesh(List<Quad> solidQuads, List<Quad> translucentQuads, Bounds bounds) {
record ObjectMesh(List<Quad> solidQuads, List<Quad> translucentQuads, List<Quad> cutAwayQuads, List<Quad> topsUnderBlockAbove,
Bounds bounds) {
private static final double SEE_THROUGH_OPACITY = 0.22D;
private static final double FULL_OPACITY = 1D;
private static final double CUT_AWAY_OPACITY = 0.1D;
static ObjectMesh build(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf) {
return build(blocks, modelOf, Bounds.of(blocks.keySet()), block -> false);
@@ -50,25 +54,57 @@ record ObjectMesh(List<Quad> solidQuads, List<Quad> translucentQuads, Bounds bou
Predicate<BlockData> isSeeThrough) {
List<Quad> solid = new ArrayList<>();
List<Quad> translucent = new ArrayList<>();
List<Quad> topsUnderBlockAbove = new ArrayList<>();
for (Map.Entry<BlockVector, BlockData> entry : blocks.entrySet()) {
BlockVector position = entry.getKey();
BlockData block = entry.getValue();
BlockModel model = modelOf.apply(block);
boolean seeThrough = isSeeThrough.test(block);
for (Face face : model.faces()) {
if (isHiddenByNeighbour(blocks, modelOf, isSeeThrough, position, block, model, face)) {
continue;
}
Quad quad = new Quad(face, position.getBlockX(), position.getBlockY(), position.getBlockZ(),
seeThrough ? SEE_THROUGH_OPACITY : FULL_OPACITY);
(seeThrough || face.texture().isTranslucent() ? translucent : solid).add(quad);
if (!isHiddenByNeighbour(blocks, modelOf, isSeeThrough, position, block, model, face)) {
(quad.isBlended() ? translucent : solid).add(quad);
} else if (face.cullFace() == CubeFace.UP) {
topsUnderBlockAbove.add(quad);
}
}
}
return new ObjectMesh(List.copyOf(solid), List.copyOf(translucent), bounds);
return new ObjectMesh(List.copyOf(solid), List.copyOf(translucent), List.of(), List.copyOf(topsUnderBlockAbove), bounds);
}
/**
* This mesh with the blocks above the given layer faded to a ghost of the object's shape, and the tops of that
* layer's blocks showing again. Quick enough to follow a slider as it moves.
*/
ObjectMesh cutAbove(int highestShownY) {
List<Quad> solid = new ArrayList<>();
List<Quad> translucent = new ArrayList<>();
List<Quad> cutAway = new ArrayList<>();
for (Quad quad : solidQuads) {
keepOrCutAway(quad, highestShownY, solid, cutAway);
}
for (Quad quad : translucentQuads) {
keepOrCutAway(quad, highestShownY, translucent, cutAway);
}
for (Quad quad : topsUnderBlockAbove) {
if (quad.y() == highestShownY) {
(quad.isBlended() ? translucent : solid).add(quad);
}
}
return new ObjectMesh(solid, translucent, cutAway, List.of(), bounds);
}
private static void keepOrCutAway(Quad quad, int highestShownY, List<Quad> kept, List<Quad> cutAway) {
if (quad.y() <= highestShownY) {
kept.add(quad);
} else {
cutAway.add(quad.faded(CUT_AWAY_OPACITY));
}
}
boolean isEmpty() {
return solidQuads.isEmpty() && translucentQuads.isEmpty();
return solidQuads.isEmpty() && translucentQuads.isEmpty() && cutAwayQuads.isEmpty();
}
int faceCount() {
@@ -102,6 +138,14 @@ record ObjectMesh(List<Quad> solidQuads, List<Quad> translucentQuads, Bounds bou
* A model face placed at a block in the object, drawn at the given opacity on top of its texture's own.
*/
record Quad(Face face, int x, int y, int z, double opacity) {
boolean isBlended() {
return opacity < FULL_OPACITY || face.texture().isTranslucent();
}
Quad faded(double factor) {
return new Quad(face, x, y, z, opacity * factor);
}
double originX() {
return x + face.origin().x();
}
@@ -31,7 +31,8 @@ import java.util.List;
/**
* Draws an object mesh with an orthographic camera into a picture, without a graphics card: each quad is filled pixel
* by pixel, and a depth buffer keeps the nearest one. See-through quads come last, farthest first, each blended over
* by pixel, and a depth buffer keeps the nearest one. A cut-away part's faint ghost only fills pixels the object
* leaves empty, so it never hazes over what is shown. See-through quads come last, farthest first, each blended over
* what is already drawn.
*/
final class ObjectRenderer {
@@ -51,6 +52,7 @@ final class ObjectRenderer {
private final int height;
private final int[] pixels;
private final double[] depths;
private final boolean[] showsObject;
private final Projection projection;
private ObjectRenderer(ObjectMesh mesh, ObjectCamera camera, BufferedImage image, int coveredLeftWidth) {
@@ -58,6 +60,7 @@ final class ObjectRenderer {
this.height = image.getHeight();
this.pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
this.depths = new double[pixels.length];
this.showsObject = new boolean[pixels.length];
this.projection = new Projection(mesh.bounds(), camera, width, height, coveredLeftWidth);
}
@@ -72,10 +75,13 @@ final class ObjectRenderer {
Arrays.fill(renderer.depths, Double.NEGATIVE_INFINITY);
renderer.drawGrid(mesh.bounds());
for (ObjectMesh.Quad quad : mesh.solidQuads()) {
renderer.drawQuad(quad, false);
renderer.drawQuad(quad, Pass.SOLID);
}
for (ObjectMesh.Quad quad : mesh.cutAwayQuads()) {
renderer.drawQuad(quad, Pass.CUT_AWAY_GHOST);
}
for (ObjectMesh.Quad quad : renderer.farthestFirst(mesh.translucentQuads())) {
renderer.drawQuad(quad, true);
renderer.drawQuad(quad, Pass.BLENDED);
}
return image;
}
@@ -131,7 +137,7 @@ final class ObjectRenderer {
}
// Later faces win ties, so an overlay drawn on the same box, like grass on a grass block's side, shows on top.
private void drawQuad(ObjectMesh.Quad quad, boolean blended) {
private void drawQuad(ObjectMesh.Quad quad, Pass pass) {
Face face = quad.face();
if (!projection.facesCamera(face.normal().x(), face.normal().y(), face.normal().z())) {
return;
@@ -182,7 +188,7 @@ final class ObjectRenderer {
}
int index = rowStart + x;
double depth = originDepth + alongU * edgeUDepth + alongV * edgeVDepth;
if (depth < depths[index]) {
if (depth < depths[index] || (pass == Pass.CUT_AWAY_GHOST && showsObject[index])) {
continue;
}
int texelX = columns.texelAt(mapping.u() + alongU * mapping.uAlongU() + alongV * mapping.uAlongV());
@@ -192,12 +198,13 @@ final class ObjectRenderer {
continue;
}
int color = BlockPreviewColors.shade(texels[texel], shade);
if (blended) {
if (pass != Pass.SOLID) {
pixels[index] = blend(color, pixels[index], (alphas == null ? 1D : alphas[texel] / FULL_ALPHA) * quad.opacity());
continue;
}
depths[index] = depth;
pixels[index] = color;
showsObject[index] = true;
}
}
}
@@ -314,4 +321,10 @@ final class ObjectRenderer {
return depthStep(normalX, normalY, normalZ) > 0D;
}
}
private enum Pass {
SOLID,
CUT_AWAY_GHOST,
BLENDED
}
}