mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +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 UUID worldId;
|
||||||
private final ObjectBrowser browser = new ObjectBrowser(this::showObject);
|
private final ObjectBrowser browser = new ObjectBrowser(this::showObject);
|
||||||
private final ObjectViewport viewport = new ObjectViewport();
|
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 JComponent viewWithLayerCut = layerCut.floatingOver(viewport);
|
||||||
private final ObjectOverviewPanel overview = new ObjectOverviewPanel(this::showSeeThrough);
|
private final ObjectOverviewPanel overview = new ObjectOverviewPanel(this::showSeeThrough);
|
||||||
private final StudioStatusBar statusBar = new StudioStatusBar();
|
private final StudioStatusBar statusBar = new StudioStatusBar();
|
||||||
@@ -72,6 +72,7 @@ final class ObjectExplorer {
|
|||||||
private volatile BiomeTintColors tintColors;
|
private volatile BiomeTintColors tintColors;
|
||||||
private String shownKey;
|
private String shownKey;
|
||||||
private LoadedObject shown;
|
private LoadedObject shown;
|
||||||
|
private ObjectMesh wholeMesh;
|
||||||
private Set<String> seeThroughTypes = Set.of();
|
private Set<String> seeThroughTypes = Set.of();
|
||||||
|
|
||||||
ObjectExplorer(Engine engine, UUID worldId) {
|
ObjectExplorer(Engine engine, UUID worldId) {
|
||||||
@@ -159,6 +160,7 @@ final class ObjectExplorer {
|
|||||||
private void showObject(String key) {
|
private void showObject(String key) {
|
||||||
shownKey = key;
|
shownKey = key;
|
||||||
shown = null;
|
shown = null;
|
||||||
|
wholeMesh = null;
|
||||||
meshRevision.incrementAndGet();
|
meshRevision.incrementAndGet();
|
||||||
long revision = objectRevision.incrementAndGet();
|
long revision = objectRevision.incrementAndGet();
|
||||||
viewport.showMessage("Loading " + key + "...");
|
viewport.showMessage("Loading " + key + "...");
|
||||||
@@ -197,6 +199,7 @@ final class ObjectExplorer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
shown = loaded;
|
shown = loaded;
|
||||||
|
wholeMesh = loaded.mesh();
|
||||||
seeThroughTypes = Set.of();
|
seeThroughTypes = Set.of();
|
||||||
statusBar.showActivity("");
|
statusBar.showActivity("");
|
||||||
viewport.show(loaded.mesh());
|
viewport.show(loaded.mesh());
|
||||||
@@ -216,46 +219,42 @@ final class ObjectExplorer {
|
|||||||
|
|
||||||
private void showSeeThrough(Set<String> blockTypes) {
|
private void showSeeThrough(Set<String> blockTypes) {
|
||||||
seeThroughTypes = 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 their hidden faces back, so the mesh is built again.
|
||||||
// blocks behind see-through ones get theirs back too.
|
private void rebuildWholeMesh() {
|
||||||
private void rebuildShownMesh() {
|
|
||||||
LoadedObject object = shown;
|
LoadedObject object = shown;
|
||||||
if (object == null) {
|
if (object == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
long revision = meshRevision.incrementAndGet();
|
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;
|
Set<String> seeThrough = seeThroughTypes;
|
||||||
loader.execute(() -> {
|
loader.execute(() -> {
|
||||||
if (revision != meshRevision.get()) {
|
if (revision != meshRevision.get()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ObjectMesh mesh = !cutting && seeThrough.isEmpty()
|
ObjectMesh mesh = seeThrough.isEmpty()
|
||||||
? object.mesh()
|
? 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()));
|
block -> seeThrough.contains(block.getMaterial().getKey().getKey()));
|
||||||
EventQueue.invokeLater(() -> {
|
EventQueue.invokeLater(() -> {
|
||||||
if (revision == meshRevision.get() && shown == object) {
|
if (revision == meshRevision.get() && shown == object) {
|
||||||
viewport.replaceMesh(mesh);
|
wholeMesh = mesh;
|
||||||
showMeshSummary(mesh);
|
showLayerCut();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<BlockVector, BlockData> blocksUpTo(Map<BlockVector, BlockData> blocks, int highestY) {
|
private void showLayerCut() {
|
||||||
Map<BlockVector, BlockData> below = new HashMap<>();
|
if (wholeMesh == null) {
|
||||||
blocks.forEach((position, block) -> {
|
return;
|
||||||
if (position.getBlockY() <= highestY) {
|
}
|
||||||
below.put(position, block);
|
int highestShownY = wholeMesh.bounds().minY() + layerCut.shownLayerCount() - 1;
|
||||||
}
|
ObjectMesh mesh = layerCut.isCutting() ? wholeMesh.cutAbove(highestShownY) : wholeMesh;
|
||||||
});
|
viewport.replaceMesh(mesh);
|
||||||
return below;
|
showMeshSummary(mesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showFailure(long revision, String key, Throwable error) {
|
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
|
* 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
|
* 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 SEE_THROUGH_OPACITY = 0.22D;
|
||||||
private static final double FULL_OPACITY = 1D;
|
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) {
|
static ObjectMesh build(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf) {
|
||||||
return build(blocks, modelOf, Bounds.of(blocks.keySet()), block -> false);
|
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) {
|
Predicate<BlockData> isSeeThrough) {
|
||||||
List<Quad> solid = new ArrayList<>();
|
List<Quad> solid = new ArrayList<>();
|
||||||
List<Quad> translucent = new ArrayList<>();
|
List<Quad> translucent = new ArrayList<>();
|
||||||
|
List<Quad> topsUnderBlockAbove = new ArrayList<>();
|
||||||
for (Map.Entry<BlockVector, BlockData> entry : blocks.entrySet()) {
|
for (Map.Entry<BlockVector, BlockData> entry : blocks.entrySet()) {
|
||||||
BlockVector position = entry.getKey();
|
BlockVector position = entry.getKey();
|
||||||
BlockData block = entry.getValue();
|
BlockData block = entry.getValue();
|
||||||
BlockModel model = modelOf.apply(block);
|
BlockModel model = modelOf.apply(block);
|
||||||
boolean seeThrough = isSeeThrough.test(block);
|
boolean seeThrough = isSeeThrough.test(block);
|
||||||
for (Face face : model.faces()) {
|
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(),
|
Quad quad = new Quad(face, position.getBlockX(), position.getBlockY(), position.getBlockZ(),
|
||||||
seeThrough ? SEE_THROUGH_OPACITY : FULL_OPACITY);
|
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() {
|
boolean isEmpty() {
|
||||||
return solidQuads.isEmpty() && translucentQuads.isEmpty();
|
return solidQuads.isEmpty() && translucentQuads.isEmpty() && cutAwayQuads.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
int faceCount() {
|
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.
|
* 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) {
|
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() {
|
double originX() {
|
||||||
return x + face.origin().x();
|
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
|
* 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.
|
* what is already drawn.
|
||||||
*/
|
*/
|
||||||
final class ObjectRenderer {
|
final class ObjectRenderer {
|
||||||
@@ -51,6 +52,7 @@ final class ObjectRenderer {
|
|||||||
private final int height;
|
private final int height;
|
||||||
private final int[] pixels;
|
private final int[] pixels;
|
||||||
private final double[] depths;
|
private final double[] depths;
|
||||||
|
private final boolean[] showsObject;
|
||||||
private final Projection projection;
|
private final Projection projection;
|
||||||
|
|
||||||
private ObjectRenderer(ObjectMesh mesh, ObjectCamera camera, BufferedImage image, int coveredLeftWidth) {
|
private ObjectRenderer(ObjectMesh mesh, ObjectCamera camera, BufferedImage image, int coveredLeftWidth) {
|
||||||
@@ -58,6 +60,7 @@ final class ObjectRenderer {
|
|||||||
this.height = image.getHeight();
|
this.height = image.getHeight();
|
||||||
this.pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
this.pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||||
this.depths = new double[pixels.length];
|
this.depths = new double[pixels.length];
|
||||||
|
this.showsObject = new boolean[pixels.length];
|
||||||
this.projection = new Projection(mesh.bounds(), camera, width, height, coveredLeftWidth);
|
this.projection = new Projection(mesh.bounds(), camera, width, height, coveredLeftWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,10 +75,13 @@ final class ObjectRenderer {
|
|||||||
Arrays.fill(renderer.depths, Double.NEGATIVE_INFINITY);
|
Arrays.fill(renderer.depths, Double.NEGATIVE_INFINITY);
|
||||||
renderer.drawGrid(mesh.bounds());
|
renderer.drawGrid(mesh.bounds());
|
||||||
for (ObjectMesh.Quad quad : mesh.solidQuads()) {
|
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())) {
|
for (ObjectMesh.Quad quad : renderer.farthestFirst(mesh.translucentQuads())) {
|
||||||
renderer.drawQuad(quad, true);
|
renderer.drawQuad(quad, Pass.BLENDED);
|
||||||
}
|
}
|
||||||
return image;
|
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.
|
// 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();
|
Face face = quad.face();
|
||||||
if (!projection.facesCamera(face.normal().x(), face.normal().y(), face.normal().z())) {
|
if (!projection.facesCamera(face.normal().x(), face.normal().y(), face.normal().z())) {
|
||||||
return;
|
return;
|
||||||
@@ -182,7 +188,7 @@ final class ObjectRenderer {
|
|||||||
}
|
}
|
||||||
int index = rowStart + x;
|
int index = rowStart + x;
|
||||||
double depth = originDepth + alongU * edgeUDepth + alongV * edgeVDepth;
|
double depth = originDepth + alongU * edgeUDepth + alongV * edgeVDepth;
|
||||||
if (depth < depths[index]) {
|
if (depth < depths[index] || (pass == Pass.CUT_AWAY_GHOST && showsObject[index])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int texelX = columns.texelAt(mapping.u() + alongU * mapping.uAlongU() + alongV * mapping.uAlongV());
|
int texelX = columns.texelAt(mapping.u() + alongU * mapping.uAlongU() + alongV * mapping.uAlongV());
|
||||||
@@ -192,12 +198,13 @@ final class ObjectRenderer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int color = BlockPreviewColors.shade(texels[texel], shade);
|
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());
|
pixels[index] = blend(color, pixels[index], (alphas == null ? 1D : alphas[texel] / FULL_ALPHA) * quad.opacity());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
depths[index] = depth;
|
depths[index] = depth;
|
||||||
pixels[index] = color;
|
pixels[index] = color;
|
||||||
|
showsObject[index] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,4 +321,10 @@ final class ObjectRenderer {
|
|||||||
return depthStep(normalX, normalY, normalZ) > 0D;
|
return depthStep(normalX, normalY, normalZ) > 0D;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum Pass {
|
||||||
|
SOLID,
|
||||||
|
CUT_AWAY_GHOST,
|
||||||
|
BLENDED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user