2 * Aukio 3D engine. Author: Svjatoslav Agejenko.
3 * This project is released under Creative Commons Zero (CC0) license.
5 package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
7 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
8 import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
9 import eu.svjatoslav.aukio.e3d.math.Vertex;
10 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
11 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture;
12 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap;
15 import java.util.List;
17 import static eu.svjatoslav.aukio.e3d.geometry.Polygon.pointWithinPolygon;
20 * A textured triangle renderer with perspective-correct texture mapping.
22 * <p>Quake-style subdivided perspective correction: (u/z, v/z, 1/z) are
23 * interpolated linearly in screen space and the exact texture coordinate
24 * is recovered with one reciprocal per subdivision interval, with affine
25 * stepping in between. This makes screen-size tessellation unnecessary —
26 * triangles of any size render with correct perspective.</p>
29 * @see Vertex#textureCoordinate
31 public class TexturedTriangle extends AbstractCoordinateShape {
33 private static final ThreadLocal<PolygonBorderInterpolator[]> INTERPOLATORS =
34 ThreadLocal.withInitial(() -> new PolygonBorderInterpolator[]{
35 new PolygonBorderInterpolator(), new PolygonBorderInterpolator(), new PolygonBorderInterpolator()
38 private static final ThreadLocal<PerspectiveBorderInterpolator[]> PERSPECTIVE_INTERPOLATORS =
39 ThreadLocal.withInitial(() -> new PerspectiveBorderInterpolator[]{
40 new PerspectiveBorderInterpolator(), new PerspectiveBorderInterpolator(),
41 new PerspectiveBorderInterpolator()
45 * Quake-style perspective correction interval: the exact texture
46 * coordinate (one reciprocal) is computed every this many pixels;
47 * between correction points the scanline steps affinely.
49 private static final int PERSPECTIVE_CORRECTION_INTERVAL = 16;
52 * Minimum camera-space z for the perspective path. Triangles with any
53 * vertex closer than this fall back to the affine path (they straddle
54 * the near plane, where 1/z interpolation is invalid).
56 private static final double PERSPECTIVE_MIN_Z = 0.001;
58 /** A/B tuning knobs for the SDF path, see paintSdf. */
59 private static final double SDF_GAMMA =
60 Double.parseDouble(System.getProperty("e3d.sdf.gamma", "0"));
61 private static final double SDF_SHARPEN =
62 Double.parseDouble(System.getProperty("e3d.sdf.sharpen", "2"));
63 /** Debug: print SDF path decisions when they change. */
64 private static final boolean SDF_DEBUG = Boolean.getBoolean("e3d.sdf.debug");
65 private static boolean sdfDebugLastPerspective;
66 private static double sdfDebugLastFootY = -1;
69 * When true (default), textured triangles render with Quake-style
70 * perspective-correct texture mapping.
71 * Volatile: consulted from parallel paint workers.
73 private static volatile boolean perspectiveCorrectionEnabled = true;
76 * Enables or disables perspective-correct texture mapping.
77 * When disabled, rendering falls back to plain affine mapping, which
78 * visibly warps textures on large on-screen triangles at steep angles.
80 * @param enabled {@code true} for perspective-correct mapping
82 public static void setPerspectiveCorrectionEnabled(final boolean enabled) {
83 perspectiveCorrectionEnabled = enabled;
87 * Returns whether perspective-correct texture mapping is enabled.
89 * @return {@code true} when perspective correction is active
91 public static boolean isPerspectiveCorrectionEnabled() {
92 return perspectiveCorrectionEnabled;
96 * The texture to apply to this triangle.
97 * Volatile: the global illumination system swaps the premultiplied
98 * lightmap composite on GI threads while paint workers read it.
99 * Read once per paint call so a triangle never shows a half-updated
102 private volatile Texture texture;
105 * Returns the current texture.
107 * @return the texture
109 public Texture getTexture() {
114 * Atomically swaps the texture. Painters pick up the new texture at the
115 * next paint call; a triangle in flight finishes with the old one.
117 * @param texture the new texture
119 public void setTexture(final Texture texture) {
120 this.texture = texture;
123 private boolean backfaceCulling = Boolean.getBoolean("e3d.backface");
125 // --- ad-hoc frame profiling (-De3d.prof=true; dead code when off)
126 /** Master switch, constant-folded when false. */
127 private static final boolean PROF =
128 Boolean.getBoolean("e3d.prof");
129 /** paintTriangle invocations. */
130 public static final java.util.concurrent.atomic.AtomicLong
131 PROF_TRIS = new java.util.concurrent.atomic.AtomicLong();
132 /** Triangles facing away (engine winding convention). */
133 public static final java.util.concurrent.atomic.AtomicLong
134 PROF_BACKFACE = new java.util.concurrent.atomic.AtomicLong();
135 /** Triangles discarded by vertical render-bounds clamp. */
136 public static final java.util.concurrent.atomic.AtomicLong
137 PROF_OFFY = new java.util.concurrent.atomic.AtomicLong();
138 /** Triangles with screen bounding box below 2x2 pixels. */
139 public static final java.util.concurrent.atomic.AtomicLong
140 PROF_TINY = new java.util.concurrent.atomic.AtomicLong();
141 /** Scanline spans drawn. */
142 public static final java.util.concurrent.atomic.AtomicLong
143 PROF_SPANS = new java.util.concurrent.atomic.AtomicLong();
144 /** Pixel loop iterations across all spans. */
145 public static final java.util.concurrent.atomic.AtomicLong
146 PROF_PIXELS = new java.util.concurrent.atomic.AtomicLong();
148 /** Resets all profiling counters. */
149 public static void profReset() {
151 PROF_BACKFACE.set(0);
159 * Total UV distance between all texture coordinate pairs.
160 * Computed at construction time to determine appropriate mipmap level.
162 private double totalTextureDistance;
165 * Creates a textured triangle with the specified vertices and texture.
167 * @param p1 the first vertex (must have textureCoordinate set)
168 * @param p2 the second vertex (must have textureCoordinate set)
169 * @param p3 the third vertex (must have textureCoordinate set)
170 * @param texture the texture to apply
172 public TexturedTriangle(Vertex p1, Vertex p2, Vertex p3, final Texture texture) {
175 this.texture = texture;
176 computeTotalTextureDistance();
180 * Constructor for flat-array-backed subclasses ({@code MeshTriangle})
181 * that carry no {@link Vertex} objects and paint exclusively through
182 * {@link #paintFlat}, which computes the mipmap metric inline.
183 * {@code totalTextureDistance} is set to a neutral 1 — never read on
186 * @param texture the texture the subclass paints with
188 protected TexturedTriangle(final Texture texture) {
190 this.texture = texture;
191 this.totalTextureDistance = 1;
195 * Computes the total UV distance between all texture coordinate pairs.
196 * Used to determine appropriate mipmap level.
198 private void computeTotalTextureDistance() {
199 totalTextureDistance = vertices.get(0).textureCoordinate.getDistanceTo(vertices.get(1).textureCoordinate);
200 totalTextureDistance += vertices.get(0).textureCoordinate.getDistanceTo(vertices.get(2).textureCoordinate);
201 totalTextureDistance += vertices.get(1).textureCoordinate.getDistanceTo(vertices.get(2).textureCoordinate);
205 * Recomputes the mipmap-selection metric after texture coordinates are
206 * replaced post-construction (used by generated-UV subclasses like
207 * lightmapped triangles).
209 protected final void refreshTextureDistance() {
210 computeTotalTextureDistance();
214 * Z-buffer span writer: the biased 1/z endpoint values ride the
215 * interpolators' zw channel, and every pixel is depth-tested BEFORE
216 * the texture fetch — rejected pixels cost one float compare instead
217 * of a texel read. Opaque texels (alpha 255) write depth; blended
218 * texels write color only, so translucency never occludes.
219 * {@code renderBuffer.depth} is always allocated (the z-buffer path
220 * is the only renderer); requires {@code setPointsZW} called on both
223 private void drawHorizontalLinePerspectiveZ(
224 final PerspectiveBorderInterpolator line1,
225 final PerspectiveBorderInterpolator line2,
227 final RenderingContext renderBuffer,
228 final TextureBitmap textureBitmap) {
230 line1.setCurrentY(y);
231 line2.setCurrentY(y);
233 int x1 = line1.getX();
234 int x2 = line2.getX();
236 final double su1, sv1, sw1, zw1;
237 final double su2, sv2, sw2, zw2;
262 final double realWidth = x2 - x1;
263 final double realX1 = x1;
265 if (x1 < renderBuffer.renderMinX)
266 x1 = renderBuffer.renderMinX;
267 if (x2 >= renderBuffer.renderMaxX)
268 x2 = renderBuffer.renderMaxX;
270 final int span = x2 - x1;
275 PROF_SPANS.incrementAndGet();
276 PROF_PIXELS.addAndGet(span);
279 int renderBufferOffset = (y * renderBuffer.width) + x1;
281 final double dsu = (su2 - su1) / realWidth;
282 final double dsv = (sv2 - sv1) / realWidth;
283 final double dsw = (sw2 - sw1) / realWidth;
284 final double dzw = (zw2 - zw1) / realWidth;
286 // Depth margin (polygon offset): fragments within dzMargin world
287 // units of the stored depth resolve coherently instead of
288 // z-fighting per pixel — near-coplanar surface pairs (kit-bashed
289 // wall pieces, draped decals, LOD shells). The queue is
290 // back-to-front (painter, Z descending), so WITHIN the window
291 // the LATER (nearer) writer must win: the test therefore rejects
292 // only fragments that are BEHIND the stored depth by more than
293 // the margin. (The previous "+margin" form made the FIRST —
294 // i.e. FARTHER — writer win the window, so dirt within margin
295 // below the road beat the pavement; combined with a per-span
296 // margin constant that inflates by (z_pixel/z_near)^2 down
297 // grazing spans, ground leaked through the road at near-horizon
298 // pitches. Fixed camera, view-dependent holes = impossible for
299 // a correct z-buffer.)
300 // The w-space margin is dz*w^2 evaluated PER PIXEL at the
301 // fragment's own depth.
303 double su = su1 + dsu * (x1 - realX1);
304 double sv = sv1 + dsv * (x1 - realX1);
305 double sw = sw1 + dsw * (x1 - realX1);
306 double zw = zw1 + dzw * (x1 - realX1);
308 final int[] texPixels = textureBitmap.pixels;
309 final int texW = textureBitmap.width;
310 final int texH = textureBitmap.height;
311 final int texWMinus1 = texW - 1;
312 final int texHMinus1 = texH - 1;
313 final int[] renderBufferPixels = renderBuffer.pixels;
314 final float[] depth = renderBuffer.depth;
315 // Alpha pass (depthPass 2): depth-test but never depth-write,
316 // so cutout foliage cannot occlude later fragments
317 final boolean writeDepth = renderBuffer.depthPass != 2;
318 // null texture (unit tests) = clamp
319 final boolean wrap = texture != null && texture.wrap;
321 // Adaptive-subdivision perspective ladder (Quake-style stepping)
322 final double ue1 = su1 / sw1;
323 final double ue2 = su2 / sw2;
324 final double ve1 = sv1 / sw1;
325 final double ve2 = sv2 / sw2;
326 final double wRatio = Math.max(sw1, sw2) / Math.min(sw1, sw2);
327 final double texelRate = Math.max(Math.abs(ue2 - ue1), Math.abs(ve2 - ve1))
328 / realWidth * wRatio;
329 final double k = Math.abs(dsw) / Math.min(sw1, sw2);
330 final double curvature = texelRate * k;
331 final int interval = curvature < 0.5 / (16 * 16) ? PERSPECTIVE_CORRECTION_INTERVAL
332 : curvature < 0.5 / (8 * 8) ? 8
333 : curvature < 0.5 / (4 * 4) ? 4
334 : curvature < 0.5 / (2 * 2) ? 2 : 1;
335 final double invInterval = 1d / interval;
338 double invW = 1d / sw;
339 double tx = su * invW;
340 double ty = sv * invW;
341 while (done < span) {
342 final int block = Math.min(interval, span - done);
347 final double invWNext = 1d / sw;
348 final double txNext = su * invWNext;
349 final double tyNext = sv * invWNext;
351 final double invBlock = block == interval ? invInterval : 1d / block;
352 final double txStep = (txNext - tx) * invBlock;
353 final double tyStep = (tyNext - ty) * invBlock;
355 for (int i = 0; i < block; i++) {
356 if (zw > depth[renderBufferOffset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) {
361 itx = Math.floorMod(itx, texW);
362 ity = Math.floorMod(ity, texH);
364 if (itx < 0) itx = 0;
365 else if (itx > texWMinus1) itx = texWMinus1;
367 if (ity < 0) ity = 0;
368 else if (ity > texHMinus1) ity = texHMinus1;
371 final int srcPixel = texPixels[ity * texW + itx];
372 final int srcAlpha = (srcPixel >> 24) & 0xff;
374 if (srcAlpha == 255) {
375 renderBufferPixels[renderBufferOffset] = srcPixel;
377 depth[renderBufferOffset] = (float) zw;
378 } else if (srcAlpha != 0) {
379 // Translucent: blend, but do NOT write depth —
380 // translucency must not occlude later fragments
381 final int destPixel = renderBufferPixels[renderBufferOffset];
382 final int destR = (destPixel >> 16) & 0xff;
383 final int destG = (destPixel >> 8) & 0xff;
384 final int destB = destPixel & 0xff;
386 final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8);
387 final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8);
388 final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8);
390 renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
397 renderBufferOffset++;
408 public void paint(final RenderingContext renderBuffer) {
409 // Near-plane clip output takes precedence: a straddling triangle
410 // clips to a triangle or a quad (one corner cut off). The quad is
411 // painted as a 2-triangle fan — the clip of a convex polygon stays
412 // convex, so fan triangulation is exact. Clipped vertices carry
413 // UVs interpolated in 3D at the cut, which is exactly what the
414 // perspective-correct path expects of a point on the edge.
415 final List<Vertex> clipped = clippedVertices(renderBuffer);
416 if (clipped == null) {
417 paintTriangle(renderBuffer, vertices.get(0), vertices.get(1), vertices.get(2));
420 for (int i = 1; i + 1 < clipped.size(); i++) {
421 paintTriangle(renderBuffer, clipped.get(0), clipped.get(i), clipped.get(i + 1));
426 * Renders one textured triangle defined by the given vertices (either
427 * the shape's own three, or a fan triple from the near-plane-clipped
430 * <p>This method performs:</p>
432 * <li>Backface culling check (if enabled)</li>
433 * <li>Mouse interaction detection</li>
434 * <li>Mipmap level selection based on screen coverage</li>
435 * <li>Scanline rasterization with texture sampling</li>
438 * @param renderBuffer the rendering context containing the pixel buffer
439 * @param v1 the first triangle vertex
440 * @param v2 the second triangle vertex
441 * @param v3 the third triangle vertex
443 private void paintTriangle(final RenderingContext renderBuffer,
444 final Vertex v1, final Vertex v2, final Vertex v3) {
446 final Point2D projectedPoint1 = v1.onScreenCoordinate(renderBuffer);
447 final Point2D projectedPoint2 = v2.onScreenCoordinate(renderBuffer);
448 final Point2D projectedPoint3 = v3.onScreenCoordinate(renderBuffer);
450 if (mouseInteractionController != null)
451 if (renderBuffer.getMouseEvent() != null)
452 if (pointWithinPolygon(
453 renderBuffer.getMouseEvent().coordinate, projectedPoint1, projectedPoint2, projectedPoint3)) {
454 final double[] uv = textureCoordinateAt(
455 renderBuffer.getMouseEvent().coordinate,
456 projectedPoint1, projectedPoint2, projectedPoint3,
457 v1, v2, v3, renderBuffer);
458 renderBuffer.setCurrentObjectUnderMouseCursor(
459 mouseInteractionController, uv[0], uv[1]);
462 // Show polygon boundaries (for debugging)
463 if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders)
464 showBorders(renderBuffer);
466 // Keep double precision to eliminate T-junction gaps from truncation errors
467 final double y1 = projectedPoint1.y;
468 final double y2 = projectedPoint2.y;
469 final double y3 = projectedPoint3.y;
471 // Find top-most point (use ceil to include all pixels triangle touches)
472 int yTop = (int) Math.ceil(Math.min(y1, Math.min(y2, y3)));
473 if (yTop < 0) yTop = 0;
475 // Find bottom-most point (use floor to include all pixels triangle touches)
476 int yBottom = (int) Math.floor(Math.max(y1, Math.max(y2, y3)));
477 if (yBottom >= renderBuffer.height) yBottom = renderBuffer.height - 1;
479 // Clamp to render Y bounds (use renderMaxY - 1 because loop is inclusive)
480 yTop = Math.max(yTop, renderBuffer.renderMinY);
481 yBottom = Math.min(yBottom, renderBuffer.renderMaxY - 1);
482 if (yTop > yBottom) {
484 PROF_OFFY.incrementAndGet();
488 // Snapshot the texture reference for the whole paint: the GI system
489 // may swap the composite lightmap on another thread mid-frame.
490 final Texture texture = this.texture;
492 final double edge12 = projectedPoint1.getDistanceTo(projectedPoint2);
493 final double edge13 = projectedPoint1.getDistanceTo(projectedPoint3);
494 final double edge23 = projectedPoint2.getDistanceTo(projectedPoint3);
495 final double totalVisibleDistance = edge12 + edge13 + edge23;
497 final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
499 // SDF text/vector-art path: coverage comes from the distance
500 // field, not from stored coverage, so the mipmap chain (which
501 // trades sharpness for alias-freedom) is bypassed entirely.
502 if (texture.isSdf()) {
503 paintSdf(yTop, yBottom, renderBuffer,
504 projectedPoint1, projectedPoint2, projectedPoint3, scaleFactor,
509 paintFlat(renderBuffer, texture, backfaceCulling,
510 projectedPoint1, projectedPoint2, projectedPoint3,
511 v1.textureCoordinate, v2.textureCoordinate, v3.textureCoordinate,
512 v1.transformedCoordinate(renderBuffer).z,
513 v2.transformedCoordinate(renderBuffer).z,
514 v3.transformedCoordinate(renderBuffer).z,
515 totalTextureDistance);
519 * Shared rasterization core for one textured triangle whose vertices
520 * are already in screen space. Called both by the object-backed path
521 * ({@link #paintTriangle}) and by {@code TriangleMeshBlock} handles,
522 * whose vertex data lives in flat per-block arrays instead of
523 * {@link Vertex} objects — the math here is identical either way,
524 * keeping both paths bit-exact.
526 * <p>Mouse interaction and debug borders stay in the object path
527 * (mesh blocks do not support picking). SDF textures are rejected:
528 * mesh blocks never carry them (enforced at block build time).</p>
530 * @param renderBuffer the rendering context containing the pixel buffer
531 * @param texture the texture to sample
532 * @param backfaceCulling whether to cull counter-clockwise triangles
533 * @param projectedPoint1 screen-space vertex 1
534 * @param projectedPoint2 screen-space vertex 2
535 * @param projectedPoint3 screen-space vertex 3
536 * @param texturePoint1 UV (primary-texture pixels) of vertex 1
537 * @param texturePoint2 UV of vertex 2
538 * @param texturePoint3 UV of vertex 3
539 * @param z1 camera-space depth of vertex 1
540 * @param z2 camera-space depth of vertex 2
541 * @param z3 camera-space depth of vertex 3
542 * @param totalTextureDistance UV perimeter for mipmap selection. For a
543 * near-plane-clipped fan this is the
544 * ORIGINAL triangle's perimeter (the clip
545 * does not change the texture's texel
546 * density), matching the object path.
548 void paintFlat(final RenderingContext renderBuffer,
549 final Texture texture,
550 final boolean backfaceCulling,
551 final Point2D projectedPoint1,
552 final Point2D projectedPoint2,
553 final Point2D projectedPoint3,
554 final Point2D texturePoint1,
555 final Point2D texturePoint2,
556 final Point2D texturePoint3,
557 final double z1, final double z2, final double z3,
558 final double totalTextureDistance) {
560 // Z-buffer two-pass classification: opaque-class triangles
561 // paint in pass 1 (depth test + write), alpha-class in pass 2
562 // (depth test, no write) — see RenderAggregator.paintSorted.
563 final boolean alphaClass = texture.isSdf() || texture.hasAlpha;
564 if ((renderBuffer.depthPass == 1) == alphaClass)
568 PROF_TRIS.incrementAndGet();
569 if ((projectedPoint2.x - projectedPoint1.x)
570 * (projectedPoint3.y - projectedPoint1.y)
571 - (projectedPoint3.x - projectedPoint1.x)
572 * (projectedPoint2.y - projectedPoint1.y) >= 0)
573 PROF_BACKFACE.incrementAndGet();
574 final double bw = Math.max(projectedPoint1.x, Math.max(
575 projectedPoint2.x, projectedPoint3.x))
576 - Math.min(projectedPoint1.x, Math.min(
577 projectedPoint2.x, projectedPoint3.x));
578 final double bh = Math.max(projectedPoint1.y, Math.max(
579 projectedPoint2.y, projectedPoint3.y))
580 - Math.min(projectedPoint1.y, Math.min(
581 projectedPoint2.y, projectedPoint3.y));
583 PROF_TINY.incrementAndGet();
586 if (backfaceCulling) {
587 final double signedArea = (projectedPoint2.x - projectedPoint1.x)
588 * (projectedPoint3.y - projectedPoint1.y)
589 - (projectedPoint3.x - projectedPoint1.x)
590 * (projectedPoint2.y - projectedPoint1.y);
595 // Keep double precision to eliminate T-junction gaps from truncation errors
596 final double y1 = projectedPoint1.y;
597 final double y2 = projectedPoint2.y;
598 final double y3 = projectedPoint3.y;
600 // Find top-most point (use ceil to include all pixels triangle touches)
601 int yTop = (int) Math.ceil(Math.min(y1, Math.min(y2, y3)));
602 if (yTop < 0) yTop = 0;
604 // Find bottom-most point (use floor to include all pixels triangle touches)
605 int yBottom = (int) Math.floor(Math.max(y1, Math.max(y2, y3)));
606 if (yBottom >= renderBuffer.height) yBottom = renderBuffer.height - 1;
608 // Clamp to render Y bounds (use renderMaxY - 1 because loop is inclusive)
609 yTop = Math.max(yTop, renderBuffer.renderMinY);
610 yBottom = Math.min(yBottom, renderBuffer.renderMaxY - 1);
611 if (yTop > yBottom) {
613 PROF_OFFY.incrementAndGet();
618 throw new IllegalStateException(
619 "SDF textures are not supported in mesh blocks");
621 final double edge12 = projectedPoint1.getDistanceTo(projectedPoint2);
622 final double edge13 = projectedPoint1.getDistanceTo(projectedPoint3);
623 final double edge23 = projectedPoint2.getDistanceTo(projectedPoint3);
624 final double totalVisibleDistance = edge12 + edge13 + edge23;
626 final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
628 final TextureBitmap mipmap = texture.getMipmapForScale(scaleFactor);
630 if (perspectiveCorrectionEnabled) {
631 if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z && z3 > PERSPECTIVE_MIN_Z) {
632 // Affine mapping is within half a texel of exact
633 // perspective for small or nearly-flat triangles, making
634 // the perspective setup pointless for them: the midpoint
635 // error of affine vs exact is ~= texelSpan*(zRatio-1)/4
636 // where texelSpan is the texture range (in selected-mip
637 // texels) the triangle covers — NOT its pixel size (a
638 // triangle can map many texels into few pixels; measured
639 // 2026-09-06: a 4px span with a 56-texel range deviated 3
640 // texels under the pixel-size rule). Distant clusters of
641 // small triangles render affine.
642 // Verified by TexturedTrianglePerspectiveTest#affineWithinHalfTexelBound.
643 final double mf0 = mipmap.multiplicationFactor;
644 final double tu1 = texturePoint1.x * mf0;
645 final double tv1 = texturePoint1.y * mf0;
646 final double tu2 = texturePoint2.x * mf0;
647 final double tv2 = texturePoint2.y * mf0;
648 final double tu3 = texturePoint3.x * mf0;
649 final double tv3 = texturePoint3.y * mf0;
650 final double texelSpan = Math.max(
651 Math.max(Math.abs(tu2 - tu1), Math.abs(tv2 - tv1)),
653 Math.max(Math.abs(tu3 - tu1), Math.abs(tv3 - tv1)),
654 Math.max(Math.abs(tu3 - tu2), Math.abs(tv3 - tv2))));
655 final double zMin = Math.min(z1, Math.min(z2, z3));
656 final double zMax = Math.max(z1, Math.max(z2, z3));
657 if (texelSpan * (zMax / zMin - 1d) < 2d) {
658 paintAffine(yTop, yBottom, mipmap, renderBuffer,
659 projectedPoint1, projectedPoint2, projectedPoint3,
660 texturePoint1, texturePoint2, texturePoint3,
665 // Quake-style perspective-correct mapping: interpolate
666 // (u/z, v/z, 1/z), which are linear in screen space, and
667 // recover exact (u, v) every PERSPECTIVE_CORRECTION_INTERVAL
668 // pixels in the scanline. The mipmap multiplication factor
669 // is folded into the gradients here, so the scanline works
670 // directly in texture pixel units.
671 final double mf = mipmap.multiplicationFactor;
673 final double sw1 = 1d / z1;
674 final double sw2 = 1d / z2;
675 final double sw3 = 1d / z3;
677 final double su1 = texturePoint1.x * mf * sw1;
678 final double sv1 = texturePoint1.y * mf * sw1;
679 final double su2 = texturePoint2.x * mf * sw2;
680 final double sv2 = texturePoint2.y * mf * sw2;
681 final double su3 = texturePoint3.x * mf * sw3;
682 final double sv3 = texturePoint3.y * mf * sw3;
684 final PerspectiveBorderInterpolator[] pi = PERSPECTIVE_INTERPOLATORS.get();
685 pi[0].setPoints(projectedPoint1, projectedPoint2, su1, sv1, sw1, su2, sv2, sw2);
686 pi[1].setPoints(projectedPoint1, projectedPoint3, su1, sv1, sw1, su3, sv3, sw3);
687 pi[2].setPoints(projectedPoint2, projectedPoint3, su2, sv2, sw2, su3, sv3, sw3);
690 // 1/z rides the same edge interpolation; spans
691 // depth-test before texturing.
692 final double zw1 = 1d / z1;
693 final double zw2 = 1d / z2;
694 final double zw3 = 1d / z3;
695 pi[0].setPointsZW(zw1, zw2);
696 pi[1].setPointsZW(zw1, zw3);
697 pi[2].setPointsZW(zw2, zw3);
698 for (int y = yTop; y <= yBottom; y++) {
699 if (pi[0].containsY(y)) {
700 if (pi[1].containsY(y))
701 drawHorizontalLinePerspectiveZ(pi[0], pi[1], y, renderBuffer, mipmap);
702 else if (pi[2].containsY(y))
703 drawHorizontalLinePerspectiveZ(pi[0], pi[2], y, renderBuffer, mipmap);
704 } else if (pi[1].containsY(y)) {
705 if (pi[2].containsY(y))
706 drawHorizontalLinePerspectiveZ(pi[1], pi[2], y, renderBuffer, mipmap);
714 paintAffine(yTop, yBottom, mipmap, renderBuffer,
715 projectedPoint1, projectedPoint2, projectedPoint3,
716 texturePoint1, texturePoint2, texturePoint3,
721 * Computes the perspective-correct texture coordinate at a screen-space
722 * point known to lie inside the triangle.
724 * <p>Screen-space barycentric weights are divided by the camera-space z
725 * of each vertex and renormalized — the same (u/z, v/z, 1/z) math the
726 * perspective-correct scanline path uses — so the returned coordinate
727 * matches the texel that was actually painted at that pixel, even at
728 * steep viewing angles. Texture coordinates are in primary-texture
729 * pixels (no mipmap factor applied).</p>
731 * @return double[2] with {u, v} in primary-texture pixels
733 private static double[] textureCoordinateAt(final Point2D point,
734 final Point2D p1, final Point2D p2, final Point2D p3,
735 final Vertex v1, final Vertex v2, final Vertex v3,
736 final RenderingContext renderBuffer) {
737 final double denom = (p2.y - p3.y) * (p1.x - p3.x)
738 + (p3.x - p2.x) * (p1.y - p3.y);
739 if (Math.abs(denom) < 1e-9)
740 // degenerate on screen; the hit pixel is effectively a vertex
741 return new double[]{v1.textureCoordinate.x, v1.textureCoordinate.y};
743 double w1 = ((p2.y - p3.y) * (point.x - p3.x)
744 + (p3.x - p2.x) * (point.y - p3.y)) / denom;
745 double w2 = ((p3.y - p1.y) * (point.x - p3.x)
746 + (p1.x - p3.x) * (point.y - p3.y)) / denom;
747 double w3 = 1d - w1 - w2;
749 final double z1 = v1.transformedCoordinate(renderBuffer).z;
750 final double z2 = v2.transformedCoordinate(renderBuffer).z;
751 final double z3 = v3.transformedCoordinate(renderBuffer).z;
753 if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z
754 && z3 > PERSPECTIVE_MIN_Z) {
758 final double sum = w1 + w2 + w3;
763 // near-plane straddlers: plain screen-space barycentric (affine),
764 // matching the affine fallback path used for painting them
767 w1 * v1.textureCoordinate.x + w2 * v2.textureCoordinate.x
768 + w3 * v3.textureCoordinate.x,
769 w1 * v1.textureCoordinate.y + w2 * v2.textureCoordinate.y
770 + w3 * v3.textureCoordinate.y};
774 * SDF (signed distance field) rendering path. Coverage is not stored
775 * in the texture; it is re-derived per pixel from a smooth distance
776 * mask, so edges stay sharp at any magnification and fade to clean
777 * gray under minification. Layers: {@code texture.primaryBitmap} is
778 * the background color layer, {@code texture.sdfForeground} the ink
779 * color layer (both sampled nearest — they are flat per region),
780 * {@code texture.sdfMask} the distance field (sampled bilinear).
782 * <p>Minification is handled analytically: the coverage window is
783 * widened by the screen-space pixel footprint, which gives correct
784 * area coverage without a mipmap chain.</p>
786 * @param scaleFactor the same screen-pixels-per-texel estimate the
787 * mipmap selection uses (times 1.2)
789 private void paintSdf(final int yTop, final int yBottom,
790 final RenderingContext renderBuffer,
791 final Point2D projectedPoint1, final Point2D projectedPoint2,
792 final Point2D projectedPoint3, final double scaleFactor,
793 final Vertex v1, final Vertex v2, final Vertex v3) {
794 // SDF (text/decal) is alpha-class — it paints in the
795 // back-to-front alpha pass only, without depth interaction.
796 if (renderBuffer.depthPass == 1)
798 // Per-axis screen-space UV gradients (affine estimate — adequate
799 // for a footprint). Text on an angled plane is minified mostly
800 // along ONE axis; an isotropic average would blur the axis that
801 // still has resolution to spare.
804 final double ex = projectedPoint2.x - projectedPoint1.x;
805 final double ey = projectedPoint2.y - projectedPoint1.y;
806 final double fx3 = projectedPoint3.x - projectedPoint1.x;
807 final double fy3 = projectedPoint3.y - projectedPoint1.y;
808 final double denom = ex * fy3 - fx3 * ey;
809 if (Math.abs(denom) > 1e-9) {
810 final double u1 = v1.textureCoordinate.x;
811 final double vv1 = v1.textureCoordinate.y;
812 final double du21 = v2.textureCoordinate.x - u1;
813 final double dv21 = v2.textureCoordinate.y - vv1;
814 final double du31 = v3.textureCoordinate.x - u1;
815 final double dv31 = v3.textureCoordinate.y - vv1;
816 final double dudx = (du21 * fy3 - du31 * ey) / denom;
817 final double dudy = (du31 * ex - du21 * fx3) / denom;
818 final double dvdx = (dv21 * fy3 - dv31 * ey) / denom;
819 final double dvdy = (dv31 * ex - dv21 * fx3) / denom;
820 footX = Math.hypot(dudx, dvdx);
821 footY = Math.hypot(dudy, dvdy);
823 footX = footY = 1.2d / scaleFactor;
826 // The coverage window follows the SHARPEST axis: one screen pixel
827 // spans texelsPerPixel texels along it, i.e.
828 // texelsPerPixel/(2*spread) of the normalized mask range; aaK
829 // converts a mask sample (0..255, edge at 127.5) into fixed-point
830 // coverage in [0, 256]: cov = (127.5 - d)*aaK + 128.
831 final double maxFootprint = Math.max(footX, footY);
832 double texelsPerPixel = Math.max(Math.min(footX, footY), 0.01d);
833 if (maxFootprint > 1d) {
834 texelsPerPixel /= SDF_SHARPEN;
836 final double aaK = (2d * texture.sdfSpreadTexels) / texelsPerPixel / 255d * 256d;
838 // No mip chain for SDF layers. A distance field's edge gradient
839 // spans just 2 texels, so a half/quarter-res mask visibly melts
840 // glyph edges — and because the two triangles of a rectangle get
841 // slightly different perspective footprints, they crossed mip
842 // thresholds at different distances, producing a hard diagonal
843 // quality split plus sudden blur steps while dollying (observed
844 // 2026-09-06). Sampling the primary field costs some bandwidth
845 // under minification, but text surfaces are small and the
846 // per-pixel sample count is what matters. Quality then degrades
847 // smoothly with distance instead of in steps.
848 final TextureBitmap mask = texture.sdfMask;
849 final TextureBitmap fg = texture.sdfForeground;
850 final TextureBitmap bg = texture.primaryBitmap;
851 final double mf = mask.multiplicationFactor;
853 // Under minification, area-correct coverage reads as low-contrast
854 // gray haze. Two perceptual corrections (A/B-tuned 2026-09-06 on
855 // far+angled text): SDF_SHARPEN narrows the coverage window below
856 // one pixel (kills the haze halo, keeps edges crisp at the cost
857 // of a little shimmer), and a mild coverage gamma < 1 (stem
858 // darkening, the small-ppm font rasterizer trick) keeps thin
860 // Knobs: -De3d.sdf.gamma=1.4 forces a fixed gamma (0 = auto),
861 // -De3d.sdf.sharpen=1 restores the pixel-exact window.
863 final double gamma = SDF_GAMMA != 0 ? SDF_GAMMA
864 : Math.max(0.75d, 1d - 0.08d * (Math.log(maxFootprint) / Math.log(2d)));
865 if (gamma != 1d && maxFootprint > 1d) {
866 covLut = new int[257];
867 for (int i = 0; i <= 256; i++) {
868 covLut[i] = Math.min(256, (int) (256d * Math.pow(i / 256d, gamma)));
874 boolean usePerspective = false;
875 double su1 = 0, sv1 = 0, sw1 = 0;
876 double su2 = 0, sv2 = 0, sw2 = 0;
877 double su3 = 0, sv3 = 0, sw3 = 0;
878 if (perspectiveCorrectionEnabled) {
879 final double z1 = v1.transformedCoordinate(renderBuffer).z;
880 final double z2 = v2.transformedCoordinate(renderBuffer).z;
881 final double z3 = v3.transformedCoordinate(renderBuffer).z;
882 if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z && z3 > PERSPECTIVE_MIN_Z) {
883 // Same affine-sufficiency test as the coverage path
884 // (mask/fg/bg are all primary resolution, mf = 1).
885 final double tu1 = v1.textureCoordinate.x;
886 final double tv1 = v1.textureCoordinate.y;
887 final double tu2 = v2.textureCoordinate.x;
888 final double tv2 = v2.textureCoordinate.y;
889 final double tu3 = v3.textureCoordinate.x;
890 final double tv3 = v3.textureCoordinate.y;
891 final double texelSpan = Math.max(
892 Math.max(Math.abs(tu2 - tu1), Math.abs(tv2 - tv1)),
894 Math.max(Math.abs(tu3 - tu1), Math.abs(tv3 - tv1)),
895 Math.max(Math.abs(tu3 - tu2), Math.abs(tv3 - tv2))));
896 final double zMin = Math.min(z1, Math.min(z2, z3));
897 final double zMax = Math.max(z1, Math.max(z2, z3));
898 usePerspective = texelSpan * (zMax / zMin - 1d) >= 2d;
899 if (usePerspective) {
903 su1 = tu1 * mf * sw1;
904 sv1 = tv1 * mf * sw1;
905 su2 = tu2 * mf * sw2;
906 sv2 = tv2 * mf * sw2;
907 su3 = tu3 * mf * sw3;
908 sv3 = tv3 * mf * sw3;
913 if (SDF_DEBUG && (usePerspective != sdfDebugLastPerspective
914 || Math.abs(footY - sdfDebugLastFootY) > 0.5)) {
915 sdfDebugLastPerspective = usePerspective;
916 sdfDebugLastFootY = footY;
917 System.err.printf("[SDF] perspective=%b footX=%.2f footY=%.2f aaK=%.3f%n",
918 usePerspective, footX, footY, aaK);
921 if (usePerspective) {
922 final PerspectiveBorderInterpolator[] pi = PERSPECTIVE_INTERPOLATORS.get();
923 pi[0].setPoints(projectedPoint1, projectedPoint2, su1, sv1, sw1, su2, sv2, sw2);
924 pi[1].setPoints(projectedPoint1, projectedPoint3, su1, sv1, sw1, su3, sv3, sw3);
925 pi[2].setPoints(projectedPoint2, projectedPoint3, su2, sv2, sw2, su3, sv3, sw3);
927 for (int y = yTop; y <= yBottom; y++) {
928 if (pi[0].containsY(y)) {
929 if (pi[1].containsY(y))
930 drawHorizontalLinePerspectiveSdf(pi[0], pi[1], y, renderBuffer, mask, fg, bg, aaK, covLut);
931 else if (pi[2].containsY(y))
932 drawHorizontalLinePerspectiveSdf(pi[0], pi[2], y, renderBuffer, mask, fg, bg, aaK, covLut);
933 } else if (pi[1].containsY(y)) {
934 if (pi[2].containsY(y))
935 drawHorizontalLinePerspectiveSdf(pi[1], pi[2], y, renderBuffer, mask, fg, bg, aaK, covLut);
941 final PolygonBorderInterpolator[] interpolators = INTERPOLATORS.get();
942 final PolygonBorderInterpolator pbi1 = interpolators[0];
943 final PolygonBorderInterpolator pbi2 = interpolators[1];
944 final PolygonBorderInterpolator pbi3 = interpolators[2];
946 pbi1.setPoints(projectedPoint1, projectedPoint2, v1.textureCoordinate, v2.textureCoordinate);
947 pbi2.setPoints(projectedPoint1, projectedPoint3, v1.textureCoordinate, v3.textureCoordinate);
948 pbi3.setPoints(projectedPoint2, projectedPoint3, v2.textureCoordinate, v3.textureCoordinate);
950 for (int y = yTop; y <= yBottom; y++) {
951 if (pbi1.containsY(y)) {
952 if (pbi2.containsY(y))
953 drawHorizontalLineSdf(pbi1, pbi2, y, renderBuffer, mask, fg, bg, aaK, mf, covLut);
954 else if (pbi3.containsY(y))
955 drawHorizontalLineSdf(pbi1, pbi3, y, renderBuffer, mask, fg, bg, aaK, mf, covLut);
956 } else if (pbi2.containsY(y)) {
957 if (pbi3.containsY(y))
958 drawHorizontalLineSdf(pbi2, pbi3, y, renderBuffer, mask, fg, bg, aaK, mf, covLut);
964 * SDF scanline, affine mapping. Texture coordinates are scaled by the
965 * selected mip's multiplication factor (all layers share one mip
966 * level, so one factor covers mask and both color layers).
968 private void drawHorizontalLineSdf(final PolygonBorderInterpolator line1,
969 final PolygonBorderInterpolator line2, final int y,
970 final RenderingContext renderBuffer,
971 final TextureBitmap mask, final TextureBitmap fg,
972 final TextureBitmap bg, final double aaK,
973 final double mf, final int[] covLut) {
974 line1.setCurrentY(y);
975 line2.setCurrentY(y);
977 int x1 = line1.getX();
978 int x2 = line2.getX();
980 final double tx1, ty1, tx2, ty2;
982 tx1 = line1.getTX() * mf;
983 ty1 = line1.getTY() * mf;
984 tx2 = line2.getTX() * mf;
985 ty2 = line2.getTY() * mf;
990 tx1 = line2.getTX() * mf;
991 ty1 = line2.getTY() * mf;
992 tx2 = line1.getTX() * mf;
993 ty2 = line1.getTY() * mf;
996 final double realWidth = x2 - x1;
997 final double realX1 = x1;
999 if (x1 < renderBuffer.renderMinX)
1000 x1 = renderBuffer.renderMinX;
1001 if (x2 >= renderBuffer.renderMaxX)
1002 x2 = renderBuffer.renderMaxX;
1004 int renderBufferOffset = (y * renderBuffer.width) + x1;
1005 final int[] renderBufferPixels = renderBuffer.pixels;
1007 final double txStep = (tx2 - tx1) / realWidth;
1008 final double tyStep = (ty2 - ty1) / realWidth;
1010 double tx = tx1 + txStep * (x1 - realX1);
1011 double ty = ty1 + tyStep * (x1 - realX1);
1013 final int[] maskPixels = mask.pixels;
1014 final int[] fgPixels = fg.pixels;
1015 final int[] bgPixels = bg.pixels;
1016 final int mw = mask.width;
1017 final int mh = mask.height;
1018 final double bilinearCapX = mw - 1.0001d;
1019 final double bilinearCapY = mh - 1.0001d;
1020 final int mw1 = mw - 1;
1021 final int mh1 = mh - 1;
1023 for (int x = x1; x < x2; x++) {
1024 // Fixed-point bilinear distance fetch (8.8 fractions)
1025 final double ctx = tx < 0 ? 0 : Math.min(tx, bilinearCapX);
1026 final double cty = ty < 0 ? 0 : Math.min(ty, bilinearCapY);
1027 final int x0 = (int) ctx;
1028 final int y0 = (int) cty;
1029 final int fx = (int) ((ctx - x0) * 256);
1030 final int fy = (int) ((cty - y0) * 256);
1031 final int row0 = y0 * mw + x0;
1032 final int row1 = row0 + mw;
1033 final int m00 = (maskPixels[row0] >> 16) & 0xff;
1034 final int m10 = (maskPixels[row0 + 1] >> 16) & 0xff;
1035 final int m01 = (maskPixels[row1] >> 16) & 0xff;
1036 final int m11 = (maskPixels[row1 + 1] >> 16) & 0xff;
1037 final int d = (m00 * (256 - fx) * (256 - fy) + m10 * fx * (256 - fy)
1038 + m01 * (256 - fx) * fy + m11 * fx * fy) >> 16;
1040 int cov = (int) ((127.5d - d) * aaK + 128d);
1041 if (cov < 0) cov = 0;
1042 else if (cov > 256) cov = 256;
1043 if (covLut != null) cov = covLut[cov];
1047 if (itx < 0) itx = 0;
1048 else if (itx > mw1) itx = mw1;
1049 if (ity < 0) ity = 0;
1050 else if (ity > mh1) ity = mh1;
1051 final int addr = ity * mw + itx;
1055 srcPixel = bgPixels[addr];
1056 } else if (cov >= 256) {
1057 srcPixel = fgPixels[addr];
1059 final int bgP = bgPixels[addr];
1060 final int fgP = fgPixels[addr];
1061 final int a = (bgP >>> 24) + ((((int) (fgP >>> 24) - (bgP >>> 24)) * cov) >> 8);
1062 final int r = ((bgP >> 16) & 0xff) + (((((fgP >> 16) & 0xff) - ((bgP >> 16) & 0xff)) * cov) >> 8);
1063 final int g = ((bgP >> 8) & 0xff) + (((((fgP >> 8) & 0xff) - ((bgP >> 8) & 0xff)) * cov) >> 8);
1064 final int b = (bgP & 0xff) + ((((fgP & 0xff) - (bgP & 0xff)) * cov) >> 8);
1065 srcPixel = (a << 24) | (r << 16) | (g << 8) | b;
1068 final int srcAlpha = (srcPixel >> 24) & 0xff;
1069 if (srcAlpha == 255) {
1070 renderBufferPixels[renderBufferOffset] = srcPixel;
1071 } else if (srcAlpha != 0) {
1072 final int destPixel = renderBufferPixels[renderBufferOffset];
1073 final int destR = (destPixel >> 16) & 0xff;
1074 final int destG = (destPixel >> 8) & 0xff;
1075 final int destB = destPixel & 0xff;
1076 final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8);
1077 final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8);
1078 final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8);
1079 renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
1084 renderBufferOffset++;
1089 * SDF scanline with Quake-style subdivided perspective correction —
1090 * same stepping structure as {@link #drawHorizontalLinePerspectiveZ},
1091 * with the coverage fetch replaced by the distance-field evaluation.
1093 private void drawHorizontalLinePerspectiveSdf(
1094 final PerspectiveBorderInterpolator line1,
1095 final PerspectiveBorderInterpolator line2,
1097 final RenderingContext renderBuffer,
1098 final TextureBitmap mask, final TextureBitmap fg,
1099 final TextureBitmap bg, final double aaK, final int[] covLut) {
1101 line1.setCurrentY(y);
1102 line2.setCurrentY(y);
1104 int x1 = line1.getX();
1105 int x2 = line2.getX();
1107 final double su1, sv1, sw1;
1108 final double su2, sv2, sw2;
1111 su1 = line1.getSU();
1112 sv1 = line1.getSV();
1113 sw1 = line1.getSW();
1114 su2 = line2.getSU();
1115 sv2 = line2.getSV();
1116 sw2 = line2.getSW();
1121 su1 = line2.getSU();
1122 sv1 = line2.getSV();
1123 sw1 = line2.getSW();
1124 su2 = line1.getSU();
1125 sv2 = line1.getSV();
1126 sw2 = line1.getSW();
1129 final double realWidth = x2 - x1;
1130 final double realX1 = x1;
1132 if (x1 < renderBuffer.renderMinX)
1133 x1 = renderBuffer.renderMinX;
1134 if (x2 >= renderBuffer.renderMaxX)
1135 x2 = renderBuffer.renderMaxX;
1137 final int span = x2 - x1;
1141 int renderBufferOffset = (y * renderBuffer.width) + x1;
1143 final double dsu = (su2 - su1) / realWidth;
1144 final double dsv = (sv2 - sv1) / realWidth;
1145 final double dsw = (sw2 - sw1) / realWidth;
1147 double su = su1 + dsu * (x1 - realX1);
1148 double sv = sv1 + dsv * (x1 - realX1);
1149 double sw = sw1 + dsw * (x1 - realX1);
1151 final int[] renderBufferPixels = renderBuffer.pixels;
1153 final int[] maskPixels = mask.pixels;
1154 final int[] fgPixels = fg.pixels;
1155 final int[] bgPixels = bg.pixels;
1156 final int mw = mask.width;
1157 final int mh = mask.height;
1158 final double bilinearCapX = mw - 1.0001d;
1159 final double bilinearCapY = mh - 1.0001d;
1160 final int mw1 = mw - 1;
1161 final int mh1 = mh - 1;
1163 // Same adaptive-interval ladder as the coverage path.
1164 final double ue1 = su1 / sw1;
1165 final double ue2 = su2 / sw2;
1166 final double ve1 = sv1 / sw1;
1167 final double ve2 = sv2 / sw2;
1168 final double wRatio = Math.max(sw1, sw2) / Math.min(sw1, sw2);
1169 final double texelRate = Math.max(Math.abs(ue2 - ue1), Math.abs(ve2 - ve1))
1170 / realWidth * wRatio;
1171 final double k = Math.abs(dsw) / Math.min(sw1, sw2);
1172 final double curvature = texelRate * k;
1173 final int interval = curvature < 0.5 / (16 * 16) ? PERSPECTIVE_CORRECTION_INTERVAL
1174 : curvature < 0.5 / (8 * 8) ? 8
1175 : curvature < 0.5 / (4 * 4) ? 4
1176 : curvature < 0.5 / (2 * 2) ? 2 : 1;
1177 final double invInterval = 1d / interval;
1180 double invW = 1d / sw;
1181 double tx = su * invW;
1182 double ty = sv * invW;
1183 while (done < span) {
1184 final int block = Math.min(interval, span - done);
1189 final double invWNext = 1d / sw;
1190 final double txNext = su * invWNext;
1191 final double tyNext = sv * invWNext;
1193 final double invBlock = block == interval ? invInterval : 1d / block;
1194 final double txStep = (txNext - tx) * invBlock;
1195 final double tyStep = (tyNext - ty) * invBlock;
1197 for (int i = 0; i < block; i++) {
1198 // Fixed-point bilinear distance fetch (8.8 fractions)
1199 final double ctx = tx < 0 ? 0 : Math.min(tx, bilinearCapX);
1200 final double cty = ty < 0 ? 0 : Math.min(ty, bilinearCapY);
1201 final int x0 = (int) ctx;
1202 final int y0 = (int) cty;
1203 final int fx = (int) ((ctx - x0) * 256);
1204 final int fy = (int) ((cty - y0) * 256);
1205 final int row0 = y0 * mw + x0;
1206 final int row1 = row0 + mw;
1207 final int m00 = (maskPixels[row0] >> 16) & 0xff;
1208 final int m10 = (maskPixels[row0 + 1] >> 16) & 0xff;
1209 final int m01 = (maskPixels[row1] >> 16) & 0xff;
1210 final int m11 = (maskPixels[row1 + 1] >> 16) & 0xff;
1211 final int d = (m00 * (256 - fx) * (256 - fy) + m10 * fx * (256 - fy)
1212 + m01 * (256 - fx) * fy + m11 * fx * fy) >> 16;
1214 int cov = (int) ((127.5d - d) * aaK + 128d);
1215 if (cov < 0) cov = 0;
1216 else if (cov > 256) cov = 256;
1217 if (covLut != null) cov = covLut[cov];
1221 if (itx < 0) itx = 0;
1222 else if (itx > mw1) itx = mw1;
1223 if (ity < 0) ity = 0;
1224 else if (ity > mh1) ity = mh1;
1225 final int addr = ity * mw + itx;
1229 srcPixel = bgPixels[addr];
1230 } else if (cov >= 256) {
1231 srcPixel = fgPixels[addr];
1233 final int bgP = bgPixels[addr];
1234 final int fgP = fgPixels[addr];
1235 final int a = (bgP >>> 24) + ((((int) (fgP >>> 24) - (bgP >>> 24)) * cov) >> 8);
1236 final int r = ((bgP >> 16) & 0xff) + (((((fgP >> 16) & 0xff) - ((bgP >> 16) & 0xff)) * cov) >> 8);
1237 final int g = ((bgP >> 8) & 0xff) + (((((fgP >> 8) & 0xff) - ((bgP >> 8) & 0xff)) * cov) >> 8);
1238 final int b = (bgP & 0xff) + ((((fgP & 0xff) - (bgP & 0xff)) * cov) >> 8);
1239 srcPixel = (a << 24) | (r << 16) | (g << 8) | b;
1242 final int srcAlpha = (srcPixel >> 24) & 0xff;
1243 if (srcAlpha == 255) {
1244 renderBufferPixels[renderBufferOffset] = srcPixel;
1245 } else if (srcAlpha != 0) {
1246 final int destPixel = renderBufferPixels[renderBufferOffset];
1247 final int destR = (destPixel >> 16) & 0xff;
1248 final int destG = (destPixel >> 8) & 0xff;
1249 final int destB = destPixel & 0xff;
1250 final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8);
1251 final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8);
1252 final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8);
1253 renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
1258 renderBufferOffset++;
1266 * Affine texture mapping (u, v linear in screen space). Used for
1267 * near-plane straddlers and for triangles small/flat enough that
1268 * affine is within half a texel of exact perspective mapping.
1270 private void paintAffine(final int yTop, final int yBottom,
1271 final TextureBitmap mipmap,
1272 final RenderingContext renderBuffer,
1273 final Point2D projectedPoint1, final Point2D projectedPoint2,
1274 final Point2D projectedPoint3,
1275 final Point2D texturePoint1, final Point2D texturePoint2,
1276 final Point2D texturePoint3,
1277 final double z1, final double z2, final double z3) {
1278 final PolygonBorderInterpolator[] interpolators = INTERPOLATORS.get();
1279 final PolygonBorderInterpolator pbi1 = interpolators[0];
1280 final PolygonBorderInterpolator pbi2 = interpolators[1];
1281 final PolygonBorderInterpolator pbi3 = interpolators[2];
1283 pbi1.setPoints(projectedPoint1, projectedPoint2, texturePoint1, texturePoint2);
1284 pbi2.setPoints(projectedPoint1, projectedPoint3, texturePoint1, texturePoint3);
1285 pbi3.setPoints(projectedPoint2, projectedPoint3, texturePoint2, texturePoint3);
1287 final double zw1 = 1d / z1;
1288 final double zw2 = 1d / z2;
1289 final double zw3 = 1d / z3;
1290 pbi1.setPointsZW(zw1, zw2);
1291 pbi2.setPointsZW(zw1, zw3);
1292 pbi3.setPointsZW(zw2, zw3);
1293 for (int y = yTop; y <= yBottom; y++) {
1294 if (pbi1.containsY(y)) {
1295 if (pbi2.containsY(y))
1296 drawHorizontalLineZ(pbi1, pbi2, y, renderBuffer, mipmap);
1297 else if (pbi3.containsY(y))
1298 drawHorizontalLineZ(pbi1, pbi3, y, renderBuffer, mipmap);
1299 } else if (pbi2.containsY(y)) {
1300 if (pbi3.containsY(y))
1301 drawHorizontalLineZ(pbi2, pbi3, y, renderBuffer, mipmap);
1308 * Z-buffer span writer: per-pixel depth test (biased 1/z, linear
1309 * along the span) BEFORE the texture fetch. Opaque texels write
1310 * depth; blended texels write color only.
1312 private void drawHorizontalLineZ(final PolygonBorderInterpolator line1,
1313 final PolygonBorderInterpolator line2,
1315 final RenderingContext renderBuffer,
1316 final TextureBitmap textureBitmap) {
1318 line1.setCurrentY(y);
1319 line2.setCurrentY(y);
1321 int x1 = line1.getX();
1322 int x2 = line2.getX();
1324 final double tx1, ty1, zw1;
1325 final double tx2, ty2, zw2;
1328 tx1 = line1.getTX() * textureBitmap.multiplicationFactor;
1329 ty1 = line1.getTY() * textureBitmap.multiplicationFactor;
1330 zw1 = line1.getZW();
1331 tx2 = line2.getTX() * textureBitmap.multiplicationFactor;
1332 ty2 = line2.getTY() * textureBitmap.multiplicationFactor;
1333 zw2 = line2.getZW();
1339 tx1 = line2.getTX() * textureBitmap.multiplicationFactor;
1340 ty1 = line2.getTY() * textureBitmap.multiplicationFactor;
1341 zw1 = line2.getZW();
1343 tx2 = line1.getTX() * textureBitmap.multiplicationFactor;
1344 ty2 = line1.getTY() * textureBitmap.multiplicationFactor;
1345 zw2 = line1.getZW();
1348 final double realWidth = x2 - x1;
1349 final double realX1 = x1;
1351 if (x1 < renderBuffer.renderMinX)
1352 x1 = renderBuffer.renderMinX;
1354 // x2 is exclusive: clamp to renderMaxX (see drawHorizontalLine)
1355 if (x2 >= renderBuffer.renderMaxX)
1356 x2 = renderBuffer.renderMaxX;
1359 PROF_SPANS.incrementAndGet();
1360 PROF_PIXELS.addAndGet(Math.max(0, x2 - x1));
1363 int renderBufferOffset = (y * renderBuffer.width) + x1;
1364 final int[] renderBufferPixels = renderBuffer.pixels;
1365 final float[] depth = renderBuffer.depth;
1366 // Alpha pass (depthPass 2): depth-test but never depth-write
1367 final boolean writeDepth = renderBuffer.depthPass != 2;
1369 final double txStep = (tx2 - tx1) / realWidth;
1370 final double tyStep = (ty2 - ty1) / realWidth;
1371 final double dzw = (zw2 - zw1) / realWidth;
1372 double tx = tx1 + txStep * (x1 - realX1);
1373 double ty = ty1 + tyStep * (x1 - realX1);
1374 double zw = zw1 + dzw * (x1 - realX1);
1376 final int[] texPixels = textureBitmap.pixels;
1377 final int texW = textureBitmap.width;
1378 final int texH = textureBitmap.height;
1379 final int texWMinus1 = texW - 1;
1380 final int texHMinus1 = texH - 1;
1381 // texture is null in unit tests: clamp (see drawHorizontalLine)
1382 final boolean wrap = texture != null && texture.wrap;
1384 for (int x = x1; x < x2; x++) {
1386 if (zw > depth[renderBufferOffset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) {
1391 itx = Math.floorMod(itx, texW);
1392 ity = Math.floorMod(ity, texH);
1394 if (itx < 0) itx = 0;
1395 else if (itx > texWMinus1) itx = texWMinus1;
1397 if (ity < 0) ity = 0;
1398 else if (ity > texHMinus1) ity = texHMinus1;
1401 final int srcPixel = texPixels[ity * texW + itx];
1402 final int srcAlpha = (srcPixel >> 24) & 0xff;
1404 if (srcAlpha == 255) {
1405 renderBufferPixels[renderBufferOffset] = srcPixel;
1407 depth[renderBufferOffset] = (float) zw;
1408 } else if (srcAlpha != 0) {
1409 // Translucent: blend without writing depth
1410 final int destPixel = renderBufferPixels[renderBufferOffset];
1411 final int destR = (destPixel >> 16) & 0xff;
1412 final int destG = (destPixel >> 8) & 0xff;
1413 final int destB = destPixel & 0xff;
1415 final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8);
1416 final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8);
1417 final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8);
1419 renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
1426 renderBufferOffset++;
1432 * Checks if backface culling is enabled for this triangle.
1434 * @return {@code true} if backface culling is enabled
1436 public boolean isBackfaceCullingEnabled() {
1437 return backfaceCulling;
1441 * Enables or disables backface culling for this triangle.
1443 * @param backfaceCulling {@code true} to enable backface culling
1445 public void setBackfaceCulling(final boolean backfaceCulling) {
1446 this.backfaceCulling = backfaceCulling;
1450 * Draws the triangle border edges in yellow (for debugging).
1452 * @param renderBuffer the rendering context
1454 private void showBorders(final RenderingContext renderBuffer) {
1456 final Point2D projectedPoint1 = vertices.get(0).onScreenCoordinate(renderBuffer);
1457 final Point2D projectedPoint2 = vertices.get(1).onScreenCoordinate(renderBuffer);
1458 final Point2D projectedPoint3 = vertices.get(2).onScreenCoordinate(renderBuffer);
1460 final int x1 = (int) projectedPoint1.x;
1461 final int y1 = (int) projectedPoint1.y;
1462 final int x2 = (int) projectedPoint2.x;
1463 final int y2 = (int) projectedPoint2.y;
1464 final int x3 = (int) projectedPoint3.x;
1465 final int y3 = (int) projectedPoint3.y;
1467 renderBuffer.executeWithGraphics(g -> {
1468 g.setColor(Color.YELLOW);
1469 g.drawLine(x1, y1, x2, y2);
1470 g.drawLine(x3, y3, x2, y2);
1471 g.drawLine(x1, y1, x3, y3);