a6c2c38552f92787fc7eac2b91076842768b4de5
[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.renderer.raster.RenderingContext;
9 import eu.svjatoslav.aukio.e3d.renderer.raster.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     // ---- per-triangle GI shading (mesh-block path only) ----
126
127     /**
128      * Per-triangle light total in light units (255 = full texture
129      * brightness), written by {@code MeshTriangle.paint} from the owning
130      * block's GI arrays before the paint core runs. Object-backed
131      * triangles never touch these: {@link #giShaded} stays false and the
132      * span loops render bit-identically to the unlit path.
133      */
134     float giShadeR, giShadeG, giShadeB;
135     /** True while {@link #giShadeR/G/B} carry a GI light total. */
136     boolean giShaded;
137
138     /**
139      * Multiplies a sampled texel by the per-triangle GI light, clamped.
140      * Called only on the {@link #giShaded} path.
141      */
142     private int shadeGi(final int texel) {
143         final int r = Math.min(255,
144                 (int) (((texel >> 16) & 0xff) * giShadeR / 255f));
145         final int g = Math.min(255,
146                 (int) (((texel >> 8) & 0xff) * giShadeG / 255f));
147         final int b = Math.min(255,
148                 (int) ((texel & 0xff) * giShadeB / 255f));
149         return (texel & 0xFF000000) | (r << 16) | (g << 8) | b;
150     }
151
152     // --- ad-hoc frame profiling (-De3d.prof=true; dead code when off)
153     /** Master switch, constant-folded when false. */
154     private static final boolean PROF =
155             Boolean.getBoolean("e3d.prof");
156     /** paintTriangle invocations. */
157     public static final java.util.concurrent.atomic.AtomicLong
158             PROF_TRIS = new java.util.concurrent.atomic.AtomicLong();
159     /** Triangles facing away (engine winding convention). */
160     public static final java.util.concurrent.atomic.AtomicLong
161             PROF_BACKFACE = new java.util.concurrent.atomic.AtomicLong();
162     /** Triangles discarded by vertical render-bounds clamp. */
163     public static final java.util.concurrent.atomic.AtomicLong
164             PROF_OFFY = new java.util.concurrent.atomic.AtomicLong();
165     /** Triangles with screen bounding box below 2x2 pixels. */
166     public static final java.util.concurrent.atomic.AtomicLong
167             PROF_TINY = new java.util.concurrent.atomic.AtomicLong();
168     /** Scanline spans drawn. */
169     public static final java.util.concurrent.atomic.AtomicLong
170             PROF_SPANS = new java.util.concurrent.atomic.AtomicLong();
171     /** Pixel loop iterations across all spans. */
172     public static final java.util.concurrent.atomic.AtomicLong
173             PROF_PIXELS = new java.util.concurrent.atomic.AtomicLong();
174
175     /** Resets all profiling counters. */
176     public static void profReset() {
177         PROF_TRIS.set(0);
178         PROF_BACKFACE.set(0);
179         PROF_OFFY.set(0);
180         PROF_TINY.set(0);
181         PROF_SPANS.set(0);
182         PROF_PIXELS.set(0);
183     }
184
185     /**
186      * Total UV distance between all texture coordinate pairs.
187      * Computed at construction time to determine appropriate mipmap level.
188      */
189     private double totalTextureDistance;
190
191     /**
192      * Creates a textured triangle with the specified vertices and texture.
193      *
194      * @param p1      the first vertex (must have textureCoordinate set)
195      * @param p2      the second vertex (must have textureCoordinate set)
196      * @param p3      the third vertex (must have textureCoordinate set)
197      * @param texture the texture to apply
198      */
199     public TexturedTriangle(Vertex p1, Vertex p2, Vertex p3, final Texture texture) {
200
201         super(p1, p2, p3);
202         this.texture = texture;
203         computeTotalTextureDistance();
204     }
205
206     /**
207      * Constructor for flat-array-backed subclasses ({@code MeshTriangle})
208      * that carry no {@link Vertex} objects and paint exclusively through
209      * {@link #paintFlat}, which computes the mipmap metric inline.
210      * {@code totalTextureDistance} is set to a neutral 1 — never read on
211      * that path.
212      *
213      * @param texture the texture the subclass paints with
214      */
215     protected TexturedTriangle(final Texture texture) {
216         super(0);
217         this.texture = texture;
218         this.totalTextureDistance = 1;
219     }
220
221     /**
222      * Computes the total UV distance between all texture coordinate pairs.
223      * Used to determine appropriate mipmap level.
224      */
225     private void computeTotalTextureDistance() {
226         totalTextureDistance = vertices.get(0).textureCoordinate.getDistanceTo(vertices.get(1).textureCoordinate);
227         totalTextureDistance += vertices.get(0).textureCoordinate.getDistanceTo(vertices.get(2).textureCoordinate);
228         totalTextureDistance += vertices.get(1).textureCoordinate.getDistanceTo(vertices.get(2).textureCoordinate);
229     }
230
231     /**
232      * Recomputes the mipmap-selection metric after texture coordinates are
233      * replaced post-construction (used by generated-UV subclasses like
234      * lightmapped triangles).
235      */
236     protected final void refreshTextureDistance() {
237         computeTotalTextureDistance();
238     }
239
240     /**
241      * Z-buffer span writer: the biased 1/z endpoint values ride the
242      * interpolators' zw channel, and every pixel is depth-tested BEFORE
243      * the texture fetch — rejected pixels cost one float compare instead
244      * of a texel read. Opaque texels (alpha 255) write depth; blended
245      * texels write color only, so translucency never occludes.
246      * {@code renderBuffer.depth} is always allocated (the z-buffer path
247      * is the only renderer); requires {@code setPointsZW} called on both
248      * interpolators.
249      */
250     private void drawHorizontalLinePerspectiveZ(
251             final PerspectiveBorderInterpolator line1,
252             final PerspectiveBorderInterpolator line2,
253             final int y,
254             final RenderingContext renderBuffer,
255             final TextureBitmap textureBitmap) {
256
257         line1.setCurrentY(y);
258         line2.setCurrentY(y);
259
260         int x1 = line1.getX();
261         int x2 = line2.getX();
262
263         final double su1, sv1, sw1, zw1;
264         final double su2, sv2, sw2, zw2;
265
266         if (x1 <= x2) {
267             su1 = line1.getSU();
268             sv1 = line1.getSV();
269             sw1 = line1.getSW();
270             zw1 = line1.getZW();
271             su2 = line2.getSU();
272             sv2 = line2.getSV();
273             sw2 = line2.getSW();
274             zw2 = line2.getZW();
275         } else {
276             final int tmp = x1;
277             x1 = x2;
278             x2 = tmp;
279             su1 = line2.getSU();
280             sv1 = line2.getSV();
281             sw1 = line2.getSW();
282             zw1 = line2.getZW();
283             su2 = line1.getSU();
284             sv2 = line1.getSV();
285             sw2 = line1.getSW();
286             zw2 = line1.getZW();
287         }
288
289         final double realWidth = x2 - x1;
290         final double realX1 = x1;
291
292         if (x1 < renderBuffer.renderMinX)
293             x1 = renderBuffer.renderMinX;
294         if (x2 >= renderBuffer.renderMaxX)
295             x2 = renderBuffer.renderMaxX;
296
297         final int span = x2 - x1;
298         if (span <= 0)
299             return;
300
301         if (PROF) {
302             PROF_SPANS.incrementAndGet();
303             PROF_PIXELS.addAndGet(span);
304         }
305
306         int renderBufferOffset = (y * renderBuffer.width) + x1;
307
308         final double dsu = (su2 - su1) / realWidth;
309         final double dsv = (sv2 - sv1) / realWidth;
310         final double dsw = (sw2 - sw1) / realWidth;
311         final double dzw = (zw2 - zw1) / realWidth;
312
313         // Depth margin (polygon offset): fragments within dzMargin world
314         // units of the stored depth resolve coherently instead of
315         // z-fighting per pixel — near-coplanar surface pairs (kit-bashed
316         // wall pieces, draped decals, LOD shells). The queue is
317         // back-to-front (painter, Z descending), so WITHIN the window
318         // the LATER (nearer) writer must win: the test therefore rejects
319         // only fragments that are BEHIND the stored depth by more than
320         // the margin. (The previous "+margin" form made the FIRST —
321         // i.e. FARTHER — writer win the window, so dirt within margin
322         // below the road beat the pavement; combined with a per-span
323         // margin constant that inflates by (z_pixel/z_near)^2 down
324         // grazing spans, ground leaked through the road at near-horizon
325         // pitches. Fixed camera, view-dependent holes = impossible for
326         // a correct z-buffer.)
327         // The w-space margin is dz*w^2 evaluated PER PIXEL at the
328         // fragment's own depth.
329
330         double su = su1 + dsu * (x1 - realX1);
331         double sv = sv1 + dsv * (x1 - realX1);
332         double sw = sw1 + dsw * (x1 - realX1);
333         double zw = zw1 + dzw * (x1 - realX1);
334
335         final int[] texPixels = textureBitmap.pixels;
336         final int texW = textureBitmap.width;
337         final int texH = textureBitmap.height;
338         final int texWMinus1 = texW - 1;
339         final int texHMinus1 = texH - 1;
340         final int[] renderBufferPixels = renderBuffer.pixels;
341         final float[] depth = renderBuffer.depth;
342         // Alpha pass (depthPass 2): depth-test but never depth-write,
343         // so cutout foliage cannot occlude later fragments
344         final boolean writeDepth = renderBuffer.depthPass != 2;
345         // null texture (unit tests) = clamp
346         final boolean wrap = texture != null && texture.wrap;
347
348         // Adaptive-subdivision perspective ladder (Quake-style stepping)
349         final double ue1 = su1 / sw1;
350         final double ue2 = su2 / sw2;
351         final double ve1 = sv1 / sw1;
352         final double ve2 = sv2 / sw2;
353         final double wRatio = Math.max(sw1, sw2) / Math.min(sw1, sw2);
354         final double texelRate = Math.max(Math.abs(ue2 - ue1), Math.abs(ve2 - ve1))
355                 / realWidth * wRatio;
356         final double k = Math.abs(dsw) / Math.min(sw1, sw2);
357         final double curvature = texelRate * k;
358         final int interval = curvature < 0.5 / (16 * 16) ? PERSPECTIVE_CORRECTION_INTERVAL
359                 : curvature < 0.5 / (8 * 8) ? 8
360                 : curvature < 0.5 / (4 * 4) ? 4
361                 : curvature < 0.5 / (2 * 2) ? 2 : 1;
362         final double invInterval = 1d / interval;
363
364         int done = 0;
365         double invW = 1d / sw;
366         double tx = su * invW;
367         double ty = sv * invW;
368         while (done < span) {
369             final int block = Math.min(interval, span - done);
370
371             su += dsu * block;
372             sv += dsv * block;
373             sw += dsw * block;
374             final double invWNext = 1d / sw;
375             final double txNext = su * invWNext;
376             final double tyNext = sv * invWNext;
377
378             final double invBlock = block == interval ? invInterval : 1d / block;
379             final double txStep = (txNext - tx) * invBlock;
380             final double tyStep = (tyNext - ty) * invBlock;
381
382             for (int i = 0; i < block; i++) {
383                 if (zw > depth[renderBufferOffset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) {
384                     int itx = (int) tx;
385                     int ity = (int) ty;
386
387                     if (wrap) {
388                         itx = Math.floorMod(itx, texW);
389                         ity = Math.floorMod(ity, texH);
390                     } else {
391                         if (itx < 0) itx = 0;
392                         else if (itx > texWMinus1) itx = texWMinus1;
393
394                         if (ity < 0) ity = 0;
395                         else if (ity > texHMinus1) ity = texHMinus1;
396                     }
397
398                     final int sampledPixel = texPixels[ity * texW + itx];
399                     final int srcPixel = giShaded ? shadeGi(sampledPixel) : sampledPixel;
400                     final int srcAlpha = (srcPixel >> 24) & 0xff;
401
402                     if (srcAlpha == 255) {
403                         renderBufferPixels[renderBufferOffset] = srcPixel;
404                         if (writeDepth)
405                             depth[renderBufferOffset] = (float) zw;
406                     } else if (srcAlpha != 0) {
407                         // Translucent: blend, but do NOT write depth —
408                         // translucency must not occlude later fragments.
409                         // Lerp form dest + ((a*(src-dest) - dest) >> 8):
410                         // algebraically ((255-a)*dest + a*src) >> 8 — proven
411                         // bit-identical to SolidPolygon's form for all
412                         // inputs, so the two span writers blend the same.
413                         final int destPixel = renderBufferPixels[renderBufferOffset];
414                         final int destR = (destPixel >> 16) & 0xff;
415                         final int destG = (destPixel >> 8) & 0xff;
416                         final int destB = destPixel & 0xff;
417
418                         final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8);
419                         final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8);
420                         final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8);
421
422                         renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
423                     }
424                 }
425
426                 tx += txStep;
427                 ty += tyStep;
428                 zw += dzw;
429                 renderBufferOffset++;
430             }
431
432             tx = txNext;
433             ty = tyNext;
434
435             done += block;
436         }
437     }
438
439     @Override
440     public void paint(final RenderingContext renderBuffer) {
441         // Near-plane clip output takes precedence: a straddling triangle
442         // clips to a triangle or a quad (one corner cut off). The quad is
443         // painted as a 2-triangle fan — the clip of a convex polygon stays
444         // convex, so fan triangulation is exact. Clipped vertices carry
445         // UVs interpolated in 3D at the cut, which is exactly what the
446         // perspective-correct path expects of a point on the edge.
447         final List<Vertex> clipped = clippedVertices(renderBuffer);
448         if (clipped == null) {
449             paintTriangle(renderBuffer, vertices.get(0), vertices.get(1), vertices.get(2));
450             return;
451         }
452         for (int i = 1; i + 1 < clipped.size(); i++) {
453             paintTriangle(renderBuffer, clipped.get(0), clipped.get(i), clipped.get(i + 1));
454         }
455     }
456
457     /**
458      * Renders one textured triangle defined by the given vertices (either
459      * the shape's own three, or a fan triple from the near-plane-clipped
460      * loop).
461      *
462      * <p>This method performs:</p>
463      * <ul>
464      *   <li>Backface culling check (if enabled)</li>
465      *   <li>Mouse interaction detection</li>
466      *   <li>Mipmap level selection based on screen coverage</li>
467      *   <li>Scanline rasterization with texture sampling</li>
468      * </ul>
469      *
470      * @param renderBuffer the rendering context containing the pixel buffer
471      * @param v1           the first triangle vertex
472      * @param v2           the second triangle vertex
473      * @param v3           the third triangle vertex
474      */
475     private void paintTriangle(final RenderingContext renderBuffer,
476                                final Vertex v1, final Vertex v2, final Vertex v3) {
477
478         final Point2D projectedPoint1 = v1.onScreenCoordinate(renderBuffer);
479         final Point2D projectedPoint2 = v2.onScreenCoordinate(renderBuffer);
480         final Point2D projectedPoint3 = v3.onScreenCoordinate(renderBuffer);
481
482         if (mouseInteractionController != null)
483             if (renderBuffer.getMouseEvent() != null)
484                 if (pointWithinPolygon(
485                         renderBuffer.getMouseEvent().coordinate, projectedPoint1, projectedPoint2, projectedPoint3)) {
486                     final double[] uv = textureCoordinateAt(
487                             renderBuffer.getMouseEvent().coordinate,
488                             projectedPoint1, projectedPoint2, projectedPoint3,
489                             v1, v2, v3, renderBuffer);
490                     renderBuffer.setCurrentObjectUnderMouseCursor(
491                             mouseInteractionController, uv[0], uv[1]);
492                 }
493
494         // Show polygon boundaries (for debugging)
495         if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders)
496             showBorders(renderBuffer);
497
498         // Keep double precision to eliminate T-junction gaps from truncation errors
499         final double y1 = projectedPoint1.y;
500         final double y2 = projectedPoint2.y;
501         final double y3 = projectedPoint3.y;
502
503         // Find top-most point (use ceil to include all pixels triangle touches)
504         int yTop = (int) Math.ceil(Math.min(y1, Math.min(y2, y3)));
505         if (yTop < 0) yTop = 0;
506
507         // Find bottom-most point (use floor to include all pixels triangle touches)
508         int yBottom = (int) Math.floor(Math.max(y1, Math.max(y2, y3)));
509         if (yBottom >= renderBuffer.height) yBottom = renderBuffer.height - 1;
510
511         // Clamp to render Y bounds (use renderMaxY - 1 because loop is inclusive)
512         yTop = Math.max(yTop, renderBuffer.renderMinY);
513         yBottom = Math.min(yBottom, renderBuffer.renderMaxY - 1);
514         if (yTop > yBottom) {
515             if (PROF)
516                 PROF_OFFY.incrementAndGet();
517             return;
518         }
519
520         // Snapshot the texture reference for the whole paint: the GI system
521         // may swap the composite lightmap on another thread mid-frame.
522         final Texture texture = this.texture;
523
524         final double edge12 = projectedPoint1.getDistanceTo(projectedPoint2);
525         final double edge13 = projectedPoint1.getDistanceTo(projectedPoint3);
526         final double edge23 = projectedPoint2.getDistanceTo(projectedPoint3);
527         final double totalVisibleDistance = edge12 + edge13 + edge23;
528
529         final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
530
531         // SDF text/vector-art path: coverage comes from the distance
532         // field, not from stored coverage, so the mipmap chain (which
533         // trades sharpness for alias-freedom) is bypassed entirely.
534         if (texture.isSdf()) {
535             paintSdf(yTop, yBottom, renderBuffer,
536                     projectedPoint1, projectedPoint2, projectedPoint3, scaleFactor,
537                     v1, v2, v3);
538             return;
539         }
540
541         paintFlat(renderBuffer, texture, backfaceCulling,
542                 projectedPoint1, projectedPoint2, projectedPoint3,
543                 v1.textureCoordinate, v2.textureCoordinate, v3.textureCoordinate,
544                 v1.transformedCoordinate(renderBuffer).z,
545                 v2.transformedCoordinate(renderBuffer).z,
546                 v3.transformedCoordinate(renderBuffer).z,
547                 totalVisibleDistance,
548                 totalTextureDistance);
549     }
550
551     /**
552      * Shared rasterization core for one textured triangle whose vertices
553      * are already in screen space. Called both by the object-backed path
554      * ({@link #paintTriangle}) and by {@code TriangleMeshBlock} handles,
555      * whose vertex data lives in flat per-block arrays instead of
556      * {@link Vertex} objects — the math here is identical either way,
557      * keeping both paths bit-exact.
558      *
559      * <p>Mouse interaction and debug borders stay in the object path
560      * (mesh blocks do not support picking). SDF textures are rejected:
561      * mesh blocks never carry them (enforced at block build time).</p>
562      *
563      * @param renderBuffer     the rendering context containing the pixel buffer
564      * @param texture          the texture to sample
565      * @param backfaceCulling  whether to cull counter-clockwise triangles
566      * @param projectedPoint1  screen-space vertex 1
567      * @param projectedPoint2  screen-space vertex 2
568      * @param projectedPoint3  screen-space vertex 3
569      * @param texturePoint1    UV (primary-texture pixels) of vertex 1
570      * @param texturePoint2    UV of vertex 2
571      * @param texturePoint3    UV of vertex 3
572      * @param z1               camera-space depth of vertex 1
573      * @param z2               camera-space depth of vertex 2
574      * @param z3               camera-space depth of vertex 3
575      * @param totalVisibleDistance screen-edge perimeter for mipmap
576      *                             selection, computed once per paint call
577      *                             (or once per slot for mesh blocks) —
578      *                             callers share it instead of the core
579      *                             recomputing per tile
580      * @param totalTextureDistance UV perimeter for mipmap selection. For a
581      *                             near-plane-clipped fan this is the
582      *                             ORIGINAL triangle's perimeter (the clip
583      *                             does not change the texture's texel
584      *                             density), matching the object path.
585      */
586     void paintFlat(final RenderingContext renderBuffer,
587                    final Texture texture,
588                    final boolean backfaceCulling,
589                    final Point2D projectedPoint1,
590                    final Point2D projectedPoint2,
591                    final Point2D projectedPoint3,
592                    final Point2D texturePoint1,
593                    final Point2D texturePoint2,
594                    final Point2D texturePoint3,
595                    final double z1, final double z2, final double z3,
596                    final double totalVisibleDistance,
597                    final double totalTextureDistance) {
598
599         // Z-buffer two-pass classification: opaque-class triangles
600         // paint in pass 1 (depth test + write), alpha-class in pass 2
601         // (depth test, no write) — see RenderAggregator.paintSorted.
602         final boolean alphaClass = texture.isSdf() || texture.hasAlpha;
603         if ((renderBuffer.depthPass == 1) == alphaClass)
604             return;
605
606                    if (PROF) {
607             PROF_TRIS.incrementAndGet();
608             if ((projectedPoint2.x - projectedPoint1.x)
609                     * (projectedPoint3.y - projectedPoint1.y)
610                     - (projectedPoint3.x - projectedPoint1.x)
611                     * (projectedPoint2.y - projectedPoint1.y) >= 0)
612                 PROF_BACKFACE.incrementAndGet();
613             final double bw = Math.max(projectedPoint1.x, Math.max(
614                     projectedPoint2.x, projectedPoint3.x))
615                     - Math.min(projectedPoint1.x, Math.min(
616                             projectedPoint2.x, projectedPoint3.x));
617             final double bh = Math.max(projectedPoint1.y, Math.max(
618                     projectedPoint2.y, projectedPoint3.y))
619                     - Math.min(projectedPoint1.y, Math.min(
620                             projectedPoint2.y, projectedPoint3.y));
621             if (bw * bh < 4)
622                 PROF_TINY.incrementAndGet();
623         }
624
625         if (backfaceCulling) {
626             final double signedArea = (projectedPoint2.x - projectedPoint1.x)
627                     * (projectedPoint3.y - projectedPoint1.y)
628                     - (projectedPoint3.x - projectedPoint1.x)
629                     * (projectedPoint2.y - projectedPoint1.y);
630             if (signedArea >= 0)
631                 return;
632         }
633
634         // Keep double precision to eliminate T-junction gaps from truncation errors
635         final double y1 = projectedPoint1.y;
636         final double y2 = projectedPoint2.y;
637         final double y3 = projectedPoint3.y;
638
639         // Find top-most point (use ceil to include all pixels triangle touches)
640         int yTop = (int) Math.ceil(Math.min(y1, Math.min(y2, y3)));
641         if (yTop < 0) yTop = 0;
642
643         // Find bottom-most point (use floor to include all pixels triangle touches)
644         int yBottom = (int) Math.floor(Math.max(y1, Math.max(y2, y3)));
645         if (yBottom >= renderBuffer.height) yBottom = renderBuffer.height - 1;
646
647         // Clamp to render Y bounds (use renderMaxY - 1 because loop is inclusive)
648         yTop = Math.max(yTop, renderBuffer.renderMinY);
649         yBottom = Math.min(yBottom, renderBuffer.renderMaxY - 1);
650         if (yTop > yBottom) {
651             if (PROF)
652                 PROF_OFFY.incrementAndGet();
653             return;
654         }
655
656         if (texture.isSdf())
657             throw new IllegalStateException(
658                     "SDF textures are not supported in mesh blocks");
659
660         final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
661
662         final TextureBitmap mipmap = texture.getMipmapForScale(scaleFactor);
663
664         if (perspectiveCorrectionEnabled) {
665             if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z && z3 > PERSPECTIVE_MIN_Z) {
666                 // Affine mapping is within half a texel of exact
667                 // perspective for small or nearly-flat triangles, making
668                 // the perspective setup pointless for them: the midpoint
669                 // error of affine vs exact is ~= texelSpan*(zRatio-1)/4
670                 // where texelSpan is the texture range (in selected-mip
671                 // texels) the triangle covers — NOT its pixel size (a
672                 // triangle can map many texels into few pixels; measured
673                 // 2026-09-06: a 4px span with a 56-texel range deviated 3
674                 // texels under the pixel-size rule). Distant clusters of
675                 // small triangles render affine.
676                 // Verified by TexturedTrianglePerspectiveTest#affineWithinHalfTexelBound.
677                 final double mf0 = mipmap.multiplicationFactor;
678                 final double tu1 = texturePoint1.x * mf0;
679                 final double tv1 = texturePoint1.y * mf0;
680                 final double tu2 = texturePoint2.x * mf0;
681                 final double tv2 = texturePoint2.y * mf0;
682                 final double tu3 = texturePoint3.x * mf0;
683                 final double tv3 = texturePoint3.y * mf0;
684                 final double texelSpan = Math.max(
685                         Math.max(Math.abs(tu2 - tu1), Math.abs(tv2 - tv1)),
686                         Math.max(
687                                 Math.max(Math.abs(tu3 - tu1), Math.abs(tv3 - tv1)),
688                                 Math.max(Math.abs(tu3 - tu2), Math.abs(tv3 - tv2))));
689                 final double zMin = Math.min(z1, Math.min(z2, z3));
690                 final double zMax = Math.max(z1, Math.max(z2, z3));
691                 if (texelSpan * (zMax / zMin - 1d) < 2d) {
692                     paintAffine(yTop, yBottom, mipmap, renderBuffer,
693                             projectedPoint1, projectedPoint2, projectedPoint3,
694                             texturePoint1, texturePoint2, texturePoint3,
695                             z1, z2, z3);
696                     return;
697                 }
698
699                 // Quake-style perspective-correct mapping: interpolate
700                 // (u/z, v/z, 1/z), which are linear in screen space, and
701                 // recover exact (u, v) every PERSPECTIVE_CORRECTION_INTERVAL
702                 // pixels in the scanline. The mipmap multiplication factor
703                 // is folded into the gradients here, so the scanline works
704                 // directly in texture pixel units.
705                 final double mf = mipmap.multiplicationFactor;
706
707                 final double sw1 = 1d / z1;
708                 final double sw2 = 1d / z2;
709                 final double sw3 = 1d / z3;
710
711                 final double su1 = texturePoint1.x * mf * sw1;
712                 final double sv1 = texturePoint1.y * mf * sw1;
713                 final double su2 = texturePoint2.x * mf * sw2;
714                 final double sv2 = texturePoint2.y * mf * sw2;
715                 final double su3 = texturePoint3.x * mf * sw3;
716                 final double sv3 = texturePoint3.y * mf * sw3;
717
718                 final PerspectiveBorderInterpolator[] pi = PERSPECTIVE_INTERPOLATORS.get();
719                 pi[0].setPoints(projectedPoint1, projectedPoint2, su1, sv1, sw1, su2, sv2, sw2);
720                 pi[1].setPoints(projectedPoint1, projectedPoint3, su1, sv1, sw1, su3, sv3, sw3);
721                 pi[2].setPoints(projectedPoint2, projectedPoint3, su2, sv2, sw2, su3, sv3, sw3);
722
723                 {
724                     // 1/z rides the same edge interpolation; spans
725                     // depth-test before texturing.
726                     final double zw1 = 1d / z1;
727                     final double zw2 = 1d / z2;
728                     final double zw3 = 1d / z3;
729                     pi[0].setPointsZW(zw1, zw2);
730                     pi[1].setPointsZW(zw1, zw3);
731                     pi[2].setPointsZW(zw2, zw3);
732                     for (int y = yTop; y <= yBottom; y++) {
733                         if (pi[0].containsY(y)) {
734                             if (pi[1].containsY(y))
735                                 drawHorizontalLinePerspectiveZ(pi[0], pi[1], y, renderBuffer, mipmap);
736                             else if (pi[2].containsY(y))
737                                 drawHorizontalLinePerspectiveZ(pi[0], pi[2], y, renderBuffer, mipmap);
738                         } else if (pi[1].containsY(y)) {
739                             if (pi[2].containsY(y))
740                                 drawHorizontalLinePerspectiveZ(pi[1], pi[2], y, renderBuffer, mipmap);
741                         }
742                     }
743                     return;
744                 }
745             }
746         }
747
748         paintAffine(yTop, yBottom, mipmap, renderBuffer,
749                 projectedPoint1, projectedPoint2, projectedPoint3,
750                 texturePoint1, texturePoint2, texturePoint3,
751                 z1, z2, z3);
752     }
753
754     /**
755      * Computes the perspective-correct texture coordinate at a screen-space
756      * point known to lie inside the triangle.
757      *
758      * <p>Screen-space barycentric weights are divided by the camera-space z
759      * of each vertex and renormalized — the same (u/z, v/z, 1/z) math the
760      * perspective-correct scanline path uses — so the returned coordinate
761      * matches the texel that was actually painted at that pixel, even at
762      * steep viewing angles. Texture coordinates are in primary-texture
763      * pixels (no mipmap factor applied).</p>
764      *
765      * @return double[2] with {u, v} in primary-texture pixels
766      */
767     private static double[] textureCoordinateAt(final Point2D point,
768                                                 final Point2D p1, final Point2D p2, final Point2D p3,
769                                                 final Vertex v1, final Vertex v2, final Vertex v3,
770                                                 final RenderingContext renderBuffer) {
771         final double denom = (p2.y - p3.y) * (p1.x - p3.x)
772                 + (p3.x - p2.x) * (p1.y - p3.y);
773         if (Math.abs(denom) < 1e-9)
774             // degenerate on screen; the hit pixel is effectively a vertex
775             return new double[]{v1.textureCoordinate.x, v1.textureCoordinate.y};
776
777         double w1 = ((p2.y - p3.y) * (point.x - p3.x)
778                 + (p3.x - p2.x) * (point.y - p3.y)) / denom;
779         double w2 = ((p3.y - p1.y) * (point.x - p3.x)
780                 + (p1.x - p3.x) * (point.y - p3.y)) / denom;
781         double w3 = 1d - w1 - w2;
782
783         final double z1 = v1.transformedCoordinate(renderBuffer).z;
784         final double z2 = v2.transformedCoordinate(renderBuffer).z;
785         final double z3 = v3.transformedCoordinate(renderBuffer).z;
786
787         if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z
788                 && z3 > PERSPECTIVE_MIN_Z) {
789             w1 /= z1;
790             w2 /= z2;
791             w3 /= z3;
792             final double sum = w1 + w2 + w3;
793             w1 /= sum;
794             w2 /= sum;
795             w3 /= sum;
796         }
797         // near-plane straddlers: plain screen-space barycentric (affine),
798         // matching the affine fallback path used for painting them
799
800         return new double[]{
801                 w1 * v1.textureCoordinate.x + w2 * v2.textureCoordinate.x
802                         + w3 * v3.textureCoordinate.x,
803                 w1 * v1.textureCoordinate.y + w2 * v2.textureCoordinate.y
804                         + w3 * v3.textureCoordinate.y};
805     }
806
807     /**
808      * SDF (signed distance field) rendering path. Coverage is not stored
809      * in the texture; it is re-derived per pixel from a smooth distance
810      * mask, so edges stay sharp at any magnification and fade to clean
811      * gray under minification. Layers: {@code texture.primaryBitmap} is
812      * the background color layer, {@code texture.sdfForeground} the ink
813      * color layer (both sampled nearest — they are flat per region),
814      * {@code texture.sdfMask} the distance field (sampled bilinear).
815      *
816      * <p>Minification is handled analytically: the coverage window is
817      * widened by the screen-space pixel footprint, which gives correct
818      * area coverage without a mipmap chain.</p>
819      *
820      * @param scaleFactor the same screen-pixels-per-texel estimate the
821      *                    mipmap selection uses (times 1.2)
822      */
823     private void paintSdf(final int yTop, final int yBottom,
824                           final RenderingContext renderBuffer,
825                           final Point2D projectedPoint1, final Point2D projectedPoint2,
826                           final Point2D projectedPoint3, final double scaleFactor,
827                           final Vertex v1, final Vertex v2, final Vertex v3) {
828         // SDF (text/decal) is alpha-class — it paints in the
829         // back-to-front alpha pass only, without depth interaction.
830         if (renderBuffer.depthPass == 1)
831             return;
832         // Per-axis screen-space UV gradients (affine estimate — adequate
833         // for a footprint). Text on an angled plane is minified mostly
834         // along ONE axis; an isotropic average would blur the axis that
835         // still has resolution to spare.
836         double footX;
837         double footY;
838         final double ex = projectedPoint2.x - projectedPoint1.x;
839         final double ey = projectedPoint2.y - projectedPoint1.y;
840         final double fx3 = projectedPoint3.x - projectedPoint1.x;
841         final double fy3 = projectedPoint3.y - projectedPoint1.y;
842         final double denom = ex * fy3 - fx3 * ey;
843         if (Math.abs(denom) > 1e-9) {
844             final double u1 = v1.textureCoordinate.x;
845             final double vv1 = v1.textureCoordinate.y;
846             final double du21 = v2.textureCoordinate.x - u1;
847             final double dv21 = v2.textureCoordinate.y - vv1;
848             final double du31 = v3.textureCoordinate.x - u1;
849             final double dv31 = v3.textureCoordinate.y - vv1;
850             final double dudx = (du21 * fy3 - du31 * ey) / denom;
851             final double dudy = (du31 * ex - du21 * fx3) / denom;
852             final double dvdx = (dv21 * fy3 - dv31 * ey) / denom;
853             final double dvdy = (dv31 * ex - dv21 * fx3) / denom;
854             footX = Math.hypot(dudx, dvdx);
855             footY = Math.hypot(dudy, dvdy);
856         } else {
857             footX = footY = 1.2d / scaleFactor;
858         }
859
860         // The coverage window follows the SHARPEST axis: one screen pixel
861         // spans texelsPerPixel texels along it, i.e.
862         // texelsPerPixel/(2*spread) of the normalized mask range; aaK
863         // converts a mask sample (0..255, edge at 127.5) into fixed-point
864         // coverage in [0, 256]: cov = (127.5 - d)*aaK + 128.
865         final double maxFootprint = Math.max(footX, footY);
866         double texelsPerPixel = Math.max(Math.min(footX, footY), 0.01d);
867         if (maxFootprint > 1d) {
868             texelsPerPixel /= SDF_SHARPEN;
869         }
870         final double aaK = (2d * texture.sdfSpreadTexels) / texelsPerPixel / 255d * 256d;
871
872         // No mip chain for SDF layers. A distance field's edge gradient
873         // spans just 2 texels, so a half/quarter-res mask visibly melts
874         // glyph edges — and because the two triangles of a rectangle get
875         // slightly different perspective footprints, they crossed mip
876         // thresholds at different distances, producing a hard diagonal
877         // quality split plus sudden blur steps while dollying (observed
878         // 2026-09-06). Sampling the primary field costs some bandwidth
879         // under minification, but text surfaces are small and the
880         // per-pixel sample count is what matters. Quality then degrades
881         // smoothly with distance instead of in steps.
882         final TextureBitmap mask = texture.sdfMask;
883         final TextureBitmap fg = texture.sdfForeground;
884         final TextureBitmap bg = texture.primaryBitmap;
885         final double mf = mask.multiplicationFactor;
886
887         // Under minification, area-correct coverage reads as low-contrast
888         // gray haze. Two perceptual corrections (A/B-tuned 2026-09-06 on
889         // far+angled text): SDF_SHARPEN narrows the coverage window below
890         // one pixel (kills the haze halo, keeps edges crisp at the cost
891         // of a little shimmer), and a mild coverage gamma < 1 (stem
892         // darkening, the small-ppm font rasterizer trick) keeps thin
893         // strokes present.
894         // Knobs: -De3d.sdf.gamma=1.4 forces a fixed gamma (0 = auto),
895         // -De3d.sdf.sharpen=1 restores the pixel-exact window.
896         final int[] covLut;
897         final double gamma = SDF_GAMMA != 0 ? SDF_GAMMA
898                 : Math.max(0.75d, 1d - 0.08d * (Math.log(maxFootprint) / Math.log(2d)));
899         if (gamma != 1d && maxFootprint > 1d) {
900             covLut = new int[257];
901             for (int i = 0; i <= 256; i++) {
902                 covLut[i] = Math.min(256, (int) (256d * Math.pow(i / 256d, gamma)));
903             }
904         } else {
905             covLut = null;
906         }
907
908         boolean usePerspective = false;
909         double su1 = 0, sv1 = 0, sw1 = 0;
910         double su2 = 0, sv2 = 0, sw2 = 0;
911         double su3 = 0, sv3 = 0, sw3 = 0;
912         if (perspectiveCorrectionEnabled) {
913             final double z1 = v1.transformedCoordinate(renderBuffer).z;
914             final double z2 = v2.transformedCoordinate(renderBuffer).z;
915             final double z3 = v3.transformedCoordinate(renderBuffer).z;
916             if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z && z3 > PERSPECTIVE_MIN_Z) {
917                 // Same affine-sufficiency test as the coverage path
918                 // (mask/fg/bg are all primary resolution, mf = 1).
919                 final double tu1 = v1.textureCoordinate.x;
920                 final double tv1 = v1.textureCoordinate.y;
921                 final double tu2 = v2.textureCoordinate.x;
922                 final double tv2 = v2.textureCoordinate.y;
923                 final double tu3 = v3.textureCoordinate.x;
924                 final double tv3 = v3.textureCoordinate.y;
925                 final double texelSpan = Math.max(
926                         Math.max(Math.abs(tu2 - tu1), Math.abs(tv2 - tv1)),
927                         Math.max(
928                                 Math.max(Math.abs(tu3 - tu1), Math.abs(tv3 - tv1)),
929                                 Math.max(Math.abs(tu3 - tu2), Math.abs(tv3 - tv2))));
930                 final double zMin = Math.min(z1, Math.min(z2, z3));
931                 final double zMax = Math.max(z1, Math.max(z2, z3));
932                 usePerspective = texelSpan * (zMax / zMin - 1d) >= 2d;
933                 if (usePerspective) {
934                     sw1 = 1d / z1;
935                     sw2 = 1d / z2;
936                     sw3 = 1d / z3;
937                     su1 = tu1 * mf * sw1;
938                     sv1 = tv1 * mf * sw1;
939                     su2 = tu2 * mf * sw2;
940                     sv2 = tv2 * mf * sw2;
941                     su3 = tu3 * mf * sw3;
942                     sv3 = tv3 * mf * sw3;
943                 }
944             }
945         }
946
947         if (SDF_DEBUG && (usePerspective != sdfDebugLastPerspective
948                 || Math.abs(footY - sdfDebugLastFootY) > 0.5)) {
949             sdfDebugLastPerspective = usePerspective;
950             sdfDebugLastFootY = footY;
951             System.err.printf("[SDF] perspective=%b footX=%.2f footY=%.2f aaK=%.3f%n",
952                     usePerspective, footX, footY, aaK);
953         }
954
955         if (usePerspective) {
956             final PerspectiveBorderInterpolator[] pi = PERSPECTIVE_INTERPOLATORS.get();
957             pi[0].setPoints(projectedPoint1, projectedPoint2, su1, sv1, sw1, su2, sv2, sw2);
958             pi[1].setPoints(projectedPoint1, projectedPoint3, su1, sv1, sw1, su3, sv3, sw3);
959             pi[2].setPoints(projectedPoint2, projectedPoint3, su2, sv2, sw2, su3, sv3, sw3);
960
961             for (int y = yTop; y <= yBottom; y++) {
962                 if (pi[0].containsY(y)) {
963                     if (pi[1].containsY(y))
964                         drawHorizontalLinePerspectiveSdf(pi[0], pi[1], y, renderBuffer, mask, fg, bg, aaK, covLut);
965                     else if (pi[2].containsY(y))
966                         drawHorizontalLinePerspectiveSdf(pi[0], pi[2], y, renderBuffer, mask, fg, bg, aaK, covLut);
967                 } else if (pi[1].containsY(y)) {
968                     if (pi[2].containsY(y))
969                         drawHorizontalLinePerspectiveSdf(pi[1], pi[2], y, renderBuffer, mask, fg, bg, aaK, covLut);
970                 }
971             }
972             return;
973         }
974
975         final PolygonBorderInterpolator[] interpolators = INTERPOLATORS.get();
976         final PolygonBorderInterpolator pbi1 = interpolators[0];
977         final PolygonBorderInterpolator pbi2 = interpolators[1];
978         final PolygonBorderInterpolator pbi3 = interpolators[2];
979
980         pbi1.setPoints(projectedPoint1, projectedPoint2, v1.textureCoordinate, v2.textureCoordinate);
981         pbi2.setPoints(projectedPoint1, projectedPoint3, v1.textureCoordinate, v3.textureCoordinate);
982         pbi3.setPoints(projectedPoint2, projectedPoint3, v2.textureCoordinate, v3.textureCoordinate);
983
984         for (int y = yTop; y <= yBottom; y++) {
985             if (pbi1.containsY(y)) {
986                 if (pbi2.containsY(y))
987                     drawHorizontalLineSdf(pbi1, pbi2, y, renderBuffer, mask, fg, bg, aaK, mf, covLut);
988                 else if (pbi3.containsY(y))
989                     drawHorizontalLineSdf(pbi1, pbi3, y, renderBuffer, mask, fg, bg, aaK, mf, covLut);
990             } else if (pbi2.containsY(y)) {
991                 if (pbi3.containsY(y))
992                     drawHorizontalLineSdf(pbi2, pbi3, y, renderBuffer, mask, fg, bg, aaK, mf, covLut);
993             }
994         }
995     }
996
997     /**
998      * SDF scanline, affine mapping. Texture coordinates are scaled by the
999      * selected mip's multiplication factor (all layers share one mip
1000      * level, so one factor covers mask and both color layers).
1001      */
1002     private void drawHorizontalLineSdf(final PolygonBorderInterpolator line1,
1003                                        final PolygonBorderInterpolator line2, final int y,
1004                                        final RenderingContext renderBuffer,
1005                                        final TextureBitmap mask, final TextureBitmap fg,
1006                                        final TextureBitmap bg, final double aaK,
1007                                        final double mf, final int[] covLut) {
1008         line1.setCurrentY(y);
1009         line2.setCurrentY(y);
1010
1011         int x1 = line1.getX();
1012         int x2 = line2.getX();
1013
1014         final double tx1, ty1, tx2, ty2;
1015         if (x1 <= x2) {
1016             tx1 = line1.getTX() * mf;
1017             ty1 = line1.getTY() * mf;
1018             tx2 = line2.getTX() * mf;
1019             ty2 = line2.getTY() * mf;
1020         } else {
1021             final int tmp = x1;
1022             x1 = x2;
1023             x2 = tmp;
1024             tx1 = line2.getTX() * mf;
1025             ty1 = line2.getTY() * mf;
1026             tx2 = line1.getTX() * mf;
1027             ty2 = line1.getTY() * mf;
1028         }
1029
1030         final double realWidth = x2 - x1;
1031         final double realX1 = x1;
1032
1033         if (x1 < renderBuffer.renderMinX)
1034             x1 = renderBuffer.renderMinX;
1035         if (x2 >= renderBuffer.renderMaxX)
1036             x2 = renderBuffer.renderMaxX;
1037
1038         int renderBufferOffset = (y * renderBuffer.width) + x1;
1039         final int[] renderBufferPixels = renderBuffer.pixels;
1040
1041         final double txStep = (tx2 - tx1) / realWidth;
1042         final double tyStep = (ty2 - ty1) / realWidth;
1043
1044         double tx = tx1 + txStep * (x1 - realX1);
1045         double ty = ty1 + tyStep * (x1 - realX1);
1046
1047         final int[] maskPixels = mask.pixels;
1048         final int[] fgPixels = fg.pixels;
1049         final int[] bgPixels = bg.pixels;
1050         final int mw = mask.width;
1051         final int mh = mask.height;
1052         final double bilinearCapX = mw - 1.0001d;
1053         final double bilinearCapY = mh - 1.0001d;
1054         final int mw1 = mw - 1;
1055         final int mh1 = mh - 1;
1056
1057         for (int x = x1; x < x2; x++) {
1058             // Fixed-point bilinear distance fetch (8.8 fractions)
1059             final double ctx = tx < 0 ? 0 : Math.min(tx, bilinearCapX);
1060             final double cty = ty < 0 ? 0 : Math.min(ty, bilinearCapY);
1061             final int x0 = (int) ctx;
1062             final int y0 = (int) cty;
1063             final int fx = (int) ((ctx - x0) * 256);
1064             final int fy = (int) ((cty - y0) * 256);
1065             final int row0 = y0 * mw + x0;
1066             final int row1 = row0 + mw;
1067             final int m00 = (maskPixels[row0] >> 16) & 0xff;
1068             final int m10 = (maskPixels[row0 + 1] >> 16) & 0xff;
1069             final int m01 = (maskPixels[row1] >> 16) & 0xff;
1070             final int m11 = (maskPixels[row1 + 1] >> 16) & 0xff;
1071             final int d = (m00 * (256 - fx) * (256 - fy) + m10 * fx * (256 - fy)
1072                     + m01 * (256 - fx) * fy + m11 * fx * fy) >> 16;
1073
1074             int cov = (int) ((127.5d - d) * aaK + 128d);
1075             if (cov < 0) cov = 0;
1076             else if (cov > 256) cov = 256;
1077             if (covLut != null) cov = covLut[cov];
1078
1079             int itx = (int) tx;
1080             int ity = (int) ty;
1081             if (itx < 0) itx = 0;
1082             else if (itx > mw1) itx = mw1;
1083             if (ity < 0) ity = 0;
1084             else if (ity > mh1) ity = mh1;
1085             final int addr = ity * mw + itx;
1086
1087             final int srcPixel;
1088             if (cov <= 0) {
1089                 srcPixel = bgPixels[addr];
1090             } else if (cov >= 256) {
1091                 srcPixel = fgPixels[addr];
1092             } else {
1093                 final int bgP = bgPixels[addr];
1094                 final int fgP = fgPixels[addr];
1095                 final int a = (bgP >>> 24) + ((((int) (fgP >>> 24) - (bgP >>> 24)) * cov) >> 8);
1096                 final int r = ((bgP >> 16) & 0xff) + (((((fgP >> 16) & 0xff) - ((bgP >> 16) & 0xff)) * cov) >> 8);
1097                 final int g = ((bgP >> 8) & 0xff) + (((((fgP >> 8) & 0xff) - ((bgP >> 8) & 0xff)) * cov) >> 8);
1098                 final int b = (bgP & 0xff) + ((((fgP & 0xff) - (bgP & 0xff)) * cov) >> 8);
1099                 srcPixel = (a << 24) | (r << 16) | (g << 8) | b;
1100             }
1101
1102             final int srcAlpha = (srcPixel >> 24) & 0xff;
1103             if (srcAlpha == 255) {
1104                 renderBufferPixels[renderBufferOffset] = srcPixel;
1105             } else if (srcAlpha != 0) {
1106                 final int destPixel = renderBufferPixels[renderBufferOffset];
1107                 final int destR = (destPixel >> 16) & 0xff;
1108                 final int destG = (destPixel >> 8) & 0xff;
1109                 final int destB = destPixel & 0xff;
1110                 final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8);
1111                 final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8);
1112                 final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8);
1113                 renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
1114             }
1115
1116             tx += txStep;
1117             ty += tyStep;
1118             renderBufferOffset++;
1119         }
1120     }
1121
1122     /**
1123      * SDF scanline with Quake-style subdivided perspective correction —
1124      * same stepping structure as {@link #drawHorizontalLinePerspectiveZ},
1125      * with the coverage fetch replaced by the distance-field evaluation.
1126      */
1127     private void drawHorizontalLinePerspectiveSdf(
1128             final PerspectiveBorderInterpolator line1,
1129             final PerspectiveBorderInterpolator line2,
1130             final int y,
1131             final RenderingContext renderBuffer,
1132             final TextureBitmap mask, final TextureBitmap fg,
1133             final TextureBitmap bg, final double aaK, final int[] covLut) {
1134
1135         line1.setCurrentY(y);
1136         line2.setCurrentY(y);
1137
1138         int x1 = line1.getX();
1139         int x2 = line2.getX();
1140
1141         final double su1, sv1, sw1;
1142         final double su2, sv2, sw2;
1143
1144         if (x1 <= x2) {
1145             su1 = line1.getSU();
1146             sv1 = line1.getSV();
1147             sw1 = line1.getSW();
1148             su2 = line2.getSU();
1149             sv2 = line2.getSV();
1150             sw2 = line2.getSW();
1151         } else {
1152             final int tmp = x1;
1153             x1 = x2;
1154             x2 = tmp;
1155             su1 = line2.getSU();
1156             sv1 = line2.getSV();
1157             sw1 = line2.getSW();
1158             su2 = line1.getSU();
1159             sv2 = line1.getSV();
1160             sw2 = line1.getSW();
1161         }
1162
1163         final double realWidth = x2 - x1;
1164         final double realX1 = x1;
1165
1166         if (x1 < renderBuffer.renderMinX)
1167             x1 = renderBuffer.renderMinX;
1168         if (x2 >= renderBuffer.renderMaxX)
1169             x2 = renderBuffer.renderMaxX;
1170
1171         final int span = x2 - x1;
1172         if (span <= 0)
1173             return;
1174
1175         int renderBufferOffset = (y * renderBuffer.width) + x1;
1176
1177         final double dsu = (su2 - su1) / realWidth;
1178         final double dsv = (sv2 - sv1) / realWidth;
1179         final double dsw = (sw2 - sw1) / realWidth;
1180
1181         double su = su1 + dsu * (x1 - realX1);
1182         double sv = sv1 + dsv * (x1 - realX1);
1183         double sw = sw1 + dsw * (x1 - realX1);
1184
1185         final int[] renderBufferPixels = renderBuffer.pixels;
1186
1187         final int[] maskPixels = mask.pixels;
1188         final int[] fgPixels = fg.pixels;
1189         final int[] bgPixels = bg.pixels;
1190         final int mw = mask.width;
1191         final int mh = mask.height;
1192         final double bilinearCapX = mw - 1.0001d;
1193         final double bilinearCapY = mh - 1.0001d;
1194         final int mw1 = mw - 1;
1195         final int mh1 = mh - 1;
1196
1197         // Same adaptive-interval ladder as the coverage path.
1198         final double ue1 = su1 / sw1;
1199         final double ue2 = su2 / sw2;
1200         final double ve1 = sv1 / sw1;
1201         final double ve2 = sv2 / sw2;
1202         final double wRatio = Math.max(sw1, sw2) / Math.min(sw1, sw2);
1203         final double texelRate = Math.max(Math.abs(ue2 - ue1), Math.abs(ve2 - ve1))
1204                 / realWidth * wRatio;
1205         final double k = Math.abs(dsw) / Math.min(sw1, sw2);
1206         final double curvature = texelRate * k;
1207         final int interval = curvature < 0.5 / (16 * 16) ? PERSPECTIVE_CORRECTION_INTERVAL
1208                 : curvature < 0.5 / (8 * 8) ? 8
1209                 : curvature < 0.5 / (4 * 4) ? 4
1210                 : curvature < 0.5 / (2 * 2) ? 2 : 1;
1211         final double invInterval = 1d / interval;
1212
1213         int done = 0;
1214         double invW = 1d / sw;
1215         double tx = su * invW;
1216         double ty = sv * invW;
1217         while (done < span) {
1218             final int block = Math.min(interval, span - done);
1219
1220             su += dsu * block;
1221             sv += dsv * block;
1222             sw += dsw * block;
1223             final double invWNext = 1d / sw;
1224             final double txNext = su * invWNext;
1225             final double tyNext = sv * invWNext;
1226
1227             final double invBlock = block == interval ? invInterval : 1d / block;
1228             final double txStep = (txNext - tx) * invBlock;
1229             final double tyStep = (tyNext - ty) * invBlock;
1230
1231             for (int i = 0; i < block; i++) {
1232                 // Fixed-point bilinear distance fetch (8.8 fractions)
1233                 final double ctx = tx < 0 ? 0 : Math.min(tx, bilinearCapX);
1234                 final double cty = ty < 0 ? 0 : Math.min(ty, bilinearCapY);
1235                 final int x0 = (int) ctx;
1236                 final int y0 = (int) cty;
1237                 final int fx = (int) ((ctx - x0) * 256);
1238                 final int fy = (int) ((cty - y0) * 256);
1239                 final int row0 = y0 * mw + x0;
1240                 final int row1 = row0 + mw;
1241                 final int m00 = (maskPixels[row0] >> 16) & 0xff;
1242                 final int m10 = (maskPixels[row0 + 1] >> 16) & 0xff;
1243                 final int m01 = (maskPixels[row1] >> 16) & 0xff;
1244                 final int m11 = (maskPixels[row1 + 1] >> 16) & 0xff;
1245                 final int d = (m00 * (256 - fx) * (256 - fy) + m10 * fx * (256 - fy)
1246                         + m01 * (256 - fx) * fy + m11 * fx * fy) >> 16;
1247
1248                 int cov = (int) ((127.5d - d) * aaK + 128d);
1249                 if (cov < 0) cov = 0;
1250                 else if (cov > 256) cov = 256;
1251                 if (covLut != null) cov = covLut[cov];
1252
1253                 int itx = (int) tx;
1254                 int ity = (int) ty;
1255                 if (itx < 0) itx = 0;
1256                 else if (itx > mw1) itx = mw1;
1257                 if (ity < 0) ity = 0;
1258                 else if (ity > mh1) ity = mh1;
1259                 final int addr = ity * mw + itx;
1260
1261                 final int srcPixel;
1262                 if (cov <= 0) {
1263                     srcPixel = bgPixels[addr];
1264                 } else if (cov >= 256) {
1265                     srcPixel = fgPixels[addr];
1266                 } else {
1267                     final int bgP = bgPixels[addr];
1268                     final int fgP = fgPixels[addr];
1269                     final int a = (bgP >>> 24) + ((((int) (fgP >>> 24) - (bgP >>> 24)) * cov) >> 8);
1270                     final int r = ((bgP >> 16) & 0xff) + (((((fgP >> 16) & 0xff) - ((bgP >> 16) & 0xff)) * cov) >> 8);
1271                     final int g = ((bgP >> 8) & 0xff) + (((((fgP >> 8) & 0xff) - ((bgP >> 8) & 0xff)) * cov) >> 8);
1272                     final int b = (bgP & 0xff) + ((((fgP & 0xff) - (bgP & 0xff)) * cov) >> 8);
1273                     srcPixel = (a << 24) | (r << 16) | (g << 8) | b;
1274                 }
1275
1276                 final int srcAlpha = (srcPixel >> 24) & 0xff;
1277                 if (srcAlpha == 255) {
1278                     renderBufferPixels[renderBufferOffset] = srcPixel;
1279                 } else if (srcAlpha != 0) {
1280                     final int destPixel = renderBufferPixels[renderBufferOffset];
1281                     final int destR = (destPixel >> 16) & 0xff;
1282                     final int destG = (destPixel >> 8) & 0xff;
1283                     final int destB = destPixel & 0xff;
1284                     final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8);
1285                     final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8);
1286                     final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8);
1287                     renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
1288                 }
1289
1290                 tx += txStep;
1291                 ty += tyStep;
1292                 renderBufferOffset++;
1293             }
1294
1295             done += block;
1296         }
1297     }
1298
1299     /**
1300      * Affine texture mapping (u, v linear in screen space). Used for
1301      * near-plane straddlers and for triangles small/flat enough that
1302      * affine is within half a texel of exact perspective mapping.
1303      */
1304     private void paintAffine(final int yTop, final int yBottom,
1305                              final TextureBitmap mipmap,
1306                              final RenderingContext renderBuffer,
1307                              final Point2D projectedPoint1, final Point2D projectedPoint2,
1308                              final Point2D projectedPoint3,
1309                              final Point2D texturePoint1, final Point2D texturePoint2,
1310                              final Point2D texturePoint3,
1311                              final double z1, final double z2, final double z3) {
1312         final PolygonBorderInterpolator[] interpolators = INTERPOLATORS.get();
1313         final PolygonBorderInterpolator pbi1 = interpolators[0];
1314         final PolygonBorderInterpolator pbi2 = interpolators[1];
1315         final PolygonBorderInterpolator pbi3 = interpolators[2];
1316
1317         pbi1.setPoints(projectedPoint1, projectedPoint2, texturePoint1, texturePoint2);
1318         pbi2.setPoints(projectedPoint1, projectedPoint3, texturePoint1, texturePoint3);
1319         pbi3.setPoints(projectedPoint2, projectedPoint3, texturePoint2, texturePoint3);
1320
1321         final double zw1 = 1d / z1;
1322         final double zw2 = 1d / z2;
1323         final double zw3 = 1d / z3;
1324         pbi1.setPointsZW(zw1, zw2);
1325         pbi2.setPointsZW(zw1, zw3);
1326         pbi3.setPointsZW(zw2, zw3);
1327         for (int y = yTop; y <= yBottom; y++) {
1328             if (pbi1.containsY(y)) {
1329                 if (pbi2.containsY(y))
1330                     drawHorizontalLineZ(pbi1, pbi2, y, renderBuffer, mipmap);
1331                 else if (pbi3.containsY(y))
1332                     drawHorizontalLineZ(pbi1, pbi3, y, renderBuffer, mipmap);
1333             } else if (pbi2.containsY(y)) {
1334                 if (pbi3.containsY(y))
1335                     drawHorizontalLineZ(pbi2, pbi3, y, renderBuffer, mipmap);
1336             }
1337         }
1338
1339     }
1340
1341     /**
1342      * Z-buffer span writer: per-pixel depth test (biased 1/z, linear
1343      * along the span) BEFORE the texture fetch. Opaque texels write
1344      * depth; blended texels write color only.
1345      */
1346     private void drawHorizontalLineZ(final PolygonBorderInterpolator line1,
1347                                      final PolygonBorderInterpolator line2,
1348                                      final int y,
1349                                      final RenderingContext renderBuffer,
1350                                      final TextureBitmap textureBitmap) {
1351
1352         line1.setCurrentY(y);
1353         line2.setCurrentY(y);
1354
1355         int x1 = line1.getX();
1356         int x2 = line2.getX();
1357
1358         final double tx1, ty1, zw1;
1359         final double tx2, ty2, zw2;
1360
1361         if (x1 <= x2) {
1362             tx1 = line1.getTX() * textureBitmap.multiplicationFactor;
1363             ty1 = line1.getTY() * textureBitmap.multiplicationFactor;
1364             zw1 = line1.getZW();
1365             tx2 = line2.getTX() * textureBitmap.multiplicationFactor;
1366             ty2 = line2.getTY() * textureBitmap.multiplicationFactor;
1367             zw2 = line2.getZW();
1368         } else {
1369             final int tmp = x1;
1370             x1 = x2;
1371             x2 = tmp;
1372
1373             tx1 = line2.getTX() * textureBitmap.multiplicationFactor;
1374             ty1 = line2.getTY() * textureBitmap.multiplicationFactor;
1375             zw1 = line2.getZW();
1376
1377             tx2 = line1.getTX() * textureBitmap.multiplicationFactor;
1378             ty2 = line1.getTY() * textureBitmap.multiplicationFactor;
1379             zw2 = line1.getZW();
1380         }
1381
1382         final double realWidth = x2 - x1;
1383         final double realX1 = x1;
1384
1385         if (x1 < renderBuffer.renderMinX)
1386             x1 = renderBuffer.renderMinX;
1387
1388         // x2 is exclusive: clamp to renderMaxX (see drawHorizontalLine)
1389         if (x2 >= renderBuffer.renderMaxX)
1390             x2 = renderBuffer.renderMaxX;
1391
1392         if (PROF) {
1393             PROF_SPANS.incrementAndGet();
1394             PROF_PIXELS.addAndGet(Math.max(0, x2 - x1));
1395         }
1396
1397         int renderBufferOffset = (y * renderBuffer.width) + x1;
1398         final int[] renderBufferPixels = renderBuffer.pixels;
1399         final float[] depth = renderBuffer.depth;
1400         // Alpha pass (depthPass 2): depth-test but never depth-write
1401         final boolean writeDepth = renderBuffer.depthPass != 2;
1402
1403         final double txStep = (tx2 - tx1) / realWidth;
1404         final double tyStep = (ty2 - ty1) / realWidth;
1405         final double dzw = (zw2 - zw1) / realWidth;
1406         double tx = tx1 + txStep * (x1 - realX1);
1407         double ty = ty1 + tyStep * (x1 - realX1);
1408         double zw = zw1 + dzw * (x1 - realX1);
1409
1410         final int[] texPixels = textureBitmap.pixels;
1411         final int texW = textureBitmap.width;
1412         final int texH = textureBitmap.height;
1413         final int texWMinus1 = texW - 1;
1414         final int texHMinus1 = texH - 1;
1415         // texture is null in unit tests: clamp (see drawHorizontalLine)
1416         final boolean wrap = texture != null && texture.wrap;
1417
1418         for (int x = x1; x < x2; x++) {
1419
1420             if (zw > depth[renderBufferOffset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) {
1421                 int itx = (int) tx;
1422                 int ity = (int) ty;
1423
1424                 if (wrap) {
1425                     itx = Math.floorMod(itx, texW);
1426                     ity = Math.floorMod(ity, texH);
1427                 } else {
1428                     if (itx < 0) itx = 0;
1429                     else if (itx > texWMinus1) itx = texWMinus1;
1430
1431                     if (ity < 0) ity = 0;
1432                     else if (ity > texHMinus1) ity = texHMinus1;
1433                 }
1434
1435                 final int sampledPixel = texPixels[ity * texW + itx];
1436                 final int srcPixel = giShaded ? shadeGi(sampledPixel) : sampledPixel;
1437                 final int srcAlpha = (srcPixel >> 24) & 0xff;
1438
1439                 if (srcAlpha == 255) {
1440                     renderBufferPixels[renderBufferOffset] = srcPixel;
1441                     if (writeDepth)
1442                         depth[renderBufferOffset] = (float) zw;
1443                 } else if (srcAlpha != 0) {
1444                     // Translucent: blend without writing depth
1445                     final int destPixel = renderBufferPixels[renderBufferOffset];
1446                     final int destR = (destPixel >> 16) & 0xff;
1447                     final int destG = (destPixel >> 8) & 0xff;
1448                     final int destB = destPixel & 0xff;
1449
1450                     final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8);
1451                     final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8);
1452                     final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8);
1453
1454                     renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
1455                 }
1456             }
1457
1458             tx += txStep;
1459             ty += tyStep;
1460             zw += dzw;
1461             renderBufferOffset++;
1462         }
1463
1464     }
1465
1466     /**
1467      * Checks if backface culling is enabled for this triangle.
1468      *
1469      * @return {@code true} if backface culling is enabled
1470      */
1471     public boolean isBackfaceCullingEnabled() {
1472         return backfaceCulling;
1473     }
1474
1475     /**
1476      * Enables or disables backface culling for this triangle.
1477      *
1478      * @param backfaceCulling {@code true} to enable backface culling
1479      */
1480     public void setBackfaceCulling(final boolean backfaceCulling) {
1481         this.backfaceCulling = backfaceCulling;
1482     }
1483
1484     /**
1485      * Draws the triangle border edges in yellow (for debugging).
1486      *
1487      * @param renderBuffer the rendering context
1488      */
1489     private void showBorders(final RenderingContext renderBuffer) {
1490
1491         final Point2D projectedPoint1 = vertices.get(0).onScreenCoordinate(renderBuffer);
1492         final Point2D projectedPoint2 = vertices.get(1).onScreenCoordinate(renderBuffer);
1493         final Point2D projectedPoint3 = vertices.get(2).onScreenCoordinate(renderBuffer);
1494
1495         final int x1 = (int) projectedPoint1.x;
1496         final int y1 = (int) projectedPoint1.y;
1497         final int x2 = (int) projectedPoint2.x;
1498         final int y2 = (int) projectedPoint2.y;
1499         final int x3 = (int) projectedPoint3.x;
1500         final int y3 = (int) projectedPoint3.y;
1501
1502         renderBuffer.executeWithGraphics(g -> {
1503             g.setColor(Color.YELLOW);
1504             g.drawLine(x1, y1, x2, y2);
1505             g.drawLine(x3, y3, x2, y2);
1506             g.drawLine(x1, y1, x3, y3);
1507         });
1508     }
1509
1510 }