alpha-blend translucent block and entity texels (stained glass, slime skin, text backgrounds)
This commit is contained in:
1 parent
37fa1d8f56
commit
9a6c8b538d
8 files changed
+283
-110
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -17,8 +17,6 @@ import java.util.List;
|
||||
*/
|
||||
public final class ObjectBaker implements EntityBaker<ObjectState> {
|
||||
|
||||
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<ObjectState> {
|
||||
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()));
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
List<CemGeometry.Baked> 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<EntityState> {
|
||||
// --- vanilla render layers -----------------------------------------------------------------------
|
||||
|
||||
private void addVanillaLayers(
|
||||
EntityState s, Choice ch, Pose pose, Set<String> show, List<CemGeometry.Baked> baked) {
|
||||
EntityState s, Choice ch, Pose pose, Set<String> show, int[][] bodyTex, List<CemGeometry.Baked> baked) {
|
||||
String base = ch.base;
|
||||
boolean baby = ch.babyModel;
|
||||
for (String n : layerModels(base)) {
|
||||
@@ -389,6 +389,11 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
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 <base>_body (trader llama: llama_body).
|
||||
if (s.bodyEquip() == null) continue;
|
||||
|
||||
@@ -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<Candidate> 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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<int[], String> 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user