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