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