}
/**
- * Draws a horizontal scanline between two interpolators with alpha blending.
+ * Draws a thick line as a series of horizontal spans.
+ *
+ * <p>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.</p>
*
* @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);
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;
}
int offset = (y * renderBuffer.width) + x1;
final int[] pixels = renderBuffer.pixels;
+ final float[] depth = renderBuffer.depth;
final int lineAlpha = color.a;
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;
}
}
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;
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;
return;
final int[] pixels = buffer.pixels;
+ final float[] depth = buffer.depth;
final int backgroundAlpha = 255 - alpha;
final int redWithAlpha = color.r * alpha;
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;
+ }
}
}
}
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;
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;
return;
final int[] pixels = buffer.pixels;
+ final float[] depth = buffer.depth;
final int backgroundAlpha = 255 - alpha;
final int redWithAlpha = color.r * alpha;
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;
+ }
}
}
}
*/
@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;
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)) {
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;
}
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);
}
}
}