diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java index 84601fdfc..024fa4bbd 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java @@ -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 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 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 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 blocksUpTo(Map blocks, int highestY) { - Map 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) { diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectMesh.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectMesh.java index 50f2a970c..c524852d2 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/ObjectMesh.java +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectMesh.java @@ -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 solidQuads, List translucentQuads, Bounds bounds) { +record ObjectMesh(List solidQuads, List translucentQuads, List cutAwayQuads, List 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 blocks, Function modelOf) { return build(blocks, modelOf, Bounds.of(blocks.keySet()), block -> false); @@ -50,25 +54,57 @@ record ObjectMesh(List solidQuads, List translucentQuads, Bounds bou Predicate isSeeThrough) { List solid = new ArrayList<>(); List translucent = new ArrayList<>(); + List topsUnderBlockAbove = new ArrayList<>(); for (Map.Entry 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 solid = new ArrayList<>(); + List translucent = new ArrayList<>(); + List 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 kept, List 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 solidQuads, List 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(); } diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectRenderer.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectRenderer.java index 662a3f0bf..227b2f3ce 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/ObjectRenderer.java +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectRenderer.java @@ -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 + } }