From 9a6c8b538d1cbed145f3daf222e87b9579ab92eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Wed, 4 Nov 2026 13:49:45 +0100 Subject: [PATCH] alpha-blend translucent block and entity texels (stained glass, slime skin, text backgrounds) --- .../render/entity/EntityIntersector.java | 14 +- .../pixelpics/render/entity/EntityScene.java | 26 +- .../pixelpics/render/entity/ObjectBaker.java | 7 +- .../pixelpics/render/entity/cem/CemBaker.java | 9 +- .../render/raytrace/ElementIntersector.java | 11 +- .../render/raytrace/SnapshotRaytracer.java | 285 ++++++++++++------ tools/ObjectTestRender.java | 5 +- tools/edgecase-plugin/pptest/PPTest.java | 36 +++ 8 files changed, 283 insertions(+), 110 deletions(-) diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityIntersector.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityIntersector.java index b5e4a52..0396385 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityIntersector.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityIntersector.java @@ -10,7 +10,7 @@ import org.bukkit.util.Vector; /** * Intersects a world-space ray with a single {@link EntityCube} (oriented box). The ray is mapped * into the cube's local frame, slab-tested, and the entry face is sampled. Fully transparent texels - * are treated as holes (alpha cutout). The returned {@code t} is a world-space distance. + * are treated as holes (alpha cutout); partially transparent ones keep their alpha for the tracer to blend. The returned {@code t} is a world-space distance. */ public final class EntityIntersector { @@ -18,7 +18,12 @@ public final class EntityIntersector { private EntityIntersector() {} - public static FaceHit intersect(EntityCube cube, double ox, double oy, double oz, double dx, double dy, double dz) { + /** + * Nearest drawn face of the cube with {@code t > minT} (entry face first, else the far face); {@code minT} + * lets the tracer continue behind a translucent hit. + */ + public static FaceHit intersect( + EntityCube cube, double ox, double oy, double oz, double dx, double dy, double dz, double minT) { double[] o = cube.toLocal.apply(ox, oy, oz); double[] d = cube.toLocal.applyLinear(dx, dy, dz); @@ -49,11 +54,12 @@ public final class EntityIntersector { } if (tmin > tmax + EPS) return null; } - if (inAxis < 0 || tmax < EPS) return null; + double lo = Math.max(EPS, minT); + if (inAxis < 0 || tmax <= lo) return null; // Vanilla draws entity models without back-face culling: a ray looking through a transparent // texel of the entry face still sees the inside of the far face (and both sides of a plane). - if (tmin >= EPS) { + if (tmin > lo) { FaceHit hit = sample(cube, o, d, tmin, inAxis, inNeg, ox, oy, oz, dx, dy, dz); if (hit != null) return hit; } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java index 5e24c2f..a66cab1 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java @@ -58,16 +58,17 @@ public final class EntityScene { return entities.isEmpty(); } - /** Nearest entity hit with {@code t < maxT}, or null. */ - public FaceHit nearestHit(double ox, double oy, double oz, double dx, double dy, double dz, double maxT) { + /** Nearest entity hit with {@code minT < t < maxT}, or null. */ + public FaceHit nearestHit( + double ox, double oy, double oz, double dx, double dy, double dz, double minT, double maxT) { FaceHit best = null; double bestT = maxT; for (RenderedEntity e : entities) { - if (!rayAabb(e.aabbMin, e.aabbMax, ox, oy, oz, dx, dy, dz, bestT)) continue; + if (!rayAabb(e.aabbMin, e.aabbMax, ox, oy, oz, dx, dy, dz, minT, bestT)) continue; for (EntityCube cube : e.cubes) { - if (!rayAabb(cube.aabbMin, cube.aabbMax, ox, oy, oz, dx, dy, dz, bestT)) continue; - FaceHit hit = EntityIntersector.intersect(cube, ox, oy, oz, dx, dy, dz); - if (hit != null && hit.t() > EPS && hit.t() < bestT) { + if (!rayAabb(cube.aabbMin, cube.aabbMax, ox, oy, oz, dx, dy, dz, minT, bestT)) continue; + FaceHit hit = EntityIntersector.intersect(cube, ox, oy, oz, dx, dy, dz, minT); + if (hit != null && hit.t() > EPS && hit.t() > minT && hit.t() < bestT) { best = hit; bestT = hit.t(); } @@ -77,8 +78,17 @@ public final class EntityScene { } private static boolean rayAabb( - double[] min, double[] max, double ox, double oy, double oz, double dx, double dy, double dz, double maxT) { - double tmin = 0, tmax = maxT; + double[] min, + double[] max, + double ox, + double oy, + double oz, + double dx, + double dy, + double dz, + double minT, + double maxT) { + double tmin = Math.max(0, minT), tmax = maxT; double[] o = {ox, oy, oz}, d = {dx, dy, dz}; for (int a = 0; a < 3; a++) { if (Math.abs(d[a]) < EPS) { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ObjectBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ObjectBaker.java index 7ae05b9..bd78b92 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ObjectBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ObjectBaker.java @@ -17,8 +17,6 @@ import java.util.List; */ public final class ObjectBaker implements EntityBaker { - private static final int MIN_BACKGROUND_ALPHA = - 0x80; // translucent text backgrounds can't be drawn: skip faint ones // Vanilla leash colours (LeashRenderer): brown, alternating with a darker shade per segment. private static final int LEASH_LIGHT = 0xFF7F6650, LEASH_DARK = 0xFF59473A; private static final int LEASH_SEGMENTS = 24; @@ -120,8 +118,9 @@ public final class ObjectBaker implements EntityBaker { new EntityCube(new double[] {x0, y0, 0.05}, new double[] {x1, y1, 0.05}, card(text), s.toWorld())); } int bg = s.backgroundArgb(); - if ((bg >>> 24) >= MIN_BACKGROUND_ALPHA && (w > 0 || !s.lines().isEmpty())) { - int[][] solid = {{bg | 0xFF000000}}; + // Translucent backgrounds (vanilla default: 25% black) keep their alpha; the tracer blends them. + if (ColorUtil.opaque(bg) && (w > 0 || !s.lines().isEmpty())) { + int[][] solid = {{bg}}; cubes.add(new EntityCube( new double[] {-w / 2.0 - 1, -1, 0}, new double[] {w / 2.0 + 1, h, 0}, card(solid), s.toWorld())); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java index 2f57944..60f4ad1 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java @@ -159,7 +159,7 @@ public final class CemBaker implements EntityBaker { List baked = new ArrayList<>(); if (!invisible) { baked.addAll(body); - if (vanilla) addVanillaLayers(s, ch, pose, show, baked); + if (vanilla) addVanillaLayers(s, ch, pose, show, tex, baked); } // Humanoid armor / elytra: drawn even when invisible (vanilla "floating armor"). if (vanilla && s.equipment() != null) addArmorLayers(s, ch, pose, baked); @@ -370,7 +370,7 @@ public final class CemBaker implements EntityBaker { // --- vanilla render layers ----------------------------------------------------------------------- private void addVanillaLayers( - EntityState s, Choice ch, Pose pose, Set show, List baked) { + EntityState s, Choice ch, Pose pose, Set show, int[][] bodyTex, List baked) { String base = ch.base; boolean baby = ch.babyModel; for (String n : layerModels(base)) { @@ -389,6 +389,11 @@ public final class CemBaker implements EntityBaker { baked.addAll(pose.bake(lm, t, show)); } case "TropicalFishPatternLayer" -> addTropicalPattern(s, n, lm, pose, baked); + case "SlimeOuterLayer" -> { + // Translucent outer cube on the body texture (alpha < 255 there; blended by the tracer). + int[][] t = lm.texture() != null ? texture(lm.texture()) : bodyTex; + if (t != null) baked.addAll(pose.bake(lm, t, show)); + } case "LlamaDecorLayer", "WolfArmorLayer" -> { // Body equipment drawn by a dedicated layer: texture folder _body (trader llama: llama_body). if (s.bodyEquip() == null) continue; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java index 9875a0d..0d3209f 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java @@ -11,9 +11,10 @@ import org.bukkit.util.Vector; /** * Intersects a ray (in block coordinates) with a {@link ResolvedModel}'s element boxes and returns - * the nearest opaque face hit. Axis-aligned boxes use a slab test; rotated elements are handled as + * the nearest drawn face hit. Axis-aligned boxes use a slab test; rotated elements are handled as * oriented boxes by transforming the ray into the element's local frame. Fully transparent texels - * (alpha ≤ threshold) are treated as holes so the ray passes through (cutout for plants, glass). + * (alpha ≤ threshold) are treated as holes so the ray passes through (cutout for plants, glass); + * partially transparent texels are returned with their alpha, the caller blends them (stained glass, ice). */ public final class ElementIntersector { @@ -25,6 +26,7 @@ public final class ElementIntersector { * @param ox,oy,oz ray origin in block-local coordinates (world origin minus block min corner) * @param dx,dy,dz ray direction (need not be normalized) * @param bx,by,bz block min corner in world coordinates (for reconstructing the world hit point) + * @param minT only faces strictly beyond this ray parameter count (continuing past a translucent hit) */ public static FaceHit intersect( ResolvedModel model, @@ -36,7 +38,8 @@ public final class ElementIntersector { double dz, int bx, int by, - int bz) { + int bz, + double minT) { List candidates = new ArrayList<>(model.elements.size()); for (int i = 0; i < model.elements.size(); i++) { Element element = model.elements.get(i); @@ -60,7 +63,7 @@ public final class ElementIntersector { ? intersectAabb(element, ox, oy, oz, dx, dy, dz) : intersectObb(element, ox, oy, oz, dx, dy, dz); } - if (c != null) candidates.add(new Candidate(c.element, c.t, c.dir, c.s, c.t2, c.normal, i)); + if (c != null && c.t > minT) candidates.add(new Candidate(c.element, c.t, c.dir, c.s, c.t2, c.normal, i)); } if (candidates.isEmpty()) return null; // Sort by depth; for coplanar faces (equal t) render later elements first, matching vanilla's diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java index 5d6ad48..cc4eebc 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java @@ -20,8 +20,8 @@ import org.bukkit.util.Vector; /** * Traces a single ray against a {@link WorldSnapshot}, sampling block models via the - * {@link ElementIntersector} and applying biome tint, directional face shading, transparency and - * reflection. Contains no Bukkit world access, so it is safe to invoke from worker threads. + * {@link ElementIntersector} and applying biome tint, directional face shading, alpha blending of + * translucent surfaces, liquid transparency and reflection. Contains no Bukkit world access, so it is safe to invoke from worker threads. */ public final class SnapshotRaytracer { @@ -35,6 +35,14 @@ public final class SnapshotRaytracer { // Vanilla-style ambient occlusion brightness per occlusion level (0=most occluded .. 3=open). Kept subtle. private static final double[] AO_BRIGHTNESS = {0.55, 0.70, 0.85, 1.0}; + // Alpha blending: texels with alpha >= OPAQUE_ALPHA end the ray; translucent ones (stained glass, ice, + // slime outer layer, text-display backgrounds) are blended over what lies behind them. Bounded so a + // stack of glass costs at most a few extra intersection rounds per ray. + private static final int OPAQUE_ALPHA = 250; + private static final int MAX_TRANSLUCENT_LAYERS = 6; + private static final double MIN_TRANSMITTANCE = 0.03; + private static final double LAYER_EPS = 1e-5; + private final BlockModelRegistry registry; private final BiomeTintProvider tintProvider; private final SkyRenderer skyRenderer; @@ -66,41 +74,166 @@ public final class SnapshotRaytracer { double ox = origin.getX(), oy = origin.getY(), oz = origin.getZ(); double dx = direction.getX(), dy = direction.getY(), dz = direction.getZ(); - VoxelDDA dda = new VoxelDDA(ox, oy, oz, dx, dy, dz); - int skyColor = skyRenderer.colorFor(direction, origin, sky); - int baseColor = skyColor; - Vector finalPoint = null; + BlockCursor blocks = new BlockCursor(snapshot, origin, direction, sky, scene, depth); + boolean entities = scene != null && !scene.isEmpty(); - int reflectionColor = 0; - double reflectionFactor = 0; - boolean reflected = false; + // Translucent surfaces in front of the final one, front to back (shaded colour, alpha 0..1, ray t). + int[] layerColor = null; + double[] layerAlpha = null, layerT = null; + int layers = 0; + double transmittance = 1; - Vector transparencyStart = null; - int transparencyColor = 0; - double transparencyFactor = 0; + int finalColor = skyColor; + double finalT = Double.POSITIVE_INFINITY; // sky + double minT = 0; + BlockHit pending = null; // nearest block surface not consumed yet - BlockData occlusion = null; - - for (int i = 0; i < maxSteps && dda.tCurrent <= maxDistance; i++) { - int bx = dda.x, by = dda.y, bz = dda.z; - BlockData data = snapshot.getBlockData(bx, by, bz); - if (data.getMaterial() == Material.AIR) { - occlusion = null; - dda.advance(); - continue; + // Walk the surfaces along the ray in depth order (blocks and entities interleaved); translucent ones + // are collected as layers and the ray continues behind them, the first opaque one ends it. + while (true) { + if (pending == null) pending = blocks.next(minT); + double limit = pending != null ? pending.t : maxDistance; + FaceHit eh = entities ? scene.nearestHit(ox, oy, oz, dx, dy, dz, minT, limit) : null; + int color; + double t; + if (eh != null) { + color = ColorUtil.shade(eh.color(), shadeFactor(eh.normal())); + t = eh.t(); + } else if (pending != null) { + color = pending.color; + t = pending.t; + pending = null; + } else { + break; } - - ResolvedModel model = registry.get(data); - FaceHit hit = ElementIntersector.intersect(model, ox - bx, oy - by, oz - bz, dx, dy, dz, bx, by, bz); - if (hit == null) { - occlusion = null; - dda.advance(); - continue; + int alpha = ColorUtil.alpha(color); + double a = alpha / 255.0; + if (alpha >= OPAQUE_ALPHA + || layers == MAX_TRANSLUCENT_LAYERS + || transmittance * (1 - a) < MIN_TRANSMITTANCE) { + // Opaque, or what lies behind would contribute next to nothing: this surface ends the ray. + finalColor = color; + finalT = t; + break; } + if (layerColor == null) { + layerColor = new int[MAX_TRANSLUCENT_LAYERS]; + layerAlpha = new double[MAX_TRANSLUCENT_LAYERS]; + layerT = new double[MAX_TRANSLUCENT_LAYERS]; + } + layerColor[layers] = color; + layerAlpha[layers] = a; + layerT[layers] = t; + layers++; + transmittance *= 1 - a; + minT = t + LAYER_EPS; + } - int color = shadeAndTint(hit, data, snapshot, bx, by, bz); + // Compose back to front: final surface (fogged), the liquid volume where it lies, translucent layers. + int color = Double.isInfinite(finalT) ? finalColor : fog(finalColor, finalT, skyColor); + boolean liquidPending = blocks.liquidT >= 0 && blocks.liquidT < finalT; + for (int i = layers - 1; i >= 0; i--) { + if (liquidPending && layerT[i] < blocks.liquidT) { + color = blocks.applyLiquid(color, finalT, skyColor); + liquidPending = false; + } + color = ColorUtil.mix(fog(layerColor[i], layerT[i], skyColor), color, layerAlpha[i], 1 - layerAlpha[i]); + } + if (liquidPending) color = blocks.applyLiquid(color, finalT, skyColor); + return color & 0xFFFFFF; + } + private int fog(int color, double distance, int skyColor) { + double f = fogFactor(distance); + return f > 0 ? ColorUtil.mix(color, skyColor, 1 - f, f) : color; + } + + /** A block surface found by the {@link BlockCursor}: shaded/tinted ARGB colour and ray parameter. */ + private record BlockHit(int color, double t) {} + + /** + * Walks the voxel grid along one ray and hands out block surfaces in depth order. It stays on the voxel + * of the last returned hit, so a later call (with {@code minT} behind that hit) also finds surfaces + * further inside the same block (slime/honey block cores, the far side of a pane). Liquids are not + * returned as surfaces: the first one is recorded (colour, transparency, reflection) and blended as a + * volume between its surface and the final surface, as before. + */ + private final class BlockCursor { + private final WorldSnapshot snapshot; + private final Vector direction; + private final SkyContext sky; + private final EntityScene scene; + private final int depth; + private final double ox, oy, oz, dx, dy, dz; + private final VoxelDDA dda; + private int steps; + private boolean done; + + double liquidT = -1; + private int liquidColor; + private double liquidFactor; + private boolean reflected; + private int reflectionColor; + private double reflectionFactor; + + BlockCursor( + WorldSnapshot snapshot, Vector origin, Vector direction, SkyContext sky, EntityScene scene, int depth) { + this.snapshot = snapshot; + this.direction = direction; + this.sky = sky; + this.scene = scene; + this.depth = depth; + this.ox = origin.getX(); + this.oy = origin.getY(); + this.oz = origin.getZ(); + this.dx = direction.getX(); + this.dy = direction.getY(); + this.dz = direction.getZ(); + this.dda = new VoxelDDA(ox, oy, oz, dx, dy, dz); + } + + /** The next block surface with {@code t > minT}, or null when the ray leaves the render distance. */ + BlockHit next(double minT) { + while (!done) { + if (steps >= maxSteps || dda.tCurrent > maxDistance) { + done = true; + break; + } + int bx = dda.x, by = dda.y, bz = dda.z; + BlockData data = snapshot.getBlockData(bx, by, bz); + if (data.getMaterial() == Material.AIR) { + advance(); + continue; + } + ResolvedModel model = registry.get(data); + FaceHit hit = + ElementIntersector.intersect(model, ox - bx, oy - by, oz - bz, dx, dy, dz, bx, by, bz, minT); + if (hit == null) { + advance(); + continue; + } + if (model.transparency > 0) { + recordLiquid(model, hit, data, bx, by, bz); + advance(); + continue; + } + int color = shadeAndTint(hit, data, snapshot, bx, by, bz); + if (ColorUtil.alpha(color) < OPAQUE_ALPHA && culledByNeighbour(hit, data, bx, by, bz)) { + minT = hit.t(); // inner face between two blocks of the same translucent kind: not drawn + continue; + } + return new BlockHit(color, hit.t()); + } + return null; + } + + private void advance() { + dda.advance(); + steps++; + } + + private void recordLiquid(ResolvedModel model, FaceHit hit, BlockData data, int bx, int by, int bz) { if (!reflected && model.reflection > 0 && depth > 0) { Vector reflectDir = MathUtil.reflectVector(direction, hit.normal()); Vector reflectStart = @@ -109,68 +242,48 @@ public final class SnapshotRaytracer { reflectionFactor = model.reflection; reflected = true; } - - if (transparencyStart == null && model.transparency > 0) { - transparencyStart = hit.point(); - transparencyColor = color; - transparencyFactor = model.transparency; + if (liquidT < 0) { + liquidT = hit.t(); + liquidColor = shadeAndTint(hit, data, snapshot, bx, by, bz); + liquidFactor = model.transparency; } + } - if (model.occluding) { - if (data.equals(occlusion)) { - dda.advance(); - continue; - } - occlusion = data; + /** Liquid volume over {@code behind}: denser with depth down to the final surface, plus reflection. */ + int applyLiquid(int behind, double finalT, int skyColor) { + double depthIn = Double.isInfinite(finalT) ? 0 : finalT - liquidT; + int surface = fog(liquidColor, liquidT, skyColor); + int c = ColorUtil.mix(behind, surface, liquidFactor, (1 - liquidFactor) * (1 + depthIn / 5.0)); + if (reflected) c = ColorUtil.mix(c, reflectionColor, 1 - reflectionFactor, reflectionFactor); + return c; + } + + /** + * Vanilla skips the faces between two translucent blocks of the same kind (stained glass walls, ice, + * connected panes): true when the hit lies on the block boundary and the neighbour across it is the + * same block type. + */ + private boolean culledByNeighbour(FaceHit hit, BlockData data, int bx, int by, int bz) { + Vector n = hit.normal(); + double nx = n.getX(), ny = n.getY(), nz = n.getZ(); + int sx = 0, sy = 0, sz = 0; + double local; + if (Math.abs(nx) > 0.99) { + sx = nx > 0 ? 1 : -1; + local = hit.point().getX() - bx; + } else if (Math.abs(ny) > 0.99) { + sy = ny > 0 ? 1 : -1; + local = hit.point().getY() - by; + } else if (Math.abs(nz) > 0.99) { + sz = nz > 0 ? 1 : -1; + local = hit.point().getZ() - bz; } else { - occlusion = null; + return false; } - - if (transparencyStart != null && model.transparency > 0) { - dda.advance(); - continue; - } - - baseColor = color; - finalPoint = hit.point(); - break; + double boundary = (sx + sy + sz) > 0 ? 1 : 0; + if (Math.abs(local - boundary) > 1e-4) return false; + return snapshot.getBlockData(bx + sx, by + sy, bz + sz).getMaterial() == data.getMaterial(); } - - // Entities: if one is closer than the opaque block/sky, it becomes the surface. - if (scene != null && !scene.isEmpty()) { - double blockDist = finalPoint != null ? origin.distance(finalPoint) : maxDistance; - FaceHit eh = scene.nearestHit(ox, oy, oz, dx, dy, dz, blockDist); - if (eh != null) { - baseColor = ColorUtil.shade(eh.color(), shadeFactor(eh.normal())); - finalPoint = eh.point(); - reflected = false; - if (transparencyStart != null && origin.distance(transparencyStart) >= eh.t()) { - transparencyStart = null; - } - } - } - - if (transparencyStart != null) { - baseColor = ColorUtil.mix( - baseColor, - transparencyColor, - transparencyFactor, - (1 - transparencyFactor) - * (1 - + transparencyStart.distance(finalPoint == null ? transparencyStart : finalPoint) - / 5.0)); - } - if (reflected) { - baseColor = ColorUtil.mix(baseColor, reflectionColor, 1 - reflectionFactor, reflectionFactor); - } - - // Distance fog (atmospheric perspective): fade distant geometry toward the sky color. - if (finalPoint != null) { - double fog = fogFactor(origin.distance(finalPoint)); - if (fog > 0) baseColor = ColorUtil.mix(baseColor, skyColor, 1 - fog, fog); - } - - return baseColor & 0xFFFFFF; } private int shadeAndTint(FaceHit hit, BlockData data, WorldSnapshot snapshot, int bx, int by, int bz) { diff --git a/tools/ObjectTestRender.java b/tools/ObjectTestRender.java index a8042e6..a273f4f 100644 --- a/tools/ObjectTestRender.java +++ b/tools/ObjectTestRender.java @@ -82,9 +82,10 @@ public class ObjectTestRender { objs.add(obj(ObjectState.Kind.TEXT, Affine.translation(1.5, 3.0, 0).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)), null, null, List.of("Hallo PixelPics!", "Zeile zwei äöü"), 0xFF203040)); - // Text seen from the BACK (front faces +Z, camera is on -Z) and a shield item. + // Text seen from the BACK (front faces +Z, camera is on -Z) on the vanilla default 25% black + // background (blended), and a shield item. objs.add(obj(ObjectState.Kind.TEXT, Affine.translation(-0.5, 3.0, 0).mul(Affine.scale(0.025)), null, null, - List.of("Rueckseite"), 0)); + List.of("Rueckseite"), 0x40000000)); objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(4, 1.0, 0.5), null, "shield", List.of(), 0)); // Dropped item (flat) on the ground. objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(-1, 0.3, 0.5).mul(Affine.scale(0.5)), null, "apple", List.of(), 0)); diff --git a/tools/edgecase-plugin/pptest/PPTest.java b/tools/edgecase-plugin/pptest/PPTest.java index 14caa60..34682fa 100644 --- a/tools/edgecase-plugin/pptest/PPTest.java +++ b/tools/edgecase-plugin/pptest/PPTest.java @@ -56,6 +56,7 @@ public class PPTest extends JavaPlugin { scenes.put("pistondown", new Scene("pistondown", this::piston, -1.6, 2.6, -1.6, 0.5, 1.4, 0.5)); scenes.put("pistonup", new Scene("pistonup", this::piston, 0.4, 2.6, -1.6, 2.5, 1.4, 0.5)); scenes.put("vehicles", new Scene("vehicles", this::vehicles, 5, 3, -7, 5, 0.6, 1.5)); + scenes.put("translucent", new Scene("translucent", this::translucent, 4.5, 2.8, -5.5, 4.5, 0.8, 2)); } @Override @@ -474,4 +475,39 @@ public class PPTest extends JavaPlugin { spawn(o, 6, 0, 3, 90, EntityType.CHERRY_CHEST_BOAT); spawn(o, 9, 0, 3, 120, EntityType.MANGROVE_BOAT); } + + /** + * Alpha blending: a stained-glass wall (inner faces culled) and stacked glass colours in front of a striped + * wool wall, ice/tinted glass/slime/honey blocks, connected panes, glass under water, a slime and a sulfur + * cube (translucent outer skins, one behind glass) and a text display with the default background. + */ + private void translucent(Location o) { + java.util.function.BiConsumer put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock() + .setBlockData(Bukkit.createBlockData("minecraft:" + data), false); + for (int x = -1; x <= 10; x++) + for (int y = 0; y <= 4; y++) put.accept(new int[] {x, y, 5}, (x + y) % 2 == 0 ? "white_wool" : "black_wool"); + for (int x = 0; x <= 2; x++) for (int y = 0; y <= 1; y++) put.accept(new int[] {x, y, 1}, "red_stained_glass"); + put.accept(new int[] {3, 0, 1}, "blue_stained_glass"); + put.accept(new int[] {3, 0, 0}, "yellow_stained_glass"); + put.accept(new int[] {4, 0, 1}, "ice"); + put.accept(new int[] {4, 1, 1}, "tinted_glass"); + put.accept(new int[] {5, 0, 1}, "slime_block"); + put.accept(new int[] {6, 0, 1}, "honey_block"); + put.accept(new int[] {7, 0, 1}, "lime_stained_glass_pane[east=true]"); + put.accept(new int[] {8, 0, 1}, "lime_stained_glass_pane[west=true,east=true]"); + put.accept(new int[] {9, 0, 1}, "lime_stained_glass_pane[west=true]"); + for (int x = 0; x <= 2; x++) put.accept(new int[] {x, -1, -2}, "water"); + put.accept(new int[] {1, -2, -2}, "magenta_stained_glass"); + put.accept(new int[] {0, -2, -2}, "sand"); + put.accept(new int[] {2, -2, -2}, "sand"); + Slime slime = spawn(o, 1.5, 0, 3, 200, Slime.class); + slime.setSize(2); + Slime front = spawn(o, 7.5, 0, -1.5, 160, Slime.class); + front.setSize(1); + EntityType sulfur = Registry.ENTITY_TYPE.get(NamespacedKey.minecraft("sulfur_cube")); + if (sulfur != null) spawn(o, 5.5, 0, -1.5, 200, sulfur); + TextDisplay t = spawn(o, 4.5, 2.6, 0, 0, TextDisplay.class); + t.text(Component.text("Default background")); + t.setBillboard(Display.Billboard.CENTER); + } }