|-------------------------------+-----------------------------------------------------+-----------------------------------------------------------|
| *Create a window* | ~ViewFrame~ (~gui/ViewFrame.java~) | ~new ViewFrame()~ → ~.getViewPanel()~ |
| *Add shapes to scene* | ~ShapeCollection~ (~raster/ShapeCollection.java~) | ~viewPanel.getRootShapeCollection().addShape(shape)~ |
-| *Position camera* | ~Camera~ (~gui/Camera.java~) | ~camera.getTransform().setTranslation(Point3D)~ |
+| *Position camera* | ~Camera~ (~geometry/Camera.java~) | ~camera.getTransform().setTranslation(Point3D)~ |
| *Create a wireframe cube* | ~WireframeCube~ (~shapes/composite/wireframe/~) | ~new WireframeCube(center, halfSize, appearance)~ |
| *Create a solid cube* | ~SolidPolygonCube~ (~shapes/composite/solid/~) | ~new SolidPolygonCube(center, halfSize, color)~ |
| *Create a line* | ~Line~ (~shapes/basic/line/~) | ~new Line(p1, p2, color, width)~ |
|-----------------+--------------------------+------------------------------+--------------------------------------------------------------------------------------------------------|
| ~ViewPanel~ | ~gui/ViewPanel.java~ | AWT Canvas, render loop | ~.getRootShapeCollection()~, ~.getCamera()~, ~.getLightingManager()~, ~.addFrameListener()~, ~.stop()~ |
| ~ViewFrame~ | ~gui/ViewFrame.java~ | JFrame wrapper | ~new ViewFrame()~ → ~.getViewPanel()~ |
-| ~Camera~ | ~gui/Camera.java~ | Viewer position/orientation | ~.getTransform()~, ~.setTransform()~ |
+| ~Camera~ | ~geometry/Camera.java~ | Viewer position/orientation | ~.getTransform()~, ~.setTransform()~ |
| ~FrameListener~ | ~gui/FrameListener.java~ | Per-frame callback interface | ~.onFrame(panel, deltaMs)~ → return true to repaint |
** Input (~gui/humaninput/~)
*** Vertex — Rendering-Ready Coordinates
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] wraps a =Point3D= and adds the coordinate spaces needed during
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.html][Vertex]] wraps a =Point3D= and adds the coordinate spaces needed during
rendering. As a shape transforms through the render pipeline, each
vertex tracks its position in multiple spaces:
has 12 edges, and complex meshes have thousands.
In *Aukio 3D*, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class implements edges as renderable shapes. Each
-Line connects two [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] endpoints and stores two properties: a
+Line connects two [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.html][Vertex]] endpoints and stores two properties: a
[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#width][width]] in world units (adjusted for perspective during rendering) and a
[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#color][color]] with alpha transparency. The rendering algorithm switches
between two modes based on the projected screen width: thin lines below
| Use case | API | Computation | Location |
|----------------------+----------------------------------------------+----------------------------+-------------------|
-| BSP/CSG operations | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#getPlane()][SolidPolygon.getPlane()]] → [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#normal][Plane.normal]] | Lazy-cached once | =Plane= |
-| Per-frame shading | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]] → =cachedNormal= field | Recomputed every frame | =SolidPolygon= |
+| BSP/CSG operations | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#getPlane()][SolidPolygon.getPlane()]] → [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html#normal][Plane.normal]] | Lazy-cached once | =Plane= |
+| Per-frame shading | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html#computeNormal()][Plane.computeNormal()]] → =cachedNormal= field | Recomputed every frame | =SolidPolygon= |
| Lighting calculation | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#computeLighting()][LightingManager.computeLighting()]] | Uses normal via =dot(L,N)= | =LightingManager= |
**Implementation notes:**
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]]: shared zero-allocation helper for computing normals from three points
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Plane]]: stores normals in Hesse normal form (normal + distance) for BSP spatial partitioning
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html#normal][Vertex.normal]]: optional field for CSG polygon splitting (not used for rendering)
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html#computeNormal()][Plane.computeNormal()]]: shared zero-allocation helper for computing normals from three points
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html][Plane]]: stores normals in Hesse normal form (normal + distance) for BSP spatial partitioning
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.html#normal][Vertex.normal]]: optional field for CSG polygon splitting (not used for rendering)
** Mesh
:PROPERTIES:
| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/GoldenImage.html][GoldenImage]] | Golden-PNG comparison, diff writer, CLI |
| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/SceneDump.html][SceneDump]] | Scene state as a reproducible text block |
| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection]] | ~transformShapes(Camera, ...)~ headless overload |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] | ~getImage()~ exposes the painted frame |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.html][RenderingContext]] | ~getImage()~ exposes the painted frame |
* Source code
:PROPERTIES:
--- /dev/null
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.diag;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Circular buffer for debug log messages.
+ *
+ * <p>Captures log messages to a fixed-size circular buffer for display
+ * in the {@link eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel}.</p>
+ *
+ * <p>This allows capturing early initialization logs before the user opens
+ * the Developer Tools panel. When the panel is opened, the buffered history
+ * becomes immediately visible.</p>
+ *
+ * @see eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel
+ */
+public class DebugLogBuffer {
+
+ private static final DateTimeFormatter TIME_FORMATTER =
+ DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
+
+ private final String[] buffer;
+ private final int capacity;
+ private volatile int head = 0;
+ private volatile int count = 0;
+
+ /**
+ * Creates a new DebugLogBuffer with the specified capacity.
+ *
+ * @param capacity the maximum number of log entries to retain
+ */
+ public DebugLogBuffer(final int capacity) {
+ this.capacity = capacity;
+ this.buffer = new String[capacity];
+ }
+
+ /**
+ * Logs a message with a timestamp prefix.
+ *
+ * @param message the message to log
+ */
+ public void log(final String message) {
+ final String timestamped = LocalDateTime.now().format(TIME_FORMATTER) + " " + message;
+
+ synchronized (this) {
+ buffer[head] = timestamped;
+ head = (head + 1) % capacity;
+ if (count < capacity) {
+ count++;
+ }
+ }
+ }
+
+ /**
+ * Returns all buffered log entries in chronological order.
+ *
+ * @return a list of timestamped log entries
+ */
+ public synchronized List<String> getEntries() {
+ final List<String> entries = new ArrayList<>(count);
+
+ if (count < capacity) {
+ for (int i = 0; i < count; i++) {
+ entries.add(buffer[i]);
+ }
+ } else {
+ for (int i = 0; i < capacity; i++) {
+ final int index = (head + i) % capacity;
+ entries.add(buffer[index]);
+ }
+ }
+
+ return entries;
+ }
+
+ /**
+ * Clears all buffered log entries.
+ */
+ public synchronized void clear() {
+ head = 0;
+ count = 0;
+ }
+
+ /**
+ * Returns the current number of log entries in the buffer.
+ *
+ * @return the number of entries
+ */
+ public synchronized int size() {
+ return count;
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.diag;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Low-overhead recorder of per-thread work intervals for the thread
+ * timeline display in the Developer Tools window.
+ *
+ * <p>Task submission sites (transform chunks, paint tiles, tile binning)
+ * and render-thread phases (orchestration, waiting, blit) record their
+ * [start, end) intervals here when enabled. The timeline component paints
+ * each thread as a row, time on the X axis, the interval kind as color —
+ * the software-renderer equivalent of a GPU frame-profiler occupancy
+ * view. Idle time is simply the absence of intervals (black).</p>
+ *
+ * <p>Overhead when disabled: one volatile read per task. When enabled:
+ * two {@code System.nanoTime()} calls, one atomic increment and four
+ * array stores per task (~100 ns), negligible at hundreds of tasks per
+ * frame.</p>
+ *
+ * <p>Frame parity distinguishes "current frame" work from "next frame"
+ * work in the colors: transform/paint/binning kinds come in an even and
+ * an odd variant, selected by the parity that was current when the task
+ * was SUBMITTED (captured at task creation, so overlapping frames keep
+ * their own color).</p>
+ */
+public final class ThreadActivityRecorder {
+
+ /** Kind base: vertex transform chunk (add frame parity 0..2). */
+ public static final int KIND_TRANSFORM = 0;
+ /** Kind base: paint tile task (add frame parity 0..2). */
+ public static final int KIND_PAINT = 3;
+ /** Kind base: tile binning task (add frame parity 0..2). */
+ public static final int KIND_BIN = 6;
+ /** Kind: render thread orchestration (tree walk, submission, merges). */
+ public static final int KIND_RENDER = 9;
+ /** Kind: render thread blocked waiting for worker tasks. */
+ public static final int KIND_AWAIT = 10;
+ /** Kind: render thread blitting the finished frame to screen. */
+ public static final int KIND_BLIT = 11;
+ /** Kind: a pass's asynchronous continuation (drain, sort, bin, paint submission). */
+ public static final int KIND_PREP = 12;
+ /** Kind: continuation sub-phase: draining and merging transform chunks. */
+ public static final int KIND_DRAIN = 13;
+ /** Kind: continuation sub-phase: depth sort. */
+ public static final int KIND_SORT = 14;
+
+ /** Number of distinct kinds (three parities each for transform/paint/bin, plus 9..14). */
+ public static final int KIND_COUNT = 15;
+
+ private static final int CAPACITY = 1 << 18;
+ private static final int MASK = CAPACITY - 1;
+
+ private static final long[] starts = new long[CAPACITY];
+ private static final long[] ends = new long[CAPACITY];
+ private static final byte[] kinds = new byte[CAPACITY];
+ private static final byte[] rows = new byte[CAPACITY];
+ private static final AtomicInteger cursor = new AtomicInteger();
+
+ private static volatile boolean enabled = false;
+ private static volatile int frameParity = 0;
+
+ private static final ConcurrentHashMap<String, Integer> rowByThreadName = new ConcurrentHashMap<>();
+ private static final CopyOnWriteArrayList<String> rowNames = new CopyOnWriteArrayList<>();
+ private static final AtomicInteger nextRow = new AtomicInteger();
+
+ private ThreadActivityRecorder() {
+ }
+
+ /**
+ * @return true when recording is active (checked by task submission sites)
+ */
+ public static boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Enables or disables recording. Enabling starts with a clean buffer
+ * and fresh thread-row assignment.
+ *
+ * @param value true to start recording
+ */
+ public static void setEnabled(final boolean value) {
+ if (value && !enabled) {
+ clear();
+ }
+ enabled = value;
+ }
+
+ /**
+ * Drops all recorded intervals and thread-row assignments.
+ */
+ public static void clear() {
+ cursor.set(0);
+ rowByThreadName.clear();
+ rowNames.clear();
+ nextRow.set(0);
+ // starts[]==0 marks an empty slot for the timeline sweep
+ java.util.Arrays.fill(starts, 0L);
+ }
+
+ /**
+ * Sets the parity (0/1) of the frame currently being prepared.
+ * Called by the render thread at the start of each frame; task
+ * submission sites capture it into their tasks so overlapping frames
+ * keep distinct colors.
+ *
+ * @param parity frame parity, 0 or 1
+ */
+ public static void setFrameParity(final int parity) {
+ frameParity = parity;
+ }
+
+ /**
+ * @return parity of the frame currently being prepared
+ */
+ public static int frameParity() {
+ return frameParity;
+ }
+
+ /**
+ * Records one work interval on the calling thread.
+ *
+ * @param kind interval kind (one of the KIND_* bases, plus parity
+ * for transform/paint/binning)
+ * @param t0 interval start, from {@link System#nanoTime()}
+ * @param t1 interval end, from {@link System#nanoTime()}
+ */
+ public static void record(final int kind, final long t0, final long t1) {
+ // Rows are keyed by thread NAME, not Thread object: after a
+ // stop()/start() cycle the executor is recreated with fresh
+ // threads under the same names, and they must reuse the same
+ // rows — otherwise the timeline fills with dead threads' rows
+ // and pushes the live workers below the visible area.
+ final String threadName = Thread.currentThread().getName();
+ Integer row = rowByThreadName.get(threadName);
+ if (row == null) {
+ row = rowByThreadName.computeIfAbsent(threadName, t -> {
+ final int r = nextRow.getAndIncrement();
+ rowNames.add(t);
+ return r;
+ });
+ }
+ if (row > 127) {
+ return;
+ }
+ final int i = cursor.getAndIncrement() & MASK;
+ starts[i] = t0;
+ ends[i] = t1;
+ kinds[i] = (byte) kind;
+ rows[i] = (byte) (int) row;
+ }
+
+ // ---- Snapshot access for the timeline component ----
+
+ /** @return total number of intervals recorded since the last clear */
+ public static int cursor() {
+ return cursor.get();
+ }
+
+ /** @return ring buffer capacity */
+ public static int capacity() {
+ return CAPACITY;
+ }
+
+ /** @return interval start array (index space of the ring buffer) */
+ public static long[] starts() {
+ return starts;
+ }
+
+ /** @return interval end array (index space of the ring buffer) */
+ public static long[] ends() {
+ return ends;
+ }
+
+ /** @return interval kind array (index space of the ring buffer) */
+ public static byte[] kinds() {
+ return kinds;
+ }
+
+ /** @return interval thread-row array (index space of the ring buffer) */
+ public static byte[] rows() {
+ return rows;
+ }
+
+ /** @return number of distinct thread rows seen so far */
+ public static int rowCount() {
+ return nextRow.get();
+ }
+
+ /**
+ * @param row thread row index
+ * @return display name for the row ("render" for the render thread,
+ * otherwise the thread name with the e3d- prefix stripped)
+ */
+ public static String rowName(final int row) {
+ if (row >= rowNames.size()) {
+ return "?";
+ }
+ final String name = rowNames.get(row);
+ if ("e3d-render".equals(name)) {
+ return "render";
+ }
+ return name.startsWith("e3d-") ? name.substring(4) : name;
+ }
+}
+++ /dev/null
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.geometry;
-
-import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A Binary Space Partitioning (BSP) tree for CSG operations.
- *
- * <p>BSP trees are the data structure that makes CSG boolean operations possible.
- * Each node divides 3D space into two half-spaces using a plane, enabling
- * efficient spatial queries and polygon clipping.</p>
- *
- * <p><b>BSP Tree Structure:</b></p>
- * <pre>
- * [Node: plane P]
- * / \
- * [Front subtree] [Back subtree]
- * (same side as P's (opposite side
- * normal) of P's normal)
- * </pre>
- *
- * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape
- * @see Plane the plane type used for spatial partitioning
- * @see SolidPolygon the polygon type stored in BSP nodes
- */
-public class BspTree {
-
- /**
- * Polygons that lie on this node's partitioning plane.
- */
- public final List<SolidPolygon> polygons = new ArrayList<>();
-
- /**
- * The partitioning plane for this node.
- */
- public Plane plane;
-
- /**
- * The front child subtree.
- */
- public BspTree front;
-
- /**
- * The back child subtree.
- */
- public BspTree back;
-
- /**
- * Creates an empty BSP tree with no plane or children.
- */
- public BspTree() {
- }
-
- /**
- * Creates a BSP tree from a list of polygons.
- *
- * @param polygons the polygons to partition into a BSP tree
- */
- public BspTree(final List<SolidPolygon> polygons) {
- addPolygons(polygons);
- }
-
- /**
- * Creates a deep clone of this BSP tree.
- *
- * @return a new BspTree with cloned data
- */
- public BspTree clone() {
- final BspTree tree = new BspTree();
-
- tree.plane = plane != null ? plane.clone() : null;
- tree.front = front != null ? front.clone() : null;
- tree.back = back != null ? back.clone() : null;
-
- for (final SolidPolygon p : polygons) {
- tree.polygons.add(p.deepClone());
- }
-
- return tree;
- }
-
- /**
- * Inverts this BSP tree, converting "inside" to "outside" and vice versa.
- */
- public void invert() {
- for (final SolidPolygon polygon : polygons) polygon.flip();
-
- if (plane != null) plane.flip();
- if (front != null) front.invert();
- if (back != null) back.invert();
-
- final BspTree temp = front;
- front = back;
- back = temp;
- }
-
- /**
- * Clips a list of polygons against this BSP tree, returning only the
- * portions that lie outside the solid represented by this tree.
- *
- * <p>This is a core CSG operation used for boolean subtraction and
- * intersection. The method recursively traverses the BSP tree, splitting
- * polygons at each partitioning plane and discarding interior fragments.</p>
- *
- * <p><b>Algorithm:</b></p>
- * <ol>
- * <li>At each node, split polygons by the partitioning plane</li>
- * <li>Recursively clip front fragments against the front subtree</li>
- * <li>Recursively clip back fragments against the back subtree</li>
- * <li>Combine and return all surviving fragments</li>
- * </ol>
- *
- * <p><b>Leaf nodes:</b> If this node has no plane (leaf node), all polygons
- * are considered outside and returned unchanged.</p>
- *
- * @param polygons the polygons to clip against this BSP tree
- * @return a new list containing only the portions outside this solid
- */
- public List<SolidPolygon> clipPolygons(final List<SolidPolygon> polygons) {
- // Leaf node: no partitioning plane means all polygons are outside
- if (plane == null) {
- return new ArrayList<>(polygons);
- }
-
- // Split polygons by this node's partitioning plane
- final List<SolidPolygon> frontList = new ArrayList<>();
- final List<SolidPolygon> backList = new ArrayList<>();
-
- for (final SolidPolygon polygon : polygons)
- // Split by plane: coplanar polygons are classified by their normal direction
- // (same-facing normal → frontList, opposite-facing normal → backList)
- plane.splitPolygon(polygon, frontList, backList, frontList, backList);
-
- // Recursively clip front fragments against front subtree
- List<SolidPolygon> resultFront = frontList;
- if (front != null) resultFront = front.clipPolygons(frontList);
-
- // Recursively clip back fragments against back subtree
- List<SolidPolygon> resultBack;
- if (back != null) resultBack = back.clipPolygons(backList);
- else resultBack = new ArrayList<>();
-
- // Combine surviving fragments from both subtrees
- final List<SolidPolygon> result = new ArrayList<>(resultFront.size() + resultBack.size());
- result.addAll(resultFront);
- result.addAll(resultBack);
- return result;
- }
-
- /**
- * Clips this BSP tree against another BSP tree.
- *
- * @param bsp the BSP tree to clip against
- */
- public void clipTo(final BspTree bsp) {
- final List<SolidPolygon> newPolygons = bsp.clipPolygons(polygons);
- polygons.clear();
- polygons.addAll(newPolygons);
-
- if (front != null) front.clipTo(bsp);
- if (back != null) back.clipTo(bsp);
- }
-
- /**
- * Collects all polygons from this BSP tree into a flat list.
- *
- * @return a new list containing all polygons in this tree
- */
- public List<SolidPolygon> allPolygons() {
- final List<SolidPolygon> result = new ArrayList<>(polygons);
-
- if (front != null) result.addAll(front.allPolygons());
- if (back != null) result.addAll(back.allPolygons());
-
- return result;
- }
-
- /**
- * Adds polygons to this BSP tree, partitioning space recursively.
- *
- * <p>This method is the core BSP tree construction algorithm. It builds or
- * extends the tree by choosing a partition plane and classifying each polygon:</p>
- *
- * <ul>
- * <li><b>Coplanar</b> — polygons on the partition plane are stored in this node</li>
- * <li><b>Front</b> — polygons in the front half-space (same side as plane normal)
- * go to the front child subtree</li>
- * <li><b>Back</b> — polygons in the back half-space (opposite to plane normal)
- * go to the back child subtree</li>
- * <li><b>Spanning</b> — polygons crossing the plane are split into front and back
- * fragments, each going to its respective subtree</li>
- * </ul>
- *
- * <p>For an empty tree, the first polygon's plane becomes the partition plane.
- * Child nodes are created lazily when polygons need to be stored in them.</p>
- *
- * <p>Can be called multiple times to incrementally extend an existing tree,
- * though the original partition planes remain unchanged.</p>
- *
- * @param polygons the polygons to insert into this BSP tree
- * @see Plane#splitPolygon the method that classifies and splits individual polygons
- */
- public void addPolygons(final List<SolidPolygon> polygons) {
- if (polygons.isEmpty()) return;
-
- if (plane == null) plane = polygons.get(0).getPlane().clone();
-
- final List<SolidPolygon> frontList = new ArrayList<>();
- final List<SolidPolygon> backList = new ArrayList<>();
-
- for (final SolidPolygon polygon : polygons)
- plane.splitPolygon(polygon, this.polygons, this.polygons, frontList, backList);
-
- if (!frontList.isEmpty()) {
- if (front == null) front = new BspTree();
- front.addPolygons(frontList);
- }
-
- if (!backList.isEmpty()) {
- if (back == null) back = new BspTree();
- back.addPolygons(backList);
- }
- }
-}
\ No newline at end of file
--- /dev/null
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.geometry;
+
+import eu.svjatoslav.aukio.e3d.geometry.Point3D;
+import eu.svjatoslav.aukio.e3d.math.Matrix3x3;
+import eu.svjatoslav.aukio.e3d.math.Quaternion;
+import eu.svjatoslav.aukio.e3d.math.Transform;
+
+/**
+ * Represents the viewer's camera in the 3D world, with position, orientation, and movement.
+ *
+ * <p>The camera is the user's "eyes" in the 3D scene. It has a position (location),
+ * a looking direction (defined by a quaternion), and a movement system with
+ * velocity, acceleration, and friction for smooth camera navigation.</p>
+ *
+ * <p>By default, the user can navigate using arrow keys (handled by
+ * {@link eu.svjatoslav.aukio.e3d.gui.humaninput.WorldNavigationUserInputTracker}),
+ * and the mouse controls the look direction (handled by
+ * {@link eu.svjatoslav.aukio.e3d.gui.humaninput.InputManager}).</p>
+ *
+ * <p><b>Programmatic camera control:</b></p>
+ * <pre>{@code
+ * Camera camera = viewPanel.getCamera();
+ *
+ * // Set camera position
+ * camera.getTransform().setTranslation(new Point3D(0, -50, -200));
+ *
+ * // Set camera orientation using a quaternion
+ * camera.getTransform().getRotation().set(Quaternion.fromAngles(0.5, -0.3));
+ *
+ * // Copy camera state from another camera
+ * Camera snapshot = new Camera(camera);
+ * }</pre>
+ *
+ * @see eu.svjatoslav.aukio.e3d.gui.ViewPanel#getCamera()
+ * @see eu.svjatoslav.aukio.e3d.gui.humaninput.WorldNavigationUserInputTracker default keyboard navigation
+ */
+public class Camera {
+
+ /**
+ * Camera movement speed limit, relative to the world. When camera coordinates are
+ * updated within the world, camera orientation relative to the world is
+ * taken into account.
+ */
+ public static final double SPEED_LIMIT = 30;
+
+ /** World units moved per millisecond per unit of velocity.
+ * Public: direct-drive controllers (SpaceMouse) reuse it to match
+ * the keyboard/mouse feel. */
+ public static final double SPEED_MULTIPLIER = .02d;
+ /**
+ * Determines amount of friction user experiences every millisecond while moving around in space.
+ */
+ private static final double MILLISECOND_FRICTION = 1.005;
+ /**
+ * Camera movement speed, relative to camera itself. When camera coordinates
+ * are updated within the world, camera orientation relative to the world is
+ * taken into account.
+ */
+ private final Point3D movementVector = new Point3D();
+ private final Point3D previousLocation = new Point3D();
+ /**
+ * Camera acceleration factor for movement speed. Higher values result in faster acceleration.
+ */
+ public double cameraAcceleration = 0.1;
+ /**
+ * The transform containing camera location and orientation.
+ */
+ private final Transform transform;
+
+ /**
+ * Creates a camera at the world origin with no rotation.
+ */
+ public Camera() {
+ transform = new Transform();
+ }
+
+ /**
+ * Creates a copy of an existing camera, cloning its position and orientation.
+ *
+ * @param sourceView the camera to copy
+ */
+ public Camera(final Camera sourceView) {
+ transform = sourceView.getTransform().clone();
+ }
+
+ /**
+ * Creates a camera with the specified transform (position and orientation).
+ *
+ * @param transform the initial transform defining position and rotation
+ */
+ public Camera(final Transform transform){
+ this.transform = transform;
+ }
+
+ /**
+ * Per-frame camera physics tick (movement integration, friction).
+ * Registered on the panel via a {@code FrameListener} adapter in
+ * {@code ViewPanel} — decoupled from the gui interface so the camera
+ * stays in the geometry package.
+ *
+ * @param millisecondsSinceLastFrame frame delta in milliseconds
+ * @return true when the camera moved enough to need a repaint
+ */
+ public boolean onFrame(final int millisecondsSinceLastFrame) {
+
+ previousLocation.clone(transform.getTranslation());
+ translateCameraLocationBasedOnMovementVector(millisecondsSinceLastFrame);
+ applyFrictionToMovement(millisecondsSinceLastFrame);
+ return isFrameRepaintNeeded();
+ }
+
+ private boolean isFrameRepaintNeeded() {
+ final double distanceMoved = transform.getTranslation().getDistanceTo(previousLocation);
+ return distanceMoved > 0.03;
+ }
+
+ /**
+ * Clamps the camera's movement speed to {@link #SPEED_LIMIT}.
+ * Called after modifying the movement vector to prevent excessive velocity.
+ */
+ public void enforceSpeedLimit() {
+ final double currentSpeed = movementVector.getVectorLength();
+
+ if (currentSpeed <= SPEED_LIMIT)
+ return;
+
+ movementVector.divide(currentSpeed / SPEED_LIMIT);
+ }
+
+ /**
+ * Returns the current movement velocity vector, relative to the camera's orientation.
+ * Modify this vector to programmatically move the camera.
+ *
+ * @return the movement vector (mutable reference)
+ */
+ public Point3D getMovementVector() {
+ return movementVector;
+ }
+
+ /**
+ * Returns the current movement speed (magnitude of the movement vector).
+ *
+ * @return the scalar speed value
+ */
+ public double getMovementSpeed() {
+ return movementVector.getVectorLength();
+ }
+
+ /**
+ * Apply friction to camera movement vector.
+ *
+ * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate.
+ * Therefore, we take frame rendering time into account when translating
+ * camera between consecutive frames.
+ */
+ private void applyFrictionToMovement(int millisecondsPassedSinceLastFrame) {
+ for (int i = 0; i < millisecondsPassedSinceLastFrame; i++)
+ applyMillisecondFrictionToUserMovementVector();
+ }
+
+ /**
+ * Apply friction to camera movement vector.
+ */
+ private void applyMillisecondFrictionToUserMovementVector() {
+ movementVector.x /= MILLISECOND_FRICTION;
+ movementVector.y /= MILLISECOND_FRICTION;
+ movementVector.z /= MILLISECOND_FRICTION;
+ }
+
+ /**
+ * Translate coordinates based on camera movement vector and camera orientation in the world.
+ *
+ * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate.
+ * Therefore, we take frame rendering time into account when translating
+ * camera between consecutive frames.
+ */
+ private void translateCameraLocationBasedOnMovementVector(int millisecondsPassedSinceLastFrame) {
+ final Matrix3x3 m = transform.getRotation().toMatrix();
+
+ final double forwardX = m.m20;
+ final double forwardY = m.m21;
+ final double forwardZ = m.m22;
+
+ final double rightX = m.m00;
+ final double rightY = m.m01;
+ final double rightZ = m.m02;
+
+ final Point3D location = transform.getTranslation();
+ final double ms = millisecondsPassedSinceLastFrame;
+
+ location.x += forwardX * movementVector.z * SPEED_MULTIPLIER * ms;
+ location.y += forwardY * movementVector.z * SPEED_MULTIPLIER * ms;
+ location.z += forwardZ * movementVector.z * SPEED_MULTIPLIER * ms;
+
+ location.x += rightX * movementVector.x * SPEED_MULTIPLIER * ms;
+ location.y += rightY * movementVector.x * SPEED_MULTIPLIER * ms;
+ location.z += rightZ * movementVector.x * SPEED_MULTIPLIER * ms;
+
+ location.y += movementVector.y * SPEED_MULTIPLIER * ms;
+ }
+
+ /**
+ * Returns the transform containing this camera's location and orientation.
+ *
+ * @return the transform (mutable reference)
+ */
+ public Transform getTransform() {
+ return transform;
+ }
+
+ /**
+ * Orients the camera to look at a target point in world coordinates.
+ *
+ * <p>Calculates the required XZ and YZ rotation angles to point the camera
+ * from its current position toward the target. Useful for programmatic
+ * camera control, cinematic sequences, and following objects.</p>
+ *
+ * <p><b>Example:</b></p>
+ * <pre>{@code
+ * Camera camera = viewPanel.getCamera();
+ * camera.getTransform().setTranslation(new Point3D(100, -50, -200));
+ * camera.lookAt(new Point3D(0, 0, 0)); // Point camera at origin
+ * }</pre>
+ *
+ * @param target the world-space point to look at
+ */
+ public void lookAt(final Point3D target) {
+ final Point3D pos = transform.getTranslation();
+ final double dx = target.x - pos.x;
+ final double dy = target.y - pos.y;
+ final double dz = target.z - pos.z;
+
+ final double angleXZ = -Math.atan2(dx, dz);
+ final double horizontalDist = Math.sqrt(dx * dx + dz * dz);
+ final double angleYZ = -Math.atan2(dy, horizontalDist);
+
+ transform.getRotation().set(Quaternion.fromAngles(angleXZ, angleYZ));
+ }
+}
\ No newline at end of file
+++ /dev/null
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.geometry;
-
-import eu.svjatoslav.aukio.e3d.gui.Camera;
-
-/**
- * View frustum for frustum culling - eliminates objects outside the camera's view.
- *
- * <p>The frustum is a truncated pyramid-shaped volume that represents everything
- * the camera can see. Objects completely outside this volume can be skipped
- * during rendering, significantly improving performance for large scenes.</p>
- *
- * <p><b>Frustum planes:</b></p>
- * <ul>
- * <li>Left, Right, Top, Bottom - define the viewport edges</li>
- * <li>Near - closest visible distance from camera</li>
- * <li>Far - farthest visible distance from camera</li>
- * </ul>
- *
- * <p><b>Usage:</b></p>
- * <pre>{@code
- * Frustum frustum = new Frustum();
- * frustum.update(camera, screenWidth, screenHeight);
- *
- * Box objectBounds = shape.getBoundingBox();
- * if (frustum.intersectsAABB(objectBounds)) {
- * // Object is potentially visible - render it
- * } else {
- * // Object outside frustum - skip rendering
- * }
- * }</pre>
- *
- * <p><b>AABB intersection algorithm:</b></p>
- * <p>Uses the optimized "P-vertex" approach: for each plane, we test only
- * the AABB corner most aligned with the plane normal. If this corner is
- * behind the plane, the entire AABB is outside the frustum.</p>
- *
- * @see Box axis-aligned bounding box for culling tests
- * @see Camera provides position and orientation for frustum computation
- */
-public class Frustum {
-
- /**
- * Index for the left clipping plane.
- */
- public static final int LEFT = 0;
- /**
- * Index for the right clipping plane.
- */
- public static final int RIGHT = 1;
- /**
- * Index for the top clipping plane.
- */
- public static final int TOP = 2;
- /**
- * Index for the bottom clipping plane.
- */
- public static final int BOTTOM = 3;
- /**
- * Index for the near clipping plane.
- */
- public static final int NEAR = 4;
- /**
- * Index for the far clipping plane.
- */
- public static final int FAR = 5;
-
- /**
- * The six clipping planes defining the frustum volume.
- * Each plane is stored as (normal, distance) in Hesse normal form.
- * Planes are in world space coordinates.
- */
- private final Plane[] planes = new Plane[6];
-
- /**
- * Default near plane distance from camera (in world units).
- * Objects closer than this are culled.
- */
- private double nearDistance = 1.0;
-
- /**
- * Default far plane distance from camera (in world units).
- * Objects farther than this are culled.
- */
- private double farDistance = 10000.0;
-
- /**
- * Creates a new frustum with uninitialized planes.
- * Call {@link #update} before using for culling.
- */
- public Frustum() {
- for (int i = 0; i < 6; i++) {
- planes[i] = new Plane(new Point3D(0, 0, 1), 0);
- }
- }
-
- /**
- * Updates the frustum planes in view space (camera at origin, looking along +Z).
- *
- * <p>This method should be called once per frame before rendering, after the
- * camera position and orientation have been updated.</p>
- *
- * <p><b>View space coordinate system:</b></p>
- * <ul>
- * <li>Camera at origin (0, 0, 0)</li>
- * <li>Forward = +Z axis (looking into the screen)</li>
- * <li>Right = +X axis</li>
- * <li>Up = -Y axis (since Y-down means smaller Y is higher visually)</li>
- * </ul>
- *
- * <p><b>Plane normals point INTO the frustum</b> (toward the visible volume).
- * A point is inside if dot(normal, point) >= distance for all planes.</p>
- *
- * <p><b>FOV calculation:</b> The Aukio 3D engine uses projectionScale = width/3.
- * This means tan(halfHFOV) = (width/2) / projectionScale = 1.5, giving a
- * horizontal FOV of approximately 112 degrees.</p>
- *
- * @param camera the camera (used only for aspect ratio derivation from width/height)
- * @param width the viewport width in pixels (defines projectionScale)
- * @param height the viewport height in pixels (used for vertical FOV)
- */
- public void update(final Camera camera, final int width, final int height) {
- // Frustum is computed in VIEW SPACE (camera at origin, looking along +Z)
- // This matches the coordinate system after applying camera transforms
-
- // Aukio 3D uses projectionScale = width/3
- // tan(halfFOV) = (halfSize) / projectionScale
- final double projectionScale = width / 3.0;
- final double tanHalfHFOV = (width / 2.0) / projectionScale; // = 1.5 (very wide FOV)
- final double tanHalfVFOV = (height / 2.0) / projectionScale; // depends on aspect ratio
-
- // Compute cosine and sine of half-FOV angles
- // cosHalfFOV = 1 / sqrt(1 + tanHalfFOV^2)
- // sinHalfFOV = tanHalfFOV * cosHalfFOV
- final double cosHalfHFOV = 1.0 / Math.sqrt(1.0 + tanHalfHFOV * tanHalfHFOV);
- final double sinHalfHFOV = tanHalfHFOV * cosHalfHFOV;
- final double cosHalfVFOV = 1.0 / Math.sqrt(1.0 + tanHalfVFOV * tanHalfVFOV);
- final double sinHalfVFOV = tanHalfVFOV * cosHalfVFOV;
-
- // Near and far distances
- nearDistance = 1.0;
- farDistance = 10000.0;
-
- // All side planes pass through origin (camera position in view space)
- // Plane equation: dot(normal, point) >= distance means inside
-
- // Left plane: inward normal pointing right-forward
- // Bounds: x >= -tanHalfHFOV * z (to the right of left edge)
- planes[LEFT].normal = new Point3D(cosHalfHFOV, 0, sinHalfHFOV);
- planes[LEFT].distance = 0;
-
- // Right plane: inward normal pointing left-forward
- // Bounds: x <= tanHalfHFOV * z (to the left of right edge)
- planes[RIGHT].normal = new Point3D(-cosHalfHFOV, 0, sinHalfHFOV);
- planes[RIGHT].distance = 0;
-
- // Top plane: inward normal pointing down-forward (Y-down system, top is smaller Y)
- // Bounds: y <= tanHalfVFOV * z (below top edge, smaller Y)
- planes[TOP].normal = new Point3D(0, -cosHalfVFOV, sinHalfVFOV);
- planes[TOP].distance = 0;
-
- // Bottom plane: inward normal pointing up-forward (larger Y is below)
- // Bounds: y >= -tanHalfVFOV * z (above bottom edge, larger Y)
- planes[BOTTOM].normal = new Point3D(0, cosHalfVFOV, sinHalfVFOV);
- planes[BOTTOM].distance = 0;
-
- // Near plane: inward normal pointing forward (+Z)
- // Bounds: z >= nearDistance (in front of near plane)
- planes[NEAR].normal = new Point3D(0, 0, 1);
- planes[NEAR].distance = nearDistance;
-
- // Far plane: inward normal pointing backward (-Z)
- // Bounds: z <= farDistance (behind far plane)
- planes[FAR].normal = new Point3D(0, 0, -1);
- planes[FAR].distance = -farDistance;
- }
-
- /**
- * Tests whether an axis-aligned bounding box intersects the frustum.
- *
- * <p>This is a conservative test: returns {@code true} if the box is
- * potentially visible (inside or partially inside the frustum), and
- * {@code false} only if the box is completely outside all frustum planes.</p>
- *
- * <p><b>Optimized algorithm:</b></p>
- * <p>For each plane, we test only the AABB corner most aligned with the
- * plane normal (the "P-vertex"). If this corner is behind the plane,
- * the entire AABB must be outside the frustum.</p>
- *
- * @param box the axis-aligned bounding box to test (in view space coordinates)
- * @return {@code true} if the box intersects or is inside the frustum,
- * {@code false} if completely outside
- */
- public boolean intersectsAABB(final Box box) {
- // Get box min/max for each axis
- final double minX = box.getMinX();
- final double maxX = box.getMaxX();
- final double minY = box.getMinY();
- final double maxY = box.getMaxY();
- final double minZ = box.getMinZ();
- final double maxZ = box.getMaxZ();
-
- for (int i = 0; i < 6; i++) {
- final Plane plane = planes[i];
- final Point3D n = plane.normal;
- final double d = plane.distance;
-
- // Find the P-vertex: the corner most aligned with the plane normal
- // If normal component is positive, use max; if negative, use min
- final double px = (n.x > 0) ? maxX : minX;
- final double py = (n.y > 0) ? maxY : minY;
- final double pz = (n.z > 0) ? maxZ : minZ;
-
- // Test if P-vertex is outside the frustum (behind the plane)
- // For inward-pointing normals: inside = dot(N,P) >= distance
- // So outside = dot(N,P) < distance
- if (n.x * px + n.y * py + n.z * pz < d) {
- return false; // AABB entirely outside this plane
- }
- }
-
- return true; // AABB intersects or inside all planes
- }
-
- /**
- * Returns the near clipping plane distance.
- *
- * @return the near distance in world units
- */
- public double getNearDistance() {
- return nearDistance;
- }
-
- /**
- * Returns the far clipping plane distance.
- *
- * @return the far distance in world units
- */
- public double getFarDistance() {
- return farDistance;
- }
-
- /**
- * Sets the near and far clipping distances.
- *
- * @param near the near plane distance (objects closer are culled)
- * @param far the far plane distance (objects farther are culled)
- */
- public void setClipDistances(final double near, final double far) {
- this.nearDistance = near;
- this.farDistance = far;
- }
-
- /**
- * Returns a specific frustum plane for debugging or advanced usage.
- *
- * @param planeIndex one of LEFT, RIGHT, TOP, BOTTOM, NEAR, FAR
- * @return the plane at the specified index
- */
- public Plane getPlane(final int planeIndex) {
- return planes[planeIndex];
- }
-}
\ No newline at end of file
--- /dev/null
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.geometry;
+
+/**
+ * Point in 3D space with integer coordinates. Used for octree voxel positions.
+ */
+public class IntegerPoint
+{
+ /** X coordinate. */
+ public int x;
+ /** Y coordinate. */
+ public int y;
+ /** Z coordinate. */
+ public int z = 0;
+
+ /**
+ * Creates a point at the origin (0, 0, 0).
+ */
+ public IntegerPoint()
+ {
+ }
+
+ /**
+ * Creates a point with the specified coordinates.
+ *
+ * @param x the X coordinate
+ * @param y the Y coordinate
+ * @param z the Z coordinate
+ */
+ public IntegerPoint(final int x, final int y, final int z)
+ {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+}
+++ /dev/null
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.geometry;
-
-import eu.svjatoslav.aukio.e3d.math.Vertex;
-import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Represents an infinite plane in 3D space using the Hesse normal form.
- *
- * <p>Planes are fundamental to BSP (Binary Space Partitioning) tree operations
- * in CSG. They divide 3D space into two half-spaces.</p>
- *
- * @see SolidPolygon polygons that reference their containing plane
- * @see BspTree BSP trees that use planes for spatial partitioning
- */
-public class Plane {
-
- /**
- * Epsilon value used for floating-point comparisons in BSP operations.
- * Smaller values provide higher precision but may cause issues with
- * near-coplanar polygons. 1e-5 is a good balance for most 3D geometry.
- */
- public static final double EPSILON = 1e-12;
-
- /**
- * The unit normal vector perpendicular to the plane surface.
- */
- public Point3D normal;
-
- /**
- * The signed distance from the origin to the plane along the normal.
- */
- public double distance;
-
- /**
- * Creates a plane with the given normal and distance.
- *
- * @param normal the unit normal vector
- * @param distance the signed distance from origin to the plane
- */
- public Plane(final Point3D normal, final double distance) {
- this.normal = normal;
- this.distance = distance;
- }
-
- /**
- * Computes the unit normal vector for a triangle defined by three points.
- *
- * <p>Zero-allocation method: fills the result point instead of creating a new one.
- * This is the shared implementation used by both {@link #fromPoints} and
- * {@link SolidPolygon} for shading calculations.</p>
- *
- * <p>The normal is computed as the cross product of two edge vectors (b-a and c-a),
- * then normalized to unit length.</p>
- *
- * @param a first point (base point for edge vectors)
- * @param b second point
- * @param c third point
- * @param result Point3D to receive the unit normal vector (modified in place)
- * @return true if normal computed successfully, false if points are collinear
- * (cross product magnitude less than EPSILON)
- */
- public static boolean computeNormal(final Point3D a, final Point3D b,
- final Point3D c, final Point3D result) {
- // Edge vectors from a to b and a to c
- final double ax = b.x - a.x;
- final double ay = b.y - a.y;
- final double az = b.z - a.z;
-
- final double bx = c.x - a.x;
- final double by = c.y - a.y;
- final double bz = c.z - a.z;
-
- // Cross product: (edge1 × edge2)
- double nx = ay * bz - az * by;
- double ny = az * bx - ax * bz;
- double nz = ax * by - ay * bx;
-
- // Normalize
- final double length = Math.sqrt(nx * nx + ny * ny + nz * nz);
- if (length < EPSILON) {
- result.x = result.y = result.z = 0;
- return false;
- }
-
- result.x = nx / length;
- result.y = ny / length;
- result.z = nz / length;
- return true;
- }
-
- /**
- * Creates a plane from three non-collinear points.
- *
- * <p>Uses {@link #computeNormal} for the normal calculation, then computes
- * the signed distance from origin using the dot product.</p>
- *
- * @param a the first point on the plane
- * @param b the second point on the plane
- * @param c the third point on the plane
- * @return a new Plane passing through the three points
- * @throws ArithmeticException if the points are collinear (cannot define a plane)
- */
- public static Plane fromPoints(final Point3D a, final Point3D b, final Point3D c) {
- final Point3D n = new Point3D();
- if (!computeNormal(a, b, c, n)) {
- throw new ArithmeticException(
- "Cannot create plane from collinear points: cross product is zero");
- }
- return new Plane(n, n.dot(a));
- }
-
- /**
- * Creates a deep clone of this plane.
- *
- * @return a new Plane with the same normal and distance
- */
- public Plane clone() {
- return new Plane(new Point3D(normal.x, normal.y, normal.z), distance);
- }
-
- /**
- * Flips the plane orientation by negating the normal and distance.
- */
- public void flip() {
- normal = normal.withNegated();
- distance = -distance;
- }
-
- /**
- * Splits a polygon by this plane, classifying and potentially dividing it.
- *
- * @param polygon the polygon to classify and potentially split
- * @param coplanarFront list to receive coplanar polygons with same-facing normals
- * @param coplanarBack list to receive coplanar polygons with opposite-facing normals
- * @param front list to receive polygons in the front half-space
- * @param back list to receive polygons in the back half-space
- */
- public void splitPolygon(final SolidPolygon polygon,
- final List<SolidPolygon> coplanarFront,
- final List<SolidPolygon> coplanarBack,
- final List<SolidPolygon> front,
- final List<SolidPolygon> back) {
-
- PolygonType polygonType = PolygonType.COPLANAR;
- final int vertexCount = polygon.getVertexCount();
- final PolygonType[] types = new PolygonType[vertexCount];
-
- for (int i = 0; i < vertexCount; i++) {
- final Vertex v = polygon.vertices.get(i);
- final double t = normal.dot(v.coordinate) - distance;
- final PolygonType type = (t < -EPSILON) ? PolygonType.BACK
- : (t > EPSILON) ? PolygonType.FRONT : PolygonType.COPLANAR;
- polygonType = polygonType.combine(type);
- types[i] = type;
- }
-
- switch (polygonType) {
- case COPLANAR:
- ((normal.dot(polygon.getPlane().normal) > 0) ? coplanarFront : coplanarBack).add(polygon);
- break;
-
- case FRONT:
- front.add(polygon);
- break;
-
- case BACK:
- back.add(polygon);
- break;
-
- case SPANNING:
- // Split spanning polygon by clipping each edge against the plane.
- // Vertices on each side go to their respective lists.
- // Edges crossing the plane create intersection vertices added to both lists.
- final List<Vertex> frontVertices = new ArrayList<>();
- final List<Vertex> backVertices = new ArrayList<>();
-
- for (int i = 0; i < vertexCount; i++) {
- final int nextIndex = (i + 1) % vertexCount;
- final PolygonType currentType = types[i];
- final PolygonType nextType = types[nextIndex];
- final Vertex currentVertex = polygon.vertices.get(i);
- final Vertex nextVertex = polygon.vertices.get(nextIndex);
-
- // Add current vertex to the polygon on its side of the plane
- if (currentType.isFront()) {
- frontVertices.add(currentVertex.clone());
- }
- if (currentType.isBack()) {
- backVertices.add(currentVertex.clone());
- }
-
- // If edge crosses the plane, create intersection vertex for both polygons
- if (currentType != nextType
- && currentType != PolygonType.COPLANAR
- && nextType != PolygonType.COPLANAR) {
- // Calculate interpolation parameter t (0 = current, 1 = next)
- // t represents where along the edge the plane intersection occurs
- final double t = (distance - normal.dot(currentVertex.coordinate))
- / normal.dot(nextVertex.coordinate.withSubtracted(currentVertex.coordinate));
-
- final Vertex intersectionVertex = currentVertex.interpolate(nextVertex, t);
- frontVertices.add(intersectionVertex);
- backVertices.add(intersectionVertex.clone());
- }
- }
-
- if (frontVertices.size() >= 3) {
- final SolidPolygon frontPoly = SolidPolygon.fromVertices(
- frontVertices, polygon.getColor(), polygon.isShadingEnabled());
- front.add(frontPoly);
- }
- if (backVertices.size() >= 3) {
- final SolidPolygon backPoly = SolidPolygon.fromVertices(
- backVertices, polygon.getColor(), polygon.isShadingEnabled());
- back.add(backPoly);
- }
- break;
- }
- }
-}
\ No newline at end of file
*/
package eu.svjatoslav.aukio.e3d.geometry;
-import eu.svjatoslav.aukio.e3d.renderer.octree.IntegerPoint;
+import eu.svjatoslav.aukio.e3d.geometry.IntegerPoint;
import static java.lang.Math.*;
* }</pre>
*
* @see Point2D the 2D equivalent
- * @see eu.svjatoslav.aukio.e3d.math.Vertex wraps a Point3D with transform support
+ * @see eu.svjatoslav.aukio.e3d.renderer.raster.Vertex wraps a Point3D with transform support
*/
public class Point3D implements Cloneable {
+++ /dev/null
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.geometry;
-
-/**
- * Classification of a polygon's position relative to a plane.
- * Used in BSP tree operations to determine how polygons should be split.
- */
-public enum PolygonType {
- /** Polygon lies on the plane. */
- COPLANAR,
- /** Polygon is entirely in front of the plane. */
- FRONT,
- /** Polygon is entirely behind the plane. */
- BACK,
- /** Polygon straddles the plane (vertices on both sides). */
- SPANNING;
-
- /**
- * Combines this type with another to compute the aggregate classification.
- * When vertices are on both sides of a plane, the result is SPANNING.
- *
- * @param other the other polygon type to combine with
- * @return the combined classification
- */
- public PolygonType combine(final PolygonType other) {
- if (this == other || other == COPLANAR) {
- return this;
- }
- if (this == COPLANAR) {
- return other;
- }
- // FRONT + BACK = SPANNING
- return SPANNING;
- }
-
- /**
- * Checks if this type represents a vertex in front of the plane.
- *
- * @return true if FRONT or COPLANAR (treated as front for classification)
- */
- public boolean isFront() {
- return this == FRONT || this == COPLANAR;
- }
-
- /**
- * Checks if this type represents a vertex behind the plane.
- *
- * @return true if BACK or COPLANAR (treated as back for classification)
- */
- public boolean isBack() {
- return this == BACK || this == COPLANAR;
- }
-}
\ No newline at end of file
* This project is released under Creative Commons Zero (CC0) license.
*/
package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
import eu.svjatoslav.aukio.e3d.diag.EngineConfig;
import eu.svjatoslav.aukio.e3d.diag.PersistentLog;
+++ /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.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.math.Matrix3x3;
-import eu.svjatoslav.aukio.e3d.math.Quaternion;
-import eu.svjatoslav.aukio.e3d.math.Transform;
-
-/**
- * Represents the viewer's camera in the 3D world, with position, orientation, and movement.
- *
- * <p>The camera is the user's "eyes" in the 3D scene. It has a position (location),
- * a looking direction (defined by a quaternion), and a movement system with
- * velocity, acceleration, and friction for smooth camera navigation.</p>
- *
- * <p>By default, the user can navigate using arrow keys (handled by
- * {@link eu.svjatoslav.aukio.e3d.gui.humaninput.WorldNavigationUserInputTracker}),
- * and the mouse controls the look direction (handled by
- * {@link eu.svjatoslav.aukio.e3d.gui.humaninput.InputManager}).</p>
- *
- * <p><b>Programmatic camera control:</b></p>
- * <pre>{@code
- * Camera camera = viewPanel.getCamera();
- *
- * // Set camera position
- * camera.getTransform().setTranslation(new Point3D(0, -50, -200));
- *
- * // Set camera orientation using a quaternion
- * camera.getTransform().getRotation().set(Quaternion.fromAngles(0.5, -0.3));
- *
- * // Copy camera state from another camera
- * Camera snapshot = new Camera(camera);
- * }</pre>
- *
- * @see ViewPanel#getCamera()
- * @see eu.svjatoslav.aukio.e3d.gui.humaninput.WorldNavigationUserInputTracker default keyboard navigation
- */
-public class Camera implements FrameListener {
-
- /**
- * Camera movement speed limit, relative to the world. When camera coordinates are
- * updated within the world, camera orientation relative to the world is
- * taken into account.
- */
- public static final double SPEED_LIMIT = 30;
-
- /** World units moved per millisecond per unit of velocity.
- * Public: direct-drive controllers (SpaceMouse) reuse it to match
- * the keyboard/mouse feel. */
- public static final double SPEED_MULTIPLIER = .02d;
- /**
- * Determines amount of friction user experiences every millisecond while moving around in space.
- */
- private static final double MILLISECOND_FRICTION = 1.005;
- /**
- * Camera movement speed, relative to camera itself. When camera coordinates
- * are updated within the world, camera orientation relative to the world is
- * taken into account.
- */
- private final Point3D movementVector = new Point3D();
- private final Point3D previousLocation = new Point3D();
- /**
- * Camera acceleration factor for movement speed. Higher values result in faster acceleration.
- */
- public double cameraAcceleration = 0.1;
- /**
- * The transform containing camera location and orientation.
- */
- private final Transform transform;
-
- /**
- * Creates a camera at the world origin with no rotation.
- */
- public Camera() {
- transform = new Transform();
- }
-
- /**
- * Creates a copy of an existing camera, cloning its position and orientation.
- *
- * @param sourceView the camera to copy
- */
- public Camera(final Camera sourceView) {
- transform = sourceView.getTransform().clone();
- }
-
- /**
- * Creates a camera with the specified transform (position and orientation).
- *
- * @param transform the initial transform defining position and rotation
- */
- public Camera(final Transform transform){
- this.transform = transform;
- }
-
- @Override
- public boolean onFrame(final ViewPanel viewPanel, final int millisecondsSinceLastFrame) {
-
- previousLocation.clone(transform.getTranslation());
- translateCameraLocationBasedOnMovementVector(millisecondsSinceLastFrame);
- applyFrictionToMovement(millisecondsSinceLastFrame);
- return isFrameRepaintNeeded();
- }
-
- private boolean isFrameRepaintNeeded() {
- final double distanceMoved = transform.getTranslation().getDistanceTo(previousLocation);
- return distanceMoved > 0.03;
- }
-
- /**
- * Clamps the camera's movement speed to {@link #SPEED_LIMIT}.
- * Called after modifying the movement vector to prevent excessive velocity.
- */
- public void enforceSpeedLimit() {
- final double currentSpeed = movementVector.getVectorLength();
-
- if (currentSpeed <= SPEED_LIMIT)
- return;
-
- movementVector.divide(currentSpeed / SPEED_LIMIT);
- }
-
- /**
- * Returns the current movement velocity vector, relative to the camera's orientation.
- * Modify this vector to programmatically move the camera.
- *
- * @return the movement vector (mutable reference)
- */
- public Point3D getMovementVector() {
- return movementVector;
- }
-
- /**
- * Returns the current movement speed (magnitude of the movement vector).
- *
- * @return the scalar speed value
- */
- public double getMovementSpeed() {
- return movementVector.getVectorLength();
- }
-
- /**
- * Apply friction to camera movement vector.
- *
- * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate.
- * Therefore, we take frame rendering time into account when translating
- * camera between consecutive frames.
- */
- private void applyFrictionToMovement(int millisecondsPassedSinceLastFrame) {
- for (int i = 0; i < millisecondsPassedSinceLastFrame; i++)
- applyMillisecondFrictionToUserMovementVector();
- }
-
- /**
- * Apply friction to camera movement vector.
- */
- private void applyMillisecondFrictionToUserMovementVector() {
- movementVector.x /= MILLISECOND_FRICTION;
- movementVector.y /= MILLISECOND_FRICTION;
- movementVector.z /= MILLISECOND_FRICTION;
- }
-
- /**
- * Translate coordinates based on camera movement vector and camera orientation in the world.
- *
- * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate.
- * Therefore, we take frame rendering time into account when translating
- * camera between consecutive frames.
- */
- private void translateCameraLocationBasedOnMovementVector(int millisecondsPassedSinceLastFrame) {
- final Matrix3x3 m = transform.getRotation().toMatrix();
-
- final double forwardX = m.m20;
- final double forwardY = m.m21;
- final double forwardZ = m.m22;
-
- final double rightX = m.m00;
- final double rightY = m.m01;
- final double rightZ = m.m02;
-
- final Point3D location = transform.getTranslation();
- final double ms = millisecondsPassedSinceLastFrame;
-
- location.x += forwardX * movementVector.z * SPEED_MULTIPLIER * ms;
- location.y += forwardY * movementVector.z * SPEED_MULTIPLIER * ms;
- location.z += forwardZ * movementVector.z * SPEED_MULTIPLIER * ms;
-
- location.x += rightX * movementVector.x * SPEED_MULTIPLIER * ms;
- location.y += rightY * movementVector.x * SPEED_MULTIPLIER * ms;
- location.z += rightZ * movementVector.x * SPEED_MULTIPLIER * ms;
-
- location.y += movementVector.y * SPEED_MULTIPLIER * ms;
- }
-
- /**
- * Returns the transform containing this camera's location and orientation.
- *
- * @return the transform (mutable reference)
- */
- public Transform getTransform() {
- return transform;
- }
-
- /**
- * Orients the camera to look at a target point in world coordinates.
- *
- * <p>Calculates the required XZ and YZ rotation angles to point the camera
- * from its current position toward the target. Useful for programmatic
- * camera control, cinematic sequences, and following objects.</p>
- *
- * <p><b>Example:</b></p>
- * <pre>{@code
- * Camera camera = viewPanel.getCamera();
- * camera.getTransform().setTranslation(new Point3D(100, -50, -200));
- * camera.lookAt(new Point3D(0, 0, 0)); // Point camera at origin
- * }</pre>
- *
- * @param target the world-space point to look at
- */
- public void lookAt(final Point3D target) {
- final Point3D pos = transform.getTranslation();
- final double dx = target.x - pos.x;
- final double dy = target.y - pos.y;
- final double dz = target.z - pos.z;
-
- final double angleXZ = -Math.atan2(dx, dz);
- final double horizontalDist = Math.sqrt(dx * dx + dz * dz);
- final double angleYZ = -Math.atan2(dy, horizontalDist);
-
- transform.getRotation().set(Quaternion.fromAngles(angleXZ, angleYZ));
- }
-}
\ No newline at end of file
+++ /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 java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Statistics for frustum culling, tracking composite-level culling efficiency.
- *
- * <p>Updated each frame during the rendering pipeline:</p>
- * <ul>
- * <li>{@link #totalComposites} - incremented before each composite's frustum test</li>
- * <li>{@link #culledComposites} - incremented when a composite fails the frustum test</li>
- * </ul>
- *
- * <p>Thread safety: counters are {@link AtomicInteger} because the parallel
- * transform phase increments them from multiple worker threads.</p>
- *
- * <p>Displayed in the {@link DeveloperToolsPanel} to help developers understand
- * culling efficiency and optimize scene graphs.</p>
- *
- * @see DeveloperToolsPanel
- * @see eu.svjatoslav.aukio.e3d.geometry.Frustum
- */
-public class CullingStatistics {
-
- /**
- * Total number of composite shapes tested against the frustum this frame.
- * Incremented before each composite's AABB frustum test.
- * Does not include the root composite (which is never frustum-tested).
- */
- public final AtomicInteger totalComposites = new AtomicInteger(0);
-
- /**
- * Number of composite shapes that were entirely outside the frustum and skipped.
- * When a composite is culled, all its children (shapes and nested composites)
- * are skipped without individual testing.
- */
- public final AtomicInteger culledComposites = new AtomicInteger(0);
-
- /**
- * Resets all statistics to zero.
- * Called at the start of each frame before computing new statistics.
- */
- public void reset() {
- totalComposites.set(0);
- culledComposites.set(0);
- }
-
- /**
- * Returns the percentage of composites that were culled.
- *
- * @return the culled percentage (0-100), or 0 if there are no composites
- */
- public double getCulledPercentage() {
- final int total = totalComposites.get();
- if (total == 0) {
- return 0.0;
- }
- return 100.0 * culledComposites.get() / total;
- }
-}
+++ /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 java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Circular buffer for debug log messages.
- *
- * <p>Captures log messages to a fixed-size circular buffer for display
- * in the {@link DeveloperToolsPanel}.</p>
- *
- * <p>This allows capturing early initialization logs before the user opens
- * the Developer Tools panel. When the panel is opened, the buffered history
- * becomes immediately visible.</p>
- *
- * @see DeveloperToolsPanel
- */
-public class DebugLogBuffer {
-
- private static final DateTimeFormatter TIME_FORMATTER =
- DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
-
- private final String[] buffer;
- private final int capacity;
- private volatile int head = 0;
- private volatile int count = 0;
-
- /**
- * Creates a new DebugLogBuffer with the specified capacity.
- *
- * @param capacity the maximum number of log entries to retain
- */
- public DebugLogBuffer(final int capacity) {
- this.capacity = capacity;
- this.buffer = new String[capacity];
- }
-
- /**
- * Logs a message with a timestamp prefix.
- *
- * @param message the message to log
- */
- public void log(final String message) {
- final String timestamped = LocalDateTime.now().format(TIME_FORMATTER) + " " + message;
-
- synchronized (this) {
- buffer[head] = timestamped;
- head = (head + 1) % capacity;
- if (count < capacity) {
- count++;
- }
- }
- }
-
- /**
- * Returns all buffered log entries in chronological order.
- *
- * @return a list of timestamped log entries
- */
- public synchronized List<String> getEntries() {
- final List<String> entries = new ArrayList<>(count);
-
- if (count < capacity) {
- for (int i = 0; i < count; i++) {
- entries.add(buffer[i]);
- }
- } else {
- for (int i = 0; i < capacity; i++) {
- final int index = (head + i) % capacity;
- entries.add(buffer[index]);
- }
- }
-
- return entries;
- }
-
- /**
- * Clears all buffered log entries.
- */
- public synchronized void clear() {
- head = 0;
- count = 0;
- }
-
- /**
- * Returns the current number of log entries in the buffer.
- *
- * @return the number of entries
- */
- public synchronized int size() {
- return count;
- }
-}
\ No newline at end of file
* This project is released under Creative Commons Zero (CC0) license.
*/
package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.diag.DebugLogBuffer;
+import eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
+import eu.svjatoslav.aukio.e3d.renderer.raster.CullingStatistics;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
+++ /dev/null
-package eu.svjatoslav.aukio.e3d.gui;
-
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * Hierarchical depth pyramid for whole-block occlusion culling
- * (Hi-Z). Built from the just-painted frame's depth buffer; queried
- * during the NEXT frame's transform to skip blocks that are fully
- * hidden behind what was drawn last frame.
- *
- * <p>Semantics: each tile stores the MINIMUM w (= 1/z, i.e. the
- * FARTHEST written depth) over its pixels. A block whose nearest
- * possible point (max w over its AABB corners) is farther than a
- * tile's farthest written depth is behind something at every written
- * pixel of that tile — and tiles with any unwritten (sky) pixel hold
- * -infinity and never occlude. That makes the test conservative: it
- * may keep a hidden block, it never culls a visible one — under a
- * static camera. (The first version max-pooled, storing the NEAREST
- * depth per tile; that culled houses visible BETWEEN nearer tree
- * trunks — per-pixel gaps inside a tile are invisible to a max.)
- * Camera motion reuses the stale pyramid, which can cull a
- * newly-visible block wrongly; the block's pixels then contain far
- * background depth, so the next frame's pyramid no longer occludes
- * it — wrong culls self-heal in one frame (sanctioned design
- * concession), and the query margin absorbs small-motion
- * parallax.</p>
- *
- * <p>Empty tiles (never depth-written) store -infinity and never
- * occlude, so the first frame after startup (or after any pass that
- * leaves the pyramid unbuilt) culls nothing.</p>
- *
- * <p>Knobs: {@code -Daukio.hiz=false} disables culling (build still
- * happens — it is cheap — so flipping the flag needs no warm-up);
- * {@code -Daukio.hiz.margin=0.02} sets the relative w-space safety
- * margin against parallax between frames.</p>
- */
-public final class HiZPyramid {
-
- /** Level-0 tile edge in pixels; level i tiles cover TILE*2^i px. */
- private static final int TILE = 8;
-
- private static final boolean ENABLED = Boolean.parseBoolean(
- System.getProperty("aukio.hiz", "true"));
- private static final double MARGIN = Double.parseDouble(
- System.getProperty("aukio.hiz.margin", "0.02"));
-
- /** Blocks occlusion-tested this process (telemetry). */
- public final AtomicLong blocksTested = new AtomicLong();
- /** Blocks culled as fully occluded (telemetry). */
- public final AtomicLong blocksCulled = new AtomicLong();
-
- private float[] tiles = new float[0];
- private int[] levelOff = new int[0];
- private int[] levelW = new int[0];
- private int[] levelH = new int[0];
- private int levels;
- private int bufW = -1, bufH = -1;
-
- /** Rebuilds the pyramid from a freshly painted depth buffer. */
- public synchronized void buildFrom(final float[] depth,
- final int width, final int height) {
- if (width != bufW || height != bufH) {
- allocate(width, height);
- }
-
- // Level 0: min-pool depth into TILE x TILE tiles. Depth
- // outside the painted area is -infinity (cleared), so a tile
- // with any unpainted pixel never occludes.
- final int w0 = levelW[0], h0 = levelH[0];
- final int off0 = levelOff[0];
- for (int ty = 0; ty < h0; ty++) {
- final int yEnd = Math.min((ty + 1) * TILE, height);
- for (int tx = 0; tx < w0; tx++) {
- final int xEnd = Math.min((tx + 1) * TILE, width);
- float m = Float.POSITIVE_INFINITY;
- for (int y = ty * TILE; y < yEnd; y++) {
- final int row = y * width;
- for (int x = tx * TILE; x < xEnd; x++)
- if (depth[row + x] < m)
- m = depth[row + x];
- }
- tiles[off0 + ty * w0 + tx] = m;
- }
- }
-
- // Higher levels: min of 2x2 children.
- for (int l = 1; l < levels; l++) {
- final int pw = levelW[l - 1], ph = levelH[l - 1];
- final int poff = levelOff[l - 1];
- final int cw = levelW[l], ch = levelH[l];
- final int coff = levelOff[l];
- for (int ty = 0; ty < ch; ty++)
- for (int tx = 0; tx < cw; tx++) {
- float m = Float.POSITIVE_INFINITY;
- for (int dy = 0; dy < 2; dy++)
- for (int dx = 0; dx < 2; dx++) {
- final int sx = tx * 2 + dx, sy = ty * 2 + dy;
- if (sx < pw && sy < ph) {
- final float v = tiles[poff + sy * pw + sx];
- if (v < m)
- m = v;
- }
- }
- tiles[coff + ty * cw + tx] = m;
- }
- }
- }
-
- /**
- * Conservative whole-block occlusion test.
- *
- * @param x1..y2 screen-space AABB of the block (will be clamped
- * to the buffer; a fully off-screen box returns
- * false)
- * @param nearestW the block's nearest possible depth = MAX 1/z
- * over its corners
- * @return true when the block is certainly hidden behind last
- * frame's occluders (within the parallax margin)
- */
- public synchronized boolean occluded(final double x1, final double y1,
- final double x2, final double y2,
- final double nearestW) {
- if (!ENABLED || levels == 0)
- return false;
-
- int bx1 = (int) Math.floor(x1), by1 = (int) Math.floor(y1);
- int bx2 = (int) Math.ceil(x2), by2 = (int) Math.ceil(y2);
- if (bx1 < 0) bx1 = 0;
- if (by1 < 0) by1 = 0;
- if (bx2 >= bufW) bx2 = bufW - 1;
- if (by2 >= bufH) by2 = bufH - 1;
- if (bx1 > bx2 || by1 > by2)
- return false;
-
- // Coarsest level where the box still covers <= 2 tiles per axis.
- int level = 0;
- while (level + 1 < levels) {
- final int s = TILE << (level + 1);
- final int tw = (bx2 / s) - (bx1 / s) + 1;
- final int th = (by2 / s) - (by1 / s) + 1;
- if (tw > 2 || th > 2)
- break;
- level++;
- }
-
- final int s = TILE << level;
- final int tx1 = bx1 / s, ty1 = by1 / s;
- final int tx2 = bx2 / s, ty2 = by2 / s;
- final int w = levelW[level], off = levelOff[level];
-
- float minStored = Float.POSITIVE_INFINITY;
- for (int ty = ty1; ty <= ty2; ty++)
- for (int tx = tx1; tx <= tx2; tx++) {
- final float v = tiles[off + ty * w + tx];
- if (v < minStored)
- minStored = v;
- }
-
- // Occluded only when the block's nearest point is clearly
- // behind the farthest written depth in the range; the
- // relative margin absorbs parallax between frames.
- return nearestW < minStored * (1.0 - MARGIN);
- }
-
- private void allocate(final int width, final int height) {
- bufW = width;
- bufH = height;
- int lw = (width + TILE - 1) / TILE;
- int lh = (height + TILE - 1) / TILE;
- int count = 0;
- levels = 0;
- while (true) {
- levels++;
- count += lw * lh;
- if (lw == 1 && lh == 1)
- break;
- lw = Math.max(1, (lw + 1) / 2);
- lh = Math.max(1, (lh + 1) / 2);
- }
- tiles = new float[count];
- levelOff = new int[levels];
- levelW = new int[levels];
- levelH = new int[levels];
- lw = (width + TILE - 1) / TILE;
- lh = (height + TILE - 1) / TILE;
- int off = 0;
- for (int l = 0; l < levels; l++) {
- levelOff[l] = off;
- levelW[l] = lw;
- levelH[l] = lh;
- off += lw * lh;
- lw = Math.max(1, (lw + 1) / 2);
- lh = Math.max(1, (lh + 1) / 2);
- }
- Arrays.fill(tiles, Float.NEGATIVE_INFINITY);
- }
-}
+++ /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.geometry.Frustum;
-import eu.svjatoslav.aukio.e3d.geometry.Point2D;
-import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent;
-import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
-import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager;
-
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.awt.image.DataBufferInt;
-import java.awt.image.WritableRaster;
-import java.util.concurrent.ExecutorService;
-import eu.svjatoslav.aukio.e3d.renderer.raster.ParallelTransformCoordinator;
-import java.util.function.Consumer;
-
-/**
- * Contains all state needed to render a single frame: the pixel buffer, graphics context,
- * screen dimensions, and mouse event tracking.
- *
- * <p>A new {@code RenderingContext} is created whenever the view panel is resized.
- * During rendering, shapes use this context to:</p>
- * <ul>
- * <li>Access the raw pixel array ({@link #pixels}) for direct pixel manipulation</li>
- * <li>Access the {@link Graphics2D} context ({@link #graphics}) for Java2D drawing</li>
- * <li>Read screen dimensions ({@link #width}, {@link #height}) and the
- * {@link #centerCoordinate} for coordinate projection</li>
- * <li>Use the {@link #projectionScale} factor for perspective projection</li>
- * </ul>
- *
- * <p>The context also manages mouse interaction detection: as shapes are painted
- * back-to-front, each shape can report itself as the object under the mouse cursor.
- * After painting completes, the topmost shape receives the mouse event.</p>
- *
- * @see ViewPanel the panel that creates and manages this context
- * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape#paint(RenderingContext)
- */
-public class RenderingContext {
-
- /**
- * The {@link BufferedImage} pixel format used for the rendering buffer.
- * TYPE_INT_RGB provides optimal performance for Java2D blitting.
- */
- public static final int bufferedImageType = BufferedImage.TYPE_INT_RGB;
-
- /**
- * Number of horizontal segments (bands) for parallel rendering.
- * Bands are finer than the paint thread count: paint threads steal
- * bands off a shared ticket until all bands are done, so a thread
- * that finishes a cheap band immediately picks up more work.
- * Derived from the render thread count via
- * {@link ViewPanel#setNumRenderThreads(int)}.
- *
- * <p>Equals {@code tilesX * tilesY * viewportCount}: the tile grid
- * covers one viewport, and in stereo mode a second grid covers the
- * other eye (segment indices for the right eye start at
- * {@code tilesX * tilesY}).</p>
- */
- public final int numRenderSegments;
-
- /** Tile columns per viewport (1 = horizontal bands only). */
- public final int tilesX;
-
- /** Tile rows per viewport. */
- public final int tilesY;
-
- /** Number of side-by-side viewports (2 in stereo mode, else 1). */
- public final int viewportCount;
-
- /**
- * Java2D graphics context for drawing text, anti-aliased shapes, and other
- * high-level graphics operations onto the render buffer.
- */
- public final Graphics2D graphics;
-
- /**
- * Segment-specific Graphics2D contexts, each pre-clipped to a horizontal band.
- * Used for thread-safe text and shape rendering without synchronization.
- * Only initialized in the main RenderingContext; null in segment views.
- */
- private Graphics2D[] segmentGraphics;
-
- /**
- * Pixels of the rendering area.
- * Each pixel is a single int in RGB format: {@code (r << 16) | (g << 8) | b}.
- */
- public final int[] pixels;
-
- /**
- * Per-pixel depth (biased 1/z, larger = nearer), always allocated —
- * the painter path was deleted 2026-09-17 and the z-buffer is the
- * only visibility mechanism. Shared with segment/pass copies like
- * {@link #pixels}. Cleared per tile by the paint workers.
- */
- public float[] depth;
-
- /**
- * Active paint pass, set internally by
- * {@code RenderAggregator.paintSorted}: 0 = not painting, 1 =
- * opaque pass (opaque-class triangles only, depth test + write),
- * 2 = alpha pass (alpha-carrying
- * triangles only, depth test, no depth write). Shapes read it to
- * decide whether they belong to the current pass.
- */
- public int depthPass;
-
- /**
- * Depth tolerance in world units for the z-buffer test, in the form
- * {@code zw > stored - DEPTH_MARGIN_DZ * zw * zw} (tolerance behind
- * stored, per-pixel at fragment depth). Default 0 = strict depth: any
- * nonzero window exports per-triangle painter-sort errors into
- * per-pixel occlusion errors (dirt whose triangles sort late beats
- * road pavement that strictly wins at margin 0 — user bugreport
- * 2026-09-16, road pose). Tunable via -Daukio.zbuffer.margin.
- */
- public static final double DEPTH_MARGIN_DZ =
- Double.parseDouble(System.getProperty("aukio.zbuffer.margin", "0"));
-
- /**
- * Width of the rendering area in pixels.
- */
- public final int width;
-
- /**
- * Height of the rendering area in pixels.
- */
- public final int height;
-
- /**
- * Center of the screen in screen space (pixels).
- * This is the point where (0,0) coordinate of the world space is rendered.
- */
- public final Point2D centerCoordinate;
-
- /**
- * Scale factor for perspective projection, derived from screen width.
- * Used to convert normalized device coordinates to screen pixels.
- * This is mutable to support stereo rendering where each eye has a different viewport width.
- */
- public double projectionScale;
-
- /**
- * Minimum Y coordinate (inclusive) to render. Used for multi-threaded rendering
- * where each thread renders a horizontal segment.
- */
- public final int renderMinY;
-
- /**
- * Maximum Y coordinate (exclusive) to render. Used for multi-threaded rendering
- * where each thread renders a horizontal segment.
- */
- public final int renderMaxY;
-
- final BufferedImage bufferedImage;
- /**
- * Unique id of the current transform cycle, assigned by
- * {@code ShapeCollection.transformShapes()} from a global counter.
- * Unlike {@link #frameNumber} (per-context, can repeat across context
- * instances), this never collides, so per-cycle memoization such as
- * composite subtree weights can safely key on it.
- */
- public long transformCycleId;
-
- /**
- * Which projection buffer slot this context writes/reads: 0, 1 or 2.
- * Cycles per render pass (per eye in stereo) when the
- * triple-buffered pipeline is active, so the transform phase of a
- * pass never overwrites the vertex state either of the two previous
- * passes' paints may still be reading. Always 0 when the pipeline is
- * off (tests, single-pass rendering).
- */
- public int vertexSlot = 0;
-
- /**
- * Near-plane distance in camera-space Z units. Polygons whose vertices
- * straddle this plane are clipped against it (new intersection vertices
- * are generated with interpolated UVs); polygons fully behind it are
- * culled. Must be > 0 so the perspective divide stays safe.
- */
- public double nearPlaneDistance = 1.0;
-
- /**
- * Number of frame that is currently being rendered.
- * Every frame has its own number.
- */
- public int frameNumber = 0;
-
- /**
- * Projected-size cull threshold in screen pixels: shapes whose screen
- * bounds span less than this in both axes are not queued for
- * rendering. 0 (the default) disables the cull. Set globally with
- * {@code -Daukio.cull.subpixel=<px>}.
- */
- public double subpixelCullingThreshold = Double.parseDouble(
- System.getProperty("aukio.cull.subpixel", "0"));
-
- /**
- * Epoch of the subpixel-culling verdict cache, stamped per frame by
- * {@code ShapeCollection.transformShapesBegin}: the epoch advances
- * when the camera moves significantly (or after a bounded number of
- * frames), which invalidates all cached skip verdicts and forces one
- * re-evaluation pass. Meaningless when the cull is off.
- */
- public int subpixelCullingEpoch;
-
- /**
- * UI component that mouse is currently hovering over.
- */
- private MouseInteractionController objectPreviouslyUnderMouseCursor;
- /**
- * Mouse click event that needs to be processed.
- * This event is processed only once per frame.
- * If there are multiple objects under the mouse cursor, the top-most object will receive the event.
- * If there are no objects under the mouse cursor, the event will be ignored.
- * If there is no event, this field will be null.
- * This field is set to null after the event is processed.
- */
- private MouseEvent mouseEvent;
- /**
- * UI component that mouse is currently hovering over.
- */
- private MouseInteractionController currentObjectUnderMouseCursor;
- /**
- * Texture coordinates of the mouse cursor on the hit shape (primary
- * texture pixels), or NaN when the hit shape has no texture.
- */
- private double currentMouseTextureU = Double.NaN;
- private double currentMouseTextureV = Double.NaN;
- /**
- * Developer tools for this rendering context.
- * Controls diagnostic features like logging and visualization.
- */
- public DeveloperTools developerTools;
-
- /**
- * Debug log buffer for capturing diagnostic output.
- * Shapes can log messages here that appear in the Developer Tools panel.
- */
- public DebugLogBuffer debugLogBuffer;
-
- /**
- * Global lighting manager for the scene.
- * All shaded polygons use this to calculate lighting. Contains all light sources
- * and ambient light settings for the world.
- */
- public LightingManager lightingManager;
-
- /**
- * Which eye is being rendered in stereo mode. NONE for normal single-view rendering.
- */
- public StereoEye stereoEye = StereoEye.NONE;
-
- /**
- * Width of the viewport for the current eye in stereo mode.
- * Equals {@link #width} when not in stereo mode.
- */
- public int stereoViewportWidth;
-
- /**
- * X offset of the current eye's viewport within the full buffer.
- * 0 for left eye, width/2 for right eye, 0 in normal mode.
- */
- public int stereoViewportOffsetX;
-
- /**
- * Minimum X coordinate (inclusive) for rendering.
- * In stereo mode, this is {@link #stereoViewportOffsetX}.
- * In normal mode, this is 0.
- */
- public int renderMinX;
-
- /**
- * Maximum X coordinate (exclusive) for rendering.
- * In stereo mode, this is {@link #stereoViewportOffsetX} + {@link #stereoViewportWidth}.
- * In normal mode, this is {@link #width}.
- */
- public int renderMaxX;
-
- /**
- * View frustum for frustum culling.
- * Updated each frame from camera state and screen dimensions.
- * Shapes can test their bounding boxes against this frustum to determine
- * if they are potentially visible before expensive vertex transformations.
- */
- public Frustum frustum;
-
- /**
- * World-space position of the viewer for this pass, copied from the
- * camera in {@link eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection#transformShapesBegin}.
- * Used by BSP painter ordering (viewpoint for tree traversal).
- * Fresh instance per context, including per-pass copies, so overlapping
- * pipeline passes each see their own viewpoint.
- */
- public final eu.svjatoslav.aukio.e3d.geometry.Point3D viewerPosition =
- new eu.svjatoslav.aukio.e3d.geometry.Point3D();
-
- /**
- * Statistics for frustum culling performance tracking.
- * Updated each frame: total shapes counted at start, visible shapes
- * incremented during rendering, culled composites tracked during transform.
- */
- public CullingStatistics cullingStatistics;
-
- /**
- * Hi-Z occlusion pyramid, rebuilt from the depth buffer after every
- * painted frame (live {@code ViewPanel} path only — headless
- * {@code Snapshot} renders leave it empty so golden renders never
- * cull). Read during the next frame's transform by
- * {@code TriangleMeshBlock} to skip fully occluded blocks. Shared
- * with pass/segment copies like {@link #depth}.
- */
- public HiZPyramid occlusionPyramid;
-
- /**
- * Executor for the parallel transform phase. When non-null, composites
- * with enough children split their render lists into chunks transformed
- * concurrently. When null, the transform phase runs serially on the
- * render thread.
- */
- public ExecutorService transformExecutor;
-
- /**
- * Per-frame coordinator for the non-blocking parallel transform fork.
- * Set by {@code ShapeCollection.transformShapes()} for the duration of
- * the root transform when {@link #transformExecutor} is available;
- * composites at any nesting level submit chunk tasks to it. Null outside
- * the transform phase and when transforming serially.
- */
- public ParallelTransformCoordinator transformCoordinator;
-
- /**
- * Present gate for this framebuffer: fires when the frame currently
- * held in this buffer has been presented to the display (or dropped
- * from the presentation mailbox). The render thread installs a fresh
- * gate at the start of each frame that reuses the buffer, and the
- * frame's paint continuation awaits the PREVIOUS gate before writing
- * pixels — without it, painting frame F+3 would overwrite the buffer
- * while the present thread is still blitting frame F from it.
- */
- public volatile java.util.concurrent.CountDownLatch presentGate = new java.util.concurrent.CountDownLatch(0);
-
- /**
- * Chunk tasks submitted during the last transform phase, across all
- * nesting levels. Diagnostics: proves nested composites forked.
- */
- public int lastTransformTaskCount;
-
- /**
- * Creates a new rendering context for full-screen rendering.
- *
- * <p>Equivalent to {@code RenderingContext(width, height, 0, height, numRenderSegments)}.</p>
- *
- * @param width the rendering area width in pixels
- * @param height the rendering area height in pixels
- * @param numRenderSegments number of parallel render segments (threads)
- */
- public RenderingContext(final int width, final int height, final int numRenderSegments) {
- this(width, height, 0, height, 1, numRenderSegments, 1);
- }
-
- /**
- * Creates a new rendering context with a rectangular tile grid.
- *
- * <p>Equivalent to the band-only constructors when {@code tilesX == 1}.
- * In stereo mode ({@code viewportCount == 2}) each viewport gets its own
- * tile grid; segment indices for viewport v start at
- * {@code v * tilesX * tilesY}.</p>
- *
- * @param width the rendering area width in pixels
- * @param height the rendering area height in pixels
- * @param tilesX tile columns per viewport (1 = bands only)
- * @param tilesY tile rows per viewport
- * @param viewportCount number of side-by-side viewports (2 = stereo)
- */
- public RenderingContext(final int width, final int height,
- final int tilesX, final int tilesY,
- final int viewportCount) {
- this(width, height, 0, height, tilesX, tilesY, viewportCount);
- }
-
- private RenderingContext(final int width, final int height,
- final int renderMinY, final int renderMaxY,
- final int tilesX, final int tilesY,
- final int viewportCount) {
- this.width = width;
- this.height = height;
- this.renderMinY = renderMinY;
- this.renderMaxY = renderMaxY;
- this.tilesX = tilesX;
- this.tilesY = tilesY;
- this.viewportCount = viewportCount;
- this.numRenderSegments = tilesX * tilesY * viewportCount;
- this.centerCoordinate = new Point2D(width / 2d, height / 2d);
- this.projectionScale = width / 3d;
- this.stereoViewportWidth = width;
- this.stereoViewportOffsetX = 0;
- this.renderMinX = 0;
- this.renderMaxX = width;
-
- // Eagerly allocated so the developer-tools panel always finds it:
- // transformPass() hands the pipeline a per-pass COPY of this context,
- // and the copy constructor shares this reference. Lazy creation in
- // ShapeCollection.transformShapesBegin() would only ever populate the
- // throwaway pass copy, leaving this frame context null forever
- // (the culling display then showed "-" permanently).
- this.cullingStatistics = new CullingStatistics();
- this.occlusionPyramid = new HiZPyramid();
-
- bufferedImage = new BufferedImage(width, height, bufferedImageType);
-
- final WritableRaster raster = bufferedImage.getRaster();
- final DataBufferInt dbi = (DataBufferInt) raster.getDataBuffer();
- pixels = dbi.getData();
-
- // Z-buffer: one w-depth (biased 1/z) value per pixel, cleared per
- // tile in the paint workers. Depth turns the queue order into a
- // performance heuristic only; correctness comes from the per-pixel
- // test. (The queue itself stays painter back-to-front — Z
- // descending, see RenderAggregator.) Always allocated: the
- // z-buffer path is the only renderer.
- depth = new float[width * height];
-
- graphics = (Graphics2D) bufferedImage.getGraphics();
- graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
- graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
-
- segmentGraphics = createSegmentGraphics();
- }
-
- /**
- * Protected constructor for creating segment views.
- * Shares the pixel buffer and graphics context with the parent.
- *
- * @param parent the parent rendering context
- * @param renderMinY minimum Y coordinate (inclusive) for this segment
- * @param renderMaxY maximum Y coordinate (exclusive) for this segment
- */
- protected RenderingContext(final RenderingContext parent,
- final int renderMinY, final int renderMaxY) {
- this.width = parent.width;
- this.height = parent.height;
- this.renderMinY = renderMinY;
- this.renderMaxY = renderMaxY;
- this.tilesX = parent.tilesX;
- this.tilesY = parent.tilesY;
- this.viewportCount = parent.viewportCount;
- this.numRenderSegments = parent.numRenderSegments;
- this.centerCoordinate = parent.centerCoordinate;
- this.projectionScale = parent.projectionScale;
- this.stereoViewportWidth = parent.stereoViewportWidth;
- this.stereoViewportOffsetX = parent.stereoViewportOffsetX;
- this.stereoEye = parent.stereoEye;
- this.renderMinX = parent.renderMinX;
- this.renderMaxX = parent.renderMaxX;
- this.bufferedImage = parent.bufferedImage;
- this.pixels = parent.pixels;
- this.depth = parent.depth;
- this.graphics = parent.graphics;
- this.vertexSlot = parent.vertexSlot;
- this.nearPlaneDistance = parent.nearPlaneDistance;
- this.developerTools = parent.developerTools;
- this.debugLogBuffer = parent.debugLogBuffer;
- this.lightingManager = parent.lightingManager;
- this.occlusionPyramid = parent.occlusionPyramid;
- this.segmentGraphics = null;
- }
-
- /**
- * Creates an independent pass context for one pipeline pass (one eye
- * in stereo): shares the frame's pixel buffer, graphics and services,
- * but owns the per-pass projection fields (center, scale, stereo
- * viewport, slot, frame/cycle stamps). The next pass's setup writes
- * to its own copy, so it cannot disturb this pass's in-flight
- * transform chunks or its asynchronous sort/bin/paint continuation.
- *
- * @param parent the frame rendering context to copy from
- */
- public RenderingContext(final RenderingContext parent) {
- this.width = parent.width;
- this.height = parent.height;
- this.renderMinY = parent.renderMinY;
- this.renderMaxY = parent.renderMaxY;
- this.tilesX = parent.tilesX;
- this.tilesY = parent.tilesY;
- this.viewportCount = parent.viewportCount;
- this.numRenderSegments = parent.numRenderSegments;
- this.centerCoordinate = new Point2D(parent.centerCoordinate.x, parent.centerCoordinate.y);
- this.projectionScale = parent.projectionScale;
- this.stereoViewportWidth = parent.stereoViewportWidth;
- this.stereoViewportOffsetX = parent.stereoViewportOffsetX;
- this.stereoEye = parent.stereoEye;
- this.renderMinX = parent.renderMinX;
- this.renderMaxX = parent.renderMaxX;
- this.bufferedImage = parent.bufferedImage;
- this.pixels = parent.pixels;
- this.depth = parent.depth;
- this.graphics = parent.graphics;
- this.vertexSlot = parent.vertexSlot;
- this.nearPlaneDistance = parent.nearPlaneDistance;
- this.frameNumber = parent.frameNumber;
- this.transformCycleId = parent.transformCycleId;
- this.transformExecutor = parent.transformExecutor;
- this.developerTools = parent.developerTools;
- this.debugLogBuffer = parent.debugLogBuffer;
- this.lightingManager = parent.lightingManager;
- this.cullingStatistics = parent.cullingStatistics;
- this.occlusionPyramid = parent.occlusionPyramid;
- this.subpixelCullingThreshold = parent.subpixelCullingThreshold;
- this.subpixelCullingEpoch = parent.subpixelCullingEpoch;
- this.setMouseEvent(parent.getMouseEvent());
- // Share the pre-clipped per-tile graphics: glyph rendering
- // (user-facing text) draws through them by segment index. Null
- // here made every glyph paint die with an NPE mid-tile (broken
- // tiles whenever text faced the reader).
- this.segmentGraphics = parent.segmentGraphics;
- // frustum stays null: created fresh per pass in transformShapesBegin
- }
-
- /**
- * Resets per-frame state in preparation for rendering a new frame.
- * Increments the frame number and clears the mouse event state.
- */
- public void prepareForNewFrameRendering() {
- frameNumber++;
- mouseEvent = null;
- currentObjectUnderMouseCursor = null;
- }
-
- /**
- * Creates Graphics2D contexts for each render segment, pre-clipped to
- * its tile rectangle. Segment index layout: viewport v, tile row ty,
- * tile column tx -> v * tilesX * tilesY + ty * tilesX + tx.
- *
- * @return array of Graphics2D objects, one per segment
- */
- private Graphics2D[] createSegmentGraphics() {
- final Graphics2D[] contexts = new Graphics2D[numRenderSegments];
- final int viewportWidth = width / viewportCount;
- final int tileW = viewportWidth / tilesX;
- final int tileH = height / tilesY;
-
- for (int v = 0; v < viewportCount; v++) {
- final int viewportX = v * viewportWidth;
- for (int ty = 0; ty < tilesY; ty++) {
- final int minY = ty * tileH;
- final int maxY = (ty == tilesY - 1) ? height : (ty + 1) * tileH;
- for (int tx = 0; tx < tilesX; tx++) {
- final int minX = viewportX + tx * tileW;
- final int maxX = (tx == tilesX - 1)
- ? viewportX + viewportWidth : minX + tileW;
-
- final Graphics2D g = bufferedImage.createGraphics();
- g.setClip(minX, minY, maxX - minX, maxY - minY);
- g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
- g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
- contexts[v * tilesX * tilesY + ty * tilesX + tx] = g;
- }
- }
- }
-
- return contexts;
- }
-
- /**
- * Returns the backing image whose pixel buffer the rasterizer paints into.
- *
- * <p>Exposed for headless rendering: after a transform/sort/paint pass the
- * image holds the finished frame and can be saved or compared directly.</p>
- *
- * @return the backing buffered image
- */
- public BufferedImage getImage() {
- return bufferedImage;
- }
-
- /**
- * Returns the Graphics2D context for a specific render segment.
- * Each segment's Graphics2D is pre-clipped to its Y bounds.
- *
- * @param segmentIndex the segment index (0 to numRenderSegments-1)
- * @return the Graphics2D for that segment
- * @throws NullPointerException if called on a segment view (not the main context)
- */
- public Graphics2D getSegmentGraphics(final int segmentIndex) {
- return segmentGraphics[segmentIndex];
- }
-
- /**
- * Disposes all Graphics2D resources associated with this context.
- * Should be called when the context is no longer needed (e.g., on resize).
- */
- public void dispose() {
- if (segmentGraphics != null) {
- for (final Graphics2D g : segmentGraphics) {
- if (g != null) {
- g.dispose();
- }
- }
- }
- if (graphics != null) {
- graphics.dispose();
- }
- }
-
- /**
- * Executes a graphics operation in a thread-safe manner.
- * This must be used for all Graphics2D operations (text, lines, etc.)
- * during multi-threaded rendering.
- *
- * @param operation the graphics operation to execute
- */
- public void executeWithGraphics(final Consumer<Graphics2D> operation) {
- synchronized (graphics) {
- operation.accept(graphics);
- }
- }
-
- /**
- * Returns the pending mouse event for this frame, or {@code null} if none.
- *
- * @return the mouse event to process, or {@code null}
- */
- public MouseEvent getMouseEvent() {
- return mouseEvent;
- }
-
- /**
- * Sets the mouse event to be processed during this frame's rendering.
- *
- * @param mouseEvent the mouse event with position and button information
- */
- public void setMouseEvent(MouseEvent mouseEvent) {
- this.mouseEvent = mouseEvent;
- }
-
- /**
- * Called when given object was detected under mouse cursor, while processing {@link #mouseEvent}.
- * Because objects are rendered back to front. The last method caller will set the top-most object, if
- * there are multiple objects under mouse cursor.
- *
- * @param currentObjectUnderMouseCursor the object that is currently under the mouse cursor
- */
- public synchronized void setCurrentObjectUnderMouseCursor(MouseInteractionController currentObjectUnderMouseCursor) {
- setCurrentObjectUnderMouseCursor(currentObjectUnderMouseCursor,
- Double.NaN, Double.NaN);
- }
-
- /**
- * Called when given object was detected under mouse cursor, with the
- * texture coordinates of the hit point (for textured shapes).
- *
- * @param currentObjectUnderMouseCursor the object under the mouse cursor
- * @param textureU texture-space X of the hit point in primary-texture pixels
- * @param textureV texture-space Y of the hit point in primary-texture pixels
- */
- public synchronized void setCurrentObjectUnderMouseCursor(
- final MouseInteractionController currentObjectUnderMouseCursor,
- final double textureU, final double textureV) {
- this.currentObjectUnderMouseCursor = currentObjectUnderMouseCursor;
- this.currentMouseTextureU = textureU;
- this.currentMouseTextureV = textureV;
- }
-
- /**
- * Returns the current object under the mouse cursor.
- * Used by segment rendering to collect mouse results.
- *
- * @return the current object under mouse cursor, or null
- */
- public synchronized MouseInteractionController getCurrentObjectUnderMouseCursor() {
- return currentObjectUnderMouseCursor;
- }
-
- /**
- * Handles mouse events for components and returns whether a view repaint is needed.
- *
- * @return {@code true} if view update is needed as a consequence of this mouse event
- */
- public boolean handlePossibleComponentMouseEvent() {
- if (mouseEvent == null) return false;
-
- boolean viewRepaintNeeded = false;
-
- if (objectPreviouslyUnderMouseCursor != currentObjectUnderMouseCursor) {
- // Mouse cursor has just entered or left component.
- viewRepaintNeeded = objectPreviouslyUnderMouseCursor != null && objectPreviouslyUnderMouseCursor.mouseExited();
- viewRepaintNeeded |= currentObjectUnderMouseCursor != null && currentObjectUnderMouseCursor.mouseEntered();
- objectPreviouslyUnderMouseCursor = currentObjectUnderMouseCursor;
- }
-
- if (mouseEvent.button != 0 && currentObjectUnderMouseCursor != null) {
- // Mouse button was clicked on some component.
- viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseClicked(
- mouseEvent.button, currentMouseTextureU, currentMouseTextureV);
- } else if (currentObjectUnderMouseCursor != null)
- // hover: let the component track the pointer position
- viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseHover(
- currentMouseTextureU, currentMouseTextureV);
-
- return viewRepaintNeeded;
- }
-
-}
+++ /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.humaninput.MouseEvent;
-import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
-
-import java.awt.*;
-import java.util.function.Consumer;
-
-/**
- * A view of a RenderingContext for rendering a horizontal screen segment.
- *
- * <p>This class wraps a parent RenderingContext and provides its own Y-bounds
- * for multi-threaded rendering. All operations delegate to the parent context,
- * but with segment-specific Y bounds for pixel operations.</p>
- *
- * <p>Mouse tracking is local to each segment and must be combined after all
- * segments complete rendering.</p>
- *
- * @see RenderingContext
- */
-public class SegmentRenderingContext extends RenderingContext {
-
- private final RenderingContext parent;
- private final int segmentIndex;
- private MouseInteractionController segmentMouseHit;
- private double segmentMouseHitU = Double.NaN;
- private double segmentMouseHitV = Double.NaN;
-
- /**
- * Creates a segment view of a parent rendering context.
- *
- * @param parent the parent rendering context to delegate to
- * @param renderMinY minimum Y coordinate (inclusive) for this segment
- * @param renderMaxY maximum Y coordinate (exclusive) for this segment
- * @param segmentIndex the index of this segment (0 to numRenderSegments-1)
- */
- public SegmentRenderingContext(final RenderingContext parent,
- final int renderMinY, final int renderMaxY,
- final int segmentIndex) {
- super(parent, renderMinY, renderMaxY);
- this.parent = parent;
- this.segmentIndex = segmentIndex;
- }
-
- @Override
- public void executeWithGraphics(final Consumer<Graphics2D> operation) {
- operation.accept(parent.getSegmentGraphics(segmentIndex));
- }
-
- @Override
- public MouseEvent getMouseEvent() {
- return parent.getMouseEvent();
- }
-
- @Override
- public void setMouseEvent(final MouseEvent mouseEvent) {
- parent.setMouseEvent(mouseEvent);
- }
-
- @Override
- public synchronized void setCurrentObjectUnderMouseCursor(final MouseInteractionController controller) {
- setCurrentObjectUnderMouseCursor(controller, Double.NaN, Double.NaN);
- }
-
- @Override
- public synchronized void setCurrentObjectUnderMouseCursor(
- final MouseInteractionController controller,
- final double textureU, final double textureV) {
- this.segmentMouseHit = controller;
- this.segmentMouseHitU = textureU;
- this.segmentMouseHitV = textureV;
- }
-
- /**
- * Returns the mouse hit detected in this segment.
- *
- * @return the MouseInteractionController that was under the mouse in this segment, or null
- */
- public MouseInteractionController getSegmentMouseHit() {
- return segmentMouseHit;
- }
-
- /**
- * Texture-space X of the hit point (primary texture pixels), NaN if none.
- */
- public double getSegmentMouseHitU() {
- return segmentMouseHitU;
- }
-
- /**
- * Texture-space Y of the hit point (primary texture pixels), NaN if none.
- */
- public double getSegmentMouseHitV() {
- return segmentMouseHitV;
- }
-
- @Override
- public synchronized MouseInteractionController getCurrentObjectUnderMouseCursor() {
- return segmentMouseHit;
- }
-}
\ No newline at end of file
+++ /dev/null
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.gui;
-
-/**
- * Identifies which eye is being rendered in stereoscopic mode.
- *
- * @see RenderingContext#stereoEye
- */
-public enum StereoEye {
- /** Normal single-view rendering (no stereo). */
- NONE,
- /** Left eye view in side-by-side stereo mode. */
- LEFT,
- /** Right eye view in side-by-side stereo mode. */
- RIGHT
-}
+++ /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 java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Low-overhead recorder of per-thread work intervals for the thread
- * timeline display in the Developer Tools window.
- *
- * <p>Task submission sites (transform chunks, paint tiles, tile binning)
- * and render-thread phases (orchestration, waiting, blit) record their
- * [start, end) intervals here when enabled. The timeline component paints
- * each thread as a row, time on the X axis, the interval kind as color —
- * the software-renderer equivalent of a GPU frame-profiler occupancy
- * view. Idle time is simply the absence of intervals (black).</p>
- *
- * <p>Overhead when disabled: one volatile read per task. When enabled:
- * two {@code System.nanoTime()} calls, one atomic increment and four
- * array stores per task (~100 ns), negligible at hundreds of tasks per
- * frame.</p>
- *
- * <p>Frame parity distinguishes "current frame" work from "next frame"
- * work in the colors: transform/paint/binning kinds come in an even and
- * an odd variant, selected by the parity that was current when the task
- * was SUBMITTED (captured at task creation, so overlapping frames keep
- * their own color).</p>
- */
-public final class ThreadActivityRecorder {
-
- /** Kind base: vertex transform chunk (add frame parity 0..2). */
- public static final int KIND_TRANSFORM = 0;
- /** Kind base: paint tile task (add frame parity 0..2). */
- public static final int KIND_PAINT = 3;
- /** Kind base: tile binning task (add frame parity 0..2). */
- public static final int KIND_BIN = 6;
- /** Kind: render thread orchestration (tree walk, submission, merges). */
- public static final int KIND_RENDER = 9;
- /** Kind: render thread blocked waiting for worker tasks. */
- public static final int KIND_AWAIT = 10;
- /** Kind: render thread blitting the finished frame to screen. */
- public static final int KIND_BLIT = 11;
- /** Kind: a pass's asynchronous continuation (drain, sort, bin, paint submission). */
- public static final int KIND_PREP = 12;
- /** Kind: continuation sub-phase: draining and merging transform chunks. */
- public static final int KIND_DRAIN = 13;
- /** Kind: continuation sub-phase: depth sort. */
- public static final int KIND_SORT = 14;
-
- /** Number of distinct kinds (three parities each for transform/paint/bin, plus 9..14). */
- public static final int KIND_COUNT = 15;
-
- private static final int CAPACITY = 1 << 18;
- private static final int MASK = CAPACITY - 1;
-
- private static final long[] starts = new long[CAPACITY];
- private static final long[] ends = new long[CAPACITY];
- private static final byte[] kinds = new byte[CAPACITY];
- private static final byte[] rows = new byte[CAPACITY];
- private static final AtomicInteger cursor = new AtomicInteger();
-
- private static volatile boolean enabled = false;
- private static volatile int frameParity = 0;
-
- private static final ConcurrentHashMap<String, Integer> rowByThreadName = new ConcurrentHashMap<>();
- private static final CopyOnWriteArrayList<String> rowNames = new CopyOnWriteArrayList<>();
- private static final AtomicInteger nextRow = new AtomicInteger();
-
- private ThreadActivityRecorder() {
- }
-
- /**
- * @return true when recording is active (checked by task submission sites)
- */
- public static boolean isEnabled() {
- return enabled;
- }
-
- /**
- * Enables or disables recording. Enabling starts with a clean buffer
- * and fresh thread-row assignment.
- *
- * @param value true to start recording
- */
- public static void setEnabled(final boolean value) {
- if (value && !enabled) {
- clear();
- }
- enabled = value;
- }
-
- /**
- * Drops all recorded intervals and thread-row assignments.
- */
- public static void clear() {
- cursor.set(0);
- rowByThreadName.clear();
- rowNames.clear();
- nextRow.set(0);
- // starts[]==0 marks an empty slot for the timeline sweep
- java.util.Arrays.fill(starts, 0L);
- }
-
- /**
- * Sets the parity (0/1) of the frame currently being prepared.
- * Called by the render thread at the start of each frame; task
- * submission sites capture it into their tasks so overlapping frames
- * keep distinct colors.
- *
- * @param parity frame parity, 0 or 1
- */
- public static void setFrameParity(final int parity) {
- frameParity = parity;
- }
-
- /**
- * @return parity of the frame currently being prepared
- */
- public static int frameParity() {
- return frameParity;
- }
-
- /**
- * Records one work interval on the calling thread.
- *
- * @param kind interval kind (one of the KIND_* bases, plus parity
- * for transform/paint/binning)
- * @param t0 interval start, from {@link System#nanoTime()}
- * @param t1 interval end, from {@link System#nanoTime()}
- */
- public static void record(final int kind, final long t0, final long t1) {
- // Rows are keyed by thread NAME, not Thread object: after a
- // stop()/start() cycle the executor is recreated with fresh
- // threads under the same names, and they must reuse the same
- // rows — otherwise the timeline fills with dead threads' rows
- // and pushes the live workers below the visible area.
- final String threadName = Thread.currentThread().getName();
- Integer row = rowByThreadName.get(threadName);
- if (row == null) {
- row = rowByThreadName.computeIfAbsent(threadName, t -> {
- final int r = nextRow.getAndIncrement();
- rowNames.add(t);
- return r;
- });
- }
- if (row > 127) {
- return;
- }
- final int i = cursor.getAndIncrement() & MASK;
- starts[i] = t0;
- ends[i] = t1;
- kinds[i] = (byte) kind;
- rows[i] = (byte) (int) row;
- }
-
- // ---- Snapshot access for the timeline component ----
-
- /** @return total number of intervals recorded since the last clear */
- public static int cursor() {
- return cursor.get();
- }
-
- /** @return ring buffer capacity */
- public static int capacity() {
- return CAPACITY;
- }
-
- /** @return interval start array (index space of the ring buffer) */
- public static long[] starts() {
- return starts;
- }
-
- /** @return interval end array (index space of the ring buffer) */
- public static long[] ends() {
- return ends;
- }
-
- /** @return interval kind array (index space of the ring buffer) */
- public static byte[] kinds() {
- return kinds;
- }
-
- /** @return interval thread-row array (index space of the ring buffer) */
- public static byte[] rows() {
- return rows;
- }
-
- /** @return number of distinct thread rows seen so far */
- public static int rowCount() {
- return nextRow.get();
- }
-
- /**
- * @param row thread row index
- * @return display name for the row ("render" for the render thread,
- * otherwise the thread name with the e3d- prefix stripped)
- */
- public static String rowName(final int row) {
- if (row >= rowNames.size()) {
- return "?";
- }
- final String name = rowNames.get(row);
- if ("e3d-render".equals(name)) {
- return "render";
- }
- return name.startsWith("e3d-") ? name.substring(4) : name;
- }
-}
* This project is released under Creative Commons Zero (CC0) license.
*/
package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder;
import javax.swing.*;
import java.awt.*;
* This project is released under Creative Commons Zero (CC0) license.
*/
package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.diag.DebugLogBuffer;
+import eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
+import eu.svjatoslav.aukio.e3d.renderer.raster.StereoEye;
+import eu.svjatoslav.aukio.e3d.renderer.raster.SegmentRenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
import eu.svjatoslav.aukio.e3d.diag.Diagnostics;
import eu.svjatoslav.aukio.e3d.diag.EngineConfig;
* Creates a new view panel with default settings.
*/
public ViewPanel() {
- frameListeners.add(camera);
+ frameListeners.add((panel, deltaMs) -> camera.onFrame(deltaMs));
frameListeners.add(inputManager);
// persistent log + telemetry (idempotent); the view telemetry
// Walks the tree and forks heavy composites into chunk tasks,
// but does NOT wait for them: draining happens inside the
// pass's continuation on a worker thread.
- rootShapeCollection.transformShapesBegin(this, passContext);
+ rootShapeCollection.transformShapesBegin(getCamera(), passContext);
return passContext;
} finally {
location.x = originalX;
* This project is released under Creative Commons Zero (CC0) license.
*/
package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
import eu.svjatoslav.aukio.e3d.math.TransformStack;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
/**
* Tracks an object's position in view/camera space for distance and angle calculations.
package eu.svjatoslav.aukio.e3d.gui.humaninput;
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
import eu.svjatoslav.aukio.e3d.gui.FrameListener;
import eu.svjatoslav.aukio.e3d.gui.ViewFrame;
import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
* This project is released under Creative Commons Zero (CC0) license.
*/
package eu.svjatoslav.aukio.e3d.gui.humaninput;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
*/
package eu.svjatoslav.aukio.e3d.gui.humaninput;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
import eu.svjatoslav.aukio.e3d.gui.FrameListener;
* <ul>
* <li>{@link eu.svjatoslav.aukio.e3d.gui.ViewPanel} - The main rendering surface (JPanel)</li>
* <li>{@link eu.svjatoslav.aukio.e3d.gui.ViewFrame} - A JFrame with embedded ViewPanel</li>
- * <li>{@link eu.svjatoslav.aukio.e3d.gui.Camera} - Represents the viewer's position and orientation</li>
+ * <li>{@link eu.svjatoslav.aukio.e3d.geometry.Camera} - Represents the viewer's position and orientation</li>
* <li>{@link eu.svjatoslav.aukio.e3d.gui.DeveloperTools} - Debugging and profiling utilities</li>
* </ul>
*
* @see eu.svjatoslav.aukio.e3d.gui.ViewPanel
- * @see eu.svjatoslav.aukio.e3d.gui.Camera
+ * @see eu.svjatoslav.aukio.e3d.geometry.Camera
*/
-package eu.svjatoslav.aukio.e3d.gui;
\ No newline at end of file
+package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
*/
package eu.svjatoslav.aukio.e3d.gui.spacemouse;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
import eu.svjatoslav.aukio.e3d.gui.FrameListener;
import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
import eu.svjatoslav.aukio.e3d.math.Matrix3x3;
import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape;
import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
import java.util.Locale;
package eu.svjatoslav.aukio.e3d.headless;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection;
import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager;
+++ /dev/null
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.math;
-
-import eu.svjatoslav.aukio.e3d.geometry.Point2D;
-import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-
-/**
- * A vertex in 3D space with transformation and screen projection support.
- *
- * <p>A vertex represents a corner point of a polygon or polyhedron. In addition to
- * the 3D coordinate, it stores the transformed position (relative to viewer) and
- * the projected screen coordinates for rendering.</p>
- *
- * <p><b>Coordinate spaces:</b></p>
- * <ul>
- * <li>{@link #coordinate} - Original position in local/model space</li>
- * <li>{@link #transformedCoordinate} - Position relative to viewer (camera space)</li>
- * <li>{@link #onScreenCoordinate} - 2D screen position after perspective projection</li>
- * </ul>
- *
- * <p><b>Example:</b></p>
- * <pre>{@code
- * Vertex v = new Vertex(new Point3D(10, 20, 30));
- * v.calculateLocationRelativeToViewer(transformStack, renderContext);
- * if (v.transformedCoordinate.z > 0) {
- * // Vertex is in front of the camera
- * }
- * }</pre>
- *
- * @see Point3D
- * @see TransformStack
- */
-public class Vertex {
-
- /**
- * Vertex coordinate in local/model 3D space.
- */
- public Point3D coordinate;
-
- /**
- * Vertex coordinate relative to the viewer after transformation (camera
- * space), per buffer slot. Slot parity lets the transform phase of the
- * NEXT frame write slot B while the paint phase of the current frame
- * still reads slot A (double-buffered pipeline). Never access directly:
- * use {@link #transformedCoordinate(RenderingContext)}.
- */
- private final Point3D transformedCoordinate0 = new Point3D();
- private final Point3D transformedCoordinate1 = new Point3D();
- private final Point3D transformedCoordinate2 = new Point3D();
-
- /**
- * Vertex position on screen in pixels, per buffer slot.
- * Use {@link #onScreenCoordinate(RenderingContext)}.
- */
- private final Point2D onScreenCoordinate0 = new Point2D();
- private final Point2D onScreenCoordinate1 = new Point2D();
- private final Point2D onScreenCoordinate2 = new Point2D();
-
- /**
- * Texture coordinate for UV mapping (optional).
- */
- public Point2D textureCoordinate;
-
- /**
- * Normal vector for this vertex (optional).
- * Used by CSG operations for smooth interpolation during polygon splitting.
- * Null for non-CSG usage; existing rendering code ignores this field.
- */
- public Point3D normal;
-
-
- /**
- * The transform cycle when each slot was last transformed (for
- * caching). Keyed by {@link RenderingContext#transformCycleId}, NOT
- * by frameNumber: in stereo mode both eye passes share the same
- * parity context and therefore the same frameNumber, while using
- * different slots — a frameNumber-based key lets one eye's pass
- * falsely hit the cache entry the other eye wrote for the same slot
- * an odd number of passes earlier, leaving opposite-eye (wrong
- * viewport-offset) screen coordinates in the slot: the affected eye
- * paints fully off-viewport, i.e. a black half-frame (observed as
- * violent left/right flashing, 2026-09-09). transformCycleId is
- * unique per pass, so a hit always means "already transformed within
- * THIS pass" — the only correct dedupe semantics.
- */
- private long lastTransformCycle0 = -1;
- private long lastTransformCycle1 = -1;
- private long lastTransformCycle2 = -1;
-
- /**
- * Creates a vertex at the origin (0, 0, 0) with no texture coordinate.
- */
- public Vertex() {
- this(new Point3D());
- }
-
- /**
- * Creates a vertex at the specified position with no texture coordinate.
- *
- * @param coordinate the 3D position of this vertex
- */
- public Vertex(final Point3D coordinate) {
- this(coordinate, null);
- }
-
- /**
- * Creates a vertex at the specified position with an optional texture coordinate.
- *
- * @param coordinate the 3D position of this vertex
- * @param textureCoordinate the UV texture coordinate, or {@code null} for none
- */
- public Vertex(final Point3D coordinate, final Point2D textureCoordinate) {
- this.coordinate = coordinate;
- this.textureCoordinate = textureCoordinate;
- }
-
- /**
- * Returns the camera-space coordinate for the rendering context's buffer
- * slot. Valid only after this vertex was transformed for that slot's
- * current frame.
- *
- * @param renderContext the rendering context (selects the buffer slot)
- * @return the transformed coordinate (camera space) for the active slot
- */
- public Point3D transformedCoordinate(final RenderingContext renderContext) {
- // Dual fields, not a slot array: measured 2026-09-05 (400-sphere
- // scene) that array indexing adds a second dependent load per access
- // and cost ~60% of transform phase time; a perfectly-predicted
- // branch + direct field load is free.
- final int slot = renderContext.vertexSlot;
- return slot == 0 ? transformedCoordinate0
- : slot == 1 ? transformedCoordinate1 : transformedCoordinate2;
- }
-
- /**
- * Returns the screen-space position for the rendering context's buffer
- * slot.
- *
- * @param renderContext the rendering context (selects the buffer slot)
- * @return the on-screen coordinate (pixels) for the active slot
- */
- public Point2D onScreenCoordinate(final RenderingContext renderContext) {
- final int slot = renderContext.vertexSlot;
- return slot == 0 ? onScreenCoordinate0
- : slot == 1 ? onScreenCoordinate1 : onScreenCoordinate2;
- }
-
-
- /**
- * Transforms this vertex from model space to screen space.
- *
- * <p>This method applies the transform stack to compute the vertex position
- * relative to the viewer, then projects it to 2D screen coordinates.
- * Results are cached per-frame per-slot to avoid redundant calculations.</p>
- *
- * @param transforms the transform stack to apply (world-to-camera transforms)
- * @param renderContext the rendering context providing projection parameters
- */
- public void calculateLocationRelativeToViewer(final TransformStack transforms,
- final RenderingContext renderContext) {
-
- final Point3D transformedCoordinate;
- final Point2D onScreenCoordinate;
- switch (renderContext.vertexSlot) {
- case 0:
- if (lastTransformCycle0 == renderContext.transformCycleId)
- return;
- lastTransformCycle0 = renderContext.transformCycleId;
- transformedCoordinate = transformedCoordinate0;
- onScreenCoordinate = onScreenCoordinate0;
- break;
- case 1:
- if (lastTransformCycle1 == renderContext.transformCycleId)
- return;
- lastTransformCycle1 = renderContext.transformCycleId;
- transformedCoordinate = transformedCoordinate1;
- onScreenCoordinate = onScreenCoordinate1;
- break;
- default:
- if (lastTransformCycle2 == renderContext.transformCycleId)
- return;
- lastTransformCycle2 = renderContext.transformCycleId;
- transformedCoordinate = transformedCoordinate2;
- onScreenCoordinate = onScreenCoordinate2;
- break;
- }
- transforms.transform(coordinate, transformedCoordinate);
- onScreenCoordinate.x = ((transformedCoordinate.x / transformedCoordinate.z) * renderContext.projectionScale);
- onScreenCoordinate.y = ((transformedCoordinate.y / transformedCoordinate.z) * renderContext.projectionScale);
- onScreenCoordinate.add(renderContext.centerCoordinate);
- onScreenCoordinate.x += renderContext.stereoViewportOffsetX;
- }
-
- /**
- * Writes a camera-space position directly into this vertex's slot state
- * and projects it to screen coordinates, bypassing the transform stack.
- *
- * <p>Used by near-plane clipping ({@code AbstractCoordinateShape}), which
- * creates intersection vertices that exist ONLY in camera space — there
- * is no model-space coordinate to transform. The given z must be > 0
- * (clip against the near plane guarantees z == nearPlaneDistance).</p>
- *
- * @param x camera-space X
- * @param y camera-space Y
- * @param z camera-space Z (depth in front of viewer, > 0)
- * @param renderContext the rendering context (selects the buffer slot and
- * provides projection parameters)
- */
- public void setCameraSpaceCoordinate(final double x, final double y, final double z,
- final RenderingContext renderContext) {
- final Point3D transformedCoordinate;
- final Point2D onScreenCoordinate;
- switch (renderContext.vertexSlot) {
- case 0:
- transformedCoordinate = transformedCoordinate0;
- onScreenCoordinate = onScreenCoordinate0;
- break;
- case 1:
- transformedCoordinate = transformedCoordinate1;
- onScreenCoordinate = onScreenCoordinate1;
- break;
- default:
- transformedCoordinate = transformedCoordinate2;
- onScreenCoordinate = onScreenCoordinate2;
- break;
- }
- transformedCoordinate.x = x;
- transformedCoordinate.y = y;
- transformedCoordinate.z = z;
- onScreenCoordinate.x = ((x / z) * renderContext.projectionScale);
- onScreenCoordinate.y = ((y / z) * renderContext.projectionScale);
- onScreenCoordinate.add(renderContext.centerCoordinate);
- onScreenCoordinate.x += renderContext.stereoViewportOffsetX;
- }
-
- // ========== CSG support methods ==========
-
- /**
- * Creates a deep copy of this vertex.
- * Clones the coordinate, normal (if present), and texture coordinate (if present).
- * The transformedCoordinate and onScreenCoordinate are not cloned (they are computed per-frame).
- *
- * @return a new Vertex with cloned data
- */
- public Vertex clone() {
- final Vertex result = new Vertex(new Point3D(coordinate),
- textureCoordinate != null ? new Point2D(textureCoordinate) : null);
- if (normal != null) {
- result.normal = new Point3D(normal);
- }
- return result;
- }
-
- /**
- * Flips the orientation of this vertex by negating the normal vector.
- * Called when the orientation of a polygon is flipped during CSG operations.
- * If normal is null, this method does nothing.
- */
- public void flip() {
- if (normal != null) {
- normal = normal.withNegated();
- }
- }
-
- /**
- * Creates a new vertex between this vertex and another by linearly interpolating
- * all properties using parameter t.
- *
- * <p>Interpolates: position, normal (if present), and texture coordinate (if present).</p>
- *
- * @param other the other vertex to interpolate towards
- * @param t the interpolation parameter (0 = this vertex, 1 = other vertex)
- * @return a new Vertex representing the interpolated position
- */
- public Vertex interpolate(final Vertex other, final double t) {
- final Vertex result = new Vertex(
- coordinate.interpolate(other.coordinate, t),
- (textureCoordinate != null && other.textureCoordinate != null)
- ? new Point2D(
- textureCoordinate.x + (other.textureCoordinate.x - textureCoordinate.x) * t,
- textureCoordinate.y + (other.textureCoordinate.y - textureCoordinate.y) * t)
- : null
- );
- if (normal != null && other.normal != null) {
- result.normal = normal.interpolate(other.normal, t);
- }
- return result;
- }
-}
+++ /dev/null
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.renderer.octree;
-
-/**
- * Point in 3D space with integer coordinates. Used for octree voxel positions.
- */
-public class IntegerPoint
-{
- /** X coordinate. */
- public int x;
- /** Y coordinate. */
- public int y;
- /** Z coordinate. */
- public int z = 0;
-
- /**
- * Creates a point at the origin (0, 0, 0).
- */
- public IntegerPoint()
- {
- }
-
- /**
- * Creates a point with the specified coordinates.
- *
- * @param x the X coordinate
- * @param y the Y coordinate
- * @param z the Z coordinate
- */
- public IntegerPoint(final int x, final int y, final int z)
- {
- this.x = x;
- this.y = y;
- this.z = z;
- }
-}
* This project is released under Creative Commons Zero (CC0) license.
*/
package eu.svjatoslav.aukio.e3d.renderer.octree;
+import eu.svjatoslav.aukio.e3d.geometry.IntegerPoint;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
import eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.Ray;
* <ul>
* <li>{@link eu.svjatoslav.aukio.e3d.renderer.octree.OctreeVolume} - the main octree data structure
* for storing and querying voxel cells</li>
- * <li>{@link eu.svjatoslav.aukio.e3d.renderer.octree.IntegerPoint} - integer 3D coordinate used
+ * <li>{@link eu.svjatoslav.aukio.e3d.geometry.IntegerPoint} - integer 3D coordinate used
* for voxel addressing</li>
* </ul>
*
*/
package eu.svjatoslav.aukio.e3d.renderer.octree;
+import eu.svjatoslav.aukio.e3d.geometry.IntegerPoint;
package eu.svjatoslav.aukio.e3d.renderer.octree.raytracer;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
import eu.svjatoslav.aukio.e3d.math.Matrix3x3;
import static eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.RaytracingCamera.SIZE;
package eu.svjatoslav.aukio.e3d.renderer.octree.raytracer;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
import eu.svjatoslav.aukio.e3d.math.Matrix3x3;
import eu.svjatoslav.aukio.e3d.math.Transform;
import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
--- /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;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Statistics for frustum culling, tracking composite-level culling efficiency.
+ *
+ * <p>Updated each frame during the rendering pipeline:</p>
+ * <ul>
+ * <li>{@link #totalComposites} - incremented before each composite's frustum test</li>
+ * <li>{@link #culledComposites} - incremented when a composite fails the frustum test</li>
+ * </ul>
+ *
+ * <p>Thread safety: counters are {@link AtomicInteger} because the parallel
+ * transform phase increments them from multiple worker threads.</p>
+ *
+ * <p>Displayed in the {@link eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel} to help developers understand
+ * culling efficiency and optimize scene graphs.</p>
+ *
+ * @see eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel
+ * @see eu.svjatoslav.aukio.e3d.renderer.raster.Frustum
+ */
+public class CullingStatistics {
+
+ /**
+ * Total number of composite shapes tested against the frustum this frame.
+ * Incremented before each composite's AABB frustum test.
+ * Does not include the root composite (which is never frustum-tested).
+ */
+ public final AtomicInteger totalComposites = new AtomicInteger(0);
+
+ /**
+ * Number of composite shapes that were entirely outside the frustum and skipped.
+ * When a composite is culled, all its children (shapes and nested composites)
+ * are skipped without individual testing.
+ */
+ public final AtomicInteger culledComposites = new AtomicInteger(0);
+
+ /**
+ * Resets all statistics to zero.
+ * Called at the start of each frame before computing new statistics.
+ */
+ public void reset() {
+ totalComposites.set(0);
+ culledComposites.set(0);
+ }
+
+ /**
+ * Returns the percentage of composites that were culled.
+ *
+ * @return the culled percentage (0-100), or 0 if there are no composites
+ */
+ public double getCulledPercentage() {
+ final int total = totalComposites.get();
+ if (total == 0) {
+ return 0.0;
+ }
+ return 100.0 * culledComposites.get() / total;
+ }
+}
--- /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;
+import eu.svjatoslav.aukio.e3d.geometry.Point3D;
+import eu.svjatoslav.aukio.e3d.geometry.Box;
+import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.Plane;
+
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
+
+/**
+ * View frustum for frustum culling - eliminates objects outside the camera's view.
+ *
+ * <p>The frustum is a truncated pyramid-shaped volume that represents everything
+ * the camera can see. Objects completely outside this volume can be skipped
+ * during rendering, significantly improving performance for large scenes.</p>
+ *
+ * <p><b>Frustum planes:</b></p>
+ * <ul>
+ * <li>Left, Right, Top, Bottom - define the viewport edges</li>
+ * <li>Near - closest visible distance from camera</li>
+ * <li>Far - farthest visible distance from camera</li>
+ * </ul>
+ *
+ * <p><b>Usage:</b></p>
+ * <pre>{@code
+ * Frustum frustum = new Frustum();
+ * frustum.update(camera, screenWidth, screenHeight);
+ *
+ * Box objectBounds = shape.getBoundingBox();
+ * if (frustum.intersectsAABB(objectBounds)) {
+ * // Object is potentially visible - render it
+ * } else {
+ * // Object outside frustum - skip rendering
+ * }
+ * }</pre>
+ *
+ * <p><b>AABB intersection algorithm:</b></p>
+ * <p>Uses the optimized "P-vertex" approach: for each plane, we test only
+ * the AABB corner most aligned with the plane normal. If this corner is
+ * behind the plane, the entire AABB is outside the frustum.</p>
+ *
+ * @see Box axis-aligned bounding box for culling tests
+ * @see Camera provides position and orientation for frustum computation
+ */
+public class Frustum {
+
+ /**
+ * Index for the left clipping plane.
+ */
+ public static final int LEFT = 0;
+ /**
+ * Index for the right clipping plane.
+ */
+ public static final int RIGHT = 1;
+ /**
+ * Index for the top clipping plane.
+ */
+ public static final int TOP = 2;
+ /**
+ * Index for the bottom clipping plane.
+ */
+ public static final int BOTTOM = 3;
+ /**
+ * Index for the near clipping plane.
+ */
+ public static final int NEAR = 4;
+ /**
+ * Index for the far clipping plane.
+ */
+ public static final int FAR = 5;
+
+ /**
+ * The six clipping planes defining the frustum volume.
+ * Each plane is stored as (normal, distance) in Hesse normal form.
+ * Planes are in world space coordinates.
+ */
+ private final Plane[] planes = new Plane[6];
+
+ /**
+ * Default near plane distance from camera (in world units).
+ * Objects closer than this are culled.
+ */
+ private double nearDistance = 1.0;
+
+ /**
+ * Default far plane distance from camera (in world units).
+ * Objects farther than this are culled.
+ */
+ private double farDistance = 10000.0;
+
+ /**
+ * Creates a new frustum with uninitialized planes.
+ * Call {@link #update} before using for culling.
+ */
+ public Frustum() {
+ for (int i = 0; i < 6; i++) {
+ planes[i] = new Plane(new Point3D(0, 0, 1), 0);
+ }
+ }
+
+ /**
+ * Updates the frustum planes in view space (camera at origin, looking along +Z).
+ *
+ * <p>This method should be called once per frame before rendering, after the
+ * camera position and orientation have been updated.</p>
+ *
+ * <p><b>View space coordinate system:</b></p>
+ * <ul>
+ * <li>Camera at origin (0, 0, 0)</li>
+ * <li>Forward = +Z axis (looking into the screen)</li>
+ * <li>Right = +X axis</li>
+ * <li>Up = -Y axis (since Y-down means smaller Y is higher visually)</li>
+ * </ul>
+ *
+ * <p><b>Plane normals point INTO the frustum</b> (toward the visible volume).
+ * A point is inside if dot(normal, point) >= distance for all planes.</p>
+ *
+ * <p><b>FOV calculation:</b> The Aukio 3D engine uses projectionScale = width/3.
+ * This means tan(halfHFOV) = (width/2) / projectionScale = 1.5, giving a
+ * horizontal FOV of approximately 112 degrees.</p>
+ *
+ * @param camera the camera (used only for aspect ratio derivation from width/height)
+ * @param width the viewport width in pixels (defines projectionScale)
+ * @param height the viewport height in pixels (used for vertical FOV)
+ */
+ public void update(final Camera camera, final int width, final int height) {
+ // Frustum is computed in VIEW SPACE (camera at origin, looking along +Z)
+ // This matches the coordinate system after applying camera transforms
+
+ // Aukio 3D uses projectionScale = width/3
+ // tan(halfFOV) = (halfSize) / projectionScale
+ final double projectionScale = width / 3.0;
+ final double tanHalfHFOV = (width / 2.0) / projectionScale; // = 1.5 (very wide FOV)
+ final double tanHalfVFOV = (height / 2.0) / projectionScale; // depends on aspect ratio
+
+ // Compute cosine and sine of half-FOV angles
+ // cosHalfFOV = 1 / sqrt(1 + tanHalfFOV^2)
+ // sinHalfFOV = tanHalfFOV * cosHalfFOV
+ final double cosHalfHFOV = 1.0 / Math.sqrt(1.0 + tanHalfHFOV * tanHalfHFOV);
+ final double sinHalfHFOV = tanHalfHFOV * cosHalfHFOV;
+ final double cosHalfVFOV = 1.0 / Math.sqrt(1.0 + tanHalfVFOV * tanHalfVFOV);
+ final double sinHalfVFOV = tanHalfVFOV * cosHalfVFOV;
+
+ // Near and far distances
+ nearDistance = 1.0;
+ farDistance = 10000.0;
+
+ // All side planes pass through origin (camera position in view space)
+ // Plane equation: dot(normal, point) >= distance means inside
+
+ // Left plane: inward normal pointing right-forward
+ // Bounds: x >= -tanHalfHFOV * z (to the right of left edge)
+ planes[LEFT].normal = new Point3D(cosHalfHFOV, 0, sinHalfHFOV);
+ planes[LEFT].distance = 0;
+
+ // Right plane: inward normal pointing left-forward
+ // Bounds: x <= tanHalfHFOV * z (to the left of right edge)
+ planes[RIGHT].normal = new Point3D(-cosHalfHFOV, 0, sinHalfHFOV);
+ planes[RIGHT].distance = 0;
+
+ // Top plane: inward normal pointing down-forward (Y-down system, top is smaller Y)
+ // Bounds: y <= tanHalfVFOV * z (below top edge, smaller Y)
+ planes[TOP].normal = new Point3D(0, -cosHalfVFOV, sinHalfVFOV);
+ planes[TOP].distance = 0;
+
+ // Bottom plane: inward normal pointing up-forward (larger Y is below)
+ // Bounds: y >= -tanHalfVFOV * z (above bottom edge, larger Y)
+ planes[BOTTOM].normal = new Point3D(0, cosHalfVFOV, sinHalfVFOV);
+ planes[BOTTOM].distance = 0;
+
+ // Near plane: inward normal pointing forward (+Z)
+ // Bounds: z >= nearDistance (in front of near plane)
+ planes[NEAR].normal = new Point3D(0, 0, 1);
+ planes[NEAR].distance = nearDistance;
+
+ // Far plane: inward normal pointing backward (-Z)
+ // Bounds: z <= farDistance (behind far plane)
+ planes[FAR].normal = new Point3D(0, 0, -1);
+ planes[FAR].distance = -farDistance;
+ }
+
+ /**
+ * Tests whether an axis-aligned bounding box intersects the frustum.
+ *
+ * <p>This is a conservative test: returns {@code true} if the box is
+ * potentially visible (inside or partially inside the frustum), and
+ * {@code false} only if the box is completely outside all frustum planes.</p>
+ *
+ * <p><b>Optimized algorithm:</b></p>
+ * <p>For each plane, we test only the AABB corner most aligned with the
+ * plane normal (the "P-vertex"). If this corner is behind the plane,
+ * the entire AABB must be outside the frustum.</p>
+ *
+ * @param box the axis-aligned bounding box to test (in view space coordinates)
+ * @return {@code true} if the box intersects or is inside the frustum,
+ * {@code false} if completely outside
+ */
+ public boolean intersectsAABB(final Box box) {
+ // Get box min/max for each axis
+ final double minX = box.getMinX();
+ final double maxX = box.getMaxX();
+ final double minY = box.getMinY();
+ final double maxY = box.getMaxY();
+ final double minZ = box.getMinZ();
+ final double maxZ = box.getMaxZ();
+
+ for (int i = 0; i < 6; i++) {
+ final Plane plane = planes[i];
+ final Point3D n = plane.normal;
+ final double d = plane.distance;
+
+ // Find the P-vertex: the corner most aligned with the plane normal
+ // If normal component is positive, use max; if negative, use min
+ final double px = (n.x > 0) ? maxX : minX;
+ final double py = (n.y > 0) ? maxY : minY;
+ final double pz = (n.z > 0) ? maxZ : minZ;
+
+ // Test if P-vertex is outside the frustum (behind the plane)
+ // For inward-pointing normals: inside = dot(N,P) >= distance
+ // So outside = dot(N,P) < distance
+ if (n.x * px + n.y * py + n.z * pz < d) {
+ return false; // AABB entirely outside this plane
+ }
+ }
+
+ return true; // AABB intersects or inside all planes
+ }
+
+ /**
+ * Returns the near clipping plane distance.
+ *
+ * @return the near distance in world units
+ */
+ public double getNearDistance() {
+ return nearDistance;
+ }
+
+ /**
+ * Returns the far clipping plane distance.
+ *
+ * @return the far distance in world units
+ */
+ public double getFarDistance() {
+ return farDistance;
+ }
+
+ /**
+ * Sets the near and far clipping distances.
+ *
+ * @param near the near plane distance (objects closer are culled)
+ * @param far the far plane distance (objects farther are culled)
+ */
+ public void setClipDistances(final double near, final double far) {
+ this.nearDistance = near;
+ this.farDistance = far;
+ }
+
+ /**
+ * Returns a specific frustum plane for debugging or advanced usage.
+ *
+ * @param planeIndex one of LEFT, RIGHT, TOP, BOTTOM, NEAR, FAR
+ * @return the plane at the specified index
+ */
+ public Plane getPlane(final int planeIndex) {
+ return planes[planeIndex];
+ }
+}
\ No newline at end of file
--- /dev/null
+package eu.svjatoslav.aukio.e3d.renderer.raster;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Hierarchical depth pyramid for whole-block occlusion culling
+ * (Hi-Z). Built from the just-painted frame's depth buffer; queried
+ * during the NEXT frame's transform to skip blocks that are fully
+ * hidden behind what was drawn last frame.
+ *
+ * <p>Semantics: each tile stores the MINIMUM w (= 1/z, i.e. the
+ * FARTHEST written depth) over its pixels. A block whose nearest
+ * possible point (max w over its AABB corners) is farther than a
+ * tile's farthest written depth is behind something at every written
+ * pixel of that tile — and tiles with any unwritten (sky) pixel hold
+ * -infinity and never occlude. That makes the test conservative: it
+ * may keep a hidden block, it never culls a visible one — under a
+ * static camera. (The first version max-pooled, storing the NEAREST
+ * depth per tile; that culled houses visible BETWEEN nearer tree
+ * trunks — per-pixel gaps inside a tile are invisible to a max.)
+ * Camera motion reuses the stale pyramid, which can cull a
+ * newly-visible block wrongly; the block's pixels then contain far
+ * background depth, so the next frame's pyramid no longer occludes
+ * it — wrong culls self-heal in one frame (sanctioned design
+ * concession), and the query margin absorbs small-motion
+ * parallax.</p>
+ *
+ * <p>Empty tiles (never depth-written) store -infinity and never
+ * occlude, so the first frame after startup (or after any pass that
+ * leaves the pyramid unbuilt) culls nothing.</p>
+ *
+ * <p>Knobs: {@code -Daukio.hiz=false} disables culling (build still
+ * happens — it is cheap — so flipping the flag needs no warm-up);
+ * {@code -Daukio.hiz.margin=0.02} sets the relative w-space safety
+ * margin against parallax between frames.</p>
+ */
+public final class HiZPyramid {
+
+ /** Level-0 tile edge in pixels; level i tiles cover TILE*2^i px. */
+ private static final int TILE = 8;
+
+ private static final boolean ENABLED = Boolean.parseBoolean(
+ System.getProperty("aukio.hiz", "true"));
+ private static final double MARGIN = Double.parseDouble(
+ System.getProperty("aukio.hiz.margin", "0.02"));
+
+ /** Blocks occlusion-tested this process (telemetry). */
+ public final AtomicLong blocksTested = new AtomicLong();
+ /** Blocks culled as fully occluded (telemetry). */
+ public final AtomicLong blocksCulled = new AtomicLong();
+
+ private float[] tiles = new float[0];
+ private int[] levelOff = new int[0];
+ private int[] levelW = new int[0];
+ private int[] levelH = new int[0];
+ private int levels;
+ private int bufW = -1, bufH = -1;
+
+ /** Rebuilds the pyramid from a freshly painted depth buffer. */
+ public synchronized void buildFrom(final float[] depth,
+ final int width, final int height) {
+ if (width != bufW || height != bufH) {
+ allocate(width, height);
+ }
+
+ // Level 0: min-pool depth into TILE x TILE tiles. Depth
+ // outside the painted area is -infinity (cleared), so a tile
+ // with any unpainted pixel never occludes.
+ final int w0 = levelW[0], h0 = levelH[0];
+ final int off0 = levelOff[0];
+ for (int ty = 0; ty < h0; ty++) {
+ final int yEnd = Math.min((ty + 1) * TILE, height);
+ for (int tx = 0; tx < w0; tx++) {
+ final int xEnd = Math.min((tx + 1) * TILE, width);
+ float m = Float.POSITIVE_INFINITY;
+ for (int y = ty * TILE; y < yEnd; y++) {
+ final int row = y * width;
+ for (int x = tx * TILE; x < xEnd; x++)
+ if (depth[row + x] < m)
+ m = depth[row + x];
+ }
+ tiles[off0 + ty * w0 + tx] = m;
+ }
+ }
+
+ // Higher levels: min of 2x2 children.
+ for (int l = 1; l < levels; l++) {
+ final int pw = levelW[l - 1], ph = levelH[l - 1];
+ final int poff = levelOff[l - 1];
+ final int cw = levelW[l], ch = levelH[l];
+ final int coff = levelOff[l];
+ for (int ty = 0; ty < ch; ty++)
+ for (int tx = 0; tx < cw; tx++) {
+ float m = Float.POSITIVE_INFINITY;
+ for (int dy = 0; dy < 2; dy++)
+ for (int dx = 0; dx < 2; dx++) {
+ final int sx = tx * 2 + dx, sy = ty * 2 + dy;
+ if (sx < pw && sy < ph) {
+ final float v = tiles[poff + sy * pw + sx];
+ if (v < m)
+ m = v;
+ }
+ }
+ tiles[coff + ty * cw + tx] = m;
+ }
+ }
+ }
+
+ /**
+ * Conservative whole-block occlusion test.
+ *
+ * @param x1..y2 screen-space AABB of the block (will be clamped
+ * to the buffer; a fully off-screen box returns
+ * false)
+ * @param nearestW the block's nearest possible depth = MAX 1/z
+ * over its corners
+ * @return true when the block is certainly hidden behind last
+ * frame's occluders (within the parallax margin)
+ */
+ public synchronized boolean occluded(final double x1, final double y1,
+ final double x2, final double y2,
+ final double nearestW) {
+ if (!ENABLED || levels == 0)
+ return false;
+
+ int bx1 = (int) Math.floor(x1), by1 = (int) Math.floor(y1);
+ int bx2 = (int) Math.ceil(x2), by2 = (int) Math.ceil(y2);
+ if (bx1 < 0) bx1 = 0;
+ if (by1 < 0) by1 = 0;
+ if (bx2 >= bufW) bx2 = bufW - 1;
+ if (by2 >= bufH) by2 = bufH - 1;
+ if (bx1 > bx2 || by1 > by2)
+ return false;
+
+ // Coarsest level where the box still covers <= 2 tiles per axis.
+ int level = 0;
+ while (level + 1 < levels) {
+ final int s = TILE << (level + 1);
+ final int tw = (bx2 / s) - (bx1 / s) + 1;
+ final int th = (by2 / s) - (by1 / s) + 1;
+ if (tw > 2 || th > 2)
+ break;
+ level++;
+ }
+
+ final int s = TILE << level;
+ final int tx1 = bx1 / s, ty1 = by1 / s;
+ final int tx2 = bx2 / s, ty2 = by2 / s;
+ final int w = levelW[level], off = levelOff[level];
+
+ float minStored = Float.POSITIVE_INFINITY;
+ for (int ty = ty1; ty <= ty2; ty++)
+ for (int tx = tx1; tx <= tx2; tx++) {
+ final float v = tiles[off + ty * w + tx];
+ if (v < minStored)
+ minStored = v;
+ }
+
+ // Occluded only when the block's nearest point is clearly
+ // behind the farthest written depth in the range; the
+ // relative margin absorbs parallax between frames.
+ return nearestW < minStored * (1.0 - MARGIN);
+ }
+
+ private void allocate(final int width, final int height) {
+ bufW = width;
+ bufH = height;
+ int lw = (width + TILE - 1) / TILE;
+ int lh = (height + TILE - 1) / TILE;
+ int count = 0;
+ levels = 0;
+ while (true) {
+ levels++;
+ count += lw * lh;
+ if (lw == 1 && lh == 1)
+ break;
+ lw = Math.max(1, (lw + 1) / 2);
+ lh = Math.max(1, (lh + 1) / 2);
+ }
+ tiles = new float[count];
+ levelOff = new int[levels];
+ levelW = new int[levels];
+ levelH = new int[levels];
+ lw = (width + TILE - 1) / TILE;
+ lh = (height + TILE - 1) / TILE;
+ int off = 0;
+ for (int l = 0; l < levels; l++) {
+ levelOff[l] = off;
+ levelW[l] = lw;
+ levelH[l] = lh;
+ off += lw * lh;
+ lw = Math.max(1, (lw + 1) / 2);
+ lh = Math.max(1, (lh + 1) / 2);
+ }
+ Arrays.fill(tiles, Float.NEGATIVE_INFINITY);
+ }
+}
* Frame parity captured at construction, stamped onto recorded
* timeline intervals so overlapping frames keep distinct colors.
*/
- private final int traceParity = eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.frameParity();
+ private final int traceParity = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.frameParity();
/**
* Frame-wide cap on chunk tasks. Deep hierarchies of heavy composites
* @param task transforms a chunk of children into a private aggregator
*/
public void submit(final Callable<RenderAggregator> task) {
- if (eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.isEnabled()) {
+ if (eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled()) {
final int parity = traceParity;
futures.add(executor.submit(() -> {
final long t0 = System.nanoTime();
try {
return task.call();
} finally {
- eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.record(
- eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.KIND_TRANSFORM + parity,
+ eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record(
+ eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_TRANSFORM + parity,
t0, System.nanoTime());
}
}));
* @param target the root aggregator to merge results into
*/
public void drainAndMergeInto(final RenderAggregator target) {
- final boolean trace = eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.isEnabled();
+ final boolean trace = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled();
final java.util.List<RenderAggregator> parts = new java.util.ArrayList<>();
Future<RenderAggregator> future;
while ((future = futures.poll()) != null) {
final long t0 = trace ? System.nanoTime() : 0;
parts.add(future.get());
if (trace) {
- eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.record(
- eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.KIND_AWAIT,
+ eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record(
+ eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_AWAIT,
t0, System.nanoTime());
}
} catch (final InterruptedException e) {
final int to = Math.min(n, from + chunk);
futures.add(executor.submit(() -> {
final boolean trace =
- eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.isEnabled();
+ eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled();
final long t0 = trace ? System.nanoTime() : 0;
try {
work.run(ti, f, to);
} finally {
if (trace)
- eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.record(
- eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.KIND_SORT,
+ eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record(
+ eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_SORT,
t0, System.nanoTime());
}
}));
*/
package eu.svjatoslav.aukio.e3d.renderer.raster;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
import java.io.Serializable;
final boolean parallel,
final ExecutorService executor) {
final int size = sortedCount;
- final boolean trace = eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.isEnabled();
- final int traceKind = eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.KIND_BIN
- + eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.frameParity();
+ final boolean trace = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled();
+ final int traceKind = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_BIN
+ + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.frameParity();
final java.util.List<Future<?>> futures =
parallel ? new java.util.ArrayList<>(chunkCount) : null;
for (int c = 0; c < chunkCount; c++) {
chunk * tileCount, countMode);
} finally {
if (trace)
- eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.record(
+ eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record(
traceKind, t0, System.nanoTime());
}
};
--- /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;
+import eu.svjatoslav.aukio.e3d.diag.DebugLogBuffer;
+import eu.svjatoslav.aukio.e3d.gui.DeveloperTools;
+
+import eu.svjatoslav.aukio.e3d.geometry.Point2D;
+import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent;
+import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
+import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.util.concurrent.ExecutorService;
+import eu.svjatoslav.aukio.e3d.renderer.raster.ParallelTransformCoordinator;
+import java.util.function.Consumer;
+
+/**
+ * Contains all state needed to render a single frame: the pixel buffer, graphics context,
+ * screen dimensions, and mouse event tracking.
+ *
+ * <p>A new {@code RenderingContext} is created whenever the view panel is resized.
+ * During rendering, shapes use this context to:</p>
+ * <ul>
+ * <li>Access the raw pixel array ({@link #pixels}) for direct pixel manipulation</li>
+ * <li>Access the {@link Graphics2D} context ({@link #graphics}) for Java2D drawing</li>
+ * <li>Read screen dimensions ({@link #width}, {@link #height}) and the
+ * {@link #centerCoordinate} for coordinate projection</li>
+ * <li>Use the {@link #projectionScale} factor for perspective projection</li>
+ * </ul>
+ *
+ * <p>The context also manages mouse interaction detection: as shapes are painted
+ * back-to-front, each shape can report itself as the object under the mouse cursor.
+ * After painting completes, the topmost shape receives the mouse event.</p>
+ *
+ * @see eu.svjatoslav.aukio.e3d.gui.ViewPanel the panel that creates and manages this context
+ * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape#paint(RenderingContext)
+ */
+public class RenderingContext {
+
+ /**
+ * The {@link BufferedImage} pixel format used for the rendering buffer.
+ * TYPE_INT_RGB provides optimal performance for Java2D blitting.
+ */
+ public static final int bufferedImageType = BufferedImage.TYPE_INT_RGB;
+
+ /**
+ * Number of horizontal segments (bands) for parallel rendering.
+ * Bands are finer than the paint thread count: paint threads steal
+ * bands off a shared ticket until all bands are done, so a thread
+ * that finishes a cheap band immediately picks up more work.
+ * Derived from the render thread count via
+ * {@link eu.svjatoslav.aukio.e3d.gui.ViewPanel#setNumRenderThreads(int)}.
+ *
+ * <p>Equals {@code tilesX * tilesY * viewportCount}: the tile grid
+ * covers one viewport, and in stereo mode a second grid covers the
+ * other eye (segment indices for the right eye start at
+ * {@code tilesX * tilesY}).</p>
+ */
+ public final int numRenderSegments;
+
+ /** Tile columns per viewport (1 = horizontal bands only). */
+ public final int tilesX;
+
+ /** Tile rows per viewport. */
+ public final int tilesY;
+
+ /** Number of side-by-side viewports (2 in stereo mode, else 1). */
+ public final int viewportCount;
+
+ /**
+ * Java2D graphics context for drawing text, anti-aliased shapes, and other
+ * high-level graphics operations onto the render buffer.
+ */
+ public final Graphics2D graphics;
+
+ /**
+ * Segment-specific Graphics2D contexts, each pre-clipped to a horizontal band.
+ * Used for thread-safe text and shape rendering without synchronization.
+ * Only initialized in the main RenderingContext; null in segment views.
+ */
+ private Graphics2D[] segmentGraphics;
+
+ /**
+ * Pixels of the rendering area.
+ * Each pixel is a single int in RGB format: {@code (r << 16) | (g << 8) | b}.
+ */
+ public final int[] pixels;
+
+ /**
+ * Per-pixel depth (biased 1/z, larger = nearer), always allocated —
+ * the painter path was deleted 2026-09-17 and the z-buffer is the
+ * only visibility mechanism. Shared with segment/pass copies like
+ * {@link #pixels}. Cleared per tile by the paint workers.
+ */
+ public float[] depth;
+
+ /**
+ * Active paint pass, set internally by
+ * {@code RenderAggregator.paintSorted}: 0 = not painting, 1 =
+ * opaque pass (opaque-class triangles only, depth test + write),
+ * 2 = alpha pass (alpha-carrying
+ * triangles only, depth test, no depth write). Shapes read it to
+ * decide whether they belong to the current pass.
+ */
+ public int depthPass;
+
+ /**
+ * Depth tolerance in world units for the z-buffer test, in the form
+ * {@code zw > stored - DEPTH_MARGIN_DZ * zw * zw} (tolerance behind
+ * stored, per-pixel at fragment depth). Default 0 = strict depth: any
+ * nonzero window exports per-triangle painter-sort errors into
+ * per-pixel occlusion errors (dirt whose triangles sort late beats
+ * road pavement that strictly wins at margin 0 — user bugreport
+ * 2026-09-16, road pose). Tunable via -Daukio.zbuffer.margin.
+ */
+ public static final double DEPTH_MARGIN_DZ =
+ Double.parseDouble(System.getProperty("aukio.zbuffer.margin", "0"));
+
+ /**
+ * Width of the rendering area in pixels.
+ */
+ public final int width;
+
+ /**
+ * Height of the rendering area in pixels.
+ */
+ public final int height;
+
+ /**
+ * Center of the screen in screen space (pixels).
+ * This is the point where (0,0) coordinate of the world space is rendered.
+ */
+ public final Point2D centerCoordinate;
+
+ /**
+ * Scale factor for perspective projection, derived from screen width.
+ * Used to convert normalized device coordinates to screen pixels.
+ * This is mutable to support stereo rendering where each eye has a different viewport width.
+ */
+ public double projectionScale;
+
+ /**
+ * Minimum Y coordinate (inclusive) to render. Used for multi-threaded rendering
+ * where each thread renders a horizontal segment.
+ */
+ public final int renderMinY;
+
+ /**
+ * Maximum Y coordinate (exclusive) to render. Used for multi-threaded rendering
+ * where each thread renders a horizontal segment.
+ */
+ public final int renderMaxY;
+
+ /** The backing image (public: the AWT shell in {@code gui} blits it directly). */
+ public final BufferedImage bufferedImage;
+ /**
+ * Unique id of the current transform cycle, assigned by
+ * {@code ShapeCollection.transformShapes()} from a global counter.
+ * Unlike {@link #frameNumber} (per-context, can repeat across context
+ * instances), this never collides, so per-cycle memoization such as
+ * composite subtree weights can safely key on it.
+ */
+ public long transformCycleId;
+
+ /**
+ * Which projection buffer slot this context writes/reads: 0, 1 or 2.
+ * Cycles per render pass (per eye in stereo) when the
+ * triple-buffered pipeline is active, so the transform phase of a
+ * pass never overwrites the vertex state either of the two previous
+ * passes' paints may still be reading. Always 0 when the pipeline is
+ * off (tests, single-pass rendering).
+ */
+ public int vertexSlot = 0;
+
+ /**
+ * Near-plane distance in camera-space Z units. Polygons whose vertices
+ * straddle this plane are clipped against it (new intersection vertices
+ * are generated with interpolated UVs); polygons fully behind it are
+ * culled. Must be > 0 so the perspective divide stays safe.
+ */
+ public double nearPlaneDistance = 1.0;
+
+ /**
+ * Number of frame that is currently being rendered.
+ * Every frame has its own number.
+ */
+ public int frameNumber = 0;
+
+ /**
+ * Projected-size cull threshold in screen pixels: shapes whose screen
+ * bounds span less than this in both axes are not queued for
+ * rendering. 0 (the default) disables the cull. Set globally with
+ * {@code -Daukio.cull.subpixel=<px>}.
+ */
+ public double subpixelCullingThreshold = Double.parseDouble(
+ System.getProperty("aukio.cull.subpixel", "0"));
+
+ /**
+ * Epoch of the subpixel-culling verdict cache, stamped per frame by
+ * {@code ShapeCollection.transformShapesBegin}: the epoch advances
+ * when the camera moves significantly (or after a bounded number of
+ * frames), which invalidates all cached skip verdicts and forces one
+ * re-evaluation pass. Meaningless when the cull is off.
+ */
+ public int subpixelCullingEpoch;
+
+ /**
+ * UI component that mouse is currently hovering over.
+ */
+ private MouseInteractionController objectPreviouslyUnderMouseCursor;
+ /**
+ * Mouse click event that needs to be processed.
+ * This event is processed only once per frame.
+ * If there are multiple objects under the mouse cursor, the top-most object will receive the event.
+ * If there are no objects under the mouse cursor, the event will be ignored.
+ * If there is no event, this field will be null.
+ * This field is set to null after the event is processed.
+ */
+ private MouseEvent mouseEvent;
+ /**
+ * UI component that mouse is currently hovering over.
+ */
+ private MouseInteractionController currentObjectUnderMouseCursor;
+ /**
+ * Texture coordinates of the mouse cursor on the hit shape (primary
+ * texture pixels), or NaN when the hit shape has no texture.
+ */
+ private double currentMouseTextureU = Double.NaN;
+ private double currentMouseTextureV = Double.NaN;
+ /**
+ * Developer tools for this rendering context.
+ * Controls diagnostic features like logging and visualization.
+ */
+ public DeveloperTools developerTools;
+
+ /**
+ * Debug log buffer for capturing diagnostic output.
+ * Shapes can log messages here that appear in the Developer Tools panel.
+ */
+ public DebugLogBuffer debugLogBuffer;
+
+ /**
+ * Global lighting manager for the scene.
+ * All shaded polygons use this to calculate lighting. Contains all light sources
+ * and ambient light settings for the world.
+ */
+ public LightingManager lightingManager;
+
+ /**
+ * Which eye is being rendered in stereo mode. NONE for normal single-view rendering.
+ */
+ public StereoEye stereoEye = StereoEye.NONE;
+
+ /**
+ * Width of the viewport for the current eye in stereo mode.
+ * Equals {@link #width} when not in stereo mode.
+ */
+ public int stereoViewportWidth;
+
+ /**
+ * X offset of the current eye's viewport within the full buffer.
+ * 0 for left eye, width/2 for right eye, 0 in normal mode.
+ */
+ public int stereoViewportOffsetX;
+
+ /**
+ * Minimum X coordinate (inclusive) for rendering.
+ * In stereo mode, this is {@link #stereoViewportOffsetX}.
+ * In normal mode, this is 0.
+ */
+ public int renderMinX;
+
+ /**
+ * Maximum X coordinate (exclusive) for rendering.
+ * In stereo mode, this is {@link #stereoViewportOffsetX} + {@link #stereoViewportWidth}.
+ * In normal mode, this is {@link #width}.
+ */
+ public int renderMaxX;
+
+ /**
+ * View frustum for frustum culling.
+ * Updated each frame from camera state and screen dimensions.
+ * Shapes can test their bounding boxes against this frustum to determine
+ * if they are potentially visible before expensive vertex transformations.
+ */
+ public Frustum frustum;
+
+ /**
+ * World-space position of the viewer for this pass, copied from the
+ * camera in {@link eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection#transformShapesBegin}.
+ * Used by BSP painter ordering (viewpoint for tree traversal).
+ * Fresh instance per context, including per-pass copies, so overlapping
+ * pipeline passes each see their own viewpoint.
+ */
+ public final eu.svjatoslav.aukio.e3d.geometry.Point3D viewerPosition =
+ new eu.svjatoslav.aukio.e3d.geometry.Point3D();
+
+ /**
+ * Statistics for frustum culling performance tracking.
+ * Updated each frame: total shapes counted at start, visible shapes
+ * incremented during rendering, culled composites tracked during transform.
+ */
+ public CullingStatistics cullingStatistics;
+
+ /**
+ * Hi-Z occlusion pyramid, rebuilt from the depth buffer after every
+ * painted frame (live {@code ViewPanel} path only — headless
+ * {@code Snapshot} renders leave it empty so golden renders never
+ * cull). Read during the next frame's transform by
+ * {@code TriangleMeshBlock} to skip fully occluded blocks. Shared
+ * with pass/segment copies like {@link #depth}.
+ */
+ public HiZPyramid occlusionPyramid;
+
+ /**
+ * Executor for the parallel transform phase. When non-null, composites
+ * with enough children split their render lists into chunks transformed
+ * concurrently. When null, the transform phase runs serially on the
+ * render thread.
+ */
+ public ExecutorService transformExecutor;
+
+ /**
+ * Per-frame coordinator for the non-blocking parallel transform fork.
+ * Set by {@code ShapeCollection.transformShapes()} for the duration of
+ * the root transform when {@link #transformExecutor} is available;
+ * composites at any nesting level submit chunk tasks to it. Null outside
+ * the transform phase and when transforming serially.
+ */
+ public ParallelTransformCoordinator transformCoordinator;
+
+ /**
+ * Present gate for this framebuffer: fires when the frame currently
+ * held in this buffer has been presented to the display (or dropped
+ * from the presentation mailbox). The render thread installs a fresh
+ * gate at the start of each frame that reuses the buffer, and the
+ * frame's paint continuation awaits the PREVIOUS gate before writing
+ * pixels — without it, painting frame F+3 would overwrite the buffer
+ * while the present thread is still blitting frame F from it.
+ */
+ public volatile java.util.concurrent.CountDownLatch presentGate = new java.util.concurrent.CountDownLatch(0);
+
+ /**
+ * Chunk tasks submitted during the last transform phase, across all
+ * nesting levels. Diagnostics: proves nested composites forked.
+ */
+ public int lastTransformTaskCount;
+
+ /**
+ * Creates a new rendering context for full-screen rendering.
+ *
+ * <p>Equivalent to {@code RenderingContext(width, height, 0, height, numRenderSegments)}.</p>
+ *
+ * @param width the rendering area width in pixels
+ * @param height the rendering area height in pixels
+ * @param numRenderSegments number of parallel render segments (threads)
+ */
+ public RenderingContext(final int width, final int height, final int numRenderSegments) {
+ this(width, height, 0, height, 1, numRenderSegments, 1);
+ }
+
+ /**
+ * Creates a new rendering context with a rectangular tile grid.
+ *
+ * <p>Equivalent to the band-only constructors when {@code tilesX == 1}.
+ * In stereo mode ({@code viewportCount == 2}) each viewport gets its own
+ * tile grid; segment indices for viewport v start at
+ * {@code v * tilesX * tilesY}.</p>
+ *
+ * @param width the rendering area width in pixels
+ * @param height the rendering area height in pixels
+ * @param tilesX tile columns per viewport (1 = bands only)
+ * @param tilesY tile rows per viewport
+ * @param viewportCount number of side-by-side viewports (2 = stereo)
+ */
+ public RenderingContext(final int width, final int height,
+ final int tilesX, final int tilesY,
+ final int viewportCount) {
+ this(width, height, 0, height, tilesX, tilesY, viewportCount);
+ }
+
+ private RenderingContext(final int width, final int height,
+ final int renderMinY, final int renderMaxY,
+ final int tilesX, final int tilesY,
+ final int viewportCount) {
+ this.width = width;
+ this.height = height;
+ this.renderMinY = renderMinY;
+ this.renderMaxY = renderMaxY;
+ this.tilesX = tilesX;
+ this.tilesY = tilesY;
+ this.viewportCount = viewportCount;
+ this.numRenderSegments = tilesX * tilesY * viewportCount;
+ this.centerCoordinate = new Point2D(width / 2d, height / 2d);
+ this.projectionScale = width / 3d;
+ this.stereoViewportWidth = width;
+ this.stereoViewportOffsetX = 0;
+ this.renderMinX = 0;
+ this.renderMaxX = width;
+
+ // Eagerly allocated so the developer-tools panel always finds it:
+ // transformPass() hands the pipeline a per-pass COPY of this context,
+ // and the copy constructor shares this reference. Lazy creation in
+ // ShapeCollection.transformShapesBegin() would only ever populate the
+ // throwaway pass copy, leaving this frame context null forever
+ // (the culling display then showed "-" permanently).
+ this.cullingStatistics = new CullingStatistics();
+ this.occlusionPyramid = new HiZPyramid();
+
+ bufferedImage = new BufferedImage(width, height, bufferedImageType);
+
+ final WritableRaster raster = bufferedImage.getRaster();
+ final DataBufferInt dbi = (DataBufferInt) raster.getDataBuffer();
+ pixels = dbi.getData();
+
+ // Z-buffer: one w-depth (biased 1/z) value per pixel, cleared per
+ // tile in the paint workers. Depth turns the queue order into a
+ // performance heuristic only; correctness comes from the per-pixel
+ // test. (The queue itself stays painter back-to-front — Z
+ // descending, see RenderAggregator.) Always allocated: the
+ // z-buffer path is the only renderer.
+ depth = new float[width * height];
+
+ graphics = (Graphics2D) bufferedImage.getGraphics();
+ graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+ segmentGraphics = createSegmentGraphics();
+ }
+
+ /**
+ * Protected constructor for creating segment views.
+ * Shares the pixel buffer and graphics context with the parent.
+ *
+ * @param parent the parent rendering context
+ * @param renderMinY minimum Y coordinate (inclusive) for this segment
+ * @param renderMaxY maximum Y coordinate (exclusive) for this segment
+ */
+ protected RenderingContext(final RenderingContext parent,
+ final int renderMinY, final int renderMaxY) {
+ this.width = parent.width;
+ this.height = parent.height;
+ this.renderMinY = renderMinY;
+ this.renderMaxY = renderMaxY;
+ this.tilesX = parent.tilesX;
+ this.tilesY = parent.tilesY;
+ this.viewportCount = parent.viewportCount;
+ this.numRenderSegments = parent.numRenderSegments;
+ this.centerCoordinate = parent.centerCoordinate;
+ this.projectionScale = parent.projectionScale;
+ this.stereoViewportWidth = parent.stereoViewportWidth;
+ this.stereoViewportOffsetX = parent.stereoViewportOffsetX;
+ this.stereoEye = parent.stereoEye;
+ this.renderMinX = parent.renderMinX;
+ this.renderMaxX = parent.renderMaxX;
+ this.bufferedImage = parent.bufferedImage;
+ this.pixels = parent.pixels;
+ this.depth = parent.depth;
+ this.graphics = parent.graphics;
+ this.vertexSlot = parent.vertexSlot;
+ this.nearPlaneDistance = parent.nearPlaneDistance;
+ this.developerTools = parent.developerTools;
+ this.debugLogBuffer = parent.debugLogBuffer;
+ this.lightingManager = parent.lightingManager;
+ this.occlusionPyramid = parent.occlusionPyramid;
+ this.segmentGraphics = null;
+ }
+
+ /**
+ * Creates an independent pass context for one pipeline pass (one eye
+ * in stereo): shares the frame's pixel buffer, graphics and services,
+ * but owns the per-pass projection fields (center, scale, stereo
+ * viewport, slot, frame/cycle stamps). The next pass's setup writes
+ * to its own copy, so it cannot disturb this pass's in-flight
+ * transform chunks or its asynchronous sort/bin/paint continuation.
+ *
+ * @param parent the frame rendering context to copy from
+ */
+ public RenderingContext(final RenderingContext parent) {
+ this.width = parent.width;
+ this.height = parent.height;
+ this.renderMinY = parent.renderMinY;
+ this.renderMaxY = parent.renderMaxY;
+ this.tilesX = parent.tilesX;
+ this.tilesY = parent.tilesY;
+ this.viewportCount = parent.viewportCount;
+ this.numRenderSegments = parent.numRenderSegments;
+ this.centerCoordinate = new Point2D(parent.centerCoordinate.x, parent.centerCoordinate.y);
+ this.projectionScale = parent.projectionScale;
+ this.stereoViewportWidth = parent.stereoViewportWidth;
+ this.stereoViewportOffsetX = parent.stereoViewportOffsetX;
+ this.stereoEye = parent.stereoEye;
+ this.renderMinX = parent.renderMinX;
+ this.renderMaxX = parent.renderMaxX;
+ this.bufferedImage = parent.bufferedImage;
+ this.pixels = parent.pixels;
+ this.depth = parent.depth;
+ this.graphics = parent.graphics;
+ this.vertexSlot = parent.vertexSlot;
+ this.nearPlaneDistance = parent.nearPlaneDistance;
+ this.frameNumber = parent.frameNumber;
+ this.transformCycleId = parent.transformCycleId;
+ this.transformExecutor = parent.transformExecutor;
+ this.developerTools = parent.developerTools;
+ this.debugLogBuffer = parent.debugLogBuffer;
+ this.lightingManager = parent.lightingManager;
+ this.cullingStatistics = parent.cullingStatistics;
+ this.occlusionPyramid = parent.occlusionPyramid;
+ this.subpixelCullingThreshold = parent.subpixelCullingThreshold;
+ this.subpixelCullingEpoch = parent.subpixelCullingEpoch;
+ this.setMouseEvent(parent.getMouseEvent());
+ // Share the pre-clipped per-tile graphics: glyph rendering
+ // (user-facing text) draws through them by segment index. Null
+ // here made every glyph paint die with an NPE mid-tile (broken
+ // tiles whenever text faced the reader).
+ this.segmentGraphics = parent.segmentGraphics;
+ // frustum stays null: created fresh per pass in transformShapesBegin
+ }
+
+ /**
+ * Resets per-frame state in preparation for rendering a new frame.
+ * Increments the frame number and clears the mouse event state.
+ */
+ public void prepareForNewFrameRendering() {
+ frameNumber++;
+ mouseEvent = null;
+ currentObjectUnderMouseCursor = null;
+ }
+
+ /**
+ * Creates Graphics2D contexts for each render segment, pre-clipped to
+ * its tile rectangle. Segment index layout: viewport v, tile row ty,
+ * tile column tx -> v * tilesX * tilesY + ty * tilesX + tx.
+ *
+ * @return array of Graphics2D objects, one per segment
+ */
+ private Graphics2D[] createSegmentGraphics() {
+ final Graphics2D[] contexts = new Graphics2D[numRenderSegments];
+ final int viewportWidth = width / viewportCount;
+ final int tileW = viewportWidth / tilesX;
+ final int tileH = height / tilesY;
+
+ for (int v = 0; v < viewportCount; v++) {
+ final int viewportX = v * viewportWidth;
+ for (int ty = 0; ty < tilesY; ty++) {
+ final int minY = ty * tileH;
+ final int maxY = (ty == tilesY - 1) ? height : (ty + 1) * tileH;
+ for (int tx = 0; tx < tilesX; tx++) {
+ final int minX = viewportX + tx * tileW;
+ final int maxX = (tx == tilesX - 1)
+ ? viewportX + viewportWidth : minX + tileW;
+
+ final Graphics2D g = bufferedImage.createGraphics();
+ g.setClip(minX, minY, maxX - minX, maxY - minY);
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+ contexts[v * tilesX * tilesY + ty * tilesX + tx] = g;
+ }
+ }
+ }
+
+ return contexts;
+ }
+
+ /**
+ * Returns the backing image whose pixel buffer the rasterizer paints into.
+ *
+ * <p>Exposed for headless rendering: after a transform/sort/paint pass the
+ * image holds the finished frame and can be saved or compared directly.</p>
+ *
+ * @return the backing buffered image
+ */
+ public BufferedImage getImage() {
+ return bufferedImage;
+ }
+
+ /**
+ * Returns the Graphics2D context for a specific render segment.
+ * Each segment's Graphics2D is pre-clipped to its Y bounds.
+ *
+ * @param segmentIndex the segment index (0 to numRenderSegments-1)
+ * @return the Graphics2D for that segment
+ * @throws NullPointerException if called on a segment view (not the main context)
+ */
+ public Graphics2D getSegmentGraphics(final int segmentIndex) {
+ return segmentGraphics[segmentIndex];
+ }
+
+ /**
+ * Disposes all Graphics2D resources associated with this context.
+ * Should be called when the context is no longer needed (e.g., on resize).
+ */
+ public void dispose() {
+ if (segmentGraphics != null) {
+ for (final Graphics2D g : segmentGraphics) {
+ if (g != null) {
+ g.dispose();
+ }
+ }
+ }
+ if (graphics != null) {
+ graphics.dispose();
+ }
+ }
+
+ /**
+ * Executes a graphics operation in a thread-safe manner.
+ * This must be used for all Graphics2D operations (text, lines, etc.)
+ * during multi-threaded rendering.
+ *
+ * @param operation the graphics operation to execute
+ */
+ public void executeWithGraphics(final Consumer<Graphics2D> operation) {
+ synchronized (graphics) {
+ operation.accept(graphics);
+ }
+ }
+
+ /**
+ * Returns the pending mouse event for this frame, or {@code null} if none.
+ *
+ * @return the mouse event to process, or {@code null}
+ */
+ public MouseEvent getMouseEvent() {
+ return mouseEvent;
+ }
+
+ /**
+ * Sets the mouse event to be processed during this frame's rendering.
+ *
+ * @param mouseEvent the mouse event with position and button information
+ */
+ public void setMouseEvent(MouseEvent mouseEvent) {
+ this.mouseEvent = mouseEvent;
+ }
+
+ /**
+ * Called when given object was detected under mouse cursor, while processing {@link #mouseEvent}.
+ * Because objects are rendered back to front. The last method caller will set the top-most object, if
+ * there are multiple objects under mouse cursor.
+ *
+ * @param currentObjectUnderMouseCursor the object that is currently under the mouse cursor
+ */
+ public synchronized void setCurrentObjectUnderMouseCursor(MouseInteractionController currentObjectUnderMouseCursor) {
+ setCurrentObjectUnderMouseCursor(currentObjectUnderMouseCursor,
+ Double.NaN, Double.NaN);
+ }
+
+ /**
+ * Called when given object was detected under mouse cursor, with the
+ * texture coordinates of the hit point (for textured shapes).
+ *
+ * @param currentObjectUnderMouseCursor the object under the mouse cursor
+ * @param textureU texture-space X of the hit point in primary-texture pixels
+ * @param textureV texture-space Y of the hit point in primary-texture pixels
+ */
+ public synchronized void setCurrentObjectUnderMouseCursor(
+ final MouseInteractionController currentObjectUnderMouseCursor,
+ final double textureU, final double textureV) {
+ this.currentObjectUnderMouseCursor = currentObjectUnderMouseCursor;
+ this.currentMouseTextureU = textureU;
+ this.currentMouseTextureV = textureV;
+ }
+
+ /**
+ * Returns the current object under the mouse cursor.
+ * Used by segment rendering to collect mouse results.
+ *
+ * @return the current object under mouse cursor, or null
+ */
+ public synchronized MouseInteractionController getCurrentObjectUnderMouseCursor() {
+ return currentObjectUnderMouseCursor;
+ }
+
+ /**
+ * Handles mouse events for components and returns whether a view repaint is needed.
+ *
+ * @return {@code true} if view update is needed as a consequence of this mouse event
+ */
+ public boolean handlePossibleComponentMouseEvent() {
+ if (mouseEvent == null) return false;
+
+ boolean viewRepaintNeeded = false;
+
+ if (objectPreviouslyUnderMouseCursor != currentObjectUnderMouseCursor) {
+ // Mouse cursor has just entered or left component.
+ viewRepaintNeeded = objectPreviouslyUnderMouseCursor != null && objectPreviouslyUnderMouseCursor.mouseExited();
+ viewRepaintNeeded |= currentObjectUnderMouseCursor != null && currentObjectUnderMouseCursor.mouseEntered();
+ objectPreviouslyUnderMouseCursor = currentObjectUnderMouseCursor;
+ }
+
+ if (mouseEvent.button != 0 && currentObjectUnderMouseCursor != null) {
+ // Mouse button was clicked on some component.
+ viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseClicked(
+ mouseEvent.button, currentMouseTextureU, currentMouseTextureV);
+ } else if (currentObjectUnderMouseCursor != null)
+ // hover: let the component track the pointer position
+ viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseHover(
+ currentMouseTextureU, currentMouseTextureV);
+
+ return viewRepaintNeeded;
+ }
+
+}
--- /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;
+
+import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent;
+import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
+
+import java.awt.*;
+import java.util.function.Consumer;
+
+/**
+ * A view of a RenderingContext for rendering a horizontal screen segment.
+ *
+ * <p>This class wraps a parent RenderingContext and provides its own Y-bounds
+ * for multi-threaded rendering. All operations delegate to the parent context,
+ * but with segment-specific Y bounds for pixel operations.</p>
+ *
+ * <p>Mouse tracking is local to each segment and must be combined after all
+ * segments complete rendering.</p>
+ *
+ * @see RenderingContext
+ */
+public class SegmentRenderingContext extends RenderingContext {
+
+ private final RenderingContext parent;
+ private final int segmentIndex;
+ private MouseInteractionController segmentMouseHit;
+ private double segmentMouseHitU = Double.NaN;
+ private double segmentMouseHitV = Double.NaN;
+
+ /**
+ * Creates a segment view of a parent rendering context.
+ *
+ * @param parent the parent rendering context to delegate to
+ * @param renderMinY minimum Y coordinate (inclusive) for this segment
+ * @param renderMaxY maximum Y coordinate (exclusive) for this segment
+ * @param segmentIndex the index of this segment (0 to numRenderSegments-1)
+ */
+ public SegmentRenderingContext(final RenderingContext parent,
+ final int renderMinY, final int renderMaxY,
+ final int segmentIndex) {
+ super(parent, renderMinY, renderMaxY);
+ this.parent = parent;
+ this.segmentIndex = segmentIndex;
+ }
+
+ @Override
+ public void executeWithGraphics(final Consumer<Graphics2D> operation) {
+ operation.accept(parent.getSegmentGraphics(segmentIndex));
+ }
+
+ @Override
+ public MouseEvent getMouseEvent() {
+ return parent.getMouseEvent();
+ }
+
+ @Override
+ public void setMouseEvent(final MouseEvent mouseEvent) {
+ parent.setMouseEvent(mouseEvent);
+ }
+
+ @Override
+ public synchronized void setCurrentObjectUnderMouseCursor(final MouseInteractionController controller) {
+ setCurrentObjectUnderMouseCursor(controller, Double.NaN, Double.NaN);
+ }
+
+ @Override
+ public synchronized void setCurrentObjectUnderMouseCursor(
+ final MouseInteractionController controller,
+ final double textureU, final double textureV) {
+ this.segmentMouseHit = controller;
+ this.segmentMouseHitU = textureU;
+ this.segmentMouseHitV = textureV;
+ }
+
+ /**
+ * Returns the mouse hit detected in this segment.
+ *
+ * @return the MouseInteractionController that was under the mouse in this segment, or null
+ */
+ public MouseInteractionController getSegmentMouseHit() {
+ return segmentMouseHit;
+ }
+
+ /**
+ * Texture-space X of the hit point (primary texture pixels), NaN if none.
+ */
+ public double getSegmentMouseHitU() {
+ return segmentMouseHitU;
+ }
+
+ /**
+ * Texture-space Y of the hit point (primary texture pixels), NaN if none.
+ */
+ public double getSegmentMouseHitV() {
+ return segmentMouseHitV;
+ }
+
+ @Override
+ public synchronized MouseInteractionController getCurrentObjectUnderMouseCursor() {
+ return segmentMouseHit;
+ }
+}
\ No newline at end of file
*/
package eu.svjatoslav.aukio.e3d.renderer.raster;
-import eu.svjatoslav.aukio.e3d.geometry.Frustum;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.CullingStatistics;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
import eu.svjatoslav.aukio.e3d.math.Quaternion;
import eu.svjatoslav.aukio.e3d.math.Transform;
import eu.svjatoslav.aukio.e3d.math.TransformStack;
* <p>The {@link #addShape} method is synchronized, making it safe to add shapes from
* any thread while the rendering loop is active.</p>
*
- * @see ViewPanel#getRootShapeCollection()
+ * @see eu.svjatoslav.aukio.e3d.gui.ViewPanel#getRootShapeCollection()
* @see AbstractShape the base class for all shapes
* @see AbstractCompositeShape the root composite that stores and processes all shapes
* @see RenderAggregator handles depth sorting and painting
* @param viewPanel the view panel providing the camera state
* @param renderingContext the rendering context with frame metadata
*/
- public synchronized void transformShapes(final ViewPanel viewPanel,
- final RenderingContext renderingContext) {
- transformShapesBegin(viewPanel, renderingContext);
- drainTransformShapes(renderingContext);
- }
-
/**
- * Camera-based variant of {@link #transformShapes(ViewPanel, RenderingContext)}
- * for headless rendering without a {@link ViewPanel} (off-screen snapshots,
+ * Camera-based variant of {@link #transformShapes(Camera, RenderingContext)}
+ * for headless rendering without a {@link eu.svjatoslav.aukio.e3d.gui.ViewPanel} (off-screen snapshots,
* golden-image tests, GI scene setup).
*
* @param camera the camera providing position and orientation
}
/**
- * First half of {@link #transformShapes}: resets the frame's
- * aggregator, computes the frustum and walks the scene tree,
- * forking heavy composites into chunk tasks on the frame's
- * coordinator. Returns WITHOUT waiting for the chunk tasks; the
- * caller hands the coordinator to {@link #drainTransformShapes}
- * later (the pipeline drains it inside the asynchronous
- * sort/bin/paint continuation on a worker thread).
- *
- * @param viewPanel the view panel providing the camera state
- * @param renderingContext the pass rendering context
- */
- public synchronized void transformShapesBegin(final ViewPanel viewPanel,
- final RenderingContext renderingContext) {
- transformShapesBegin(viewPanel.getCamera(), renderingContext);
- }
-
- /**
- * Camera-based variant of {@link #transformShapesBegin(ViewPanel, RenderingContext)}
- * for headless rendering without a {@link ViewPanel}.
+ * Camera-based variant of {@link #transformShapesBegin(Camera, RenderingContext)}
+ * for headless rendering without a {@link eu.svjatoslav.aukio.e3d.gui.ViewPanel}.
*
* @param camera the camera providing position and orientation
* @param renderingContext the pass rendering context
--- /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;
+
+/**
+ * Identifies which eye is being rendered in stereoscopic mode.
+ *
+ * @see RenderingContext#stereoEye
+ */
+public enum StereoEye {
+ /** Normal single-view rendering (no stereo). */
+ NONE,
+ /** Left eye view in side-by-side stereo mode. */
+ LEFT,
+ /** Right eye view in side-by-side stereo mode. */
+ RIGHT
+}
--- /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;
+import eu.svjatoslav.aukio.e3d.math.TransformStack;
+
+import eu.svjatoslav.aukio.e3d.geometry.Point2D;
+import eu.svjatoslav.aukio.e3d.geometry.Point3D;
+
+/**
+ * A vertex in 3D space with transformation and screen projection support.
+ *
+ * <p>A vertex represents a corner point of a polygon or polyhedron. In addition to
+ * the 3D coordinate, it stores the transformed position (relative to viewer) and
+ * the projected screen coordinates for rendering.</p>
+ *
+ * <p><b>Coordinate spaces:</b></p>
+ * <ul>
+ * <li>{@link #coordinate} - Original position in local/model space</li>
+ * <li>{@link #transformedCoordinate} - Position relative to viewer (camera space)</li>
+ * <li>{@link #onScreenCoordinate} - 2D screen position after perspective projection</li>
+ * </ul>
+ *
+ * <p><b>Example:</b></p>
+ * <pre>{@code
+ * Vertex v = new Vertex(new Point3D(10, 20, 30));
+ * v.calculateLocationRelativeToViewer(transformStack, renderContext);
+ * if (v.transformedCoordinate.z > 0) {
+ * // Vertex is in front of the camera
+ * }
+ * }</pre>
+ *
+ * @see Point3D
+ * @see TransformStack
+ */
+public class Vertex {
+
+ /**
+ * Vertex coordinate in local/model 3D space.
+ */
+ public Point3D coordinate;
+
+ /**
+ * Vertex coordinate relative to the viewer after transformation (camera
+ * space), per buffer slot. Slot parity lets the transform phase of the
+ * NEXT frame write slot B while the paint phase of the current frame
+ * still reads slot A (double-buffered pipeline). Never access directly:
+ * use {@link #transformedCoordinate(RenderingContext)}.
+ */
+ private final Point3D transformedCoordinate0 = new Point3D();
+ private final Point3D transformedCoordinate1 = new Point3D();
+ private final Point3D transformedCoordinate2 = new Point3D();
+
+ /**
+ * Vertex position on screen in pixels, per buffer slot.
+ * Use {@link #onScreenCoordinate(RenderingContext)}.
+ */
+ private final Point2D onScreenCoordinate0 = new Point2D();
+ private final Point2D onScreenCoordinate1 = new Point2D();
+ private final Point2D onScreenCoordinate2 = new Point2D();
+
+ /**
+ * Texture coordinate for UV mapping (optional).
+ */
+ public Point2D textureCoordinate;
+
+ /**
+ * Normal vector for this vertex (optional).
+ * Used by CSG operations for smooth interpolation during polygon splitting.
+ * Null for non-CSG usage; existing rendering code ignores this field.
+ */
+ public Point3D normal;
+
+
+ /**
+ * The transform cycle when each slot was last transformed (for
+ * caching). Keyed by {@link RenderingContext#transformCycleId}, NOT
+ * by frameNumber: in stereo mode both eye passes share the same
+ * parity context and therefore the same frameNumber, while using
+ * different slots — a frameNumber-based key lets one eye's pass
+ * falsely hit the cache entry the other eye wrote for the same slot
+ * an odd number of passes earlier, leaving opposite-eye (wrong
+ * viewport-offset) screen coordinates in the slot: the affected eye
+ * paints fully off-viewport, i.e. a black half-frame (observed as
+ * violent left/right flashing, 2026-09-09). transformCycleId is
+ * unique per pass, so a hit always means "already transformed within
+ * THIS pass" — the only correct dedupe semantics.
+ */
+ private long lastTransformCycle0 = -1;
+ private long lastTransformCycle1 = -1;
+ private long lastTransformCycle2 = -1;
+
+ /**
+ * Creates a vertex at the origin (0, 0, 0) with no texture coordinate.
+ */
+ public Vertex() {
+ this(new Point3D());
+ }
+
+ /**
+ * Creates a vertex at the specified position with no texture coordinate.
+ *
+ * @param coordinate the 3D position of this vertex
+ */
+ public Vertex(final Point3D coordinate) {
+ this(coordinate, null);
+ }
+
+ /**
+ * Creates a vertex at the specified position with an optional texture coordinate.
+ *
+ * @param coordinate the 3D position of this vertex
+ * @param textureCoordinate the UV texture coordinate, or {@code null} for none
+ */
+ public Vertex(final Point3D coordinate, final Point2D textureCoordinate) {
+ this.coordinate = coordinate;
+ this.textureCoordinate = textureCoordinate;
+ }
+
+ /**
+ * Returns the camera-space coordinate for the rendering context's buffer
+ * slot. Valid only after this vertex was transformed for that slot's
+ * current frame.
+ *
+ * @param renderContext the rendering context (selects the buffer slot)
+ * @return the transformed coordinate (camera space) for the active slot
+ */
+ public Point3D transformedCoordinate(final RenderingContext renderContext) {
+ // Dual fields, not a slot array: measured 2026-09-05 (400-sphere
+ // scene) that array indexing adds a second dependent load per access
+ // and cost ~60% of transform phase time; a perfectly-predicted
+ // branch + direct field load is free.
+ final int slot = renderContext.vertexSlot;
+ return slot == 0 ? transformedCoordinate0
+ : slot == 1 ? transformedCoordinate1 : transformedCoordinate2;
+ }
+
+ /**
+ * Returns the screen-space position for the rendering context's buffer
+ * slot.
+ *
+ * @param renderContext the rendering context (selects the buffer slot)
+ * @return the on-screen coordinate (pixels) for the active slot
+ */
+ public Point2D onScreenCoordinate(final RenderingContext renderContext) {
+ final int slot = renderContext.vertexSlot;
+ return slot == 0 ? onScreenCoordinate0
+ : slot == 1 ? onScreenCoordinate1 : onScreenCoordinate2;
+ }
+
+
+ /**
+ * Transforms this vertex from model space to screen space.
+ *
+ * <p>This method applies the transform stack to compute the vertex position
+ * relative to the viewer, then projects it to 2D screen coordinates.
+ * Results are cached per-frame per-slot to avoid redundant calculations.</p>
+ *
+ * @param transforms the transform stack to apply (world-to-camera transforms)
+ * @param renderContext the rendering context providing projection parameters
+ */
+ public void calculateLocationRelativeToViewer(final TransformStack transforms,
+ final RenderingContext renderContext) {
+
+ final Point3D transformedCoordinate;
+ final Point2D onScreenCoordinate;
+ switch (renderContext.vertexSlot) {
+ case 0:
+ if (lastTransformCycle0 == renderContext.transformCycleId)
+ return;
+ lastTransformCycle0 = renderContext.transformCycleId;
+ transformedCoordinate = transformedCoordinate0;
+ onScreenCoordinate = onScreenCoordinate0;
+ break;
+ case 1:
+ if (lastTransformCycle1 == renderContext.transformCycleId)
+ return;
+ lastTransformCycle1 = renderContext.transformCycleId;
+ transformedCoordinate = transformedCoordinate1;
+ onScreenCoordinate = onScreenCoordinate1;
+ break;
+ default:
+ if (lastTransformCycle2 == renderContext.transformCycleId)
+ return;
+ lastTransformCycle2 = renderContext.transformCycleId;
+ transformedCoordinate = transformedCoordinate2;
+ onScreenCoordinate = onScreenCoordinate2;
+ break;
+ }
+ transforms.transform(coordinate, transformedCoordinate);
+ onScreenCoordinate.x = ((transformedCoordinate.x / transformedCoordinate.z) * renderContext.projectionScale);
+ onScreenCoordinate.y = ((transformedCoordinate.y / transformedCoordinate.z) * renderContext.projectionScale);
+ onScreenCoordinate.add(renderContext.centerCoordinate);
+ onScreenCoordinate.x += renderContext.stereoViewportOffsetX;
+ }
+
+ /**
+ * Writes a camera-space position directly into this vertex's slot state
+ * and projects it to screen coordinates, bypassing the transform stack.
+ *
+ * <p>Used by near-plane clipping ({@code AbstractCoordinateShape}), which
+ * creates intersection vertices that exist ONLY in camera space — there
+ * is no model-space coordinate to transform. The given z must be > 0
+ * (clip against the near plane guarantees z == nearPlaneDistance).</p>
+ *
+ * @param x camera-space X
+ * @param y camera-space Y
+ * @param z camera-space Z (depth in front of viewer, > 0)
+ * @param renderContext the rendering context (selects the buffer slot and
+ * provides projection parameters)
+ */
+ public void setCameraSpaceCoordinate(final double x, final double y, final double z,
+ final RenderingContext renderContext) {
+ final Point3D transformedCoordinate;
+ final Point2D onScreenCoordinate;
+ switch (renderContext.vertexSlot) {
+ case 0:
+ transformedCoordinate = transformedCoordinate0;
+ onScreenCoordinate = onScreenCoordinate0;
+ break;
+ case 1:
+ transformedCoordinate = transformedCoordinate1;
+ onScreenCoordinate = onScreenCoordinate1;
+ break;
+ default:
+ transformedCoordinate = transformedCoordinate2;
+ onScreenCoordinate = onScreenCoordinate2;
+ break;
+ }
+ transformedCoordinate.x = x;
+ transformedCoordinate.y = y;
+ transformedCoordinate.z = z;
+ onScreenCoordinate.x = ((x / z) * renderContext.projectionScale);
+ onScreenCoordinate.y = ((y / z) * renderContext.projectionScale);
+ onScreenCoordinate.add(renderContext.centerCoordinate);
+ onScreenCoordinate.x += renderContext.stereoViewportOffsetX;
+ }
+
+ // ========== CSG support methods ==========
+
+ /**
+ * Creates a deep copy of this vertex.
+ * Clones the coordinate, normal (if present), and texture coordinate (if present).
+ * The transformedCoordinate and onScreenCoordinate are not cloned (they are computed per-frame).
+ *
+ * @return a new Vertex with cloned data
+ */
+ public Vertex clone() {
+ final Vertex result = new Vertex(new Point3D(coordinate),
+ textureCoordinate != null ? new Point2D(textureCoordinate) : null);
+ if (normal != null) {
+ result.normal = new Point3D(normal);
+ }
+ return result;
+ }
+
+ /**
+ * Flips the orientation of this vertex by negating the normal vector.
+ * Called when the orientation of a polygon is flipped during CSG operations.
+ * If normal is null, this method does nothing.
+ */
+ public void flip() {
+ if (normal != null) {
+ normal = normal.withNegated();
+ }
+ }
+
+ /**
+ * Creates a new vertex between this vertex and another by linearly interpolating
+ * all properties using parameter t.
+ *
+ * <p>Interpolates: position, normal (if present), and texture coordinate (if present).</p>
+ *
+ * @param other the other vertex to interpolate towards
+ * @param t the interpolation parameter (0 = this vertex, 1 = other vertex)
+ * @return a new Vertex representing the interpolated position
+ */
+ public Vertex interpolate(final Vertex other, final double t) {
+ final Vertex result = new Vertex(
+ coordinate.interpolate(other.coordinate, t),
+ (textureCoordinate != null && other.textureCoordinate != null)
+ ? new Point2D(
+ textureCoordinate.x + (other.textureCoordinate.x - textureCoordinate.x) * t,
+ textureCoordinate.y + (other.textureCoordinate.y - textureCoordinate.y) * t)
+ : null
+ );
+ if (normal != null && other.normal != null) {
+ result.normal = normal.interpolate(other.normal, t);
+ }
+ return result;
+ }
+}
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle;
import eu.svjatoslav.aukio.e3d.geometry.Box;
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
import eu.svjatoslav.aukio.e3d.math.TransformStack;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator;
import java.util.ArrayList;
import eu.svjatoslav.aukio.e3d.geometry.Box;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
import eu.svjatoslav.aukio.e3d.math.TransformStack;
import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator;
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture;
import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap;
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
*/
package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon;
-import eu.svjatoslav.aukio.e3d.geometry.Plane;
+import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.Plane;
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
import eu.svjatoslav.aukio.e3d.math.TransformStack;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator;
import eu.svjatoslav.aukio.e3d.math.TransformStack;
package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture;
import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap;
import eu.svjatoslav.aukio.e3d.geometry.Box;
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.HiZPyramid;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.gui.StereoEye;
+import eu.svjatoslav.aukio.e3d.renderer.raster.HiZPyramid;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.StereoEye;
import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape;
import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture;
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
import eu.svjatoslav.aukio.e3d.math.Transform;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture;
package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base;
import eu.svjatoslav.aukio.e3d.geometry.Box;
-import eu.svjatoslav.aukio.e3d.geometry.BspTree;
-import eu.svjatoslav.aukio.e3d.geometry.Frustum;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Frustum;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
import eu.svjatoslav.aukio.e3d.gui.ViewSpaceTracker;
import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
import eu.svjatoslav.aukio.e3d.math.Transform;
import eu.svjatoslav.aukio.e3d.math.TransformStack;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
import eu.svjatoslav.aukio.e3d.renderer.raster.ParallelTransformCoordinator;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
--- /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;
+
+/**
+ * A Binary Space Partitioning (BSP) tree for CSG operations.
+ *
+ * <p>BSP trees are the data structure that makes CSG boolean operations possible.
+ * Each node divides 3D space into two half-spaces using a plane, enabling
+ * efficient spatial queries and polygon clipping.</p>
+ *
+ * <p><b>BSP Tree Structure:</b></p>
+ * <pre>
+ * [Node: plane P]
+ * / \
+ * [Front subtree] [Back subtree]
+ * (same side as P's (opposite side
+ * normal) of P's normal)
+ * </pre>
+ *
+ * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape
+ * @see Plane the plane type used for spatial partitioning
+ * @see SolidPolygon the polygon type stored in BSP nodes
+ */
+public class BspTree {
+
+ /**
+ * Polygons that lie on this node's partitioning plane.
+ */
+ public final List<SolidPolygon> polygons = new ArrayList<>();
+
+ /**
+ * The partitioning plane for this node.
+ */
+ public Plane plane;
+
+ /**
+ * The front child subtree.
+ */
+ public BspTree front;
+
+ /**
+ * The back child subtree.
+ */
+ public BspTree back;
+
+ /**
+ * Creates an empty BSP tree with no plane or children.
+ */
+ public BspTree() {
+ }
+
+ /**
+ * Creates a BSP tree from a list of polygons.
+ *
+ * @param polygons the polygons to partition into a BSP tree
+ */
+ public BspTree(final List<SolidPolygon> polygons) {
+ addPolygons(polygons);
+ }
+
+ /**
+ * Creates a deep clone of this BSP tree.
+ *
+ * @return a new BspTree with cloned data
+ */
+ public BspTree clone() {
+ final BspTree tree = new BspTree();
+
+ tree.plane = plane != null ? plane.clone() : null;
+ tree.front = front != null ? front.clone() : null;
+ tree.back = back != null ? back.clone() : null;
+
+ for (final SolidPolygon p : polygons) {
+ tree.polygons.add(p.deepClone());
+ }
+
+ return tree;
+ }
+
+ /**
+ * Inverts this BSP tree, converting "inside" to "outside" and vice versa.
+ */
+ public void invert() {
+ for (final SolidPolygon polygon : polygons) polygon.flip();
+
+ if (plane != null) plane.flip();
+ if (front != null) front.invert();
+ if (back != null) back.invert();
+
+ final BspTree temp = front;
+ front = back;
+ back = temp;
+ }
+
+ /**
+ * Clips a list of polygons against this BSP tree, returning only the
+ * portions that lie outside the solid represented by this tree.
+ *
+ * <p>This is a core CSG operation used for boolean subtraction and
+ * intersection. The method recursively traverses the BSP tree, splitting
+ * polygons at each partitioning plane and discarding interior fragments.</p>
+ *
+ * <p><b>Algorithm:</b></p>
+ * <ol>
+ * <li>At each node, split polygons by the partitioning plane</li>
+ * <li>Recursively clip front fragments against the front subtree</li>
+ * <li>Recursively clip back fragments against the back subtree</li>
+ * <li>Combine and return all surviving fragments</li>
+ * </ol>
+ *
+ * <p><b>Leaf nodes:</b> If this node has no plane (leaf node), all polygons
+ * are considered outside and returned unchanged.</p>
+ *
+ * @param polygons the polygons to clip against this BSP tree
+ * @return a new list containing only the portions outside this solid
+ */
+ public List<SolidPolygon> clipPolygons(final List<SolidPolygon> polygons) {
+ // Leaf node: no partitioning plane means all polygons are outside
+ if (plane == null) {
+ return new ArrayList<>(polygons);
+ }
+
+ // Split polygons by this node's partitioning plane
+ final List<SolidPolygon> frontList = new ArrayList<>();
+ final List<SolidPolygon> backList = new ArrayList<>();
+
+ for (final SolidPolygon polygon : polygons)
+ // Split by plane: coplanar polygons are classified by their normal direction
+ // (same-facing normal → frontList, opposite-facing normal → backList)
+ plane.splitPolygon(polygon, frontList, backList, frontList, backList);
+
+ // Recursively clip front fragments against front subtree
+ List<SolidPolygon> resultFront = frontList;
+ if (front != null) resultFront = front.clipPolygons(frontList);
+
+ // Recursively clip back fragments against back subtree
+ List<SolidPolygon> resultBack;
+ if (back != null) resultBack = back.clipPolygons(backList);
+ else resultBack = new ArrayList<>();
+
+ // Combine surviving fragments from both subtrees
+ final List<SolidPolygon> result = new ArrayList<>(resultFront.size() + resultBack.size());
+ result.addAll(resultFront);
+ result.addAll(resultBack);
+ return result;
+ }
+
+ /**
+ * Clips this BSP tree against another BSP tree.
+ *
+ * @param bsp the BSP tree to clip against
+ */
+ public void clipTo(final BspTree bsp) {
+ final List<SolidPolygon> newPolygons = bsp.clipPolygons(polygons);
+ polygons.clear();
+ polygons.addAll(newPolygons);
+
+ if (front != null) front.clipTo(bsp);
+ if (back != null) back.clipTo(bsp);
+ }
+
+ /**
+ * Collects all polygons from this BSP tree into a flat list.
+ *
+ * @return a new list containing all polygons in this tree
+ */
+ public List<SolidPolygon> allPolygons() {
+ final List<SolidPolygon> result = new ArrayList<>(polygons);
+
+ if (front != null) result.addAll(front.allPolygons());
+ if (back != null) result.addAll(back.allPolygons());
+
+ return result;
+ }
+
+ /**
+ * Adds polygons to this BSP tree, partitioning space recursively.
+ *
+ * <p>This method is the core BSP tree construction algorithm. It builds or
+ * extends the tree by choosing a partition plane and classifying each polygon:</p>
+ *
+ * <ul>
+ * <li><b>Coplanar</b> — polygons on the partition plane are stored in this node</li>
+ * <li><b>Front</b> — polygons in the front half-space (same side as plane normal)
+ * go to the front child subtree</li>
+ * <li><b>Back</b> — polygons in the back half-space (opposite to plane normal)
+ * go to the back child subtree</li>
+ * <li><b>Spanning</b> — polygons crossing the plane are split into front and back
+ * fragments, each going to its respective subtree</li>
+ * </ul>
+ *
+ * <p>For an empty tree, the first polygon's plane becomes the partition plane.
+ * Child nodes are created lazily when polygons need to be stored in them.</p>
+ *
+ * <p>Can be called multiple times to incrementally extend an existing tree,
+ * though the original partition planes remain unchanged.</p>
+ *
+ * @param polygons the polygons to insert into this BSP tree
+ * @see Plane#splitPolygon the method that classifies and splits individual polygons
+ */
+ public void addPolygons(final List<SolidPolygon> polygons) {
+ if (polygons.isEmpty()) return;
+
+ if (plane == null) plane = polygons.get(0).getPlane().clone();
+
+ final List<SolidPolygon> frontList = new ArrayList<>();
+ final List<SolidPolygon> backList = new ArrayList<>();
+
+ for (final SolidPolygon polygon : polygons)
+ plane.splitPolygon(polygon, this.polygons, this.polygons, frontList, backList);
+
+ if (!frontList.isEmpty()) {
+ if (front == null) front = new BspTree();
+ front.addPolygons(frontList);
+ }
+
+ if (!backList.isEmpty()) {
+ if (back == null) back = new BspTree();
+ back.addPolygons(backList);
+ }
+ }
+}
\ No newline at end of file
--- /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.geometry.Point3D;
+
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents an infinite plane in 3D space using the Hesse normal form.
+ *
+ * <p>Planes are fundamental to BSP (Binary Space Partitioning) tree operations
+ * in CSG. They divide 3D space into two half-spaces.</p>
+ *
+ * @see SolidPolygon polygons that reference their containing plane
+ * @see BspTree BSP trees that use planes for spatial partitioning
+ */
+public class Plane {
+
+ /**
+ * Epsilon value used for floating-point comparisons in BSP operations.
+ * Smaller values provide higher precision but may cause issues with
+ * near-coplanar polygons. 1e-5 is a good balance for most 3D geometry.
+ */
+ public static final double EPSILON = 1e-12;
+
+ /**
+ * The unit normal vector perpendicular to the plane surface.
+ */
+ public Point3D normal;
+
+ /**
+ * The signed distance from the origin to the plane along the normal.
+ */
+ public double distance;
+
+ /**
+ * Creates a plane with the given normal and distance.
+ *
+ * @param normal the unit normal vector
+ * @param distance the signed distance from origin to the plane
+ */
+ public Plane(final Point3D normal, final double distance) {
+ this.normal = normal;
+ this.distance = distance;
+ }
+
+ /**
+ * Computes the unit normal vector for a triangle defined by three points.
+ *
+ * <p>Zero-allocation method: fills the result point instead of creating a new one.
+ * This is the shared implementation used by both {@link #fromPoints} and
+ * {@link SolidPolygon} for shading calculations.</p>
+ *
+ * <p>The normal is computed as the cross product of two edge vectors (b-a and c-a),
+ * then normalized to unit length.</p>
+ *
+ * @param a first point (base point for edge vectors)
+ * @param b second point
+ * @param c third point
+ * @param result Point3D to receive the unit normal vector (modified in place)
+ * @return true if normal computed successfully, false if points are collinear
+ * (cross product magnitude less than EPSILON)
+ */
+ public static boolean computeNormal(final Point3D a, final Point3D b,
+ final Point3D c, final Point3D result) {
+ // Edge vectors from a to b and a to c
+ final double ax = b.x - a.x;
+ final double ay = b.y - a.y;
+ final double az = b.z - a.z;
+
+ final double bx = c.x - a.x;
+ final double by = c.y - a.y;
+ final double bz = c.z - a.z;
+
+ // Cross product: (edge1 × edge2)
+ double nx = ay * bz - az * by;
+ double ny = az * bx - ax * bz;
+ double nz = ax * by - ay * bx;
+
+ // Normalize
+ final double length = Math.sqrt(nx * nx + ny * ny + nz * nz);
+ if (length < EPSILON) {
+ result.x = result.y = result.z = 0;
+ return false;
+ }
+
+ result.x = nx / length;
+ result.y = ny / length;
+ result.z = nz / length;
+ return true;
+ }
+
+ /**
+ * Creates a plane from three non-collinear points.
+ *
+ * <p>Uses {@link #computeNormal} for the normal calculation, then computes
+ * the signed distance from origin using the dot product.</p>
+ *
+ * @param a the first point on the plane
+ * @param b the second point on the plane
+ * @param c the third point on the plane
+ * @return a new Plane passing through the three points
+ * @throws ArithmeticException if the points are collinear (cannot define a plane)
+ */
+ public static Plane fromPoints(final Point3D a, final Point3D b, final Point3D c) {
+ final Point3D n = new Point3D();
+ if (!computeNormal(a, b, c, n)) {
+ throw new ArithmeticException(
+ "Cannot create plane from collinear points: cross product is zero");
+ }
+ return new Plane(n, n.dot(a));
+ }
+
+ /**
+ * Creates a deep clone of this plane.
+ *
+ * @return a new Plane with the same normal and distance
+ */
+ public Plane clone() {
+ return new Plane(new Point3D(normal.x, normal.y, normal.z), distance);
+ }
+
+ /**
+ * Flips the plane orientation by negating the normal and distance.
+ */
+ public void flip() {
+ normal = normal.withNegated();
+ distance = -distance;
+ }
+
+ /**
+ * Splits a polygon by this plane, classifying and potentially dividing it.
+ *
+ * @param polygon the polygon to classify and potentially split
+ * @param coplanarFront list to receive coplanar polygons with same-facing normals
+ * @param coplanarBack list to receive coplanar polygons with opposite-facing normals
+ * @param front list to receive polygons in the front half-space
+ * @param back list to receive polygons in the back half-space
+ */
+ public void splitPolygon(final SolidPolygon polygon,
+ final List<SolidPolygon> coplanarFront,
+ final List<SolidPolygon> coplanarBack,
+ final List<SolidPolygon> front,
+ final List<SolidPolygon> back) {
+
+ PolygonType polygonType = PolygonType.COPLANAR;
+ final int vertexCount = polygon.getVertexCount();
+ final PolygonType[] types = new PolygonType[vertexCount];
+
+ for (int i = 0; i < vertexCount; i++) {
+ final Vertex v = polygon.vertices.get(i);
+ final double t = normal.dot(v.coordinate) - distance;
+ final PolygonType type = (t < -EPSILON) ? PolygonType.BACK
+ : (t > EPSILON) ? PolygonType.FRONT : PolygonType.COPLANAR;
+ polygonType = polygonType.combine(type);
+ types[i] = type;
+ }
+
+ switch (polygonType) {
+ case COPLANAR:
+ ((normal.dot(polygon.getPlane().normal) > 0) ? coplanarFront : coplanarBack).add(polygon);
+ break;
+
+ case FRONT:
+ front.add(polygon);
+ break;
+
+ case BACK:
+ back.add(polygon);
+ break;
+
+ case SPANNING:
+ // Split spanning polygon by clipping each edge against the plane.
+ // Vertices on each side go to their respective lists.
+ // Edges crossing the plane create intersection vertices added to both lists.
+ final List<Vertex> frontVertices = new ArrayList<>();
+ final List<Vertex> backVertices = new ArrayList<>();
+
+ for (int i = 0; i < vertexCount; i++) {
+ final int nextIndex = (i + 1) % vertexCount;
+ final PolygonType currentType = types[i];
+ final PolygonType nextType = types[nextIndex];
+ final Vertex currentVertex = polygon.vertices.get(i);
+ final Vertex nextVertex = polygon.vertices.get(nextIndex);
+
+ // Add current vertex to the polygon on its side of the plane
+ if (currentType.isFront()) {
+ frontVertices.add(currentVertex.clone());
+ }
+ if (currentType.isBack()) {
+ backVertices.add(currentVertex.clone());
+ }
+
+ // If edge crosses the plane, create intersection vertex for both polygons
+ if (currentType != nextType
+ && currentType != PolygonType.COPLANAR
+ && nextType != PolygonType.COPLANAR) {
+ // Calculate interpolation parameter t (0 = current, 1 = next)
+ // t represents where along the edge the plane intersection occurs
+ final double t = (distance - normal.dot(currentVertex.coordinate))
+ / normal.dot(nextVertex.coordinate.withSubtracted(currentVertex.coordinate));
+
+ final Vertex intersectionVertex = currentVertex.interpolate(nextVertex, t);
+ frontVertices.add(intersectionVertex);
+ backVertices.add(intersectionVertex.clone());
+ }
+ }
+
+ if (frontVertices.size() >= 3) {
+ final SolidPolygon frontPoly = SolidPolygon.fromVertices(
+ frontVertices, polygon.getColor(), polygon.isShadingEnabled());
+ front.add(frontPoly);
+ }
+ if (backVertices.size() >= 3) {
+ final SolidPolygon backPoly = SolidPolygon.fromVertices(
+ backVertices, polygon.getColor(), polygon.isShadingEnabled());
+ back.add(backPoly);
+ }
+ break;
+ }
+ }
+}
\ No newline at end of file
--- /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;
+
+/**
+ * Classification of a polygon's position relative to a plane.
+ * Used in BSP tree operations to determine how polygons should be split.
+ */
+public enum PolygonType {
+ /** Polygon lies on the plane. */
+ COPLANAR,
+ /** Polygon is entirely in front of the plane. */
+ FRONT,
+ /** Polygon is entirely behind the plane. */
+ BACK,
+ /** Polygon straddles the plane (vertices on both sides). */
+ SPANNING;
+
+ /**
+ * Combines this type with another to compute the aggregate classification.
+ * When vertices are on both sides of a plane, the result is SPANNING.
+ *
+ * @param other the other polygon type to combine with
+ * @return the combined classification
+ */
+ public PolygonType combine(final PolygonType other) {
+ if (this == other || other == COPLANAR) {
+ return this;
+ }
+ if (this == COPLANAR) {
+ return other;
+ }
+ // FRONT + BACK = SPANNING
+ return SPANNING;
+ }
+
+ /**
+ * Checks if this type represents a vertex in front of the plane.
+ *
+ * @return true if FRONT or COPLANAR (treated as front for classification)
+ */
+ public boolean isFront() {
+ return this == FRONT || this == COPLANAR;
+ }
+
+ /**
+ * Checks if this type represents a vertex behind the plane.
+ *
+ * @return true if BACK or COPLANAR (treated as back for classification)
+ */
+ public boolean isBack() {
+ return this == BACK || this == COPLANAR;
+ }
+}
\ No newline at end of file
* Creates a new texture with the specified dimensions and upscale capacity.
*
* <p>The underlying {@link java.awt.image.BufferedImage} is created using
- * {@link eu.svjatoslav.aukio.e3d.gui.RenderingContext#bufferedImageType} for
+ * {@link eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext#bufferedImageType} for
* compatibility with the raster rendering pipeline.</p>
*
* @param width the width of the primary bitmap in pixels
package eu.svjatoslav.aukio.e3d.headless;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
package eu.svjatoslav.aukio.e3d.renderer.raster;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
import eu.svjatoslav.aukio.e3d.math.Transform;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
// Serial run: no executor -> serial fallback path
final RenderingContext serialCtx = new RenderingContext(W, H, 1);
serialCtx.prepareForNewFrameRendering();
- scene.transformShapes(panel, serialCtx);
+ scene.transformShapes(panel.getCamera(), serialCtx);
scene.sortShapes();
final int[] serialIds = snapshotIds(scene);
final double[] serialZs = snapshotZs(scene);
parallelCtx.transformExecutor = executor;
parallelCtx.prepareForNewFrameRendering();
parallelCtx.prepareForNewFrameRendering(); // distinct frameNumber -> full re-transform
- scene.transformShapes(panel, parallelCtx);
+ scene.transformShapes(panel.getCamera(), parallelCtx);
scene.sortShapes();
final int[] parallelIds = snapshotIds(scene);
final double[] parallelZs = snapshotZs(scene);
// Serial run
final RenderingContext serialCtx = new RenderingContext(W, H, 1);
serialCtx.prepareForNewFrameRendering();
- scene.transformShapes(panel, serialCtx);
+ scene.transformShapes(panel.getCamera(), serialCtx);
scene.sortShapes();
final int[] serialIds = snapshotIds(scene);
final double[] serialZs = snapshotZs(scene);
parallelCtx.transformExecutor = executor;
parallelCtx.prepareForNewFrameRendering();
parallelCtx.prepareForNewFrameRendering();
- scene.transformShapes(panel, parallelCtx);
+ scene.transformShapes(panel.getCamera(), parallelCtx);
scene.sortShapes();
final int[] parallelIds = snapshotIds(scene);
final double[] parallelZs = snapshotZs(scene);
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.gui.SegmentRenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.SegmentRenderingContext;
import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
import eu.svjatoslav.aukio.e3d.math.Transform;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.GlowingPoint;
import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.Line;
private void transformAndSort(final ViewPanel panel, final ShapeCollection scene,
final RenderingContext context) {
context.prepareForNewFrameRendering();
- scene.transformShapes(panel, context);
+ scene.transformShapes(panel.getCamera(), context);
scene.sortShapes();
}
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap;
import org.junit.Test;
import eu.svjatoslav.aukio.e3d.geometry.Point2D;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap;
import org.junit.Test;