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][];
70 private final int[] clipUsed = new int[3];
72 // Scratch for the composed top transform of the current transform call.
73 private final double[] top = new double[12];
76 * Builds a block from baked world-space triangle soup.
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
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");
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");
100 this.handles = new MeshTriangle[triCount];
101 for (int t = 0; t < triCount; t++)
102 handles[t] = new MeshTriangle(this, t, textures[t]);
104 this.cullEpoch = new int[triCount];
105 java.util.Arrays.fill(cullEpoch, -1);
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];
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];
129 boundingBox = new Box(new Point3D(minX, minY, minZ),
130 new Point3D(maxX, maxY, maxZ));
133 public int triCount() {
138 public Box getBoundingBox() {
143 public int getTransformWeight(final RenderingContext renderingContext) {
144 return Math.max(1, triCount);
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.
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];
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];
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;
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;
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;
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;
212 if (usable && ax1 <= ax2 && ay1 <= ay2
213 && hiz.occluded(ax1, ay1, ax2, ay2, nearestW)) {
214 hiz.blocksCulled.incrementAndGet();
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;
225 for (int t = 0; t < triCount; t++) {
226 if (cullThreshold > 0 && cullEpoch[t] == epoch)
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;
244 final boolean in0 = cz0 > near;
245 final boolean in1 = cz1 > near;
246 final boolean in2 = cz2 > near;
248 if (!in0 && !in1 && !in2) {
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);
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;
280 final double triZ = (cz0 + cz1 + cz2) / 3;
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));
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);
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;
301 // Viewport cull (paint margins are 0 for mesh triangles).
302 if (maxX < rMinX || minX >= rMaxX || maxY < rMinY || minY >= rMaxY)
305 aggregator.queueShapeForRendering(handles[t]);
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
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);
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;
335 final int base = used;
336 clipTtd[slot][base / CLIP_ENTRY] = origTtd(t);
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;
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;
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];
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;
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;
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;
386 // Degenerate sliver: fewer loop points than a renderable triangle.
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;
404 handles[t].publishSlotState(slot, sumZ / n, cMinY, cMaxY, cMinX, cMaxX);
405 aggregator.queueShapeForRendering(handles[t]);
408 // ---- handle-facing accessors (package-private) ----
410 double camZ(final int slot, final int tri, final int vertex) {
411 return camZ[slot][tri * 3 + vertex];
414 Texture texture(final int tri) {
415 return textures[tri];
418 boolean backfaceCull() {
422 int clipOffset(final int slot, final int tri) {
423 final int ref = clipRef[slot][tri];
424 return ref < 0 ? -1 : ref >> 3;
427 int clipCount(final int slot, final int tri) {
428 return clipRef[slot][tri] & 7;
431 double[] clipStore(final int slot) {
432 return clipStore[slot];
435 double clipTtd(final int slot, final int clipOffset) {
436 return clipTtd[slot][clipOffset / CLIP_ENTRY];
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).
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])));
459 * Loads one unclipped triangle vertex (screen + UV) into the scratch
460 * carriers, values exactly as computed at transform time.
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];