648baec4e253c20e840b195f58a3a08b37aca268
[aukio-3d.git] /
1 /*
2  * Aukio 3D engine. Author: Svjatoslav Agejenko.
3  * This project is released under Creative Commons Zero (CC0) license.
4  */
5 package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
6
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;
13
14 import java.awt.*;
15 import java.util.List;
16
17 import static eu.svjatoslav.aukio.e3d.geometry.Polygon.pointWithinPolygon;
18
19 /**
20  * A textured triangle renderer with perspective-correct texture mapping.
21  *
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>
27  *
28  * @see Texture
29  * @see Vertex#textureCoordinate
30  */
31 public class TexturedTriangle extends AbstractCoordinateShape {
32
33     private static final ThreadLocal<PolygonBorderInterpolator[]> INTERPOLATORS =
34             ThreadLocal.withInitial(() -> new PolygonBorderInterpolator[]{
35                     new PolygonBorderInterpolator(), new PolygonBorderInterpolator(), new PolygonBorderInterpolator()
36             });
37
38     private static final ThreadLocal<PerspectiveBorderInterpolator[]> PERSPECTIVE_INTERPOLATORS =
39             ThreadLocal.withInitial(() -> new PerspectiveBorderInterpolator[]{
40                     new PerspectiveBorderInterpolator(), new PerspectiveBorderInterpolator(),
41                     new PerspectiveBorderInterpolator()
42             });
43
44     /**
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.
48      */
49     private static final int PERSPECTIVE_CORRECTION_INTERVAL = 16;
50
51     /**
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).
55      */
56     private static final double PERSPECTIVE_MIN_Z = 0.001;
57
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;
67
68     /**
69      * When true (default), textured triangles render with Quake-style
70      * perspective-correct texture mapping.
71      * Volatile: consulted from parallel paint workers.
72      */
73     private static volatile boolean perspectiveCorrectionEnabled = true;
74
75     /**
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.
79      *
80      * @param enabled {@code true} for perspective-correct mapping
81      */
82     public static void setPerspectiveCorrectionEnabled(final boolean enabled) {
83         perspectiveCorrectionEnabled = enabled;
84     }
85
86     /**
87      * Returns whether perspective-correct texture mapping is enabled.
88      *
89      * @return {@code true} when perspective correction is active
90      */
91     public static boolean isPerspectiveCorrectionEnabled() {
92         return perspectiveCorrectionEnabled;
93     }
94
95     /**
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
100      * texture.
101      */
102     private volatile Texture texture;
103
104     /**
105      * Returns the current texture.
106      *
107      * @return the texture
108      */
109     public Texture getTexture() {
110         return texture;
111     }
112
113     /**
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.
116      *
117      * @param texture the new texture
118      */
119     public void setTexture(final Texture texture) {
120         this.texture = texture;
121     }
122
123     private boolean backfaceCulling = Boolean.getBoolean("e3d.backface");
124
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();
147
148     /** Resets all profiling counters. */
149     public static void profReset() {
150         PROF_TRIS.set(0);
151         PROF_BACKFACE.set(0);
152         PROF_OFFY.set(0);
153         PROF_TINY.set(0);
154         PROF_SPANS.set(0);
155         PROF_PIXELS.set(0);
156     }
157
158     /**
159      * Total UV distance between all texture coordinate pairs.
160      * Computed at construction time to determine appropriate mipmap level.
161      */
162     private double totalTextureDistance;
163
164     /**
165      * Creates a textured triangle with the specified vertices and texture.
166      *
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
171      */
172     public TexturedTriangle(Vertex p1, Vertex p2, Vertex p3, final Texture texture) {
173
174         super(p1, p2, p3);
175         this.texture = texture;
176         computeTotalTextureDistance();
177     }
178
179     /**
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
184      * that path.
185      *
186      * @param texture the texture the subclass paints with
187      */
188     protected TexturedTriangle(final Texture texture) {
189         super(0);
190         this.texture = texture;
191         this.totalTextureDistance = 1;
192     }
193
194     /**
195      * Computes the total UV distance between all texture coordinate pairs.
196      * Used to determine appropriate mipmap level.
197      */
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);
202     }
203
204     /**
205      * Recomputes the mipmap-selection metric after texture coordinates are
206      * replaced post-construction (used by generated-UV subclasses like
207      * lightmapped triangles).
208      */
209     protected final void refreshTextureDistance() {
210         computeTotalTextureDistance();
211     }
212
213     /**
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
221      * interpolators.
222      */
223     private void drawHorizontalLinePerspectiveZ(
224             final PerspectiveBorderInterpolator line1,
225             final PerspectiveBorderInterpolator line2,
226             final int y,
227             final RenderingContext renderBuffer,
228             final TextureBitmap textureBitmap) {
229
230         line1.setCurrentY(y);
231         line2.setCurrentY(y);
232
233         int x1 = line1.getX();
234         int x2 = line2.getX();
235
236         final double su1, sv1, sw1, zw1;
237         final double su2, sv2, sw2, zw2;
238
239         if (x1 <= x2) {
240             su1 = line1.getSU();
241             sv1 = line1.getSV();
242             sw1 = line1.getSW();
243             zw1 = line1.getZW();
244             su2 = line2.getSU();
245             sv2 = line2.getSV();
246             sw2 = line2.getSW();
247             zw2 = line2.getZW();
248         } else {
249             final int tmp = x1;
250             x1 = x2;
251             x2 = tmp;
252             su1 = line2.getSU();
253             sv1 = line2.getSV();
254             sw1 = line2.getSW();
255             zw1 = line2.getZW();
256             su2 = line1.getSU();
257             sv2 = line1.getSV();
258             sw2 = line1.getSW();
259             zw2 = line1.getZW();
260         }
261
262         final double realWidth = x2 - x1;
263         final double realX1 = x1;
264
265         if (x1 < renderBuffer.renderMinX)
266             x1 = renderBuffer.renderMinX;
267         if (x2 >= renderBuffer.renderMaxX)
268             x2 = renderBuffer.renderMaxX;
269
270         final int span = x2 - x1;
271         if (span <= 0)
272             return;
273
274         if (PROF) {
275             PROF_SPANS.incrementAndGet();
276             PROF_PIXELS.addAndGet(span);
277         }
278
279         int renderBufferOffset = (y * renderBuffer.width) + x1;
280
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;
285
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.
302
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);
307
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;
320
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;
336
337         int done = 0;
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);
343
344             su += dsu * block;
345             sv += dsv * block;
346             sw += dsw * block;
347             final double invWNext = 1d / sw;
348             final double txNext = su * invWNext;
349             final double tyNext = sv * invWNext;
350
351             final double invBlock = block == interval ? invInterval : 1d / block;
352             final double txStep = (txNext - tx) * invBlock;
353             final double tyStep = (tyNext - ty) * invBlock;
354
355             for (int i = 0; i < block; i++) {
356                 if (zw > depth[renderBufferOffset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) {
357                     int itx = (int) tx;
358                     int ity = (int) ty;
359
360                     if (wrap) {
361                         itx = Math.floorMod(itx, texW);
362                         ity = Math.floorMod(ity, texH);
363                     } else {
364                         if (itx < 0) itx = 0;
365                         else if (itx > texWMinus1) itx = texWMinus1;
366
367                         if (ity < 0) ity = 0;
368                         else if (ity > texHMinus1) ity = texHMinus1;
369                     }
370
371                     final int srcPixel = texPixels[ity * texW + itx];
372                     final int srcAlpha = (srcPixel >> 24) & 0xff;
373
374                     if (srcAlpha == 255) {
375                         renderBufferPixels[renderBufferOffset] = srcPixel;
376                         if (writeDepth)
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;
385
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);
389
390                         renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
391                     }
392                 }
393
394                 tx += txStep;
395                 ty += tyStep;
396                 zw += dzw;
397                 renderBufferOffset++;
398             }
399
400             tx = txNext;
401             ty = tyNext;
402
403             done += block;
404         }
405     }
406
407     @Override
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));
418             return;
419         }
420         for (int i = 1; i + 1 < clipped.size(); i++) {
421             paintTriangle(renderBuffer, clipped.get(0), clipped.get(i), clipped.get(i + 1));
422         }
423     }
424
425     /**
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
428      * loop).
429      *
430      * <p>This method performs:</p>
431      * <ul>
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>
436      * </ul>
437      *
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
442      */
443     private void paintTriangle(final RenderingContext renderBuffer,
444                                final Vertex v1, final Vertex v2, final Vertex v3) {
445
446         final Point2D projectedPoint1 = v1.onScreenCoordinate(renderBuffer);
447         final Point2D projectedPoint2 = v2.onScreenCoordinate(renderBuffer);
448         final Point2D projectedPoint3 = v3.onScreenCoordinate(renderBuffer);
449
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]);
460                 }
461
462         // Show polygon boundaries (for debugging)
463         if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders)
464             showBorders(renderBuffer);
465
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;
470
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;
474
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;
478
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) {
483             if (PROF)
484                 PROF_OFFY.incrementAndGet();
485             return;
486         }
487
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;
491
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;
496
497         final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
498
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,
505                     v1, v2, v3);
506             return;
507         }
508
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                 totalVisibleDistance,
516                 totalTextureDistance);
517     }
518
519     /**
520      * Shared rasterization core for one textured triangle whose vertices
521      * are already in screen space. Called both by the object-backed path
522      * ({@link #paintTriangle}) and by {@code TriangleMeshBlock} handles,
523      * whose vertex data lives in flat per-block arrays instead of
524      * {@link Vertex} objects — the math here is identical either way,
525      * keeping both paths bit-exact.
526      *
527      * <p>Mouse interaction and debug borders stay in the object path
528      * (mesh blocks do not support picking). SDF textures are rejected:
529      * mesh blocks never carry them (enforced at block build time).</p>
530      *
531      * @param renderBuffer     the rendering context containing the pixel buffer
532      * @param texture          the texture to sample
533      * @param backfaceCulling  whether to cull counter-clockwise triangles
534      * @param projectedPoint1  screen-space vertex 1
535      * @param projectedPoint2  screen-space vertex 2
536      * @param projectedPoint3  screen-space vertex 3
537      * @param texturePoint1    UV (primary-texture pixels) of vertex 1
538      * @param texturePoint2    UV of vertex 2
539      * @param texturePoint3    UV of vertex 3
540      * @param z1               camera-space depth of vertex 1
541      * @param z2               camera-space depth of vertex 2
542      * @param z3               camera-space depth of vertex 3
543      * @param totalVisibleDistance screen-edge perimeter for mipmap
544      *                             selection, computed once per paint call
545      *                             (or once per slot for mesh blocks) —
546      *                             callers share it instead of the core
547      *                             recomputing per tile
548      * @param totalTextureDistance UV perimeter for mipmap selection. For a
549      *                             near-plane-clipped fan this is the
550      *                             ORIGINAL triangle's perimeter (the clip
551      *                             does not change the texture's texel
552      *                             density), matching the object path.
553      */
554     void paintFlat(final RenderingContext renderBuffer,
555                    final Texture texture,
556                    final boolean backfaceCulling,
557                    final Point2D projectedPoint1,
558                    final Point2D projectedPoint2,
559                    final Point2D projectedPoint3,
560                    final Point2D texturePoint1,
561                    final Point2D texturePoint2,
562                    final Point2D texturePoint3,
563                    final double z1, final double z2, final double z3,
564                    final double totalVisibleDistance,
565                    final double totalTextureDistance) {
566
567         // Z-buffer two-pass classification: opaque-class triangles
568         // paint in pass 1 (depth test + write), alpha-class in pass 2
569         // (depth test, no write) — see RenderAggregator.paintSorted.
570         final boolean alphaClass = texture.isSdf() || texture.hasAlpha;
571         if ((renderBuffer.depthPass == 1) == alphaClass)
572             return;
573
574                    if (PROF) {
575             PROF_TRIS.incrementAndGet();
576             if ((projectedPoint2.x - projectedPoint1.x)
577                     * (projectedPoint3.y - projectedPoint1.y)
578                     - (projectedPoint3.x - projectedPoint1.x)
579                     * (projectedPoint2.y - projectedPoint1.y) >= 0)
580                 PROF_BACKFACE.incrementAndGet();
581             final double bw = Math.max(projectedPoint1.x, Math.max(
582                     projectedPoint2.x, projectedPoint3.x))
583                     - Math.min(projectedPoint1.x, Math.min(
584                             projectedPoint2.x, projectedPoint3.x));
585             final double bh = Math.max(projectedPoint1.y, Math.max(
586                     projectedPoint2.y, projectedPoint3.y))
587                     - Math.min(projectedPoint1.y, Math.min(
588                             projectedPoint2.y, projectedPoint3.y));
589             if (bw * bh < 4)
590                 PROF_TINY.incrementAndGet();
591         }
592
593         if (backfaceCulling) {
594             final double signedArea = (projectedPoint2.x - projectedPoint1.x)
595                     * (projectedPoint3.y - projectedPoint1.y)
596                     - (projectedPoint3.x - projectedPoint1.x)
597                     * (projectedPoint2.y - projectedPoint1.y);
598             if (signedArea >= 0)
599                 return;
600         }
601
602         // Keep double precision to eliminate T-junction gaps from truncation errors
603         final double y1 = projectedPoint1.y;
604         final double y2 = projectedPoint2.y;
605         final double y3 = projectedPoint3.y;
606
607         // Find top-most point (use ceil to include all pixels triangle touches)
608         int yTop = (int) Math.ceil(Math.min(y1, Math.min(y2, y3)));
609         if (yTop < 0) yTop = 0;
610
611         // Find bottom-most point (use floor to include all pixels triangle touches)
612         int yBottom = (int) Math.floor(Math.max(y1, Math.max(y2, y3)));
613         if (yBottom >= renderBuffer.height) yBottom = renderBuffer.height - 1;
614
615         // Clamp to render Y bounds (use renderMaxY - 1 because loop is inclusive)
616         yTop = Math.max(yTop, renderBuffer.renderMinY);
617         yBottom = Math.min(yBottom, renderBuffer.renderMaxY - 1);
618         if (yTop > yBottom) {
619             if (PROF)
620                 PROF_OFFY.incrementAndGet();
621             return;
622         }
623
624         if (texture.isSdf())
625             throw new IllegalStateException(
626                     "SDF textures are not supported in mesh blocks");
627
628         final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
629
630         final TextureBitmap mipmap = texture.getMipmapForScale(scaleFactor);
631
632         if (perspectiveCorrectionEnabled) {
633             if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z && z3 > PERSPECTIVE_MIN_Z) {
634                 // Affine mapping is within half a texel of exact
635                 // perspective for small or nearly-flat triangles, making
636                 // the perspective setup pointless for them: the midpoint
637                 // error of affine vs exact is ~= texelSpan*(zRatio-1)/4
638                 // where texelSpan is the texture range (in selected-mip
639                 // texels) the triangle covers — NOT its pixel size (a
640                 // triangle can map many texels into few pixels; measured
641                 // 2026-09-06: a 4px span with a 56-texel range deviated 3
642                 // texels under the pixel-size rule). Distant clusters of
643                 // small triangles render affine.
644                 // Verified by TexturedTrianglePerspectiveTest#affineWithinHalfTexelBound.
645                 final double mf0 = mipmap.multiplicationFactor;
646                 final double tu1 = texturePoint1.x * mf0;
647                 final double tv1 = texturePoint1.y * mf0;
648                 final double tu2 = texturePoint2.x * mf0;
649                 final double tv2 = texturePoint2.y * mf0;
650                 final double tu3 = texturePoint3.x * mf0;
651                 final double tv3 = texturePoint3.y * mf0;
652                 final double texelSpan = Math.max(
653                         Math.max(Math.abs(tu2 - tu1), Math.abs(tv2 - tv1)),
654                         Math.max(
655                                 Math.max(Math.abs(tu3 - tu1), Math.abs(tv3 - tv1)),
656                                 Math.max(Math.abs(tu3 - tu2), Math.abs(tv3 - tv2))));
657                 final double zMin = Math.min(z1, Math.min(z2, z3));
658                 final double zMax = Math.max(z1, Math.max(z2, z3));
659                 if (texelSpan * (zMax / zMin - 1d) < 2d) {
660                     paintAffine(yTop, yBottom, mipmap, renderBuffer,
661                             projectedPoint1, projectedPoint2, projectedPoint3,
662                             texturePoint1, texturePoint2, texturePoint3,
663                             z1, z2, z3);
664                     return;
665                 }
666
667                 // Quake-style perspective-correct mapping: interpolate
668                 // (u/z, v/z, 1/z), which are linear in screen space, and
669                 // recover exact (u, v) every PERSPECTIVE_CORRECTION_INTERVAL
670                 // pixels in the scanline. The mipmap multiplication factor
671                 // is folded into the gradients here, so the scanline works
672                 // directly in texture pixel units.
673                 final double mf = mipmap.multiplicationFactor;
674
675                 final double sw1 = 1d / z1;
676                 final double sw2 = 1d / z2;
677                 final double sw3 = 1d / z3;
678
679                 final double su1 = texturePoint1.x * mf * sw1;
680                 final double sv1 = texturePoint1.y * mf * sw1;
681                 final double su2 = texturePoint2.x * mf * sw2;
682                 final double sv2 = texturePoint2.y * mf * sw2;
683                 final double su3 = texturePoint3.x * mf * sw3;
684                 final double sv3 = texturePoint3.y * mf * sw3;
685
686                 final PerspectiveBorderInterpolator[] pi = PERSPECTIVE_INTERPOLATORS.get();
687                 pi[0].setPoints(projectedPoint1, projectedPoint2, su1, sv1, sw1, su2, sv2, sw2);
688                 pi[1].setPoints(projectedPoint1, projectedPoint3, su1, sv1, sw1, su3, sv3, sw3);
689                 pi[2].setPoints(projectedPoint2, projectedPoint3, su2, sv2, sw2, su3, sv3, sw3);
690
691                 {
692                     // 1/z rides the same edge interpolation; spans
693                     // depth-test before texturing.
694                     final double zw1 = 1d / z1;
695                     final double zw2 = 1d / z2;
696                     final double zw3 = 1d / z3;
697                     pi[0].setPointsZW(zw1, zw2);
698                     pi[1].setPointsZW(zw1, zw3);
699                     pi[2].setPointsZW(zw2, zw3);
700                     for (int y = yTop; y <= yBottom; y++) {
701                         if (pi[0].containsY(y)) {
702                             if (pi[1].containsY(y))
703                                 drawHorizontalLinePerspectiveZ(pi[0], pi[1], y, renderBuffer, mipmap);
704                             else if (pi[2].containsY(y))
705                                 drawHorizontalLinePerspectiveZ(pi[0], pi[2], y, renderBuffer, mipmap);
706                         } else if (pi[1].containsY(y)) {
707                             if (pi[2].containsY(y))
708                                 drawHorizontalLinePerspectiveZ(pi[1], pi[2], y, renderBuffer, mipmap);
709                         }
710                     }
711                     return;
712                 }
713             }
714         }
715
716         paintAffine(yTop, yBottom, mipmap, renderBuffer,
717                 projectedPoint1, projectedPoint2, projectedPoint3,
718                 texturePoint1, texturePoint2, texturePoint3,
719                 z1, z2, z3);
720     }
721
722     /**
723      * Computes the perspective-correct texture coordinate at a screen-space
724      * point known to lie inside the triangle.
725      *
726      * <p>Screen-space barycentric weights are divided by the camera-space z
727      * of each vertex and renormalized — the same (u/z, v/z, 1/z) math the
728      * perspective-correct scanline path uses — so the returned coordinate
729      * matches the texel that was actually painted at that pixel, even at
730      * steep viewing angles. Texture coordinates are in primary-texture
731      * pixels (no mipmap factor applied).</p>
732      *
733      * @return double[2] with {u, v} in primary-texture pixels
734      */
735     private static double[] textureCoordinateAt(final Point2D point,
736                                                 final Point2D p1, final Point2D p2, final Point2D p3,
737                                                 final Vertex v1, final Vertex v2, final Vertex v3,
738                                                 final RenderingContext renderBuffer) {
739         final double denom = (p2.y - p3.y) * (p1.x - p3.x)
740                 + (p3.x - p2.x) * (p1.y - p3.y);
741         if (Math.abs(denom) < 1e-9)
742             // degenerate on screen; the hit pixel is effectively a vertex
743             return new double[]{v1.textureCoordinate.x, v1.textureCoordinate.y};
744
745         double w1 = ((p2.y - p3.y) * (point.x - p3.x)
746                 + (p3.x - p2.x) * (point.y - p3.y)) / denom;
747         double w2 = ((p3.y - p1.y) * (point.x - p3.x)
748                 + (p1.x - p3.x) * (point.y - p3.y)) / denom;
749         double w3 = 1d - w1 - w2;
750
751         final double z1 = v1.transformedCoordinate(renderBuffer).z;
752         final double z2 = v2.transformedCoordinate(renderBuffer).z;
753         final double z3 = v3.transformedCoordinate(renderBuffer).z;
754
755         if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z
756                 && z3 > PERSPECTIVE_MIN_Z) {
757             w1 /= z1;
758             w2 /= z2;
759             w3 /= z3;
760             final double sum = w1 + w2 + w3;
761             w1 /= sum;
762             w2 /= sum;
763             w3 /= sum;
764         }
765         // near-plane straddlers: plain screen-space barycentric (affine),
766         // matching the affine fallback path used for painting them
767
768         return new double[]{
769                 w1 * v1.textureCoordinate.x + w2 * v2.textureCoordinate.x
770                         + w3 * v3.textureCoordinate.x,
771                 w1 * v1.textureCoordinate.y + w2 * v2.textureCoordinate.y
772                         + w3 * v3.textureCoordinate.y};
773     }
774
775     /**
776      * SDF (signed distance field) rendering path. Coverage is not stored
777      * in the texture; it is re-derived per pixel from a smooth distance
778      * mask, so edges stay sharp at any magnification and fade to clean
779      * gray under minification. Layers: {@code texture.primaryBitmap} is
780      * the background color layer, {@code texture.sdfForeground} the ink
781      * color layer (both sampled nearest — they are flat per region),
782      * {@code texture.sdfMask} the distance field (sampled bilinear).
783      *
784      * <p>Minification is handled analytically: the coverage window is
785      * widened by the screen-space pixel footprint, which gives correct
786      * area coverage without a mipmap chain.</p>
787      *
788      * @param scaleFactor the same screen-pixels-per-texel estimate the
789      *                    mipmap selection uses (times 1.2)
790      */
791     private void paintSdf(final int yTop, final int yBottom,
792                           final RenderingContext renderBuffer,
793                           final Point2D projectedPoint1, final Point2D projectedPoint2,
794                           final Point2D projectedPoint3, final double scaleFactor,
795                           final Vertex v1, final Vertex v2, final Vertex v3) {
796         // SDF (text/decal) is alpha-class — it paints in the
797         // back-to-front alpha pass only, without depth interaction.
798         if (renderBuffer.depthPass == 1)
799             return;
800         // Per-axis screen-space UV gradients (affine estimate — adequate
801         // for a footprint). Text on an angled plane is minified mostly
802         // along ONE axis; an isotropic average would blur the axis that
803         // still has resolution to spare.
804         double footX;
805         double footY;
806         final double ex = projectedPoint2.x - projectedPoint1.x;
807         final double ey = projectedPoint2.y - projectedPoint1.y;
808         final double fx3 = projectedPoint3.x - projectedPoint1.x;
809         final double fy3 = projectedPoint3.y - projectedPoint1.y;
810         final double denom = ex * fy3 - fx3 * ey;
811         if (Math.abs(denom) > 1e-9) {
812             final double u1 = v1.textureCoordinate.x;
813             final double vv1 = v1.textureCoordinate.y;
814             final double du21 = v2.textureCoordinate.x - u1;
815             final double dv21 = v2.textureCoordinate.y - vv1;
816             final double du31 = v3.textureCoordinate.x - u1;
817             final double dv31 = v3.textureCoordinate.y - vv1;
818             final double dudx = (du21 * fy3 - du31 * ey) / denom;
819             final double dudy = (du31 * ex - du21 * fx3) / denom;
820             final double dvdx = (dv21 * fy3 - dv31 * ey) / denom;
821             final double dvdy = (dv31 * ex - dv21 * fx3) / denom;
822             footX = Math.hypot(dudx, dvdx);
823             footY = Math.hypot(dudy, dvdy);
824         } else {
825             footX = footY = 1.2d / scaleFactor;
826         }
827
828         // The coverage window follows the SHARPEST axis: one screen pixel
829         // spans texelsPerPixel texels along it, i.e.
830         // texelsPerPixel/(2*spread) of the normalized mask range; aaK
831         // converts a mask sample (0..255, edge at 127.5) into fixed-point
832         // coverage in [0, 256]: cov = (127.5 - d)*aaK + 128.
833         final double maxFootprint = Math.max(footX, footY);
834         double texelsPerPixel = Math.max(Math.min(footX, footY), 0.01d);
835         if (maxFootprint > 1d) {
836             texelsPerPixel /= SDF_SHARPEN;
837         }
838         final double aaK = (2d * texture.sdfSpreadTexels) / texelsPerPixel / 255d * 256d;
839
840         // No mip chain for SDF layers. A distance field's edge gradient
841         // spans just 2 texels, so a half/quarter-res mask visibly melts
842         // glyph edges — and because the two triangles of a rectangle get
843         // slightly different perspective footprints, they crossed mip
844         // thresholds at different distances, producing a hard diagonal
845         // quality split plus sudden blur steps while dollying (observed
846         // 2026-09-06). Sampling the primary field costs some bandwidth
847         // under minification, but text surfaces are small and the
848         // per-pixel sample count is what matters. Quality then degrades
849         // smoothly with distance instead of in steps.
850         final TextureBitmap mask = texture.sdfMask;
851         final TextureBitmap fg = texture.sdfForeground;
852         final TextureBitmap bg = texture.primaryBitmap;
853         final double mf = mask.multiplicationFactor;
854
855         // Under minification, area-correct coverage reads as low-contrast
856         // gray haze. Two perceptual corrections (A/B-tuned 2026-09-06 on
857         // far+angled text): SDF_SHARPEN narrows the coverage window below
858         // one pixel (kills the haze halo, keeps edges crisp at the cost
859         // of a little shimmer), and a mild coverage gamma < 1 (stem
860         // darkening, the small-ppm font rasterizer trick) keeps thin
861         // strokes present.
862         // Knobs: -De3d.sdf.gamma=1.4 forces a fixed gamma (0 = auto),
863         // -De3d.sdf.sharpen=1 restores the pixel-exact window.
864         final int[] covLut;
865         final double gamma = SDF_GAMMA != 0 ? SDF_GAMMA
866                 : Math.max(0.75d, 1d - 0.08d * (Math.log(maxFootprint) / Math.log(2d)));
867         if (gamma != 1d && maxFootprint > 1d) {
868             covLut = new int[257];
869             for (int i = 0; i <= 256; i++) {
870                 covLut[i] = Math.min(256, (int) (256d * Math.pow(i / 256d, gamma)));
871             }
872         } else {
873             covLut = null;
874         }
875
876         boolean usePerspective = false;
877         double su1 = 0, sv1 = 0, sw1 = 0;
878         double su2 = 0, sv2 = 0, sw2 = 0;
879         double su3 = 0, sv3 = 0, sw3 = 0;
880         if (perspectiveCorrectionEnabled) {
881             final double z1 = v1.transformedCoordinate(renderBuffer).z;
882             final double z2 = v2.transformedCoordinate(renderBuffer).z;
883             final double z3 = v3.transformedCoordinate(renderBuffer).z;
884             if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z && z3 > PERSPECTIVE_MIN_Z) {
885                 // Same affine-sufficiency test as the coverage path
886                 // (mask/fg/bg are all primary resolution, mf = 1).
887                 final double tu1 = v1.textureCoordinate.x;
888                 final double tv1 = v1.textureCoordinate.y;
889                 final double tu2 = v2.textureCoordinate.x;
890                 final double tv2 = v2.textureCoordinate.y;
891                 final double tu3 = v3.textureCoordinate.x;
892                 final double tv3 = v3.textureCoordinate.y;
893                 final double texelSpan = Math.max(
894                         Math.max(Math.abs(tu2 - tu1), Math.abs(tv2 - tv1)),
895                         Math.max(
896                                 Math.max(Math.abs(tu3 - tu1), Math.abs(tv3 - tv1)),
897                                 Math.max(Math.abs(tu3 - tu2), Math.abs(tv3 - tv2))));
898                 final double zMin = Math.min(z1, Math.min(z2, z3));
899                 final double zMax = Math.max(z1, Math.max(z2, z3));
900                 usePerspective = texelSpan * (zMax / zMin - 1d) >= 2d;
901                 if (usePerspective) {
902                     sw1 = 1d / z1;
903                     sw2 = 1d / z2;
904                     sw3 = 1d / z3;
905                     su1 = tu1 * mf * sw1;
906                     sv1 = tv1 * mf * sw1;
907                     su2 = tu2 * mf * sw2;
908                     sv2 = tv2 * mf * sw2;
909                     su3 = tu3 * mf * sw3;
910                     sv3 = tv3 * mf * sw3;
911                 }
912             }
913         }
914
915         if (SDF_DEBUG && (usePerspective != sdfDebugLastPerspective
916                 || Math.abs(footY - sdfDebugLastFootY) > 0.5)) {
917             sdfDebugLastPerspective = usePerspective;
918             sdfDebugLastFootY = footY;
919             System.err.printf("[SDF] perspective=%b footX=%.2f footY=%.2f aaK=%.3f%n",
920                     usePerspective, footX, footY, aaK);
921         }
922
923         if (usePerspective) {
924             final PerspectiveBorderInterpolator[] pi = PERSPECTIVE_INTERPOLATORS.get();
925             pi[0].setPoints(projectedPoint1, projectedPoint2, su1, sv1, sw1, su2, sv2, sw2);
926             pi[1].setPoints(projectedPoint1, projectedPoint3, su1, sv1, sw1, su3, sv3, sw3);
927             pi[2].setPoints(projectedPoint2, projectedPoint3, su2, sv2, sw2, su3, sv3, sw3);
928
929             for (int y = yTop; y <= yBottom; y++) {
930                 if (pi[0].containsY(y)) {
931                     if (pi[1].containsY(y))
932                         drawHorizontalLinePerspectiveSdf(pi[0], pi[1], y, renderBuffer, mask, fg, bg, aaK, covLut);
933                     else if (pi[2].containsY(y))
934                         drawHorizontalLinePerspectiveSdf(pi[0], pi[2], y, renderBuffer, mask, fg, bg, aaK, covLut);
935                 } else if (pi[1].containsY(y)) {
936                     if (pi[2].containsY(y))
937                         drawHorizontalLinePerspectiveSdf(pi[1], pi[2], y, renderBuffer, mask, fg, bg, aaK, covLut);
938                 }
939             }
940             return;
941         }
942
943         final PolygonBorderInterpolator[] interpolators = INTERPOLATORS.get();
944         final PolygonBorderInterpolator pbi1 = interpolators[0];
945         final PolygonBorderInterpolator pbi2 = interpolators[1];
946         final PolygonBorderInterpolator pbi3 = interpolators[2];
947
948         pbi1.setPoints(projectedPoint1, projectedPoint2, v1.textureCoordinate, v2.textureCoordinate);
949         pbi2.setPoints(projectedPoint1, projectedPoint3, v1.textureCoordinate, v3.textureCoordinate);
950         pbi3.setPoints(projectedPoint2, projectedPoint3, v2.textureCoordinate, v3.textureCoordinate);
951
952         for (int y = yTop; y <= yBottom; y++) {
953             if (pbi1.containsY(y)) {
954                 if (pbi2.containsY(y))
955                     drawHorizontalLineSdf(pbi1, pbi2, y, renderBuffer, mask, fg, bg, aaK, mf, covLut);
956                 else if (pbi3.containsY(y))
957                     drawHorizontalLineSdf(pbi1, pbi3, y, renderBuffer, mask, fg, bg, aaK, mf, covLut);
958             } else if (pbi2.containsY(y)) {
959                 if (pbi3.containsY(y))
960                     drawHorizontalLineSdf(pbi2, pbi3, y, renderBuffer, mask, fg, bg, aaK, mf, covLut);
961             }
962         }
963     }
964
965     /**
966      * SDF scanline, affine mapping. Texture coordinates are scaled by the
967      * selected mip's multiplication factor (all layers share one mip
968      * level, so one factor covers mask and both color layers).
969      */
970     private void drawHorizontalLineSdf(final PolygonBorderInterpolator line1,
971                                        final PolygonBorderInterpolator line2, final int y,
972                                        final RenderingContext renderBuffer,
973                                        final TextureBitmap mask, final TextureBitmap fg,
974                                        final TextureBitmap bg, final double aaK,
975                                        final double mf, final int[] covLut) {
976         line1.setCurrentY(y);
977         line2.setCurrentY(y);
978
979         int x1 = line1.getX();
980         int x2 = line2.getX();
981
982         final double tx1, ty1, tx2, ty2;
983         if (x1 <= x2) {
984             tx1 = line1.getTX() * mf;
985             ty1 = line1.getTY() * mf;
986             tx2 = line2.getTX() * mf;
987             ty2 = line2.getTY() * mf;
988         } else {
989             final int tmp = x1;
990             x1 = x2;
991             x2 = tmp;
992             tx1 = line2.getTX() * mf;
993             ty1 = line2.getTY() * mf;
994             tx2 = line1.getTX() * mf;
995             ty2 = line1.getTY() * mf;
996         }
997
998         final double realWidth = x2 - x1;
999         final double realX1 = x1;
1000
1001         if (x1 < renderBuffer.renderMinX)
1002             x1 = renderBuffer.renderMinX;
1003         if (x2 >= renderBuffer.renderMaxX)
1004             x2 = renderBuffer.renderMaxX;
1005
1006         int renderBufferOffset = (y * renderBuffer.width) + x1;
1007         final int[] renderBufferPixels = renderBuffer.pixels;
1008
1009         final double txStep = (tx2 - tx1) / realWidth;
1010         final double tyStep = (ty2 - ty1) / realWidth;
1011
1012         double tx = tx1 + txStep * (x1 - realX1);
1013         double ty = ty1 + tyStep * (x1 - realX1);
1014
1015         final int[] maskPixels = mask.pixels;
1016         final int[] fgPixels = fg.pixels;
1017         final int[] bgPixels = bg.pixels;
1018         final int mw = mask.width;
1019         final int mh = mask.height;
1020         final double bilinearCapX = mw - 1.0001d;
1021         final double bilinearCapY = mh - 1.0001d;
1022         final int mw1 = mw - 1;
1023         final int mh1 = mh - 1;
1024
1025         for (int x = x1; x < x2; x++) {
1026             // Fixed-point bilinear distance fetch (8.8 fractions)
1027             final double ctx = tx < 0 ? 0 : Math.min(tx, bilinearCapX);
1028             final double cty = ty < 0 ? 0 : Math.min(ty, bilinearCapY);
1029             final int x0 = (int) ctx;
1030             final int y0 = (int) cty;
1031             final int fx = (int) ((ctx - x0) * 256);
1032             final int fy = (int) ((cty - y0) * 256);
1033             final int row0 = y0 * mw + x0;
1034             final int row1 = row0 + mw;
1035             final int m00 = (maskPixels[row0] >> 16) & 0xff;
1036             final int m10 = (maskPixels[row0 + 1] >> 16) & 0xff;
1037             final int m01 = (maskPixels[row1] >> 16) & 0xff;
1038             final int m11 = (maskPixels[row1 + 1] >> 16) & 0xff;
1039             final int d = (m00 * (256 - fx) * (256 - fy) + m10 * fx * (256 - fy)
1040                     + m01 * (256 - fx) * fy + m11 * fx * fy) >> 16;
1041
1042             int cov = (int) ((127.5d - d) * aaK + 128d);
1043             if (cov < 0) cov = 0;
1044             else if (cov > 256) cov = 256;
1045             if (covLut != null) cov = covLut[cov];
1046
1047             int itx = (int) tx;
1048             int ity = (int) ty;
1049             if (itx < 0) itx = 0;
1050             else if (itx > mw1) itx = mw1;
1051             if (ity < 0) ity = 0;
1052             else if (ity > mh1) ity = mh1;
1053             final int addr = ity * mw + itx;
1054
1055             final int srcPixel;
1056             if (cov <= 0) {
1057                 srcPixel = bgPixels[addr];
1058             } else if (cov >= 256) {
1059                 srcPixel = fgPixels[addr];
1060             } else {
1061                 final int bgP = bgPixels[addr];
1062                 final int fgP = fgPixels[addr];
1063                 final int a = (bgP >>> 24) + ((((int) (fgP >>> 24) - (bgP >>> 24)) * cov) >> 8);
1064                 final int r = ((bgP >> 16) & 0xff) + (((((fgP >> 16) & 0xff) - ((bgP >> 16) & 0xff)) * cov) >> 8);
1065                 final int g = ((bgP >> 8) & 0xff) + (((((fgP >> 8) & 0xff) - ((bgP >> 8) & 0xff)) * cov) >> 8);
1066                 final int b = (bgP & 0xff) + ((((fgP & 0xff) - (bgP & 0xff)) * cov) >> 8);
1067                 srcPixel = (a << 24) | (r << 16) | (g << 8) | b;
1068             }
1069
1070             final int srcAlpha = (srcPixel >> 24) & 0xff;
1071             if (srcAlpha == 255) {
1072                 renderBufferPixels[renderBufferOffset] = srcPixel;
1073             } else if (srcAlpha != 0) {
1074                 final int destPixel = renderBufferPixels[renderBufferOffset];
1075                 final int destR = (destPixel >> 16) & 0xff;
1076                 final int destG = (destPixel >> 8) & 0xff;
1077                 final int destB = destPixel & 0xff;
1078                 final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8);
1079                 final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8);
1080                 final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8);
1081                 renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
1082             }
1083
1084             tx += txStep;
1085             ty += tyStep;
1086             renderBufferOffset++;
1087         }
1088     }
1089
1090     /**
1091      * SDF scanline with Quake-style subdivided perspective correction —
1092      * same stepping structure as {@link #drawHorizontalLinePerspectiveZ},
1093      * with the coverage fetch replaced by the distance-field evaluation.
1094      */
1095     private void drawHorizontalLinePerspectiveSdf(
1096             final PerspectiveBorderInterpolator line1,
1097             final PerspectiveBorderInterpolator line2,
1098             final int y,
1099             final RenderingContext renderBuffer,
1100             final TextureBitmap mask, final TextureBitmap fg,
1101             final TextureBitmap bg, final double aaK, final int[] covLut) {
1102
1103         line1.setCurrentY(y);
1104         line2.setCurrentY(y);
1105
1106         int x1 = line1.getX();
1107         int x2 = line2.getX();
1108
1109         final double su1, sv1, sw1;
1110         final double su2, sv2, sw2;
1111
1112         if (x1 <= x2) {
1113             su1 = line1.getSU();
1114             sv1 = line1.getSV();
1115             sw1 = line1.getSW();
1116             su2 = line2.getSU();
1117             sv2 = line2.getSV();
1118             sw2 = line2.getSW();
1119         } else {
1120             final int tmp = x1;
1121             x1 = x2;
1122             x2 = tmp;
1123             su1 = line2.getSU();
1124             sv1 = line2.getSV();
1125             sw1 = line2.getSW();
1126             su2 = line1.getSU();
1127             sv2 = line1.getSV();
1128             sw2 = line1.getSW();
1129         }
1130
1131         final double realWidth = x2 - x1;
1132         final double realX1 = x1;
1133
1134         if (x1 < renderBuffer.renderMinX)
1135             x1 = renderBuffer.renderMinX;
1136         if (x2 >= renderBuffer.renderMaxX)
1137             x2 = renderBuffer.renderMaxX;
1138
1139         final int span = x2 - x1;
1140         if (span <= 0)
1141             return;
1142
1143         int renderBufferOffset = (y * renderBuffer.width) + x1;
1144
1145         final double dsu = (su2 - su1) / realWidth;
1146         final double dsv = (sv2 - sv1) / realWidth;
1147         final double dsw = (sw2 - sw1) / realWidth;
1148
1149         double su = su1 + dsu * (x1 - realX1);
1150         double sv = sv1 + dsv * (x1 - realX1);
1151         double sw = sw1 + dsw * (x1 - realX1);
1152
1153         final int[] renderBufferPixels = renderBuffer.pixels;
1154
1155         final int[] maskPixels = mask.pixels;
1156         final int[] fgPixels = fg.pixels;
1157         final int[] bgPixels = bg.pixels;
1158         final int mw = mask.width;
1159         final int mh = mask.height;
1160         final double bilinearCapX = mw - 1.0001d;
1161         final double bilinearCapY = mh - 1.0001d;
1162         final int mw1 = mw - 1;
1163         final int mh1 = mh - 1;
1164
1165         // Same adaptive-interval ladder as the coverage path.
1166         final double ue1 = su1 / sw1;
1167         final double ue2 = su2 / sw2;
1168         final double ve1 = sv1 / sw1;
1169         final double ve2 = sv2 / sw2;
1170         final double wRatio = Math.max(sw1, sw2) / Math.min(sw1, sw2);
1171         final double texelRate = Math.max(Math.abs(ue2 - ue1), Math.abs(ve2 - ve1))
1172                 / realWidth * wRatio;
1173         final double k = Math.abs(dsw) / Math.min(sw1, sw2);
1174         final double curvature = texelRate * k;
1175         final int interval = curvature < 0.5 / (16 * 16) ? PERSPECTIVE_CORRECTION_INTERVAL
1176                 : curvature < 0.5 / (8 * 8) ? 8
1177                 : curvature < 0.5 / (4 * 4) ? 4
1178                 : curvature < 0.5 / (2 * 2) ? 2 : 1;
1179         final double invInterval = 1d / interval;
1180
1181         int done = 0;
1182         double invW = 1d / sw;
1183         double tx = su * invW;
1184         double ty = sv * invW;
1185         while (done < span) {
1186             final int block = Math.min(interval, span - done);
1187
1188             su += dsu * block;
1189             sv += dsv * block;
1190             sw += dsw * block;
1191             final double invWNext = 1d / sw;
1192             final double txNext = su * invWNext;
1193             final double tyNext = sv * invWNext;
1194
1195             final double invBlock = block == interval ? invInterval : 1d / block;
1196             final double txStep = (txNext - tx) * invBlock;
1197             final double tyStep = (tyNext - ty) * invBlock;
1198
1199             for (int i = 0; i < block; i++) {
1200                 // Fixed-point bilinear distance fetch (8.8 fractions)
1201                 final double ctx = tx < 0 ? 0 : Math.min(tx, bilinearCapX);
1202                 final double cty = ty < 0 ? 0 : Math.min(ty, bilinearCapY);
1203                 final int x0 = (int) ctx;
1204                 final int y0 = (int) cty;
1205                 final int fx = (int) ((ctx - x0) * 256);
1206                 final int fy = (int) ((cty - y0) * 256);
1207                 final int row0 = y0 * mw + x0;
1208                 final int row1 = row0 + mw;
1209                 final int m00 = (maskPixels[row0] >> 16) & 0xff;
1210                 final int m10 = (maskPixels[row0 + 1] >> 16) & 0xff;
1211                 final int m01 = (maskPixels[row1] >> 16) & 0xff;
1212                 final int m11 = (maskPixels[row1 + 1] >> 16) & 0xff;
1213                 final int d = (m00 * (256 - fx) * (256 - fy) + m10 * fx * (256 - fy)
1214                         + m01 * (256 - fx) * fy + m11 * fx * fy) >> 16;
1215
1216                 int cov = (int) ((127.5d - d) * aaK + 128d);
1217                 if (cov < 0) cov = 0;
1218                 else if (cov > 256) cov = 256;
1219                 if (covLut != null) cov = covLut[cov];
1220
1221                 int itx = (int) tx;
1222                 int ity = (int) ty;
1223                 if (itx < 0) itx = 0;
1224                 else if (itx > mw1) itx = mw1;
1225                 if (ity < 0) ity = 0;
1226                 else if (ity > mh1) ity = mh1;
1227                 final int addr = ity * mw + itx;
1228
1229                 final int srcPixel;
1230                 if (cov <= 0) {
1231                     srcPixel = bgPixels[addr];
1232                 } else if (cov >= 256) {
1233                     srcPixel = fgPixels[addr];
1234                 } else {
1235                     final int bgP = bgPixels[addr];
1236                     final int fgP = fgPixels[addr];
1237                     final int a = (bgP >>> 24) + ((((int) (fgP >>> 24) - (bgP >>> 24)) * cov) >> 8);
1238                     final int r = ((bgP >> 16) & 0xff) + (((((fgP >> 16) & 0xff) - ((bgP >> 16) & 0xff)) * cov) >> 8);
1239                     final int g = ((bgP >> 8) & 0xff) + (((((fgP >> 8) & 0xff) - ((bgP >> 8) & 0xff)) * cov) >> 8);
1240                     final int b = (bgP & 0xff) + ((((fgP & 0xff) - (bgP & 0xff)) * cov) >> 8);
1241                     srcPixel = (a << 24) | (r << 16) | (g << 8) | b;
1242                 }
1243
1244                 final int srcAlpha = (srcPixel >> 24) & 0xff;
1245                 if (srcAlpha == 255) {
1246                     renderBufferPixels[renderBufferOffset] = srcPixel;
1247                 } else if (srcAlpha != 0) {
1248                     final int destPixel = renderBufferPixels[renderBufferOffset];
1249                     final int destR = (destPixel >> 16) & 0xff;
1250                     final int destG = (destPixel >> 8) & 0xff;
1251                     final int destB = destPixel & 0xff;
1252                     final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8);
1253                     final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8);
1254                     final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8);
1255                     renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
1256                 }
1257
1258                 tx += txStep;
1259                 ty += tyStep;
1260                 renderBufferOffset++;
1261             }
1262
1263             done += block;
1264         }
1265     }
1266
1267     /**
1268      * Affine texture mapping (u, v linear in screen space). Used for
1269      * near-plane straddlers and for triangles small/flat enough that
1270      * affine is within half a texel of exact perspective mapping.
1271      */
1272     private void paintAffine(final int yTop, final int yBottom,
1273                              final TextureBitmap mipmap,
1274                              final RenderingContext renderBuffer,
1275                              final Point2D projectedPoint1, final Point2D projectedPoint2,
1276                              final Point2D projectedPoint3,
1277                              final Point2D texturePoint1, final Point2D texturePoint2,
1278                              final Point2D texturePoint3,
1279                              final double z1, final double z2, final double z3) {
1280         final PolygonBorderInterpolator[] interpolators = INTERPOLATORS.get();
1281         final PolygonBorderInterpolator pbi1 = interpolators[0];
1282         final PolygonBorderInterpolator pbi2 = interpolators[1];
1283         final PolygonBorderInterpolator pbi3 = interpolators[2];
1284
1285         pbi1.setPoints(projectedPoint1, projectedPoint2, texturePoint1, texturePoint2);
1286         pbi2.setPoints(projectedPoint1, projectedPoint3, texturePoint1, texturePoint3);
1287         pbi3.setPoints(projectedPoint2, projectedPoint3, texturePoint2, texturePoint3);
1288
1289         final double zw1 = 1d / z1;
1290         final double zw2 = 1d / z2;
1291         final double zw3 = 1d / z3;
1292         pbi1.setPointsZW(zw1, zw2);
1293         pbi2.setPointsZW(zw1, zw3);
1294         pbi3.setPointsZW(zw2, zw3);
1295         for (int y = yTop; y <= yBottom; y++) {
1296             if (pbi1.containsY(y)) {
1297                 if (pbi2.containsY(y))
1298                     drawHorizontalLineZ(pbi1, pbi2, y, renderBuffer, mipmap);
1299                 else if (pbi3.containsY(y))
1300                     drawHorizontalLineZ(pbi1, pbi3, y, renderBuffer, mipmap);
1301             } else if (pbi2.containsY(y)) {
1302                 if (pbi3.containsY(y))
1303                     drawHorizontalLineZ(pbi2, pbi3, y, renderBuffer, mipmap);
1304             }
1305         }
1306
1307     }
1308
1309     /**
1310      * Z-buffer span writer: per-pixel depth test (biased 1/z, linear
1311      * along the span) BEFORE the texture fetch. Opaque texels write
1312      * depth; blended texels write color only.
1313      */
1314     private void drawHorizontalLineZ(final PolygonBorderInterpolator line1,
1315                                      final PolygonBorderInterpolator line2,
1316                                      final int y,
1317                                      final RenderingContext renderBuffer,
1318                                      final TextureBitmap textureBitmap) {
1319
1320         line1.setCurrentY(y);
1321         line2.setCurrentY(y);
1322
1323         int x1 = line1.getX();
1324         int x2 = line2.getX();
1325
1326         final double tx1, ty1, zw1;
1327         final double tx2, ty2, zw2;
1328
1329         if (x1 <= x2) {
1330             tx1 = line1.getTX() * textureBitmap.multiplicationFactor;
1331             ty1 = line1.getTY() * textureBitmap.multiplicationFactor;
1332             zw1 = line1.getZW();
1333             tx2 = line2.getTX() * textureBitmap.multiplicationFactor;
1334             ty2 = line2.getTY() * textureBitmap.multiplicationFactor;
1335             zw2 = line2.getZW();
1336         } else {
1337             final int tmp = x1;
1338             x1 = x2;
1339             x2 = tmp;
1340
1341             tx1 = line2.getTX() * textureBitmap.multiplicationFactor;
1342             ty1 = line2.getTY() * textureBitmap.multiplicationFactor;
1343             zw1 = line2.getZW();
1344
1345             tx2 = line1.getTX() * textureBitmap.multiplicationFactor;
1346             ty2 = line1.getTY() * textureBitmap.multiplicationFactor;
1347             zw2 = line1.getZW();
1348         }
1349
1350         final double realWidth = x2 - x1;
1351         final double realX1 = x1;
1352
1353         if (x1 < renderBuffer.renderMinX)
1354             x1 = renderBuffer.renderMinX;
1355
1356         // x2 is exclusive: clamp to renderMaxX (see drawHorizontalLine)
1357         if (x2 >= renderBuffer.renderMaxX)
1358             x2 = renderBuffer.renderMaxX;
1359
1360         if (PROF) {
1361             PROF_SPANS.incrementAndGet();
1362             PROF_PIXELS.addAndGet(Math.max(0, x2 - x1));
1363         }
1364
1365         int renderBufferOffset = (y * renderBuffer.width) + x1;
1366         final int[] renderBufferPixels = renderBuffer.pixels;
1367         final float[] depth = renderBuffer.depth;
1368         // Alpha pass (depthPass 2): depth-test but never depth-write
1369         final boolean writeDepth = renderBuffer.depthPass != 2;
1370
1371         final double txStep = (tx2 - tx1) / realWidth;
1372         final double tyStep = (ty2 - ty1) / realWidth;
1373         final double dzw = (zw2 - zw1) / realWidth;
1374         double tx = tx1 + txStep * (x1 - realX1);
1375         double ty = ty1 + tyStep * (x1 - realX1);
1376         double zw = zw1 + dzw * (x1 - realX1);
1377
1378         final int[] texPixels = textureBitmap.pixels;
1379         final int texW = textureBitmap.width;
1380         final int texH = textureBitmap.height;
1381         final int texWMinus1 = texW - 1;
1382         final int texHMinus1 = texH - 1;
1383         // texture is null in unit tests: clamp (see drawHorizontalLine)
1384         final boolean wrap = texture != null && texture.wrap;
1385
1386         for (int x = x1; x < x2; x++) {
1387
1388             if (zw > depth[renderBufferOffset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) {
1389                 int itx = (int) tx;
1390                 int ity = (int) ty;
1391
1392                 if (wrap) {
1393                     itx = Math.floorMod(itx, texW);
1394                     ity = Math.floorMod(ity, texH);
1395                 } else {
1396                     if (itx < 0) itx = 0;
1397                     else if (itx > texWMinus1) itx = texWMinus1;
1398
1399                     if (ity < 0) ity = 0;
1400                     else if (ity > texHMinus1) ity = texHMinus1;
1401                 }
1402
1403                 final int srcPixel = texPixels[ity * texW + itx];
1404                 final int srcAlpha = (srcPixel >> 24) & 0xff;
1405
1406                 if (srcAlpha == 255) {
1407                     renderBufferPixels[renderBufferOffset] = srcPixel;
1408                     if (writeDepth)
1409                         depth[renderBufferOffset] = (float) zw;
1410                 } else if (srcAlpha != 0) {
1411                     // Translucent: blend without writing depth
1412                     final int destPixel = renderBufferPixels[renderBufferOffset];
1413                     final int destR = (destPixel >> 16) & 0xff;
1414                     final int destG = (destPixel >> 8) & 0xff;
1415                     final int destB = destPixel & 0xff;
1416
1417                     final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8);
1418                     final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8);
1419                     final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8);
1420
1421                     renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
1422                 }
1423             }
1424
1425             tx += txStep;
1426             ty += tyStep;
1427             zw += dzw;
1428             renderBufferOffset++;
1429         }
1430
1431     }
1432
1433     /**
1434      * Checks if backface culling is enabled for this triangle.
1435      *
1436      * @return {@code true} if backface culling is enabled
1437      */
1438     public boolean isBackfaceCullingEnabled() {
1439         return backfaceCulling;
1440     }
1441
1442     /**
1443      * Enables or disables backface culling for this triangle.
1444      *
1445      * @param backfaceCulling {@code true} to enable backface culling
1446      */
1447     public void setBackfaceCulling(final boolean backfaceCulling) {
1448         this.backfaceCulling = backfaceCulling;
1449     }
1450
1451     /**
1452      * Draws the triangle border edges in yellow (for debugging).
1453      *
1454      * @param renderBuffer the rendering context
1455      */
1456     private void showBorders(final RenderingContext renderBuffer) {
1457
1458         final Point2D projectedPoint1 = vertices.get(0).onScreenCoordinate(renderBuffer);
1459         final Point2D projectedPoint2 = vertices.get(1).onScreenCoordinate(renderBuffer);
1460         final Point2D projectedPoint3 = vertices.get(2).onScreenCoordinate(renderBuffer);
1461
1462         final int x1 = (int) projectedPoint1.x;
1463         final int y1 = (int) projectedPoint1.y;
1464         final int x2 = (int) projectedPoint2.x;
1465         final int y2 = (int) projectedPoint2.y;
1466         final int x3 = (int) projectedPoint3.x;
1467         final int y3 = (int) projectedPoint3.y;
1468
1469         renderBuffer.executeWithGraphics(g -> {
1470             g.setColor(Color.YELLOW);
1471             g.drawLine(x1, y1, x2, y2);
1472             g.drawLine(x3, y3, x2, y2);
1473             g.drawLine(x1, y1, x3, y3);
1474         });
1475     }
1476
1477 }