--- /dev/null
+/*
+ * 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.
+ *
+ * <p>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}.</p>
+ */
+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;
+ }
+ }
+}
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;
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. */
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));
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();
}
/**
* 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;
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];
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;
* @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);
}
* @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()));
}
/**
* @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.
- *
- * <p>CSG operations modify polygons in-place via BSP tree operations.
- * Cloning ensures the original polygon data is preserved.</p>
- *
- * @param polygons the polygons to clone
- * @return a new list containing deep clones of all polygons
- */
- private List<SolidPolygon> clonePolygons(final List<SolidPolygon> polygons) {
- final List<SolidPolygon> cloned = new ArrayList<>(polygons.size());
- for (final SolidPolygon p : polygons) {
- cloned.add(p.deepClone());
- }
- return cloned;
+ replaceSolidPolygons(Csg.intersect(extractSolidPolygons(),
+ other.extractSolidPolygons()));
}
/**
* value.</p>
*
* @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,
--- /dev/null
+/*
+ * 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.
+ *
+ * <p>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.</p>
+ *
+ * <p>All operations clone their inputs first (BSP operations mutate
+ * polygons in place), so caller lists are never modified.</p>
+ */
+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<SolidPolygon> union(final List<SolidPolygon> a,
+ final List<SolidPolygon> 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<SolidPolygon> subtract(final List<SolidPolygon> a,
+ final List<SolidPolygon> 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<SolidPolygon> intersect(final List<SolidPolygon> a,
+ final List<SolidPolygon> 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<SolidPolygon> clonePolygons(final List<SolidPolygon> polygons) {
+ final List<SolidPolygon> cloned = new ArrayList<>(polygons.size());
+ for (final SolidPolygon p : polygons) {
+ cloned.add(p.deepClone());
+ }
+ return cloned;
+ }
+}