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