From: Svjatoslav Agejenko Date: Sun, 20 Sep 2026 08:46:18 +0000 (+0300) Subject: Package relocation: make package structure real and acyclic X-Git-Tag: aukio-3d-1.0.0~6 X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=307a71dd604773c6c7113a562f80d5591f99cec2;p=aukio-3d.git Package relocation: make package structure real and acyclic Moved (git mv, move-only, no behavior change): - gui -> renderer.raster: RenderingContext, SegmentRenderingContext, HiZPyramid, CullingStatistics, StereoEye, Frustum, Vertex (math) - gui -> diag: ThreadActivityRecorder, DebugLogBuffer - gui -> geometry: Camera (decoupled from gui.FrameListener: dead ViewPanel param dropped, panel registers a lambda adapter) - geometry -> renderer.raster.shapes.composite.base: Plane, BspTree, PolygonType (CSG machinery, used only by shapes) - renderer.octree -> geometry: IntegerPoint (zero-dep coordinate type) - ShapeCollection's ViewPanel overloads deleted (trivial delegates; callers pass getCamera()); RenderingContext.bufferedImage made public (AWT shell blits it); javadoc {@link}s re-qualified. Resulting layering: math <- geometry <- renderer.raster <- gui, with diag a standalone leaf. Remaining known renderer->gui edges (next refactor): DeveloperTools field + mouse-picking API types (MouseEvent/MouseInteractionController/ViewSpaceTracker/TextPointer) and demo-only RayTracer->ViewPanel. Verified: engine 37/37 tests, all 3 consumer repos (demos, aukio, aukio-environment-fo4) compile, demo goldens bit-identical to the pre-relocation run. --- diff --git a/AGENTS.org b/AGENTS.org index f383da2..df9f9b9 100644 --- a/AGENTS.org +++ b/AGENTS.org @@ -18,7 +18,7 @@ navigation. |-------------------------------+-----------------------------------------------------+-----------------------------------------------------------| | *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)~ | @@ -344,7 +344,7 @@ scene.addShape(clickableCube); |-----------------+--------------------------+------------------------------+--------------------------------------------------------------------------------------------------------| | ~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/~) diff --git a/Documentation/index.org b/Documentation/index.org index 4068d09..c40ce32 100644 --- a/Documentation/index.org +++ b/Documentation/index.org @@ -238,7 +238,7 @@ p1.rotate(new Point3D(0,0,0), Math.PI/4, 0); // Rotate p1 in place, *** 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: @@ -304,7 +304,7 @@ visible when surfaces are not rendered. A triangle has 3 edges, a cube 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 @@ -434,14 +434,14 @@ determines how bright a surface appears. | 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: @@ -1200,7 +1200,7 @@ the rendered screenshots. | [[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: diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/diag/DebugLogBuffer.java b/src/main/java/eu/svjatoslav/aukio/e3d/diag/DebugLogBuffer.java new file mode 100644 index 0000000..ba81b5b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/diag/DebugLogBuffer.java @@ -0,0 +1,99 @@ +/* + * 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. + * + *

Captures log messages to a fixed-size circular buffer for display + * in the {@link eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel}.

+ * + *

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.

+ * + * @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 getEntries() { + final List 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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/diag/ThreadActivityRecorder.java b/src/main/java/eu/svjatoslav/aukio/e3d/diag/ThreadActivityRecorder.java new file mode 100644 index 0000000..203d061 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/diag/ThreadActivityRecorder.java @@ -0,0 +1,212 @@ +/* + * 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. + * + *

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).

+ * + *

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.

+ * + *

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).

+ */ +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 rowByThreadName = new ConcurrentHashMap<>(); + private static final CopyOnWriteArrayList 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; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/BspTree.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/BspTree.java deleted file mode 100644 index f6542b2..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/BspTree.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * 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. - * - *

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.

- * - *

BSP Tree Structure:

- *
- *                 [Node: plane P]
- *                /               \
- *        [Front subtree]     [Back subtree]
- *     (same side as P's     (opposite side
- *        normal)             of P's normal)
- * 
- * - * @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 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 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. - * - *

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.

- * - *

Algorithm:

- *
    - *
  1. At each node, split polygons by the partitioning plane
  2. - *
  3. Recursively clip front fragments against the front subtree
  4. - *
  5. Recursively clip back fragments against the back subtree
  6. - *
  7. Combine and return all surviving fragments
  8. - *
- * - *

Leaf nodes: If this node has no plane (leaf node), all polygons - * are considered outside and returned unchanged.

- * - * @param polygons the polygons to clip against this BSP tree - * @return a new list containing only the portions outside this solid - */ - public List clipPolygons(final List 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 frontList = new ArrayList<>(); - final List 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 resultFront = frontList; - if (front != null) resultFront = front.clipPolygons(frontList); - - // Recursively clip back fragments against back subtree - List resultBack; - if (back != null) resultBack = back.clipPolygons(backList); - else resultBack = new ArrayList<>(); - - // Combine surviving fragments from both subtrees - final List 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 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 allPolygons() { - final List 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. - * - *

This method is the core BSP tree construction algorithm. It builds or - * extends the tree by choosing a partition plane and classifying each polygon:

- * - *
    - *
  • Coplanar — polygons on the partition plane are stored in this node
  • - *
  • Front — polygons in the front half-space (same side as plane normal) - * go to the front child subtree
  • - *
  • Back — polygons in the back half-space (opposite to plane normal) - * go to the back child subtree
  • - *
  • Spanning — polygons crossing the plane are split into front and back - * fragments, each going to its respective subtree
  • - *
- * - *

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.

- * - *

Can be called multiple times to incrementally extend an existing tree, - * though the original partition planes remain unchanged.

- * - * @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 polygons) { - if (polygons.isEmpty()) return; - - if (plane == null) plane = polygons.get(0).getPlane().clone(); - - final List frontList = new ArrayList<>(); - final List 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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Camera.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Camera.java new file mode 100644 index 0000000..c4c534b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Camera.java @@ -0,0 +1,243 @@ +/* + * 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. + * + *

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.

+ * + *

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}).

+ * + *

Programmatic camera control:

+ *
{@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);
+ * }
+ * + * @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. + * + *

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.

+ * + *

Example:

+ *
{@code
+     * Camera camera = viewPanel.getCamera();
+     * camera.getTransform().setTranslation(new Point3D(100, -50, -200));
+     * camera.lookAt(new Point3D(0, 0, 0));  // Point camera at origin
+     * }
+ * + * @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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Frustum.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Frustum.java deleted file mode 100644 index d1075c6..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Frustum.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * 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. - * - *

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.

- * - *

Frustum planes:

- *
    - *
  • Left, Right, Top, Bottom - define the viewport edges
  • - *
  • Near - closest visible distance from camera
  • - *
  • Far - farthest visible distance from camera
  • - *
- * - *

Usage:

- *
{@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
- * }
- * }
- * - *

AABB intersection algorithm:

- *

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.

- * - * @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). - * - *

This method should be called once per frame before rendering, after the - * camera position and orientation have been updated.

- * - *

View space coordinate system:

- *
    - *
  • Camera at origin (0, 0, 0)
  • - *
  • Forward = +Z axis (looking into the screen)
  • - *
  • Right = +X axis
  • - *
  • Up = -Y axis (since Y-down means smaller Y is higher visually)
  • - *
- * - *

Plane normals point INTO the frustum (toward the visible volume). - * A point is inside if dot(normal, point) >= distance for all planes.

- * - *

FOV calculation: 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.

- * - * @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. - * - *

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.

- * - *

Optimized algorithm:

- *

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.

- * - * @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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/IntegerPoint.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/IntegerPoint.java new file mode 100644 index 0000000..955267b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/IntegerPoint.java @@ -0,0 +1,39 @@ +/* + * 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; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Plane.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Plane.java deleted file mode 100644 index 1d5c289..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Plane.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * 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. - * - *

Planes are fundamental to BSP (Binary Space Partitioning) tree operations - * in CSG. They divide 3D space into two half-spaces.

- * - * @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. - * - *

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.

- * - *

The normal is computed as the cross product of two edge vectors (b-a and c-a), - * then normalized to unit length.

- * - * @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. - * - *

Uses {@link #computeNormal} for the normal calculation, then computes - * the signed distance from origin using the dot product.

- * - * @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 coplanarFront, - final List coplanarBack, - final List front, - final List 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 frontVertices = new ArrayList<>(); - final List 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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point3D.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point3D.java index 91f7aa9..8b227c6 100755 --- a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point3D.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point3D.java @@ -4,7 +4,7 @@ */ 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.*; @@ -57,7 +57,7 @@ import static java.lang.Math.*; * } * * @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 { diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/PolygonType.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/PolygonType.java deleted file mode 100644 index effc9d4..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/PolygonType.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/BugReport.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/BugReport.java index e684fe4..665b7cb 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/BugReport.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/BugReport.java @@ -3,6 +3,7 @@ * 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/Camera.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/Camera.java deleted file mode 100644 index a212821..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/Camera.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 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. - * - *

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.

- * - *

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}).

- * - *

Programmatic camera control:

- *
{@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);
- * }
- * - * @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. - * - *

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.

- * - *

Example:

- *
{@code
-     * Camera camera = viewPanel.getCamera();
-     * camera.getTransform().setTranslation(new Point3D(100, -50, -200));
-     * camera.lookAt(new Point3D(0, 0, 0));  // Point camera at origin
-     * }
- * - * @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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/CullingStatistics.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/CullingStatistics.java deleted file mode 100644 index 3b112c5..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/CullingStatistics.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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. - * - *

Updated each frame during the rendering pipeline:

- *
    - *
  • {@link #totalComposites} - incremented before each composite's frustum test
  • - *
  • {@link #culledComposites} - incremented when a composite fails the frustum test
  • - *
- * - *

Thread safety: counters are {@link AtomicInteger} because the parallel - * transform phase increments them from multiple worker threads.

- * - *

Displayed in the {@link DeveloperToolsPanel} to help developers understand - * culling efficiency and optimize scene graphs.

- * - * @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; - } -} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/DebugLogBuffer.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DebugLogBuffer.java deleted file mode 100644 index 5104b37..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/DebugLogBuffer.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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. - * - *

Captures log messages to a fixed-size circular buffer for display - * in the {@link DeveloperToolsPanel}.

- * - *

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.

- * - * @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 getEntries() { - final List 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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperToolsPanel.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperToolsPanel.java index b8b5a3a..179a4f0 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperToolsPanel.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperToolsPanel.java @@ -3,6 +3,11 @@ * 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/HiZPyramid.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/HiZPyramid.java deleted file mode 100644 index a12608b..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/HiZPyramid.java +++ /dev/null @@ -1,198 +0,0 @@ -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. - * - *

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.

- * - *

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.

- * - *

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.

- */ -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); - } -} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/RenderingContext.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/RenderingContext.java deleted file mode 100644 index 627bc81..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/RenderingContext.java +++ /dev/null @@ -1,707 +0,0 @@ -/* - * 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. - * - *

A new {@code RenderingContext} is created whenever the view panel is resized. - * During rendering, shapes use this context to:

- *
    - *
  • Access the raw pixel array ({@link #pixels}) for direct pixel manipulation
  • - *
  • Access the {@link Graphics2D} context ({@link #graphics}) for Java2D drawing
  • - *
  • Read screen dimensions ({@link #width}, {@link #height}) and the - * {@link #centerCoordinate} for coordinate projection
  • - *
  • Use the {@link #projectionScale} factor for perspective projection
  • - *
- * - *

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.

- * - * @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)}. - * - *

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}).

- */ - 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=}. - */ - 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. - * - *

Equivalent to {@code RenderingContext(width, height, 0, height, numRenderSegments)}.

- * - * @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. - * - *

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}.

- * - * @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. - * - *

Exposed for headless rendering: after a transform/sort/paint pass the - * image holds the finished frame and can be saved or compared directly.

- * - * @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 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; - } - -} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/SegmentRenderingContext.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/SegmentRenderingContext.java deleted file mode 100644 index a7cc17b..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/SegmentRenderingContext.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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. - * - *

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.

- * - *

Mouse tracking is local to each segment and must be combined after all - * segments complete rendering.

- * - * @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 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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/StereoEye.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/StereoEye.java deleted file mode 100644 index aa9c99f..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/StereoEye.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 -} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadActivityRecorder.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadActivityRecorder.java deleted file mode 100644 index eff1e39..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadActivityRecorder.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * 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. - * - *

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).

- * - *

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.

- * - *

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).

- */ -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 rowByThreadName = new ConcurrentHashMap<>(); - private static final CopyOnWriteArrayList 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; - } -} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadTimelineComponent.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadTimelineComponent.java index aafa7ae..ad9bf35 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadTimelineComponent.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadTimelineComponent.java @@ -3,6 +3,7 @@ * 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.*; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java index 96bd4bf..0a69725 100755 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java @@ -3,6 +3,12 @@ * 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; @@ -317,7 +323,7 @@ public class ViewPanel extends Canvas { * 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 @@ -1117,7 +1123,7 @@ public class ViewPanel extends Canvas { // 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewSpaceTracker.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewSpaceTracker.java index 40f602a..24ddb36 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewSpaceTracker.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewSpaceTracker.java @@ -3,10 +3,11 @@ * 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. diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.java index cd7db6c..b0b7d29 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.java @@ -5,7 +5,7 @@ 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardFocusStack.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardFocusStack.java index dc9360f..f89d60e 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardFocusStack.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardFocusStack.java @@ -3,6 +3,7 @@ * 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/WorldNavigationUserInputTracker.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/WorldNavigationUserInputTracker.java index dcdc78b..152e69c 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/WorldNavigationUserInputTracker.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/WorldNavigationUserInputTracker.java @@ -4,7 +4,7 @@ */ 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/package-info.java index 1dc70e4..1bb7041 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/package-info.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/package-info.java @@ -13,12 +13,13 @@ *
    *
  • {@link eu.svjatoslav.aukio.e3d.gui.ViewPanel} - The main rendering surface (JPanel)
  • *
  • {@link eu.svjatoslav.aukio.e3d.gui.ViewFrame} - A JFrame with embedded ViewPanel
  • - *
  • {@link eu.svjatoslav.aukio.e3d.gui.Camera} - Represents the viewer's position and orientation
  • + *
  • {@link eu.svjatoslav.aukio.e3d.geometry.Camera} - Represents the viewer's position and orientation
  • *
  • {@link eu.svjatoslav.aukio.e3d.gui.DeveloperTools} - Debugging and profiling utilities
  • *
* * @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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseController.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseController.java index 89469c6..1951873 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseController.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseController.java @@ -4,7 +4,7 @@ */ 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/headless/SceneDump.java b/src/main/java/eu/svjatoslav/aukio/e3d/headless/SceneDump.java index 3d32901..5c4d781 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/headless/SceneDump.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/headless/SceneDump.java @@ -9,7 +9,7 @@ import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource; 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/headless/Snapshot.java b/src/main/java/eu/svjatoslav/aukio/e3d/headless/Snapshot.java index 010c15e..d91db0a 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/headless/Snapshot.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/headless/Snapshot.java @@ -5,8 +5,8 @@ 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/math/Vertex.java b/src/main/java/eu/svjatoslav/aukio/e3d/math/Vertex.java deleted file mode 100644 index 0eb8bcd..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/math/Vertex.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * 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. - * - *

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.

- * - *

Coordinate spaces:

- *
    - *
  • {@link #coordinate} - Original position in local/model space
  • - *
  • {@link #transformedCoordinate} - Position relative to viewer (camera space)
  • - *
  • {@link #onScreenCoordinate} - 2D screen position after perspective projection
  • - *
- * - *

Example:

- *
{@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
- * }
- * }
- * - * @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. - * - *

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.

- * - * @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. - * - *

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).

- * - * @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. - * - *

Interpolates: position, normal (if present), and texture coordinate (if present).

- * - * @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; - } -} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/IntegerPoint.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/IntegerPoint.java deleted file mode 100644 index 01acee1..0000000 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/IntegerPoint.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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; - } -} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolume.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolume.java index 5ae69d8..d07f08d 100755 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolume.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolume.java @@ -3,6 +3,7 @@ * 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/package-info.java index 4509e7e..f79029e 100755 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/package-info.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/package-info.java @@ -9,7 +9,7 @@ *
    *
  • {@link eu.svjatoslav.aukio.e3d.renderer.octree.OctreeVolume} - the main octree data structure * for storing and querying voxel cells
  • - *
  • {@link eu.svjatoslav.aukio.e3d.renderer.octree.IntegerPoint} - integer 3D coordinate used + *
  • {@link eu.svjatoslav.aukio.e3d.geometry.IntegerPoint} - integer 3D coordinate used * for voxel addressing
  • *
* @@ -17,4 +17,5 @@ */ package eu.svjatoslav.aukio.e3d.renderer.octree; +import eu.svjatoslav.aukio.e3d.geometry.IntegerPoint; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/CameraView.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/CameraView.java index fa36212..5813889 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/CameraView.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/CameraView.java @@ -5,7 +5,7 @@ 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RaytracingCamera.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RaytracingCamera.java index ed67441..d4721d0 100755 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RaytracingCamera.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RaytracingCamera.java @@ -5,7 +5,7 @@ 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/CullingStatistics.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/CullingStatistics.java new file mode 100644 index 0000000..95bfc4e --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/CullingStatistics.java @@ -0,0 +1,64 @@ +/* + * 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. + * + *

Updated each frame during the rendering pipeline:

+ *
    + *
  • {@link #totalComposites} - incremented before each composite's frustum test
  • + *
  • {@link #culledComposites} - incremented when a composite fails the frustum test
  • + *
+ * + *

Thread safety: counters are {@link AtomicInteger} because the parallel + * transform phase increments them from multiple worker threads.

+ * + *

Displayed in the {@link eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel} to help developers understand + * culling efficiency and optimize scene graphs.

+ * + * @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; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Frustum.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Frustum.java new file mode 100644 index 0000000..47f7f39 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Frustum.java @@ -0,0 +1,269 @@ +/* + * 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. + * + *

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.

+ * + *

Frustum planes:

+ *
    + *
  • Left, Right, Top, Bottom - define the viewport edges
  • + *
  • Near - closest visible distance from camera
  • + *
  • Far - farthest visible distance from camera
  • + *
+ * + *

Usage:

+ *
{@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
+ * }
+ * }
+ * + *

AABB intersection algorithm:

+ *

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.

+ * + * @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). + * + *

This method should be called once per frame before rendering, after the + * camera position and orientation have been updated.

+ * + *

View space coordinate system:

+ *
    + *
  • Camera at origin (0, 0, 0)
  • + *
  • Forward = +Z axis (looking into the screen)
  • + *
  • Right = +X axis
  • + *
  • Up = -Y axis (since Y-down means smaller Y is higher visually)
  • + *
+ * + *

Plane normals point INTO the frustum (toward the visible volume). + * A point is inside if dot(normal, point) >= distance for all planes.

+ * + *

FOV calculation: 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.

+ * + * @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. + * + *

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.

+ * + *

Optimized algorithm:

+ *

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.

+ * + * @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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/HiZPyramid.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/HiZPyramid.java new file mode 100644 index 0000000..e04e579 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/HiZPyramid.java @@ -0,0 +1,198 @@ +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. + * + *

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.

+ * + *

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.

+ * + *

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.

+ */ +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); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformCoordinator.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformCoordinator.java index 2a48f08..54f544f 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformCoordinator.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformCoordinator.java @@ -95,7 +95,7 @@ public final class ParallelTransformCoordinator { * 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 @@ -133,15 +133,15 @@ public final class ParallelTransformCoordinator { * @param task transforms a chunk of children into a private aggregator */ public void submit(final Callable 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()); } })); @@ -173,7 +173,7 @@ public final class ParallelTransformCoordinator { * @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 parts = new java.util.ArrayList<>(); Future future; while ((future = futures.poll()) != null) { @@ -181,8 +181,8 @@ public final class ParallelTransformCoordinator { 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) { diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSort.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSort.java index ed0f794..f8701ba 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSort.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSort.java @@ -194,14 +194,14 @@ final class RadixLongSort { 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()); } })); diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderAggregator.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderAggregator.java index 698a51b..1e9ab17 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderAggregator.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderAggregator.java @@ -4,7 +4,6 @@ */ 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; @@ -546,9 +545,9 @@ public class RenderAggregator { 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> futures = parallel ? new java.util.ArrayList<>(chunkCount) : null; for (int c = 0; c < chunkCount; c++) { @@ -563,7 +562,7 @@ public class RenderAggregator { chunk * tileCount, countMode); } finally { if (trace) - eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.record( + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record( traceKind, t0, System.nanoTime()); } }; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.java new file mode 100644 index 0000000..3ba085b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.java @@ -0,0 +1,709 @@ +/* + * 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. + * + *

A new {@code RenderingContext} is created whenever the view panel is resized. + * During rendering, shapes use this context to:

+ *
    + *
  • Access the raw pixel array ({@link #pixels}) for direct pixel manipulation
  • + *
  • Access the {@link Graphics2D} context ({@link #graphics}) for Java2D drawing
  • + *
  • Read screen dimensions ({@link #width}, {@link #height}) and the + * {@link #centerCoordinate} for coordinate projection
  • + *
  • Use the {@link #projectionScale} factor for perspective projection
  • + *
+ * + *

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.

+ * + * @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)}. + * + *

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}).

+ */ + 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=}. + */ + 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. + * + *

Equivalent to {@code RenderingContext(width, height, 0, height, numRenderSegments)}.

+ * + * @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. + * + *

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}.

+ * + * @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. + * + *

Exposed for headless rendering: after a transform/sort/paint pass the + * image holds the finished frame and can be saved or compared directly.

+ * + * @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 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; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentRenderingContext.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentRenderingContext.java new file mode 100644 index 0000000..567bdcb --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentRenderingContext.java @@ -0,0 +1,105 @@ +/* + * 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. + * + *

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.

+ * + *

Mouse tracking is local to each segment and must be combined after all + * segments complete rendering.

+ * + * @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 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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.java index 5856d6b..da63431 100755 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.java @@ -4,12 +4,8 @@ */ 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; @@ -64,7 +60,7 @@ import java.util.concurrent.ExecutorService; *

The {@link #addShape} method is synchronized, making it safe to add shapes from * any thread while the rendering loop is active.

* - * @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 @@ -267,15 +263,9 @@ public class ShapeCollection { * @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 @@ -288,25 +278,8 @@ public class ShapeCollection { } /** - * 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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/StereoEye.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/StereoEye.java new file mode 100644 index 0000000..39ceafb --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/StereoEye.java @@ -0,0 +1,19 @@ +/* + * 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 +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.java new file mode 100644 index 0000000..9ee819d --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.java @@ -0,0 +1,293 @@ +/* + * 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. + * + *

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.

+ * + *

Coordinate spaces:

+ *
    + *
  • {@link #coordinate} - Original position in local/model space
  • + *
  • {@link #transformedCoordinate} - Position relative to viewer (camera space)
  • + *
  • {@link #onScreenCoordinate} - 2D screen position after perspective projection
  • + *
+ * + *

Example:

+ *
{@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
+ * }
+ * }
+ * + * @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. + * + *

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.

+ * + * @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. + * + *

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).

+ * + * @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. + * + *

Interpolates: position, normal (if present), and texture coordinate (if present).

+ * + * @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; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.java index e285b97..0df0d60 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.java @@ -6,7 +6,7 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.gi; 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.java index 94fafb0..5d18d0a 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.java @@ -7,9 +7,9 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes; 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.java index f6e81a3..633a3bd 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.java @@ -6,7 +6,7 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes; 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java index 1924793..77f08f5 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java @@ -6,8 +6,8 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic; 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java index d327c38..7b1e7a4 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java @@ -6,8 +6,8 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line; 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java index 6dd3b19..313b66b 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java @@ -4,13 +4,13 @@ */ 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/MeshTriangle.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/MeshTriangle.java index 9e040eb..391b17d 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/MeshTriangle.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/MeshTriangle.java @@ -1,7 +1,7 @@ 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java index 648baec..76f4b29 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java @@ -5,8 +5,8 @@ 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TriangleMeshBlock.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TriangleMeshBlock.java index 94d8195..5220870 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TriangleMeshBlock.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TriangleMeshBlock.java @@ -3,9 +3,9 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon; 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/TexturedRectangle.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/TexturedRectangle.java index 89e8d79..a6b5db3 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/TexturedRectangle.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/TexturedRectangle.java @@ -7,7 +7,7 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite; 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java index 46724bb..fa0ca38 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java @@ -5,15 +5,14 @@ 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; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/BspTree.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/BspTree.java new file mode 100644 index 0000000..6b92eca --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/BspTree.java @@ -0,0 +1,230 @@ +/* + * 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. + * + *

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.

+ * + *

BSP Tree Structure:

+ *
+ *                 [Node: plane P]
+ *                /               \
+ *        [Front subtree]     [Back subtree]
+ *     (same side as P's     (opposite side
+ *        normal)             of P's normal)
+ * 
+ * + * @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 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 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. + * + *

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.

+ * + *

Algorithm:

+ *
    + *
  1. At each node, split polygons by the partitioning plane
  2. + *
  3. Recursively clip front fragments against the front subtree
  4. + *
  5. Recursively clip back fragments against the back subtree
  6. + *
  7. Combine and return all surviving fragments
  8. + *
+ * + *

Leaf nodes: If this node has no plane (leaf node), all polygons + * are considered outside and returned unchanged.

+ * + * @param polygons the polygons to clip against this BSP tree + * @return a new list containing only the portions outside this solid + */ + public List clipPolygons(final List 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 frontList = new ArrayList<>(); + final List 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 resultFront = frontList; + if (front != null) resultFront = front.clipPolygons(frontList); + + // Recursively clip back fragments against back subtree + List resultBack; + if (back != null) resultBack = back.clipPolygons(backList); + else resultBack = new ArrayList<>(); + + // Combine surviving fragments from both subtrees + final List 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 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 allPolygons() { + final List 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. + * + *

This method is the core BSP tree construction algorithm. It builds or + * extends the tree by choosing a partition plane and classifying each polygon:

+ * + *
    + *
  • Coplanar — polygons on the partition plane are stored in this node
  • + *
  • Front — polygons in the front half-space (same side as plane normal) + * go to the front child subtree
  • + *
  • Back — polygons in the back half-space (opposite to plane normal) + * go to the back child subtree
  • + *
  • Spanning — polygons crossing the plane are split into front and back + * fragments, each going to its respective subtree
  • + *
+ * + *

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.

+ * + *

Can be called multiple times to incrementally extend an existing tree, + * though the original partition planes remain unchanged.

+ * + * @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 polygons) { + if (polygons.isEmpty()) return; + + if (plane == null) plane = polygons.get(0).getPlane().clone(); + + final List frontList = new ArrayList<>(); + final List 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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.java new file mode 100644 index 0000000..d405b42 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.java @@ -0,0 +1,228 @@ +/* + * 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. + * + *

Planes are fundamental to BSP (Binary Space Partitioning) tree operations + * in CSG. They divide 3D space into two half-spaces.

+ * + * @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. + * + *

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.

+ * + *

The normal is computed as the cross product of two edge vectors (b-a and c-a), + * then normalized to unit length.

+ * + * @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. + * + *

Uses {@link #computeNormal} for the normal calculation, then computes + * the signed distance from origin using the dot product.

+ * + * @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 coplanarFront, + final List coplanarBack, + final List front, + final List 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 frontVertices = new ArrayList<>(); + final List 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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/PolygonType.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/PolygonType.java new file mode 100644 index 0000000..506c9db --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/PolygonType.java @@ -0,0 +1,56 @@ +/* + * 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 diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.java index 92d69b9..5792de1 100644 --- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.java +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.java @@ -139,7 +139,7 @@ public class Texture { * Creates a new texture with the specified dimensions and upscale capacity. * *

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.

* * @param width the width of the primary bitmap in pixels diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/headless/HeadlessToolkitTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/headless/HeadlessToolkitTest.java index c648bdd..c75d42c 100644 --- a/src/test/java/eu/svjatoslav/aukio/e3d/headless/HeadlessToolkitTest.java +++ b/src/test/java/eu/svjatoslav/aukio/e3d/headless/HeadlessToolkitTest.java @@ -5,7 +5,7 @@ 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; diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformTest.java index 0d6fea0..6eb00d5 100644 --- a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformTest.java +++ b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformTest.java @@ -5,7 +5,7 @@ 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; @@ -115,7 +115,7 @@ public class ParallelTransformTest { // 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); @@ -128,7 +128,7 @@ public class ParallelTransformTest { 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); @@ -183,7 +183,7 @@ public class ParallelTransformTest { // 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); @@ -194,7 +194,7 @@ public class ParallelTransformTest { 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); diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentBinningTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentBinningTest.java index ef7c9af..a93dec7 100644 --- a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentBinningTest.java +++ b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentBinningTest.java @@ -6,11 +6,11 @@ package eu.svjatoslav.aukio.e3d.renderer.raster; 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; @@ -182,7 +182,7 @@ public class SegmentBinningTest { 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(); } diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangleBlendTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangleBlendTest.java index 881e716..50b9418 100644 --- a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangleBlendTest.java +++ b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangleBlendTest.java @@ -6,8 +6,8 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon; 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; diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTrianglePerspectiveTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTrianglePerspectiveTest.java index 0c04e7f..4fe9ef3 100644 --- a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTrianglePerspectiveTest.java +++ b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTrianglePerspectiveTest.java @@ -6,8 +6,8 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon; 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;