mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
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:
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user