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