94d8195cc4bebdafd722a67bb7c8a65fced3561d
[aukio-3d.git] /
1 package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
2
3 import eu.svjatoslav.aukio.e3d.geometry.Box;
4 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
5 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
6 import eu.svjatoslav.aukio.e3d.gui.HiZPyramid;
7 import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
8 import eu.svjatoslav.aukio.e3d.gui.StereoEye;
9 import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator;
10 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape;
11 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture;
12 import eu.svjatoslav.aukio.e3d.math.TransformStack;
13
14 /**
15  * A block of textured triangles stored as flat primitive arrays
16  * (struct-of-arrays) instead of one object graph per triangle. Built once
17  * (off the render thread), then every frame a single tight loop applies
18  * the composed camera transform to all vertices — sequential memory
19  * access instead of pointer chasing through {@code Vertex}/{@code Point3D}
20  * soup, which is what made the transform phase memory-latency-bound.
21  *
22  * <p>Interaction with the rest of the pipeline is unchanged: each visible
23  * triangle is queued as a thin preallocated {@link MeshTriangle} handle
24  * into the same {@link RenderAggregator}, sorted by the same comparator,
25  * binned into the same tile grid, and painted by the same
26  * {@link TexturedTriangle#paintFlat} core — bit-exact with the
27  * object-backed path.</p>
28  *
29  * <p>Subpixel-cull verdicts are cached per triangle ({@link #cullEpoch}):
30  * while the verdict epoch holds, a culled triangle costs one integer
31  * compare per frame — no vertex math at all.</p>
32  *
33  * <p>Limitations vs object-backed triangles: no mouse picking, no SDF
34  * textures (rejected at build), no GI/lightmap integration.</p>
35  */
36 public final class TriangleMeshBlock extends AbstractShape {
37
38     // Build-time geometry: 9 world doubles and 6 UV doubles per triangle.
39     private final double[] world;
40     private final double[] uv;
41     private final Texture[] textures;
42     private final boolean backfaceCull;
43     private final int triCount;
44     private final MeshTriangle[] handles;
45     private final Box boundingBox;
46
47     // Per-slot projected state: 3 screen/camera doubles per triangle per
48     // slot. (Sort Z and binning bounds are published into the handles'
49     // per-slot fields at transform time, not kept here.)
50     private final double[][] projX = new double[3][];
51     private final double[][] projY = new double[3][];
52     private final double[][] camZ = new double[3][];
53
54     // Subpixel-cull verdicts: epoch in which the triangle was found tiny.
55     private final int[] cullEpoch;
56
57     // Near-plane clip output per slot: packed (offset << 3) | count into
58     // clipStore, -1 = not clipped. Clip data is 7 doubles per loop vertex
59     // (camera x, y, z, u, v + screen x, y), at most 4 vertices per
60     // straddler. Sort Z and binning bounds for straddlers are derived
61     // from the store, exactly like the object path derives them from the
62     // clipped loop. clipTtd keeps the ORIGINAL triangle's UV perimeter
63     // per straddler (index = clip offset / 28): mipmap selection must
64     // use the unclipped texel density, exactly like the object path.
65     private static final int CLIP_STRIDE = 7;
66     private static final int CLIP_ENTRY = 4 * CLIP_STRIDE;
67     private final int[][] clipRef = new int[3][];
68     private final double[][] clipStore = new double[3][];
69     private final double[][] clipTtd = new double[3][];
70
71     /** Per-triangle UV perimeter (mipmap metric), computed once at build:
72      * the uv array never changes afterwards. Same expression and
73      * accumulation order as {@code TexturedTriangle}'s
74      * computeTotalTextureDistance: d(0,1) + d(0,2) + d(1,2). */
75     private final double[] uvPerimeter;
76
77     /** Per-triangle screen-edge perimeter (mipmap metric's visible side),
78      * computed once per slot in the transform loop — identical expression
79      * to what the paint core used to recompute per tile. */
80     private final double[][] screenPerim = new double[3][];
81     private final int[] clipUsed = new int[3];
82
83     // Scratch for the composed top transform of the current transform call.
84     private final double[] top = new double[12];
85
86     /**
87      * Builds a block from baked world-space triangle soup.
88      *
89      * @param world         9 doubles per triangle (x0,y0,z0,x1,...), world
90      *                      space; the array is adopted, not copied
91      * @param uv            6 doubles per triangle (u0,v0,...) in primary
92      *                      texture pixels; adopted
93      * @param textures      one texture per triangle
94      * @param backfaceCull  cull clockwise triangles on screen
95      */
96     public TriangleMeshBlock(final double[] world, final double[] uv,
97                              final Texture[] textures,
98                              final boolean backfaceCull) {
99         this.triCount = textures.length;
100         if (world.length != triCount * 9 || uv.length != triCount * 6)
101             throw new IllegalArgumentException("array length mismatch");
102         this.world = world;
103         this.uv = uv;
104         this.textures = textures;
105         this.backfaceCull = backfaceCull;
106         for (final Texture texture : textures)
107             if (texture != null && texture.isSdf())
108                 throw new IllegalArgumentException(
109                         "SDF textures are not supported in mesh blocks");
110
111         this.handles = new MeshTriangle[triCount];
112         for (int t = 0; t < triCount; t++)
113             handles[t] = new MeshTriangle(this, t, textures[t]);
114
115         this.cullEpoch = new int[triCount];
116         java.util.Arrays.fill(cullEpoch, -1);
117
118         for (int s = 0; s < 3; s++) {
119             projX[s] = new double[triCount * 3];
120             projY[s] = new double[triCount * 3];
121             camZ[s] = new double[triCount * 3];
122             clipRef[s] = new int[triCount];
123             java.util.Arrays.fill(clipRef[s], -1);
124             clipStore[s] = new double[256];
125             clipTtd[s] = new double[16];
126             screenPerim[s] = new double[triCount];
127         }
128
129         uvPerimeter = new double[triCount];
130         for (int t = 0; t < triCount; t++) {
131             final int u = t * 6;
132             final double d1 = Math.sqrt(
133                     ((uv[u] - uv[u + 2]) * (uv[u] - uv[u + 2]))
134                             + ((uv[u + 1] - uv[u + 3]) * (uv[u + 1] - uv[u + 3])));
135             final double d2 = Math.sqrt(
136                     ((uv[u] - uv[u + 4]) * (uv[u] - uv[u + 4]))
137                             + ((uv[u + 1] - uv[u + 5]) * (uv[u + 1] - uv[u + 5])));
138             final double d3 = Math.sqrt(
139                     ((uv[u + 2] - uv[u + 4]) * (uv[u + 2] - uv[u + 4]))
140                             + ((uv[u + 3] - uv[u + 5]) * (uv[u + 3] - uv[u + 5])));
141             uvPerimeter[t] = d1 + d2 + d3;
142         }
143
144         double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE,
145                 minZ = Double.MAX_VALUE;
146         double maxX = -Double.MAX_VALUE, maxY = -Double.MAX_VALUE,
147                 maxZ = -Double.MAX_VALUE;
148         for (int i = 0; i < world.length; i += 3) {
149             if (world[i] < minX) minX = world[i];
150             if (world[i] > maxX) maxX = world[i];
151             if (world[i + 1] < minY) minY = world[i + 1];
152             if (world[i + 1] > maxY) maxY = world[i + 1];
153             if (world[i + 2] < minZ) minZ = world[i + 2];
154             if (world[i + 2] > maxZ) maxZ = world[i + 2];
155         }
156         boundingBox = new Box(new Point3D(minX, minY, minZ),
157                 new Point3D(maxX, maxY, maxZ));
158     }
159
160     public int triCount() {
161         return triCount;
162     }
163
164     @Override
165     public Box getBoundingBox() {
166         return boundingBox;
167     }
168
169     @Override
170     public int getTransformWeight(final RenderingContext renderingContext) {
171         return Math.max(1, triCount);
172     }
173
174     /**
175      * Transforms every triangle of the block with the composed top
176      * transform of the stack (hoisted out of the loop), culls (near
177      * plane, subpixel with verdict cache, viewport) and queues a thin
178      * handle per surviving triangle. All expressions replicate
179      * {@code TransformStack.transform} /
180      * {@code Vertex.calculateLocationRelativeToViewer} exactly, so output
181      * is bit-identical with the object-backed path.
182      */
183     @Override
184     public void transform(final TransformStack transforms,
185                           final RenderAggregator aggregator,
186                           final RenderingContext renderingContext) {
187         final int slot = renderingContext.vertexSlot;
188         final double[] px = projX[slot];
189         final double[] py = projY[slot];
190         final double[] cz = camZ[slot];
191         final int[] cref = clipRef[slot];
192         clipUsed[slot] = 0;
193
194         transforms.getTopTransform(top);
195         final double r0 = top[0], r1 = top[1], r2 = top[2];
196         final double r3 = top[3], r4 = top[4], r5 = top[5];
197         final double r6 = top[6], r7 = top[7], r8 = top[8];
198         final double t0 = top[9], t1 = top[10], t2 = top[11];
199
200         final double near = renderingContext.nearPlaneDistance;
201         final double scale = renderingContext.projectionScale;
202         final double centerX = renderingContext.centerCoordinate.x;
203         final double centerY = renderingContext.centerCoordinate.y;
204         final double stereo = renderingContext.stereoViewportOffsetX;
205
206         // Hi-Z whole-block occlusion: test the world AABB against last
207         // frame's depth pyramid before touching a single triangle.
208         // Skipped in stereo (the pyramid is mono-view) and whenever a
209         // corner crosses the near plane (its projection is unreliable).
210         final HiZPyramid hiz = renderingContext.occlusionPyramid;
211         if (hiz != null
212                 && renderingContext.stereoEye == StereoEye.NONE) {
213             hiz.blocksTested.incrementAndGet();
214             final Point3D lo = boundingBox.p1, hi = boundingBox.p2;
215             double ax1 = Double.MAX_VALUE, ay1 = Double.MAX_VALUE;
216             double ax2 = -Double.MAX_VALUE, ay2 = -Double.MAX_VALUE;
217             double nearestW = -Double.MAX_VALUE;
218             boolean usable = true;
219             for (int c = 0; c < 8; c++) {
220                 final double wx = (c & 1) != 0 ? hi.x : lo.x;
221                 final double wy = (c & 2) != 0 ? hi.y : lo.y;
222                 final double wz = (c & 4) != 0 ? hi.z : lo.z;
223                 final double ccz = r6 * wx + r7 * wy + r8 * wz + t2;
224                 if (ccz <= near) {
225                     usable = false;
226                     break;
227                 }
228                 final double ccx = r0 * wx + r1 * wy + r2 * wz + t0;
229                 final double ccy = r3 * wx + r4 * wy + r5 * wz + t1;
230                 final double sx = ((ccx / ccz) * scale) + centerX + stereo;
231                 final double sy = ((ccy / ccz) * scale) + centerY;
232                 if (sx < ax1) ax1 = sx;
233                 if (sx > ax2) ax2 = sx;
234                 if (sy < ay1) ay1 = sy;
235                 if (sy > ay2) ay2 = sy;
236                 final double w = 1d / ccz;
237                 if (w > nearestW) nearestW = w;
238             }
239             if (usable && ax1 <= ax2 && ay1 <= ay2
240                     && hiz.occluded(ax1, ay1, ax2, ay2, nearestW)) {
241                 hiz.blocksCulled.incrementAndGet();
242                 return;
243             }
244         }
245         final double cullThreshold = renderingContext.subpixelCullingThreshold;
246         final int epoch = renderingContext.subpixelCullingEpoch;
247         final double rMinX = renderingContext.renderMinX;
248         final double rMaxX = renderingContext.renderMaxX;
249         final double rMinY = renderingContext.renderMinY;
250         final double rMaxY = renderingContext.renderMaxY;
251
252         for (int t = 0; t < triCount; t++) {
253             if (cullThreshold > 0 && cullEpoch[t] == epoch)
254                 continue;
255
256             final int w = t * 9;
257             // Same expression order as TransformStack.transform.
258             final double x0 = world[w], y0 = world[w + 1], z0 = world[w + 2];
259             final double cx0 = r0 * x0 + r1 * y0 + r2 * z0 + t0;
260             final double cy0 = r3 * x0 + r4 * y0 + r5 * z0 + t1;
261             final double cz0 = r6 * x0 + r7 * y0 + r8 * z0 + t2;
262             final double x1 = world[w + 3], y1 = world[w + 4], z1 = world[w + 5];
263             final double cx1 = r0 * x1 + r1 * y1 + r2 * z1 + t0;
264             final double cy1 = r3 * x1 + r4 * y1 + r5 * z1 + t1;
265             final double cz1 = r6 * x1 + r7 * y1 + r8 * z1 + t2;
266             final double x2 = world[w + 6], y2 = world[w + 7], z2 = world[w + 8];
267             final double cx2 = r0 * x2 + r1 * y2 + r2 * z2 + t0;
268             final double cy2 = r3 * x2 + r4 * y2 + r5 * z2 + t1;
269             final double cz2 = r6 * x2 + r7 * y2 + r8 * z2 + t2;
270
271             final boolean in0 = cz0 > near;
272             final boolean in1 = cz1 > near;
273             final boolean in2 = cz2 > near;
274
275             if (!in0 && !in1 && !in2) {
276                 cref[t] = -1;
277                 continue;
278             }
279
280             final int v = t * 3;
281             if (!(in0 && in1 && in2)) {
282                 clipAndQueue(t, v, slot, cx0, cy0, cz0, cx1, cy1, cz1,
283                         cx2, cy2, cz2, in0, in1, in2, near, cref,
284                         aggregator, renderingContext);
285                 continue;
286             }
287
288             cref[t] = -1;
289             cz[v] = cz0;
290             cz[v + 1] = cz1;
291             cz[v + 2] = cz2;
292             // Same expression order as
293             // Vertex.calculateLocationRelativeToViewer (divide, scale,
294             // add center, add stereo offset).
295             final double sx0 = ((cx0 / cz0) * scale) + centerX + stereo;
296             final double sy0 = ((cy0 / cz0) * scale) + centerY;
297             final double sx1 = ((cx1 / cz1) * scale) + centerX + stereo;
298             final double sy1 = ((cy1 / cz1) * scale) + centerY;
299             final double sx2 = ((cx2 / cz2) * scale) + centerX + stereo;
300             final double sy2 = ((cy2 / cz2) * scale) + centerY;
301             px[v] = sx0;
302             py[v] = sy0;
303             px[v + 1] = sx1;
304             py[v + 1] = sy1;
305             px[v + 2] = sx2;
306             py[v + 2] = sy2;
307             final double triZ = (cz0 + cz1 + cz2) / 3;
308
309             final double minX = Math.min(sx0, Math.min(sx1, sx2));
310             final double maxX = Math.max(sx0, Math.max(sx1, sx2));
311             final double minY = Math.min(sy0, Math.min(sy1, sy2));
312             final double maxY = Math.max(sy0, Math.max(sy1, sy2));
313
314             // Publish the same per-slot state an object-backed triangle
315             // would have written (paint margins are 0 for mesh tris):
316             // comparator and tile binning then read plain fields.
317             handles[t].publishSlotState(slot, triZ, minY, maxY, minX, maxX);
318
319             // Subpixel verdict with per-triangle cache (same raw-span
320             // semantics as AbstractCoordinateShape).
321             if (cullThreshold > 0
322                     && maxX - minX < cullThreshold
323                     && maxY - minY < cullThreshold) {
324                 cullEpoch[t] = epoch;
325                 continue;
326             }
327
328             // Viewport cull (paint margins are 0 for mesh triangles).
329             if (maxX < rMinX || minX >= rMaxX || maxY < rMinY || minY >= rMaxY)
330                 continue;
331
332             // Mipmap metric's visible side, once per slot instead of per
333             // tile: edge12 + edge13 + edge23, the same expression the
334             // paint core ran per tile (getDistanceTo sequence).
335             final double dx01 = sx0 - sx1, dy01 = sy0 - sy1;
336             final double dx02 = sx0 - sx2, dy02 = sy0 - sy2;
337             final double dx12 = sx1 - sx2, dy12 = sy1 - sy2;
338             screenPerim[slot][t] = Math.sqrt(dx01 * dx01 + dy01 * dy01)
339                     + Math.sqrt(dx02 * dx02 + dy02 * dy02)
340                     + Math.sqrt(dx12 * dx12 + dy12 * dy12);
341
342             aggregator.queueShapeForRendering(handles[t]);
343         }
344     }
345
346     /**
347      * Near-plane clip for one straddling triangle, Sutherland-Hodgman
348      * over the three edges with the exact interpolation expressions of
349      * {@code AbstractCoordinateShape.interpolateAtPlane}. Output goes to
350      * the slot's grow-only clip store; the handle is queued with a packed
351      * reference.
352      */
353     private void clipAndQueue(final int t, final int v, final int slot,
354                               final double cx0, final double cy0, final double cz0,
355                               final double cx1, final double cy1, final double cz1,
356                               final double cx2, final double cy2, final double cz2,
357                               final boolean in0, final boolean in1, final boolean in2,
358                               final double near, final int[] cref,
359                               final RenderAggregator aggregator,
360                               final RenderingContext renderingContext) {
361         double[] store = clipStore[slot];
362         int used = clipUsed[slot];
363         if (used + CLIP_ENTRY > store.length) {
364             final double[] grown = new double[store.length * 2];
365             System.arraycopy(store, 0, grown, 0, used);
366             store = grown;
367             clipStore[slot] = grown;
368             final double[] grownTtd = new double[grown.length / CLIP_ENTRY];
369             System.arraycopy(clipTtd[slot], 0, grownTtd, 0, clipTtd[slot].length);
370             clipTtd[slot] = grownTtd;
371         }
372         final int base = used;
373         clipTtd[slot][base / CLIP_ENTRY] = uvPerimeter[t];
374
375         final double scale = renderingContext.projectionScale;
376         final double centerX = renderingContext.centerCoordinate.x;
377         final double centerY = renderingContext.centerCoordinate.y;
378         final double stereo = renderingContext.stereoViewportOffsetX;
379
380         final double[] cx = {cx0, cx1, cx2};
381         final double[] cy = {cy0, cy1, cy2};
382         final double[] czz = {cz0, cz1, cz2};
383         final boolean[] in = {in0, in1, in2};
384         final int uvi = t * 6;
385
386         int n = 0;
387         double sumZ = 0;
388         for (int i = 0; i < 3; i++) {
389             final int j = (i + 1) % 3;
390             final boolean currentIn = in[i];
391             final boolean nextIn = in[j];
392             if (currentIn) {
393                 store[used++] = cx[i];
394                 store[used++] = cy[i];
395                 store[used++] = czz[i];
396                 store[used++] = uv[uvi + i * 2];
397                 store[used++] = uv[uvi + i * 2 + 1];
398                 // setCameraSpaceCoordinate expression, same order
399                 store[used++] = ((cx[i] / czz[i]) * scale) + centerX + stereo;
400                 store[used++] = ((cy[i] / czz[i]) * scale) + centerY;
401                 n++;
402                 sumZ += czz[i];
403             }
404             if (currentIn != nextIn) {
405                 final double tt = (near - czz[i]) / (czz[j] - czz[i]);
406                 final double ix = cx[i] + (cx[j] - cx[i]) * tt;
407                 final double iy = cy[i] + (cy[j] - cy[i]) * tt;
408                 final double iz = czz[i] + (czz[j] - czz[i]) * tt;
409                 store[used++] = ix;
410                 store[used++] = iy;
411                 store[used++] = iz;
412                 store[used++] = uv[uvi + i * 2]
413                         + (uv[uvi + j * 2] - uv[uvi + i * 2]) * tt;
414                 store[used++] = uv[uvi + i * 2 + 1]
415                         + (uv[uvi + j * 2 + 1] - uv[uvi + i * 2 + 1]) * tt;
416                 store[used++] = ((ix / iz) * scale) + centerX + stereo;
417                 store[used++] = ((iy / iz) * scale) + centerY;
418                 n++;
419                 sumZ += iz;
420             }
421         }
422
423         // Degenerate sliver: fewer loop points than a renderable triangle.
424         if (n < 3) {
425             cref[t] = -1;
426             return;
427         }
428         clipUsed[slot] = used;
429         cref[t] = (base << 3) | n;
430         // Object path averages Z and derives bounds over the clipped loop
431         double cMinX = Double.MAX_VALUE, cMaxX = -Double.MAX_VALUE;
432         double cMinY = Double.MAX_VALUE, cMaxY = -Double.MAX_VALUE;
433         for (int i = 0; i < n; i++) {
434             final double sx = store[base + i * CLIP_STRIDE + 5];
435             final double sy = store[base + i * CLIP_STRIDE + 6];
436             if (sx < cMinX) cMinX = sx;
437             if (sx > cMaxX) cMaxX = sx;
438             if (sy < cMinY) cMinY = sy;
439             if (sy > cMaxY) cMaxY = sy;
440         }
441         handles[t].publishSlotState(slot, sumZ / n, cMinY, cMaxY, cMinX, cMaxX);
442         aggregator.queueShapeForRendering(handles[t]);
443     }
444
445     // ---- handle-facing accessors (package-private) ----
446
447     double camZ(final int slot, final int tri, final int vertex) {
448         return camZ[slot][tri * 3 + vertex];
449     }
450
451     Texture texture(final int tri) {
452         return textures[tri];
453     }
454
455     boolean backfaceCull() {
456         return backfaceCull;
457     }
458
459     int clipOffset(final int slot, final int tri) {
460         final int ref = clipRef[slot][tri];
461         return ref < 0 ? -1 : ref >> 3;
462     }
463
464     int clipCount(final int slot, final int tri) {
465         return clipRef[slot][tri] & 7;
466     }
467
468     double[] clipStore(final int slot) {
469         return clipStore[slot];
470     }
471
472     double clipTtd(final int slot, final int clipOffset) {
473         return clipTtd[slot][clipOffset / CLIP_ENTRY];
474     }
475
476     /** The triangle's UV perimeter, precomputed at build (see field). */
477     double uvPerimeter(final int tri) {
478         return uvPerimeter[tri];
479     }
480
481     /**
482      * The triangle's screen-edge perimeter for this slot, precomputed in
483      * the transform loop (only valid for triangles queued unclipped).
484      */
485     double screenPerim(final int slot, final int tri) {
486         return screenPerim[slot][tri];
487     }
488
489     /**
490      * Loads one unclipped triangle vertex (screen + UV) into the scratch
491      * carriers, values exactly as computed at transform time.
492      */
493     void loadScreenVertex(final Point2D screen, final Point2D uvOut,
494                           final int slot, final int tri, final int vertex,
495                           final RenderingContext ctx) {
496         final int v = tri * 3 + vertex;
497         screen.x = projX[slot][v];
498         screen.y = projY[slot][v];
499         uvOut.x = uv[tri * 6 + vertex * 2];
500         uvOut.y = uv[tri * 6 + vertex * 2 + 1];
501     }
502 }