1 package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
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;
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.
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>
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>
33 * <p>Limitations vs object-backed triangles: no mouse picking, no SDF
34 * textures (rejected at build), no GI/lightmap integration.</p>
36 public final class TriangleMeshBlock extends AbstractShape {
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;
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][];
54 // Subpixel-cull verdicts: epoch in which the triangle was found tiny.
55 private final int[] cullEpoch;
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][];
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;
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];
83 // Scratch for the composed top transform of the current transform call.
84 private final double[] top = new double[12];
87 * Builds a block from baked world-space triangle soup.
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
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");
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");
111 this.handles = new MeshTriangle[triCount];
112 for (int t = 0; t < triCount; t++)
113 handles[t] = new MeshTriangle(this, t, textures[t]);
115 this.cullEpoch = new int[triCount];
116 java.util.Arrays.fill(cullEpoch, -1);
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];
129 uvPerimeter = new double[triCount];
130 for (int t = 0; t < triCount; t++) {
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;
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];
156 boundingBox = new Box(new Point3D(minX, minY, minZ),
157 new Point3D(maxX, maxY, maxZ));
160 public int triCount() {
165 public Box getBoundingBox() {
170 public int getTransformWeight(final RenderingContext renderingContext) {
171 return Math.max(1, triCount);
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.
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];
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];
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;
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;
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;
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;
239 if (usable && ax1 <= ax2 && ay1 <= ay2
240 && hiz.occluded(ax1, ay1, ax2, ay2, nearestW)) {
241 hiz.blocksCulled.incrementAndGet();
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;
252 for (int t = 0; t < triCount; t++) {
253 if (cullThreshold > 0 && cullEpoch[t] == epoch)
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;
271 final boolean in0 = cz0 > near;
272 final boolean in1 = cz1 > near;
273 final boolean in2 = cz2 > near;
275 if (!in0 && !in1 && !in2) {
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);
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;
307 final double triZ = (cz0 + cz1 + cz2) / 3;
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));
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);
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;
328 // Viewport cull (paint margins are 0 for mesh triangles).
329 if (maxX < rMinX || minX >= rMaxX || maxY < rMinY || minY >= rMaxY)
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);
342 aggregator.queueShapeForRendering(handles[t]);
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
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);
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;
372 final int base = used;
373 clipTtd[slot][base / CLIP_ENTRY] = uvPerimeter[t];
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;
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;
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];
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;
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;
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;
423 // Degenerate sliver: fewer loop points than a renderable triangle.
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;
441 handles[t].publishSlotState(slot, sumZ / n, cMinY, cMaxY, cMinX, cMaxX);
442 aggregator.queueShapeForRendering(handles[t]);
445 // ---- handle-facing accessors (package-private) ----
447 double camZ(final int slot, final int tri, final int vertex) {
448 return camZ[slot][tri * 3 + vertex];
451 Texture texture(final int tri) {
452 return textures[tri];
455 boolean backfaceCull() {
459 int clipOffset(final int slot, final int tri) {
460 final int ref = clipRef[slot][tri];
461 return ref < 0 ? -1 : ref >> 3;
464 int clipCount(final int slot, final int tri) {
465 return clipRef[slot][tri] & 7;
468 double[] clipStore(final int slot) {
469 return clipStore[slot];
472 double clipTtd(final int slot, final int clipOffset) {
473 return clipTtd[slot][clipOffset / CLIP_ENTRY];
476 /** The triangle's UV perimeter, precomputed at build (see field). */
477 double uvPerimeter(final int tri) {
478 return uvPerimeter[tri];
482 * The triangle's screen-edge perimeter for this slot, precomputed in
483 * the transform loop (only valid for triangles queued unclipped).
485 double screenPerim(final int slot, final int tri) {
486 return screenPerim[slot][tri];
490 * Loads one unclipped triangle vertex (screen + UV) into the scratch
491 * carriers, values exactly as computed at transform time.
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];