6084ee3ed06c04e3f7843ce1293d5776bc06b6c3
[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     private final int[] clipUsed = new int[3];
71
72     // Scratch for the composed top transform of the current transform call.
73     private final double[] top = new double[12];
74
75     /**
76      * Builds a block from baked world-space triangle soup.
77      *
78      * @param world         9 doubles per triangle (x0,y0,z0,x1,...), world
79      *                      space; the array is adopted, not copied
80      * @param uv            6 doubles per triangle (u0,v0,...) in primary
81      *                      texture pixels; adopted
82      * @param textures      one texture per triangle
83      * @param backfaceCull  cull clockwise triangles on screen
84      */
85     public TriangleMeshBlock(final double[] world, final double[] uv,
86                              final Texture[] textures,
87                              final boolean backfaceCull) {
88         this.triCount = textures.length;
89         if (world.length != triCount * 9 || uv.length != triCount * 6)
90             throw new IllegalArgumentException("array length mismatch");
91         this.world = world;
92         this.uv = uv;
93         this.textures = textures;
94         this.backfaceCull = backfaceCull;
95         for (final Texture texture : textures)
96             if (texture != null && texture.isSdf())
97                 throw new IllegalArgumentException(
98                         "SDF textures are not supported in mesh blocks");
99
100         this.handles = new MeshTriangle[triCount];
101         for (int t = 0; t < triCount; t++)
102             handles[t] = new MeshTriangle(this, t, textures[t]);
103
104         this.cullEpoch = new int[triCount];
105         java.util.Arrays.fill(cullEpoch, -1);
106
107         for (int s = 0; s < 3; s++) {
108             projX[s] = new double[triCount * 3];
109             projY[s] = new double[triCount * 3];
110             camZ[s] = new double[triCount * 3];
111             clipRef[s] = new int[triCount];
112             java.util.Arrays.fill(clipRef[s], -1);
113             clipStore[s] = new double[256];
114             clipTtd[s] = new double[16];
115         }
116
117         double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE,
118                 minZ = Double.MAX_VALUE;
119         double maxX = -Double.MAX_VALUE, maxY = -Double.MAX_VALUE,
120                 maxZ = -Double.MAX_VALUE;
121         for (int i = 0; i < world.length; i += 3) {
122             if (world[i] < minX) minX = world[i];
123             if (world[i] > maxX) maxX = world[i];
124             if (world[i + 1] < minY) minY = world[i + 1];
125             if (world[i + 1] > maxY) maxY = world[i + 1];
126             if (world[i + 2] < minZ) minZ = world[i + 2];
127             if (world[i + 2] > maxZ) maxZ = world[i + 2];
128         }
129         boundingBox = new Box(new Point3D(minX, minY, minZ),
130                 new Point3D(maxX, maxY, maxZ));
131     }
132
133     public int triCount() {
134         return triCount;
135     }
136
137     @Override
138     public Box getBoundingBox() {
139         return boundingBox;
140     }
141
142     @Override
143     public int getTransformWeight(final RenderingContext renderingContext) {
144         return Math.max(1, triCount);
145     }
146
147     /**
148      * Transforms every triangle of the block with the composed top
149      * transform of the stack (hoisted out of the loop), culls (near
150      * plane, subpixel with verdict cache, viewport) and queues a thin
151      * handle per surviving triangle. All expressions replicate
152      * {@code TransformStack.transform} /
153      * {@code Vertex.calculateLocationRelativeToViewer} exactly, so output
154      * is bit-identical with the object-backed path.
155      */
156     @Override
157     public void transform(final TransformStack transforms,
158                           final RenderAggregator aggregator,
159                           final RenderingContext renderingContext) {
160         final int slot = renderingContext.vertexSlot;
161         final double[] px = projX[slot];
162         final double[] py = projY[slot];
163         final double[] cz = camZ[slot];
164         final int[] cref = clipRef[slot];
165         clipUsed[slot] = 0;
166
167         transforms.getTopTransform(top);
168         final double r0 = top[0], r1 = top[1], r2 = top[2];
169         final double r3 = top[3], r4 = top[4], r5 = top[5];
170         final double r6 = top[6], r7 = top[7], r8 = top[8];
171         final double t0 = top[9], t1 = top[10], t2 = top[11];
172
173         final double near = renderingContext.nearPlaneDistance;
174         final double scale = renderingContext.projectionScale;
175         final double centerX = renderingContext.centerCoordinate.x;
176         final double centerY = renderingContext.centerCoordinate.y;
177         final double stereo = renderingContext.stereoViewportOffsetX;
178
179         // Hi-Z whole-block occlusion: test the world AABB against last
180         // frame's depth pyramid before touching a single triangle.
181         // Skipped in stereo (the pyramid is mono-view) and whenever a
182         // corner crosses the near plane (its projection is unreliable).
183         final HiZPyramid hiz = renderingContext.occlusionPyramid;
184         if (hiz != null
185                 && renderingContext.stereoEye == StereoEye.NONE) {
186             hiz.blocksTested.incrementAndGet();
187             final Point3D lo = boundingBox.p1, hi = boundingBox.p2;
188             double ax1 = Double.MAX_VALUE, ay1 = Double.MAX_VALUE;
189             double ax2 = -Double.MAX_VALUE, ay2 = -Double.MAX_VALUE;
190             double nearestW = -Double.MAX_VALUE;
191             boolean usable = true;
192             for (int c = 0; c < 8; c++) {
193                 final double wx = (c & 1) != 0 ? hi.x : lo.x;
194                 final double wy = (c & 2) != 0 ? hi.y : lo.y;
195                 final double wz = (c & 4) != 0 ? hi.z : lo.z;
196                 final double ccz = r6 * wx + r7 * wy + r8 * wz + t2;
197                 if (ccz <= near) {
198                     usable = false;
199                     break;
200                 }
201                 final double ccx = r0 * wx + r1 * wy + r2 * wz + t0;
202                 final double ccy = r3 * wx + r4 * wy + r5 * wz + t1;
203                 final double sx = ((ccx / ccz) * scale) + centerX + stereo;
204                 final double sy = ((ccy / ccz) * scale) + centerY;
205                 if (sx < ax1) ax1 = sx;
206                 if (sx > ax2) ax2 = sx;
207                 if (sy < ay1) ay1 = sy;
208                 if (sy > ay2) ay2 = sy;
209                 final double w = 1d / ccz;
210                 if (w > nearestW) nearestW = w;
211             }
212             if (usable && ax1 <= ax2 && ay1 <= ay2
213                     && hiz.occluded(ax1, ay1, ax2, ay2, nearestW)) {
214                 hiz.blocksCulled.incrementAndGet();
215                 return;
216             }
217         }
218         final double cullThreshold = renderingContext.subpixelCullingThreshold;
219         final int epoch = renderingContext.subpixelCullingEpoch;
220         final double rMinX = renderingContext.renderMinX;
221         final double rMaxX = renderingContext.renderMaxX;
222         final double rMinY = renderingContext.renderMinY;
223         final double rMaxY = renderingContext.renderMaxY;
224
225         for (int t = 0; t < triCount; t++) {
226             if (cullThreshold > 0 && cullEpoch[t] == epoch)
227                 continue;
228
229             final int w = t * 9;
230             // Same expression order as TransformStack.transform.
231             final double x0 = world[w], y0 = world[w + 1], z0 = world[w + 2];
232             final double cx0 = r0 * x0 + r1 * y0 + r2 * z0 + t0;
233             final double cy0 = r3 * x0 + r4 * y0 + r5 * z0 + t1;
234             final double cz0 = r6 * x0 + r7 * y0 + r8 * z0 + t2;
235             final double x1 = world[w + 3], y1 = world[w + 4], z1 = world[w + 5];
236             final double cx1 = r0 * x1 + r1 * y1 + r2 * z1 + t0;
237             final double cy1 = r3 * x1 + r4 * y1 + r5 * z1 + t1;
238             final double cz1 = r6 * x1 + r7 * y1 + r8 * z1 + t2;
239             final double x2 = world[w + 6], y2 = world[w + 7], z2 = world[w + 8];
240             final double cx2 = r0 * x2 + r1 * y2 + r2 * z2 + t0;
241             final double cy2 = r3 * x2 + r4 * y2 + r5 * z2 + t1;
242             final double cz2 = r6 * x2 + r7 * y2 + r8 * z2 + t2;
243
244             final boolean in0 = cz0 > near;
245             final boolean in1 = cz1 > near;
246             final boolean in2 = cz2 > near;
247
248             if (!in0 && !in1 && !in2) {
249                 cref[t] = -1;
250                 continue;
251             }
252
253             final int v = t * 3;
254             if (!(in0 && in1 && in2)) {
255                 clipAndQueue(t, v, slot, cx0, cy0, cz0, cx1, cy1, cz1,
256                         cx2, cy2, cz2, in0, in1, in2, near, cref,
257                         aggregator, renderingContext);
258                 continue;
259             }
260
261             cref[t] = -1;
262             cz[v] = cz0;
263             cz[v + 1] = cz1;
264             cz[v + 2] = cz2;
265             // Same expression order as
266             // Vertex.calculateLocationRelativeToViewer (divide, scale,
267             // add center, add stereo offset).
268             final double sx0 = ((cx0 / cz0) * scale) + centerX + stereo;
269             final double sy0 = ((cy0 / cz0) * scale) + centerY;
270             final double sx1 = ((cx1 / cz1) * scale) + centerX + stereo;
271             final double sy1 = ((cy1 / cz1) * scale) + centerY;
272             final double sx2 = ((cx2 / cz2) * scale) + centerX + stereo;
273             final double sy2 = ((cy2 / cz2) * scale) + centerY;
274             px[v] = sx0;
275             py[v] = sy0;
276             px[v + 1] = sx1;
277             py[v + 1] = sy1;
278             px[v + 2] = sx2;
279             py[v + 2] = sy2;
280             final double triZ = (cz0 + cz1 + cz2) / 3;
281
282             final double minX = Math.min(sx0, Math.min(sx1, sx2));
283             final double maxX = Math.max(sx0, Math.max(sx1, sx2));
284             final double minY = Math.min(sy0, Math.min(sy1, sy2));
285             final double maxY = Math.max(sy0, Math.max(sy1, sy2));
286
287             // Publish the same per-slot state an object-backed triangle
288             // would have written (paint margins are 0 for mesh tris):
289             // comparator and tile binning then read plain fields.
290             handles[t].publishSlotState(slot, triZ, minY, maxY, minX, maxX);
291
292             // Subpixel verdict with per-triangle cache (same raw-span
293             // semantics as AbstractCoordinateShape).
294             if (cullThreshold > 0
295                     && maxX - minX < cullThreshold
296                     && maxY - minY < cullThreshold) {
297                 cullEpoch[t] = epoch;
298                 continue;
299             }
300
301             // Viewport cull (paint margins are 0 for mesh triangles).
302             if (maxX < rMinX || minX >= rMaxX || maxY < rMinY || minY >= rMaxY)
303                 continue;
304
305             aggregator.queueShapeForRendering(handles[t]);
306         }
307     }
308
309     /**
310      * Near-plane clip for one straddling triangle, Sutherland-Hodgman
311      * over the three edges with the exact interpolation expressions of
312      * {@code AbstractCoordinateShape.interpolateAtPlane}. Output goes to
313      * the slot's grow-only clip store; the handle is queued with a packed
314      * reference.
315      */
316     private void clipAndQueue(final int t, final int v, final int slot,
317                               final double cx0, final double cy0, final double cz0,
318                               final double cx1, final double cy1, final double cz1,
319                               final double cx2, final double cy2, final double cz2,
320                               final boolean in0, final boolean in1, final boolean in2,
321                               final double near, final int[] cref,
322                               final RenderAggregator aggregator,
323                               final RenderingContext renderingContext) {
324         double[] store = clipStore[slot];
325         int used = clipUsed[slot];
326         if (used + CLIP_ENTRY > store.length) {
327             final double[] grown = new double[store.length * 2];
328             System.arraycopy(store, 0, grown, 0, used);
329             store = grown;
330             clipStore[slot] = grown;
331             final double[] grownTtd = new double[grown.length / CLIP_ENTRY];
332             System.arraycopy(clipTtd[slot], 0, grownTtd, 0, clipTtd[slot].length);
333             clipTtd[slot] = grownTtd;
334         }
335         final int base = used;
336         clipTtd[slot][base / CLIP_ENTRY] = origTtd(t);
337
338         final double scale = renderingContext.projectionScale;
339         final double centerX = renderingContext.centerCoordinate.x;
340         final double centerY = renderingContext.centerCoordinate.y;
341         final double stereo = renderingContext.stereoViewportOffsetX;
342
343         final double[] cx = {cx0, cx1, cx2};
344         final double[] cy = {cy0, cy1, cy2};
345         final double[] czz = {cz0, cz1, cz2};
346         final boolean[] in = {in0, in1, in2};
347         final int uvi = t * 6;
348
349         int n = 0;
350         double sumZ = 0;
351         for (int i = 0; i < 3; i++) {
352             final int j = (i + 1) % 3;
353             final boolean currentIn = in[i];
354             final boolean nextIn = in[j];
355             if (currentIn) {
356                 store[used++] = cx[i];
357                 store[used++] = cy[i];
358                 store[used++] = czz[i];
359                 store[used++] = uv[uvi + i * 2];
360                 store[used++] = uv[uvi + i * 2 + 1];
361                 // setCameraSpaceCoordinate expression, same order
362                 store[used++] = ((cx[i] / czz[i]) * scale) + centerX + stereo;
363                 store[used++] = ((cy[i] / czz[i]) * scale) + centerY;
364                 n++;
365                 sumZ += czz[i];
366             }
367             if (currentIn != nextIn) {
368                 final double tt = (near - czz[i]) / (czz[j] - czz[i]);
369                 final double ix = cx[i] + (cx[j] - cx[i]) * tt;
370                 final double iy = cy[i] + (cy[j] - cy[i]) * tt;
371                 final double iz = czz[i] + (czz[j] - czz[i]) * tt;
372                 store[used++] = ix;
373                 store[used++] = iy;
374                 store[used++] = iz;
375                 store[used++] = uv[uvi + i * 2]
376                         + (uv[uvi + j * 2] - uv[uvi + i * 2]) * tt;
377                 store[used++] = uv[uvi + i * 2 + 1]
378                         + (uv[uvi + j * 2 + 1] - uv[uvi + i * 2 + 1]) * tt;
379                 store[used++] = ((ix / iz) * scale) + centerX + stereo;
380                 store[used++] = ((iy / iz) * scale) + centerY;
381                 n++;
382                 sumZ += iz;
383             }
384         }
385
386         // Degenerate sliver: fewer loop points than a renderable triangle.
387         if (n < 3) {
388             cref[t] = -1;
389             return;
390         }
391         clipUsed[slot] = used;
392         cref[t] = (base << 3) | n;
393         // Object path averages Z and derives bounds over the clipped loop
394         double cMinX = Double.MAX_VALUE, cMaxX = -Double.MAX_VALUE;
395         double cMinY = Double.MAX_VALUE, cMaxY = -Double.MAX_VALUE;
396         for (int i = 0; i < n; i++) {
397             final double sx = store[base + i * CLIP_STRIDE + 5];
398             final double sy = store[base + i * CLIP_STRIDE + 6];
399             if (sx < cMinX) cMinX = sx;
400             if (sx > cMaxX) cMaxX = sx;
401             if (sy < cMinY) cMinY = sy;
402             if (sy > cMaxY) cMaxY = sy;
403         }
404         handles[t].publishSlotState(slot, sumZ / n, cMinY, cMaxY, cMinX, cMaxX);
405         aggregator.queueShapeForRendering(handles[t]);
406     }
407
408     // ---- handle-facing accessors (package-private) ----
409
410     double camZ(final int slot, final int tri, final int vertex) {
411         return camZ[slot][tri * 3 + vertex];
412     }
413
414     Texture texture(final int tri) {
415         return textures[tri];
416     }
417
418     boolean backfaceCull() {
419         return backfaceCull;
420     }
421
422     int clipOffset(final int slot, final int tri) {
423         final int ref = clipRef[slot][tri];
424         return ref < 0 ? -1 : ref >> 3;
425     }
426
427     int clipCount(final int slot, final int tri) {
428         return clipRef[slot][tri] & 7;
429     }
430
431     double[] clipStore(final int slot) {
432         return clipStore[slot];
433     }
434
435     double clipTtd(final int slot, final int clipOffset) {
436         return clipTtd[slot][clipOffset / CLIP_ENTRY];
437     }
438
439     /**
440      * The triangle's UV perimeter, with the exact expression and
441      * accumulation order of {@code TexturedTriangle}'s
442      * computeTotalTextureDistance: d(0,1) + d(0,2) + d(1,2).
443      */
444     double origTtd(final int tri) {
445         final int u = tri * 6;
446         final double d1 = Math.sqrt(
447                 ((uv[u] - uv[u + 2]) * (uv[u] - uv[u + 2]))
448                         + ((uv[u + 1] - uv[u + 3]) * (uv[u + 1] - uv[u + 3])));
449         final double d2 = Math.sqrt(
450                 ((uv[u] - uv[u + 4]) * (uv[u] - uv[u + 4]))
451                         + ((uv[u + 1] - uv[u + 5]) * (uv[u + 1] - uv[u + 5])));
452         final double d3 = Math.sqrt(
453                 ((uv[u + 2] - uv[u + 4]) * (uv[u + 2] - uv[u + 4]))
454                         + ((uv[u + 3] - uv[u + 5]) * (uv[u + 3] - uv[u + 5])));
455         return d1 + d2 + d3;
456     }
457
458     /**
459      * Loads one unclipped triangle vertex (screen + UV) into the scratch
460      * carriers, values exactly as computed at transform time.
461      */
462     void loadScreenVertex(final Point2D screen, final Point2D uvOut,
463                           final int slot, final int tri, final int vertex,
464                           final RenderingContext ctx) {
465         final int v = tri * 3 + vertex;
466         screen.x = projX[slot][v];
467         screen.y = projY[slot][v];
468         uvOut.x = uv[tri * 6 + vertex * 2];
469         uvOut.y = uv[tri * 6 + vertex * 2 + 1];
470     }
471 }