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