From: Svjatoslav Agejenko Date: Sun, 20 Sep 2026 10:54:56 +0000 (+0300) Subject: Fix overlay shapes ignoring the z-buffer (bugreport 20260920-131610) X-Git-Tag: aukio-3d-1.0.0~2 X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=8eeecdb82b8947a6f96c2026606ad7ca5dacc31f;p=aukio-3d.git Fix overlay shapes ignoring the z-buffer (bugreport 20260920-131610) Solid geometry never occluded Line or Billboard/GlowingPoint output: their span writers blended pixels unconditionally ('overlay semantics'), so wireframe shapes, text sprites and glow points showed through solid surfaces — e.g. wireframe pyramids visible through the solid sphere in ShapeGalleryDemo. Lines: interpolate 1/z per pixel (projectively linear along the 2D projected segment; thick path projects each span pixel onto the line parameter, thin paths reuse their walk parameter) and depth-test with the same idiom as SolidPolygon. Sprites: screen-aligned, so one 1/z value covers the whole quad. Both depth-TEST only and never depth- WRITE (like pass-2 translucent geometry): a line/sprite must not occlude geometry painted after it, and the HiZ pyramid is unaffected. Verified: bug-pose render (Snapshot.cameraFromPose on the bugreport pose) shows a clean sphere disc; all 21 unaffected goldens bit- identical; shape-gallery + stereo goldens re-blessed after visual review confirmed every changed pixel is a correct occlusion (wireframe lines/grid/particles blocked by the solid sphere). Engine 48/48. --- diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java index 77f08f5..fa40bdc 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java @@ -95,13 +95,16 @@ public class Billboard extends AbstractCoordinateShape { */ @Override public void paint(final RenderingContext targetRenderingArea) { - // Sprites don't participate in depth (overlay semantics) — - // paint only in the alpha pass. + // Sprites paint only in the alpha pass: they blend, depth-TEST + // against the opaque z-buffer (solid geometry occludes them), + // and never depth-WRITE. The sprite is screen-aligned, so 1/z + // is constant across the quad — one depth value for all pixels. if (targetRenderingArea.depthPass == 1) return; // distance from camera/viewer to center of the texture final double z = vertices.get(0).transformedCoordinate(targetRenderingArea).z; + final double zw = 1d / z; // compute forward oriented texture visible distance from center final double visibleHorizontalDistanceFromCenter = (targetRenderingArea.width @@ -159,9 +162,11 @@ public class Billboard extends AbstractCoordinateShape { return; final int[] targetPixels = targetRenderingArea.pixels; + final float[] targetDepth = targetRenderingArea.depth; final int[] sourcePixels = textureBitmap.pixels; final int textureWidth = textureBitmap.width; final int textureHeight = textureBitmap.height; + final double depthMargin = RenderingContext.DEPTH_MARGIN_DZ * zw * zw; final int targetWidth = targetRenderingArea.width; // Fixed-point (16.16) texture stepping values - eliminates per-pixel division @@ -186,6 +191,13 @@ public class Billboard extends AbstractCoordinateShape { for (int x = onScreenCappedXStart; x < onScreenCappedXEnd; x++) { + // depth-test (never write): solids occlude sprites + if (zw <= targetDepth[targetOffset] - depthMargin) { + sourceX += sourceXStep; + targetOffset++; + continue; + } + // Convert fixed-point X to integer and compute source address final int sourceAddress = scanlineBase + (sourceX >> 16); diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java index 7b1e7a4..a0478d6 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java @@ -92,16 +92,30 @@ public class Line extends AbstractCoordinateShape { } /** - * Draws a horizontal scanline between two interpolators with alpha blending. + * Draws a thick line as a series of horizontal spans. + * + *

Each pixel is depth-tested against the z-buffer (lines + * participate in occlusion: solid geometry hides the parts of a + * line that lie behind it), but depth is never written — a line + * must not occlude geometry painted after it.

* * @param line1 the left edge interpolator * @param line2 the right edge interpolator * @param y the Y coordinate of the scanline * @param renderBuffer the rendering context to draw into + * @param p1x X of the line's first projected endpoint + * @param p1y Y of the line's first projected endpoint + * @param dtDx d(t)/dx of the 2D line parameter (xp / len²) + * @param dtDy d(t)/dy of the 2D line parameter (yp / len²) + * @param zwP1 1/z at the first endpoint + * @param zwDelta (1/z at second endpoint) - zwP1 */ private void drawHorizontalLine(final LineInterpolator line1, final LineInterpolator line2, final int y, - final RenderingContext renderBuffer) { + final RenderingContext renderBuffer, + final double p1x, final double p1y, + final double dtDx, final double dtDy, + final double zwP1, final double zwDelta) { int x1 = line1.getX(y); int x2 = line2.getX(y); @@ -122,8 +136,16 @@ public class Line extends AbstractCoordinateShape { final int unclippedWidth = x2 - x1; final double dinc = (d2 - d1) / unclippedWidth; + // 1/z at the first (leftmost) span pixel: project the pixel onto + // the 2D line for its parameter t, then interpolate 1/z linearly + // (projectively correct along a projected 3D line). Computed + // after the endpoint swap so x1 is genuinely the smaller X. + double zw = zwP1 + ((((x1 - p1x) * dtDx) + ((y - p1y) * dtDy)) * zwDelta); + final double zwInc = dtDx * zwDelta; + if (x1 < renderBuffer.renderMinX) { d1 += (dinc * (renderBuffer.renderMinX - x1)); + zw += zwInc * (renderBuffer.renderMinX - x1); x1 = renderBuffer.renderMinX; } @@ -136,6 +158,7 @@ public class Line extends AbstractCoordinateShape { int offset = (y * renderBuffer.width) + x1; final int[] pixels = renderBuffer.pixels; + final float[] depth = renderBuffer.depth; final int lineAlpha = color.a; @@ -150,18 +173,23 @@ public class Line extends AbstractCoordinateShape { final int realLineAlpha = (int) (lineAlpha * alphaMultiplier); final int backgroundAlpha = 255 - realLineAlpha; - final int dest = pixels[offset]; - final int destR = (dest >> 16) & 0xff; - final int destG = (dest >> 8) & 0xff; - final int destB = dest & 0xff; + // Depth-test like pass-2 translucent geometry: never write. + if (zw > depth[offset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) { + final int dest = pixels[offset]; + final int destR = (dest >> 16) & 0xff; + final int destG = (dest >> 8) & 0xff; + final int destB = dest & 0xff; - final int newR = ((destR * backgroundAlpha) + (colorR * realLineAlpha)) >> 8; - final int newG = ((destG * backgroundAlpha) + (colorG * realLineAlpha)) >> 8; - final int newB = ((destB * backgroundAlpha) + (colorB * realLineAlpha)) >> 8; + final int newR = ((destR * backgroundAlpha) + (colorR * realLineAlpha)) >> 8; + final int newG = ((destG * backgroundAlpha) + (colorG * realLineAlpha)) >> 8; + final int newB = ((destB * backgroundAlpha) + (colorB * realLineAlpha)) >> 8; - pixels[offset++] = (newR << 16) | (newG << 8) | newB; + pixels[offset] = (newR << 16) | (newG << 8) | newB; + } + offset++; d1 += dinc; + zw += zwInc; } } @@ -176,12 +204,16 @@ public class Line extends AbstractCoordinateShape { private void drawSinglePixelHorizontalLine(final RenderingContext buffer, final int alpha, final Point2D onScreenPoint1, - final Point2D onScreenPoint2) { + final Point2D onScreenPoint2, + final double zwP1, + final double zwP2) { int xStart = (int) onScreenPoint1.x; int xEnd = (int) onScreenPoint2.x; int lineHeight; int yBase; + final double zwA; + final double zwB; if (xStart > xEnd) { final int tmp = xStart; @@ -189,9 +221,14 @@ public class Line extends AbstractCoordinateShape { xEnd = tmp; lineHeight = (int) (onScreenPoint1.y - onScreenPoint2.y); yBase = (int) onScreenPoint2.y; + // walk runs from endpoint 2 to endpoint 1 + zwA = zwP2; + zwB = zwP1; } else { yBase = (int) onScreenPoint1.y; lineHeight = (int) (onScreenPoint2.y - onScreenPoint1.y); + zwA = zwP1; + zwB = zwP2; } final int lineWidth = xEnd - xStart; @@ -199,6 +236,7 @@ public class Line extends AbstractCoordinateShape { return; final int[] pixels = buffer.pixels; + final float[] depth = buffer.depth; final int backgroundAlpha = 255 - alpha; final int redWithAlpha = color.r * alpha; @@ -215,16 +253,21 @@ public class Line extends AbstractCoordinateShape { if ((y >= 0) && (y < buffer.height)) { int offset = (y * buffer.width) + x; - final int dest = pixels[offset]; - final int destR = (dest >> 16) & 0xff; - final int destG = (dest >> 8) & 0xff; - final int destB = dest & 0xff; - - final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8; - final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8; - final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8; - - pixels[offset] = (newR << 16) | (newG << 8) | newB; + // depth-test (never write): solids occlude lines + final double zw = zwA + + (((double) relativeX / lineWidth) * (zwB - zwA)); + if (zw > depth[offset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) { + final int dest = pixels[offset]; + final int destR = (dest >> 16) & 0xff; + final int destG = (dest >> 8) & 0xff; + final int destB = dest & 0xff; + + final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8; + final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8; + final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8; + + pixels[offset] = (newR << 16) | (newG << 8) | newB; + } } } } @@ -242,12 +285,16 @@ public class Line extends AbstractCoordinateShape { private void drawSinglePixelVerticalLine(final RenderingContext buffer, final int alpha, final Point2D onScreenPoint1, - final Point2D onScreenPoint2) { + final Point2D onScreenPoint2, + final double zwP1, + final double zwP2) { int yStart = (int) onScreenPoint1.y; int yEnd = (int) onScreenPoint2.y; int lineWidth; int xBase; + final double zwA; + final double zwB; if (yStart > yEnd) { final int tmp = yStart; @@ -255,9 +302,14 @@ public class Line extends AbstractCoordinateShape { yEnd = tmp; lineWidth = (int) (onScreenPoint1.x - onScreenPoint2.x); xBase = (int) onScreenPoint2.x; + // walk runs from endpoint 2 to endpoint 1 + zwA = zwP2; + zwB = zwP1; } else { xBase = (int) onScreenPoint1.x; lineWidth = (int) (onScreenPoint2.x - onScreenPoint1.x); + zwA = zwP1; + zwB = zwP2; } final int lineHeight = yEnd - yStart; @@ -265,6 +317,7 @@ public class Line extends AbstractCoordinateShape { return; final int[] pixels = buffer.pixels; + final float[] depth = buffer.depth; final int backgroundAlpha = 255 - alpha; final int redWithAlpha = color.r * alpha; @@ -281,16 +334,21 @@ public class Line extends AbstractCoordinateShape { if ((x >= buffer.renderMinX) && (x < buffer.renderMaxX)) { int offset = (y * buffer.width) + x; - final int dest = pixels[offset]; - final int destR = (dest >> 16) & 0xff; - final int destG = (dest >> 8) & 0xff; - final int destB = dest & 0xff; - - final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8; - final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8; - final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8; - - pixels[offset] = (newR << 16) | (newG << 8) | newB; + // depth-test (never write): solids occlude lines + final double zw = zwA + + (((double) relativeY / lineHeight) * (zwB - zwA)); + if (zw > depth[offset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) { + final int dest = pixels[offset]; + final int destR = (dest >> 16) & 0xff; + final int destG = (dest >> 8) & 0xff; + final int destB = dest & 0xff; + + final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8; + final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8; + final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8; + + pixels[offset] = (newR << 16) | (newG << 8) | newB; + } } } } @@ -356,8 +414,9 @@ public class Line extends AbstractCoordinateShape { */ @Override public void paint(final RenderingContext buffer) { - // Lines don't participate in depth (overlay semantics) — paint - // only in the alpha pass. + // Lines paint only in the alpha pass: they blend, depth-TEST + // against the opaque z-buffer (solid geometry occludes them), + // and never depth-WRITE (a line must not occlude later geometry). if (buffer.depthPass == 1) return; @@ -373,10 +432,16 @@ public class Line extends AbstractCoordinateShape { final double xp = onScreenPoint2.x - onScreenPoint1.x; final double yp = onScreenPoint2.y - onScreenPoint1.y; - final double point1radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width) - / endpoint1.transformedCoordinate(buffer).z; - final double point2radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width) - / endpoint2.transformedCoordinate(buffer).z; + final double z1 = endpoint1.transformedCoordinate(buffer).z; + final double z2 = endpoint2.transformedCoordinate(buffer).z; + + final double point1radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width) / z1; + final double point2radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width) / z2; + + // 1/z at the endpoints: interpolates linearly in screen space + // (projectively correct) — the basis for per-pixel depth tests. + final double zwP1 = 1d / z1; + final double zwP2 = 1d / z2; if ((point1radius < MINIMUM_WIDTH_THRESHOLD) || (point2radius < MINIMUM_WIDTH_THRESHOLD)) { @@ -391,9 +456,9 @@ public class Line extends AbstractCoordinateShape { return; if (Math.abs(xp) > Math.abs(yp)) - drawSinglePixelHorizontalLine(buffer, alpha, onScreenPoint1, onScreenPoint2); + drawSinglePixelHorizontalLine(buffer, alpha, onScreenPoint1, onScreenPoint2, zwP1, zwP2); else - drawSinglePixelVerticalLine(buffer, alpha, onScreenPoint1, onScreenPoint2); + drawSinglePixelVerticalLine(buffer, alpha, onScreenPoint1, onScreenPoint2, zwP1, zwP2); return; } @@ -452,12 +517,19 @@ public class Line extends AbstractCoordinateShape { if (ymin > ymax) return; + // 2D-line parameter gradients for per-pixel 1/z interpolation + final double len2 = (xp * xp) + (yp * yp); + final double dtDx = xp / len2; + final double dtDy = yp / len2; + for (int y = (int) ymin; y <= ymax; y++) { final int li1 = getLineInterpolator(lineInterpolators, 0, y); if (li1 != -1) { final int li2 = getLineInterpolator(lineInterpolators, li1 + 1, y); if (li2 != -1) - drawHorizontalLine(lineInterpolators[li1], lineInterpolators[li2], y, buffer); + drawHorizontalLine(lineInterpolators[li1], lineInterpolators[li2], y, + buffer, onScreenPoint1.x, onScreenPoint1.y, dtDx, dtDy, + zwP1, zwP2 - zwP1); } } }