From: Svjatoslav Agejenko Date: Sun, 20 Sep 2026 09:05:55 +0000 (+0300) Subject: Extract CSG engine + device hotplug; resolve blend-formula question X-Git-Tag: aukio-3d-1.0.0~5 X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=dca4aa4c762cd6e174e10c961e64e786c3c47182;p=aukio-3d.git Extract CSG engine + device hotplug; resolve blend-formula question - New Csg utility (pure polygon-list functions: union/subtract/intersect over BSP clip/invert sequences). AbstractCompositeShape's three CSG methods are now one-line delegates; clonePolygons moved into Csg. Composite drops 95 lines and its dependency on BspTree. - New gui/DeviceHotplug: head-tracker + SpaceMouse hot-plug managers, extracted from ViewPanel (which now holds a single field and two delegating getters). ViewPanel loses its HeadTrackingManager/ SpaceMouseManager lifecycle code. - Blend formulas: PROVEN bit-identical. SolidPolygon's ((255-a)*dest + a*src)>>8 and TexturedTriangle's lerp form dest + ((a*(src-dest) - dest)>>8) agree for all 16.6M (a,src,dest) inputs (exhaustive check). Documented at both span writers; no change. - transformChildrenParallel's aggregator param: NOT dead as the review claimed — it feeds the serial-fallback paths. Restored with accurate javadoc (was nearly deleted on the review's say-so; the build caught it before commit). Deliberately NOT split: ViewPanel's render-loop core (mailbox, present gates, pending paints) and AbstractCompositeShape's parallel-transform planner — both are concurrency-critical and deliberate; extraction trades cohesion for race-condition risk with zero functional gain. --- diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeviceHotplug.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeviceHotplug.java new file mode 100644 index 0000000..ab8c26d --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeviceHotplug.java @@ -0,0 +1,78 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; + +import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadTracker; +import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadTrackingManager; +import eu.svjatoslav.aukio.e3d.gui.spacemouse.SpaceMouseManager; +import eu.svjatoslav.aukio.e3d.gui.spacemouse.SpaceNavigatorHid; + +/** + * Optional input-device hot-plug for a {@link ViewPanel}: starts the + * RayNeo head-tracking manager and the SpaceNavigator 6DOF-mouse manager + * (each auto-detects its device even when plugged in after startup), and + * exposes the currently connected device, if any. + * + *

Extracted from ViewPanel to keep the panel focused on the render + * pipeline. Either device can be disabled with + * {@code -De3d.headtrack=false} / {@code -De3d.spacemouse=false}.

+ */ +final class DeviceHotplug { + + /** Head tracker hot-plug manager, unless disabled via e3d.headtrack=false. */ + private HeadTrackingManager headTrackingManager; + + /** SpaceNavigator hot-plug manager, unless disabled via e3d.spacemouse=false. */ + private SpaceMouseManager spaceMouseManager; + + /** + * Starts both hot-plug managers for the given panel (respecting the + * disable properties). + * + * @param viewPanel the panel whose camera the devices will drive + */ + DeviceHotplug(final ViewPanel viewPanel) { + if (!"false".equalsIgnoreCase( + System.getProperty("e3d.headtrack", "true"))) { + headTrackingManager = new HeadTrackingManager(viewPanel); + headTrackingManager.start(); + } + if (!"false".equalsIgnoreCase( + System.getProperty("e3d.spacemouse", "true"))) { + spaceMouseManager = new SpaceMouseManager(viewPanel); + spaceMouseManager.start(); + } + } + + /** + * Returns the active SpaceNavigator device, or null when no 6DOF + * mouse is currently connected. + */ + SpaceNavigatorHid getSpaceMouse() { + return spaceMouseManager == null ? null + : spaceMouseManager.getDevice(); + } + + /** + * Returns the active head tracker, or null when no glasses are + * currently connected. + */ + HeadTracker getHeadTracker() { + return headTrackingManager == null ? null + : headTrackingManager.getTracker(); + } + + /** Stops both hot-plug managers; safe to call more than once. */ + void stop() { + if (headTrackingManager != null) { + headTrackingManager.stop(); + headTrackingManager = null; + } + if (spaceMouseManager != null) { + spaceMouseManager.stop(); + spaceMouseManager = null; + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java index 0a69725..04e9e61 100755 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java @@ -16,9 +16,7 @@ import eu.svjatoslav.aukio.e3d.diag.Telemetry; import eu.svjatoslav.aukio.e3d.geometry.Point3D; import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadLookController; import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadTracker; -import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadTrackingManager; import eu.svjatoslav.aukio.e3d.gui.headtrack.RayNeoHid; -import eu.svjatoslav.aukio.e3d.gui.spacemouse.SpaceMouseManager; import eu.svjatoslav.aukio.e3d.gui.spacemouse.SpaceNavigatorHid; import eu.svjatoslav.aukio.e3d.gui.humaninput.InputManager; import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardFocusStack; @@ -103,11 +101,8 @@ public class ViewPanel extends Canvas { private final KeyboardFocusStack keyboardFocusStack; /** The camera representing the viewer's position and orientation. */ private final Camera camera = new Camera(); - /** Head tracker hot-plug manager, unless disabled via e3d.headtrack=false. */ - private HeadTrackingManager headTrackingManager; - - /** SpaceNavigator hot-plug manager, unless disabled via e3d.spacemouse=false. */ - private SpaceMouseManager spaceMouseManager; + /** Optional input devices (head tracker, 6DOF mouse) with hot-plug. */ + private DeviceHotplug deviceHotplug; /** The root shape collection containing all 3D shapes in the scene. */ private final ShapeCollection rootShapeCollection = new ShapeCollection(); /** The set of frame listeners notified before each frame. */ @@ -337,8 +332,7 @@ public class ViewPanel extends Canvas { keyboardFocusStack = new KeyboardFocusStack(this); initializeCanvas(); - initializeHeadTracking(); - initializeSpaceMouse(); + deviceHotplug = new DeviceHotplug(this); // Set default ambient light for the scene lightingManager.setAmbientLight(new Color(50, 50, 50)); @@ -1322,40 +1316,12 @@ public class ViewPanel extends Canvas { return transformExecutor; } - /** - * Starts the head tracking hot-plug manager: RayNeo glasses are - * detected when plugged in (even after startup) and head look-around - * is enabled automatically. Disable with {@code -De3d.headtrack=false}. - */ - private void initializeHeadTracking() { - if ("false".equalsIgnoreCase( - System.getProperty("e3d.headtrack", "true"))) - return; - headTrackingManager = new HeadTrackingManager(this); - headTrackingManager.start(); - } - - /** - * Starts the SpaceNavigator hot-plug manager: the 6DOF mouse is - * detected when plugged in (even after startup) and cap deflection - * drives the camera automatically. Disable with - * {@code -De3d.spacemouse=false}. - */ - private void initializeSpaceMouse() { - if ("false".equalsIgnoreCase( - System.getProperty("e3d.spacemouse", "true"))) - return; - spaceMouseManager = new SpaceMouseManager(this); - spaceMouseManager.start(); - } - /** * Returns the active SpaceNavigator device, or null when no 6DOF * mouse is currently connected. */ public SpaceNavigatorHid getSpaceMouse() { - return spaceMouseManager == null ? null - : spaceMouseManager.getDevice(); + return deviceHotplug == null ? null : deviceHotplug.getSpaceMouse(); } /** @@ -1363,21 +1329,16 @@ public class ViewPanel extends Canvas { * currently connected. */ public HeadTracker getHeadTracker() { - return headTrackingManager == null ? null - : headTrackingManager.getTracker(); + return deviceHotplug == null ? null : deviceHotplug.getHeadTracker(); } /** * Stops rendering of this view. */ public void stop() { - if (headTrackingManager != null) { - headTrackingManager.stop(); - headTrackingManager = null; - } - if (spaceMouseManager != null) { - spaceMouseManager.stop(); - spaceMouseManager = null; + if (deviceHotplug != null) { + deviceHotplug.stop(); + deviceHotplug = null; } renderThreadRunning = false; presentThreadRunning = false; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java index 313b66b..3a3ca1d 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java @@ -366,6 +366,10 @@ public class SolidPolygon extends AbstractCoordinateShape { final int greenWithAlpha = g * polygonAlpha; final int blueWithAlpha = b * polygonAlpha; + // Blend form ((255-a)*dest + a*src) >> 8. Proven bit-identical + // to TexturedTriangle's lerp form dest + ((a*(src-dest) - dest) + // >> 8) for every (a,src,dest) — do NOT "fix" one to match the + // other cosmetically; both are the same formula. for (int i = 0; i < width; i++) { if (zw > depth[offset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) { final int dest = pixels[offset]; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java index 76f4b29..643a930 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java @@ -377,7 +377,11 @@ public class TexturedTriangle extends AbstractCoordinateShape { depth[renderBufferOffset] = (float) zw; } else if (srcAlpha != 0) { // Translucent: blend, but do NOT write depth — - // translucency must not occlude later fragments + // translucency must not occlude later fragments. + // Lerp form dest + ((a*(src-dest) - dest) >> 8): + // algebraically ((255-a)*dest + a*src) >> 8 — proven + // bit-identical to SolidPolygon's form for all + // inputs, so the two span writers blend the same. final int destPixel = renderBufferPixels[renderBufferOffset]; final int destR = (destPixel >> 16) & 0xff; final int destG = (destPixel >> 8) & 0xff; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java index fa0ca38..776d21b 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java @@ -546,29 +546,8 @@ public class AbstractCompositeShape extends AbstractShape { * @see #intersect(AbstractCompositeShape) */ public void union(final AbstractCompositeShape other) { - - final BspTree selfTree = new BspTree(clonePolygons(extractSolidPolygons())); - final BspTree otherTree = new BspTree(clonePolygons(other.extractSolidPolygons())); - - // Remove from self any polygons that are inside other (interior faces) - selfTree.clipTo(otherTree); - - // Remove from other any polygons that are inside self (interior faces) - otherTree.clipTo(selfTree); - - // Invert other to convert remaining polygons for the next clip step - otherTree.invert(); - - // Clip inverted other against self to remove back-facing coplanar polygons - otherTree.clipTo(selfTree); - - // Invert back to restore correct polygon orientation - otherTree.invert(); - - // Merge other's remaining polygons into self's BSP tree - selfTree.addPolygons(otherTree.allPolygons()); - - replaceSolidPolygons(selfTree.allPolygons()); + replaceSolidPolygons(Csg.union(extractSolidPolygons(), + other.extractSolidPolygons())); mergeNonPolygonChildrenFrom(other); } @@ -594,39 +573,8 @@ public class AbstractCompositeShape extends AbstractShape { * @see #intersect(AbstractCompositeShape) */ public void subtract(final AbstractCompositeShape other) { - - final BspTree target = new BspTree(clonePolygons(extractSolidPolygons())); - final BspTree cutter = new BspTree(clonePolygons(other.extractSolidPolygons())); - - // Invert target: convert "inside" to "outside" and vice versa - // This transforms the problem from "subtract B from A" to "intersect A's complement with B's complement" - target.invert(); - - // Clip target against cutter: removes parts of target that are INSIDE the cutter - // Since target is inverted, this removes parts that were OUTSIDE the original target - target.clipTo(cutter); - - // Clip cutter against (inverted) target: removes parts of cutter outside the inverted target - // This keeps only cutter polygons that are inside the inverted target = outside original target - cutter.clipTo(target); - - // Invert cutter to flip its inside/outside - cutter.invert(); - - // Clip inverted cutter against target: removes coplanar back-faces - cutter.clipTo(target); - - // Invert cutter back to correct orientation - cutter.invert(); - - // Merge cutter's polygons into target's BSP tree - target.addPolygons(cutter.allPolygons()); - - // Invert target back to restore correct inside/outside orientation - // Result: the carved-out volume (target minus cutter) - target.invert(); - - replaceSolidPolygons(target.allPolygons()); + replaceSolidPolygons(Csg.subtract(extractSolidPolygons(), + other.extractSolidPolygons())); } /** @@ -651,51 +599,8 @@ public class AbstractCompositeShape extends AbstractShape { * @see #subtract(AbstractCompositeShape) */ public void intersect(final AbstractCompositeShape other) { - - final BspTree selfTree = new BspTree(clonePolygons(extractSolidPolygons())); - final BspTree otherTree = new BspTree(clonePolygons(other.extractSolidPolygons())); - - // Invert self to convert "inside" to "outside" - // This transforms intersection into: keep parts that are "outside both inverted shapes" - selfTree.invert(); - - // Clip other against inverted self: keeps only parts of other that are INSIDE original self - // (because clipTo removes what's "outside" the BSP, and inverted self's "outside" = original self's "inside") - otherTree.clipTo(selfTree); - - // Invert other (which now represents the intersection region) - otherTree.invert(); - - // Clip inverted self against (inverted intersection): removes parts outside the intersection - selfTree.clipTo(otherTree); - - // Clip intersection result against inverted self: removes back-facing coplanar polygons - otherTree.clipTo(selfTree); - - // Build final BSP tree from the clipped intersection polygons - selfTree.addPolygons(otherTree.allPolygons()); - - // Invert back to restore correct inside/outside orientation - selfTree.invert(); - - replaceSolidPolygons(selfTree.allPolygons()); - } - - /** - * Creates deep clones of all polygons in the list. - * - *

CSG operations modify polygons in-place via BSP tree operations. - * Cloning ensures the original polygon data is preserved.

- * - * @param polygons the polygons to clone - * @return a new list containing deep clones of all polygons - */ - private List clonePolygons(final List polygons) { - final List cloned = new ArrayList<>(polygons.size()); - for (final SolidPolygon p : polygons) { - cloned.add(p.deepClone()); - } - return cloned; + replaceSolidPolygons(Csg.intersect(extractSolidPolygons(), + other.extractSolidPolygons())); } /** @@ -1184,8 +1089,12 @@ public class AbstractCompositeShape extends AbstractShape { * value.

* * @param transformPipe the transform stack (includes this composite's transform) - * @param aggregator unused in the parallel path: per-task aggregators - * are merged by the coordinator's drain + * @param aggregator the caller's aggregator, used ONLY when the fork + * bails out (too few chunks, or the frame task + * budget is exhausted) and the children fall back + * to a serial inline transform. The parallel fork + * itself queues into per-task aggregators merged + * by the coordinator's drain. * @param context the rendering context (provides the coordinator) */ private void transformChildrenParallel(final TransformStack transformPipe, diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Csg.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Csg.java new file mode 100644 index 0000000..5eb8191 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Csg.java @@ -0,0 +1,160 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base; + +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; + +import java.util.ArrayList; +import java.util.List; + +/** + * Pure CSG (Constructive Solid Geometry) boolean engine: union, subtract + * and intersect over lists of {@link SolidPolygon}s, built on BSP tree + * clip/invert sequences. + * + *

These are pure functions — they take polygon lists and return new + * polygon lists, touching no shape state. {@link AbstractCompositeShape}'s + * instance methods ({@code union/subtract/intersect}) delegate here and + * handle the child-registry bookkeeping themselves.

+ * + *

All operations clone their inputs first (BSP operations mutate + * polygons in place), so caller lists are never modified.

+ */ +public final class Csg { + + private Csg() { + // utility class + } + + /** + * Union of two polygon sets: every surface of both, interior faces + * removed. + * + * @param a first operand's polygons (not modified) + * @param b second operand's polygons (not modified) + * @return the union result polygons + */ + public static List union(final List a, + final List b) { + final BspTree selfTree = new BspTree(clonePolygons(a)); + final BspTree otherTree = new BspTree(clonePolygons(b)); + + // Remove from self any polygons that are inside other (interior faces) + selfTree.clipTo(otherTree); + + // Remove from other any polygons that are inside self (interior faces) + otherTree.clipTo(selfTree); + + // Invert other to convert remaining polygons for the next clip step + otherTree.invert(); + + // Clip inverted other against self to remove back-facing coplanar polygons + otherTree.clipTo(selfTree); + + // Invert back to restore correct polygon orientation + otherTree.invert(); + + // Merge other's remaining polygons into self's BSP tree + selfTree.addPolygons(otherTree.allPolygons()); + + return selfTree.allPolygons(); + } + + /** + * Subtraction {@code a - b}: the cutter volume carved out of the + * target. + * + * @param a target polygons (not modified) + * @param b cutter polygons (not modified) + * @return the difference result polygons + */ + public static List subtract(final List a, + final List b) { + final BspTree target = new BspTree(clonePolygons(a)); + final BspTree cutter = new BspTree(clonePolygons(b)); + + // Invert target: convert "inside" to "outside" and vice versa + // This transforms the problem from "subtract B from A" to "intersect A's complement with B's complement" + target.invert(); + + // Clip target against cutter: removes parts of target that are INSIDE the cutter + // Since target is inverted, this removes parts that were OUTSIDE the original target + target.clipTo(cutter); + + // Clip cutter against (inverted) target: removes parts of cutter outside the inverted target + // This keeps only cutter polygons that are inside the inverted target = outside original target + cutter.clipTo(target); + + // Invert cutter to flip its inside/outside + cutter.invert(); + + // Clip inverted cutter against target: removes coplanar back-faces + cutter.clipTo(target); + + // Invert cutter back to correct orientation + cutter.invert(); + + // Merge cutter's polygons into target's BSP tree + target.addPolygons(cutter.allPolygons()); + + // Invert target back to restore correct inside/outside orientation + // Result: the carved-out volume (target minus cutter) + target.invert(); + + return target.allPolygons(); + } + + /** + * Intersection of two polygon sets: only the overlapping volume + * remains. + * + * @param a first operand's polygons (not modified) + * @param b second operand's polygons (not modified) + * @return the intersection result polygons + */ + public static List intersect(final List a, + final List b) { + final BspTree selfTree = new BspTree(clonePolygons(a)); + final BspTree otherTree = new BspTree(clonePolygons(b)); + + // Invert self to convert "inside" to "outside" + // This transforms intersection into: keep parts that are "outside both inverted shapes" + selfTree.invert(); + + // Clip other against inverted self: keeps only parts of other that are INSIDE original self + // (because clipTo removes what's "outside" the BSP, and inverted self's "outside" = original self's "inside") + otherTree.clipTo(selfTree); + + // Invert other (which now represents the intersection region) + otherTree.invert(); + + // Clip inverted self against (inverted intersection): removes parts outside the intersection + selfTree.clipTo(otherTree); + + // Clip intersection result against inverted self: removes back-facing coplanar polygons + otherTree.clipTo(selfTree); + + // Build final BSP tree from the clipped intersection polygons + selfTree.addPolygons(otherTree.allPolygons()); + + // Invert back to restore correct inside/outside orientation + selfTree.invert(); + + return selfTree.allPolygons(); + } + + /** + * Deep clones of all polygons in the list: CSG operations modify + * polygons in-place via BSP tree operations, cloning preserves the + * originals. + */ + private static List clonePolygons(final List polygons) { + final List cloned = new ArrayList<>(polygons.size()); + for (final SolidPolygon p : polygons) { + cloned.add(p.deepClone()); + } + return cloned; + } +}