- `ShapeCollection` is the root container with `RenderAggregator` and `TransformStack`
- `RenderAggregator` collects projected shapes, sorts by Z-index, paints back-to-front
- `ViewPanel` (extends `JPanel`) drives render loop, notifies `FrameListener` per frame
-- Backface culling uses screen-space normal Z-component
+- Backface culling uses signed area in screen space: `signedArea < 0` = front-facing
## Color
3. **Mutable geometry:** `Point3D`/`Point2D` are mutable — clone when storing references that shouldn't be shared
4. **Render pipeline:** Shapes must implement `transform()` and `paint()` methods
5. **Depth sorting:** Set `onScreenZ` correctly during `transform()` for proper rendering order
-6. **Backface culling:** Uses screen-space Z-component of normal; positive = front-facing
-7. **Polygon winding:** Counter-clockwise winding for front-facing polygons (when viewed from outside)
+6. **Backface culling:** Uses signed area in screen space; `signedArea < 0` = front-facing (CCW)
+7. **Polygon winding:** CCW in screen space = front face. Vertex order: top → lower-left → lower-right (as seen from camera). See `WindingOrderDemo` in sixth-3d-demos.
8. **Testing:** Write JUnit 4 tests in `src/test/java/` with matching package structure
:ID: a31a1f4d-5368-4fd9-aaf8-fa6d81851187
:END:
-[[file:example.png]]
+[[file:Example.png]]
*Sixth 3D* is a realtime 3D rendering engine written in pure Java. It
runs entirely on the CPU — no GPU required, no OpenGL, no Vulkan, no
:ID: 564fa596-9b2b-418a-9df9-baa46f0d0a66
:END:
-Here is a minimal working example:
+Here is a [[https://www2.svjatoslav.eu/gitweb/?p=sixth-3d-demos.git;a=blob;f=src/main/java/eu/svjatoslav/sixth/e3d/examples/MinimalExample.java;h=af755e8a159c64b3ab8a14c8e76441608ecbf8ee;hb=HEAD][minimal working example]]:
#+BEGIN_SRC java
import eu.svjatoslav.sixth.e3d.geometry.Point3D;
shapes.addShape(box);
// Position your camera
- viewFrame.getViewPanel().getCamera().setLocation(new Point3D(0, -100, -300));
+ viewFrame.getViewPanel().getCamera().getTransform().setTranslation(new Point3D(0, -100, -300));
- // Update the screen
- viewFrame.getViewPanel().repaintDuringNextViewUpdate();
+ // Start the render thread
+ viewFrame.getViewPanel().ensureRenderThreadStarted();
}
}
#+END_SRC
-Compile and run *MyFirstScene* class. New window should open that will
+Compile and run *MyFirstScene* class. A new window should open that will
display 3D scene with red box.
+This example is available in the [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] project. Run it directly:
+
+: java -cp sixth-3d-demos.jar eu.svjatoslav.sixth.e3d.examples.MyFirstScene
+
+You should see this:
+
+[[file:Minimal example.png]]
+
+
*Navigating the scene:*
| Input | Action |
motion. The faster you're moving, the more acceleration builds up,
creating an intuitive flying experience.
-* In-depth understanding
+* Defining scene
+:PROPERTIES:
+:ID: 4b6c1355-0afe-40c6-86c3-14bf8a11a8d0
+:END:
+
+- Note: To understand main render loop, see dedicated page: [[file:rendering-loop.org][Rendering
+ loop]]
+
+- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] for practical examples.
+
** Vertex
#+BEGIN_EXPORT html
- A triangle = 3 vertices, a cube = 8 vertices
- Vertex maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Point3D.html][Point3D]] class in Sixth 3D engine.
-
** Edge
#+BEGIN_EXPORT html
#+BEGIN_EXPORT html
<svg viewBox="0 0 320 240" width="320" height="240">
- <rect width="320" height="240" fill="#f8f8f8"/>
- <polygon points="80,50 30,180 130,180" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
- <path d="M95,80 C 120,80 130,130 110,155" fill="none" stroke="#30a050" stroke-width="1.5" stroke-dasharray="4 2"/>
- <polygon points="110,155 115,143 104,148" fill="#30a050"/>
- <text x="97" y="122" fill="#30a050" font-size="10" font-weight="700" font-family="monospace">CCW</text>
+ <defs>
+ <marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5"
+ markerWidth="8" markerHeight="8" orient="auto-start-reverse">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/>
+ </marker>
+ <marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5"
+ markerWidth="8" markerHeight="8" orient="auto-start-reverse">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="rgba(208,64,64,0.5)"/>
+ </marker>
+ </defs>
+ <rect width="320" height="240" fill="#061018"/>
+ <!-- Green front-face triangle: V1=top, V2=bottom-left, V3=bottom-right -->
+ <polygon points="80,50 130,180 30,180" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+ <!-- CCW arrow: arc from near V1, curves LEFT and DOWN toward V2 -->
+ <path d="M70,72 A 52,52 0 0,0 37,155" fill="none" stroke="#30a050" stroke-width="1.5" stroke-dasharray="4 2" marker-end="url(#arrow-green)"/>
+ <text x="34" y="120" fill="#30a050" font-size="10" font-weight="700" font-family="monospace">CCW</text>
<circle cx="80" cy="50" r="3" fill="#30a050"/>
<circle cx="30" cy="180" r="3" fill="#30a050"/>
<circle cx="130" cy="180" r="3" fill="#30a050"/>
- <text x="58" y="44" fill="#666" font-size="9" font-family="monospace">V₁</text>
- <text x="14" y="198" fill="#666" font-size="9" font-family="monospace">V₂</text>
- <text x="132" y="198" fill="#666" font-size="9" font-family="monospace">V₃</text>
+ <text x="78" y="44" fill="#aaa" font-size="9" font-family="monospace">V₁</text>
+ <text x="14" y="198" fill="#aaa" font-size="9" font-family="monospace">V₂</text>
+ <text x="132" y="198" fill="#aaa" font-size="9" font-family="monospace">V₃</text>
<text x="36" y="220" fill="#30a050" font-size="11" font-weight="700" font-family="monospace">FRONT FACE ✓</text>
+ <!-- Red back-face triangle -->
<polygon points="240,50 290,180 190,180" fill="rgba(208,64,64,0.06)" stroke="rgba(208,64,64,0.3)" stroke-width="1.5" stroke-dasharray="6 3"/>
- <path d="M225,80 C 200,80 190,130 210,155" fill="none" stroke="rgba(208,64,64,0.5)" stroke-width="1.5" stroke-dasharray="4 2"/>
- <polygon points="210,155 205,143 216,148" fill="rgba(208,64,64,0.5)"/>
- <text x="210" y="122" fill="rgba(208,64,64,0.6)" font-size="10" font-weight="700" font-family="monospace">CW</text>
+ <!-- CW arrow: arc from near V1, curves RIGHT and DOWN -->
+ <path d="M250,72 A 52,52 0 0,1 283,155" fill="none" stroke="rgba(208,64,64,0.5)" stroke-width="1.5" stroke-dasharray="4 2" marker-end="url(#arrow-red)"/>
+ <text x="268" y="120" fill="rgba(208,64,64,0.6)" font-size="10" font-weight="700" font-family="monospace">CW</text>
<line x1="228" y1="108" x2="252" y2="132" stroke="rgba(208,64,64,0.4)" stroke-width="3"/>
<line x1="252" y1="108" x2="228" y2="132" stroke="rgba(208,64,64,0.4)" stroke-width="3"/>
<text x="186" y="220" fill="rgba(208,64,64,0.7)" font-size="11" font-weight="700" font-family="monospace">BACK FACE ✗</text>
- <text x="195" y="234" fill="#999" font-size="9" font-family="monospace">(culled — not drawn)</text>
+ <text x="195" y="234" fill="#aaa" font-size="9" font-family="monospace">(culled — not drawn)</text>
</svg>
#+END_EXPORT
-The order in which a triangle's vertices are listed determines its *winding order*. Counter-clockwise (CCW) typically means front-facing. *Backface culling* skips rendering triangles that face away from the camera — a major performance optimization.
-
-- CCW winding → front face (visible)
-- CW winding → back face (culled)
+The order in which a triangle's vertices are listed determines its
+*winding order*. In Sixth 3D, screen coordinates have Y-axis pointing
+*down*, which inverts the apparent winding direction compared to
+standard mathematical convention (Y-up). *Counter-clockwise (CCW)* in
+screen space means front-facing. *Backface culling* skips rendering
+triangles that face away from the camera — a major performance
+optimization.
+
+- CCW winding (in screen space) → front face (visible)
+- CW winding (in screen space) → back face (culled)
+- When viewing a polygon from outside: define vertices in *counter-clockwise* order as seen from the camera
- Saves ~50% of triangle rendering
-- Normal direction derived from winding order via =cross(V₂-V₁, V₃-V₁)=
+- Implementation uses signed area: =signedArea < 0= means front-facing
+ (in Y-down screen coordinates, negative signed area corresponds to
+ visually CCW winding)
+
+
+*Minimal Example: WindingOrderDemo*
+
+The [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][sixth-3d-demos]] project includes a [[https://www2.svjatoslav.eu/gitweb/?p=sixth-3d-demos.git;a=blob;f=src/main/java/eu/svjatoslav/sixth/e3d/examples/WindingOrderDemo.java][winding order demo]] to
+demonstrate how winding order affects backface culling:
+
+#+BEGIN_SRC java
+// WindingOrderDemo.java - validates CCW winding = front face
+public class WindingOrderDemo {
+ public static void main(String[] args) {
+ ViewFrame viewFrame = new ViewFrame();
+ ShapeCollection shapes = viewFrame.getViewPanel().getRootShapeCollection();
+
+ double size = 150;
+
+ // CCW winding: top → lower-left → lower-right
+ Point3D upperCenter = new Point3D(0, -size, 0);
+ Point3D lowerLeft = new Point3D(-size, +size, 0);
+ Point3D lowerRight = new Point3D(+size, +size, 0);
+
+ SolidPolygon triangle = new SolidPolygon(upperCenter, lowerLeft, lowerRight, Color.GREEN);
+ triangle.setBackfaceCulling(true);
+
+ shapes.addShape(triangle);
+
+ viewFrame.getViewPanel().getCamera().getTransform().setTranslation(new Point3D(0, 0, -500));
+ viewFrame.getViewPanel().ensureRenderThreadStarted();
+ }
+}
+#+END_SRC
+
+Run this demo: if the green triangle is visible, the winding order is
+correct (CCW = front face)
+
+[[file:Winding order demo.png]]
In Sixth 3D, backface culling is *optional* and disabled by default. Enable it per-shape:
- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#setBackfaceCulling(boolean)][SolidPolygon.setBackfaceCulling(true)]]
Color hex = new Color("FF8040CC"); // same orange with alpha
#+END_SRC
+* Developer tools
+:PROPERTIES:
+:CUSTOM_ID: developer-tools
+:ID: 8c5e2a1f-9d3b-4f6a-b8e7-1c4d5f7a9b2e
+:END:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[file:Developer tools.png]]
+
+Press *F12* anywhere in the application to open the Developer Tools panel.
+This debugging interface helps you understand what the engine is doing
+internally and diagnose rendering issues.
+
+The Developer Tools panel provides real-time insight into the rendering
+pipeline with two diagnostic toggles and a live log viewer that's always
+recording.
+
+** Render frame logging (always on)
+
+Render frame diagnostics are always logged to a circular buffer. When you
+open the Developer Tools panel, you can see the complete rendering history.
+Each frame goes through 6 phases:
+
+| Phase | Description |
+|-------+------------------------------------------------|
+| 1 | Canvas cleared with background color |
+| 2 | Shapes transformed (3D → screen coordinates) |
+| 3 | Shapes sorted by depth (back-to-front) |
+| 4 | Segments painted (parallel rendering) |
+| 5 | Mouse hit results combined |
+| 6 | Blit to screen (buffer strategy show) |
+
+Log entries include:
+- Frame number and timestamp
+- Shape count and queued shapes
+- Buffer strategy status (contents lost/restored)
+- Blit retry count for page-flip diagnostics
+
+Example log output:
+#+BEGIN_EXAMPLE
+22:44:55.202 [VIEWPANEL] renderFrame #1105 START, shapes=26
+22:44:55.202 [VIEWPANEL] Phase 1 done: canvas cleared
+22:44:55.202 [VIEWPANEL] Phase 2 done: shapes transformed, queued=26
+22:44:55.202 [VIEWPANEL] Phase 3 done: shapes sorted
+22:44:55.210 [VIEWPANEL] Phase 4 done: segments painted
+22:44:55.210 [VIEWPANEL] Phase 5 done: mouse results combined
+22:44:55.211 [VIEWPANEL] Phase 6 done: blit complete (x1)
+#+END_EXAMPLE
+
+
+Use this for:
+- Performance profiling (identify slow phases)
+- Understanding rendering order
+- Diagnosing buffer strategy issues (screen tearing, blank frames)
+- Verifying shapes are actually being rendered
+
+** Show polygon borders
+
+Draws yellow outlines around all textured polygons to visualize:
+- Triangle tessellation patterns
+- Perspective-correct texture slicing
+- Polygon coverage and overlap
+
+This is particularly useful when debugging:
+- Texture mapping issues
+- Perspective distortion problems
+- Mesh density and triangulation quality
+- Z-fighting between overlapping polygons
+
+The yellow borders are rendered on top of the final image, making it
+easy to see the underlying geometric structure of textured surfaces.
+
+** Render alternate segments (overdraw debug)
+
+Renders only even-numbered horizontal segments (0, 2, 4, 6) while
+leaving odd segments (1, 3, 5, 7) black.
+
+The engine divides the screen into 8 horizontal segments for parallel
+multi-threaded rendering. This toggle helps detect overdraw (threads writing outside their allocated segment).
+
+If you see rendering artifacts in the black segments, it indicates
+that threads are writing pixels outside their assigned area — a clear
+sign of a bug.
+
+** Live log viewer
+
+The scrollable text area shows captured debug output in real-time:
+- Green text on black background for readability
+- Auto-scrolls to show latest entries
+- Updates every 500ms while panel is open
+- Captures logs even when panel is closed (replays when reopened)
+
+Use the *Clear Logs* button to reset the log buffer for fresh
+diagnostic captures.
+
+** API access
+
+You can access and control developer tools programmatically:
+
+#+BEGIN_SRC java
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.gui.DeveloperTools;
+
+ViewPanel viewPanel = ...; // get your view panel
+DeveloperTools tools = viewPanel.getDeveloperTools();
+
+// Enable diagnostics programmatically
+tools.showPolygonBorders = true;
+tools.renderAlternateSegments = false;
+#+END_SRC
+
+This allows you to:
+- Enable debugging based on command-line flags
+- Toggle features during automated testing
+- Create custom debug overlays or controls
+- Integrate with external logging frameworks
+
+** Technical details
+
+The Developer Tools panel is implemented as a non-modal =JDialog= that:
+- Centers on the parent =ViewFrame= window
+- Runs on the Event Dispatch Thread (EDT)
+- Does not block the render loop
+- Automatically closes when parent window closes
+
+Log entries are stored in a circular buffer (=DebugLogBuffer=) with
+configurable capacity (default: 10,000 entries). When full, oldest
+entries are discarded.
+
+Each =ViewPanel= has its own independent =DeveloperTools= instance,
+so multiple views can have different debug configurations simultaneously.
+
* Source code
:PROPERTIES:
:CUSTOM_ID: source-code
** Understanding the Sixth 3D source code
+- Study how [[id:4b6c1355-0afe-40c6-86c3-14bf8a11a8d0][scene definition]] works.
+- Understand [[file:rendering-loop.org][main rendering loop]].
- Read online [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/][JavaDoc]].
- See [[https://www3.svjatoslav.eu/projects/sixth-3d/graphs/][Sixth 3D class diagrams]]. (Diagrams were generated by using
[[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility)
+ Partial region/frame repaint: when only one small object changed on
the scene, it would be faster to re-render that specific area.
- + Once partial rendering works, in would be easy to add multi-core
- rendering support. So that each core renders it's own region of
+ + Once partial rendering works, it would be easy to add multi-core
+ rendering support. So that each core renders its own region of
the screen.
-+ Anti-aliasing. Would improve text readability. If antialiazing is
++ Anti-aliasing. Would improve text readability. If antialiasing is
too expensive for every frame, it could be used only for last frame
before animations become still and waiting for user input starts.
+ Dynamically detect and replace invisible objects from the
scene with simplified bounding box.
- + Dynamically replace boudnig box with actual object once it
+ + Dynamically replace bounding box with actual object once it
becomes visible.
+ Dynamically unload unused textures from RAM.
--- /dev/null
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Rendering Loop - Sixth 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+begin_export html
+<style>
+ .flex-center {
+ display: flex;
+ justify-content: center;
+ }
+ .flex-center video {
+ width: min(90%, 1000px);
+ height: auto;
+ }
+ .responsive-img {
+ width: min(100%, 1000px);
+ height: auto;
+ }
+</style>
+#+end_export
+
+[[file:index.org][Back to main documentation]]
+
+* Rendering loop
+:PROPERTIES:
+:CUSTOM_ID: rendering-loop
+:ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
+:END:
+
+The rendering loop is the heart of the engine, continuously generating
+frames on a dedicated background thread. It orchestrates the entire
+rendering pipeline from 3D world space to pixels on screen.
+
+** Main loop structure
+
+The render thread runs continuously in a dedicated daemon thread:
+
+#+BEGIN_SRC java
+while (renderThreadRunning) {
+ ensureThatViewIsUpToDate(); // Render one frame
+ maintainTargetFps(); // Sleep if needed
+}
+#+END_SRC
+
+The thread is a daemon, so it automatically stops when the JVM exits.
+You can stop it explicitly with [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html#stop()][ViewPanel.stop()]].
+
+** Frame rate control
+
+The engine supports two modes:
+
+- *Target FPS mode*: Set with [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html#setFrameRate(int)][setFrameRate(int)]]. The thread sleeps
+ between frames to maintain the target rate. If rendering takes
+ longer than the frame interval, the engine catches up naturally
+ without sleeping.
+
+- *Unlimited mode*: Set =setFrameRate(0)= or negative. No sleeping —
+ renders as fast as possible. Useful for benchmarking.
+
+** Frame listeners
+
+Before each frame, the engine notifies all registered [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/FrameListener.html][FrameListener]]s:
+
+#+BEGIN_SRC java
+viewPanel.addFrameListener((panel, deltaMs) -> {
+ // Update animations, physics, game logic
+ shape.rotate(0.01);
+ return true; // true = force repaint
+});
+#+END_SRC
+
+Frame listeners can trigger repaints by returning =true=. Built-in listeners include:
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/Camera.html][Camera]] — handles keyboard/mouse navigation
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.html][InputManager]] — processes input events
+
+* Rendering phases
+
+Each frame goes through 6 phases. Open the Developer Tools panel (F12)
+to see these phases logged in real-time:
+
+** Phase 1: Clear canvas
+
+The pixel buffer is filled with the background color (default: black).
+
+#+BEGIN_SRC java
+Arrays.fill(pixels, 0, width * height, backgroundColorRgb);
+#+END_SRC
+
+This is a simple =Arrays.fill= operation — very fast, single-threaded.
+
+** Phase 2: Transform shapes
+
+All shapes are transformed from world space to screen space:
+
+1. Build camera-relative transform (inverse of camera position/rotation)
+2. For each shape:
+ - Apply camera transform
+ - Project 3D → 2D (perspective projection)
+ - Calculate =onScreenZ= for depth sorting
+ - Queue for rendering
+
+This is single-threaded but very fast — just math, no pixel operations.
+
+** Phase 3: Sort shapes
+
+Shapes are sorted by =onScreenZ= (depth) in descending order:
+
+#+BEGIN_SRC java
+Collections.sort(queuedShapes, (a, b) -> Double.compare(b.onScreenZ, a.onScreenZ));
+#+END_SRC
+
+Back-to-front sorting is essential for correct transparency and
+occlusion. Shapes further from the camera are painted first.
+
+** Phase 4: Paint shapes (multi-threaded)
+
+The screen is divided into 8 horizontal segments, each rendered by a separate thread:
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 400 200" width="400" height="200">
+ <rect width="400" height="200" fill="#061018"/>
+ <rect x="10" y="5" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="30" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="55" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="80" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="105" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="130" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="155" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="180" width="380" height="15" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <text x="20" y="21" fill="#30a050" font-size="11" font-family="monospace">Segment 0 (Thread 0)</text>
+ <text x="20" y="46" fill="#30a050" font-size="11" font-family="monospace">Segment 1 (Thread 1)</text>
+ <text x="20" y="71" fill="#30a050" font-size="11" font-family="monospace">Segment 2 (Thread 2)</text>
+ <text x="20" y="96" fill="#30a050" font-size="11" font-family="monospace">Segment 3 (Thread 3)</text>
+ <text x="20" y="121" fill="#30a050" font-size="11" font-family="monospace">Segment 4 (Thread 4)</text>
+ <text x="20" y="146" fill="#30a050" font-size="11" font-family="monospace">Segment 5 (Thread 5)</text>
+ <text x="20" y="171" fill="#30a050" font-size="11" font-family="monospace">Segment 6 (Thread 6)</text>
+ <text x="20" y="193" fill="#30a050" font-size="11" font-family="monospace">Segment 7 (Thread 7)</text>
+</svg>
+#+END_EXPORT
+
+Each thread:
+- Gets a =SegmentRenderingContext= with Y-bounds (minY, maxY)
+- Iterates all shapes and paints pixels within its Y-range
+- Clips triangles/lines at segment boundaries
+- Detects mouse hits (before clipping)
+
+A =CountDownLatch= waits for all 8 threads to complete before proceeding.
+
+**Why 8 segments?** This matches the typical core count of modern CPUs.
+The fixed thread pool (=Executors.newFixedThreadPool(8)=) avoids the
+overhead of creating threads per frame.
+
+** Phase 5: Combine mouse results
+
+During painting, each segment tracks which shape is under the mouse cursor.
+Since all segments paint the same shapes (just different Y-ranges), they
+should all report the same hit. Phase 5 takes the first non-null result:
+
+#+BEGIN_SRC java
+for (SegmentRenderingContext ctx : segmentContexts) {
+ if (ctx.getSegmentMouseHit() != null) {
+ renderingContext.setCurrentObjectUnderMouseCursor(ctx.getSegmentMouseHit());
+ break;
+ }
+}
+#+END_SRC
+
+** Phase 6: Blit to screen
+
+The rendered =BufferedImage= is copied to the screen using
+[[https://docs.oracle.com/javase/21/docs/api/java/awt/image/BufferStrategy.html][BufferStrategy]] for tear-free page-flipping:
+
+#+BEGIN_SRC java
+do {
+ Graphics2D g = bufferStrategy.getDrawGraphics();
+ g.drawImage(renderingContext.bufferedImage, 0, 0, null);
+ g.dispose();
+} while (bufferStrategy.contentsRestored());
+
+bufferStrategy.show();
+Toolkit.getDefaultToolkit().sync();
+#+END_SRC
+
+The =do-while= loop handles the case where the OS recreates the back
+buffer (common during window resizing). Since our offscreen
+=BufferedImage= still has the correct pixels, we only need to re-blit,
+not re-render.
+
+* Smart repaint skipping
+
+The engine avoids unnecessary rendering:
+
+- =viewRepaintNeeded= flag: Set to =true= only when something changes
+- Frame listeners can return =false= to skip repaint
+- Resizing, component events, and explicit =repaintDuringNextViewUpdate()=
+ calls set the flag
+
+This means a static scene consumes almost zero CPU — the render thread
+just spins checking the flag.
+
+* Rendering context
+
+The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/RenderingContext.html][RenderingContext]] holds all state for a single frame:
+
+| Field | Purpose |
+|-------+---------|
+| =pixels[]= | Raw pixel buffer (int[] in RGB format) |
+| =bufferedImage= | Java2D wrapper around pixels |
+| =graphics= | Graphics2D for text, lines, shapes |
+| =width=, =height= | Screen dimensions |
+| =centerCoordinate= | Screen center (for projection) |
+| =projectionScale= | Perspective scale factor |
+| =frameNumber= | Monotonically increasing frame counter |
+
+A new context is created when the window is resized. Otherwise, the
+same context is reused — =prepareForNewFrameRendering()= just resets
+per-frame state like mouse tracking.
import static java.lang.Math.abs;
/**
- * Same as: 3D rectangle, rectangular box, rectangular parallelopiped, cuboid,
- * rhumboid, hexahedron, rectangular prism.
+ * A 3D axis-aligned bounding box defined by two corner points.
+ *
+ * <p>Also known as: 3D rectangle, rectangular box, rectangular parallelepiped,
+ * cuboid, rhomboid, hexahedron, or rectangular prism.</p>
+ *
+ * <p>The box is defined by two points ({@link #p1} and {@link #p2}) that represent
+ * opposite corners. The box does not enforce ordering of these points.</p>
+ *
+ * <p><b>Example usage:</b></p>
+ * <pre>{@code
+ * Box box = new Box(new Point3D(0, 0, 0), new Point3D(100, 50, 200));
+ * double volume = box.getWidth() * box.getHeight() * box.getDepth();
+ * box.enlarge(10); // expand by 10 units in all directions
+ * }</pre>
+ *
+ * @see Point3D
*/
public class Box implements Cloneable {
/**
- * The first point of the box.
+ * The first corner point of the box.
*/
public final Point3D p1;
/**
- * The second point of the box.
+ * The second corner point of the box (opposite corner from p1).
*/
public final Point3D p2;
/**
- * Creates a new box with two points at the origin.
+ * Creates a new box with both corner points at the origin.
*/
public Box() {
p1 = new Point3D();
}
/**
- * Creates a new box with two points at the specified coordinates.
+ * Creates a new box with the specified corner points.
+ *
+ * @param p1 the first corner point
+ * @param p2 the second corner point (opposite corner)
*/
public Box(final Point3D p1, final Point3D p2) {
this.p1 = p1;
return this;
}
+ /**
+ * Creates a copy of this box with cloned corner points.
+ *
+ * @return a new box with the same corner coordinates
+ */
@Override
public Box clone() {
return new Box(p1.clone(), p2.clone());
}
/**
- * @return The depth of the box. The depth is the distance between the two points on the z-axis.
+ * Returns the depth of the box (distance along the Z-axis).
+ *
+ * @return the depth (always positive)
*/
public double getDepth() {
return abs(p1.z - p2.z);
}
/**
- * @return The height of the box. The height is the distance between the two points on the y-axis.
+ * Returns the height of the box (distance along the Y-axis).
+ *
+ * @return the height (always positive)
*/
public double getHeight() {
return abs(p1.y - p2.y);
}
/**
- * @return The width of the box. The width is the distance between the two points on the x-axis.
+ * Returns the width of the box (distance along the X-axis).
+ *
+ * @return the width (always positive)
*/
public double getWidth() {
return abs(p1.x - p2.x);
package eu.svjatoslav.sixth.e3d.geometry;
/**
- * Circle in 2D space.
+ * A circle in 2D space defined by a center point and radius.
+ *
+ * @see Point2D
*/
public class Circle {
/**
- * The center of the circle.
+ * The center point of the circle.
*/
Point2D location;
*/
double radius;
+ /**
+ * Creates a circle with default values.
+ */
+ public Circle() {
+ }
+
}
import static java.lang.Math.sqrt;
/**
- * Used to represent point in a 2D space or vector.
+ * A mutable 2D point or vector with double-precision coordinates.
*
- * @see Point3D
+ * <p>{@code Point2D} represents either a position in 2D space or a directional vector,
+ * with public {@code x} and {@code y} fields for direct access. It is commonly used
+ * for screen-space coordinates after 3D-to-2D projection.</p>
+ *
+ * <p>All mutation methods return {@code this} for fluent chaining:</p>
+ * <pre>{@code
+ * Point2D p = new Point2D(10, 20)
+ * .add(new Point2D(5, 5))
+ * .invert();
+ * // p is now (-15, -25)
+ * }</pre>
+ *
+ * @see Point3D the 3D equivalent
*/
public class Point2D implements Cloneable {
- public double x, y;
+ /** X coordinate (horizontal axis). */
+ public double x;
+ /** Y coordinate (vertical axis, positive = down in screen space). */
+ public double y;
+ /**
+ * Creates a point at the origin (0, 0).
+ */
public Point2D() {
}
+ /**
+ * Creates a point with the specified coordinates.
+ *
+ * @param x the X coordinate
+ * @param y the Y coordinate
+ */
public Point2D(final double x, final double y) {
this.x = x;
this.y = y;
}
+ /**
+ * Creates a point by copying coordinates from another point.
+ *
+ * @param parent the point to copy from
+ */
public Point2D(final Point2D parent) {
x = parent.x;
y = parent.y;
/**
- * Add other point to current point. Value of other point will not be changed.
+ * Adds another point to this point. The other point is not modified.
*
- * @return current point.
+ * @param otherPoint the point to add
+ * @return this point (for chaining)
*/
public Point2D add(final Point2D otherPoint) {
x += otherPoint.x;
}
/**
- * @return true if current point coordinates are equal to zero.
+ * Checks if both coordinates are zero.
+ *
+ * @return {@code true} if current point coordinates are equal to zero
*/
public boolean isZero() {
return (x == 0) && (y == 0);
}
+ /**
+ * Creates a new point by copying this point's coordinates.
+ *
+ * @return a new point with the same coordinates
+ */
@Override
public Point2D clone() {
return new Point2D(this);
}
/**
- * Copy coordinates from other point to current point. Value of other point will not be changed.
+ * Copies coordinates from another point into this point.
+ *
+ * @param otherPoint the point to copy coordinates from
*/
public void clone(final Point2D otherPoint) {
x = otherPoint.x;
}
/**
- * Set current point to middle of two other points.
+ * Sets this point to the midpoint between two other points.
*
- * @param p1 first point.
- * @param p2 second point.
- * @return current point.
+ * @param p1 the first point
+ * @param p2 the second point
+ * @return this point (for chaining)
*/
public Point2D setToMiddle(final Point2D p1, final Point2D p2) {
x = (p1.x + p2.x) / 2d;
return this;
}
+ /**
+ * Computes the angle on the X-Y plane between this point and another point.
+ *
+ * @param anotherPoint the other point
+ * @return the angle in radians
+ */
public double getAngleXY(final Point2D anotherPoint) {
return Math.atan2(x - anotherPoint.x, y - anotherPoint.y);
}
/**
- * Compute distance to another point.
+ * Computes the Euclidean distance from this point to another point.
*
- * @param anotherPoint point to compute distance to.
- * @return distance from current point to another point.
+ * @param anotherPoint the point to compute distance to
+ * @return the distance between the two points
*/
public double getDistanceTo(final Point2D anotherPoint) {
final double xDiff = x - anotherPoint.x;
}
/**
- * Calculate length of vector.
+ * Computes the length of this vector (magnitude).
*
- * @return length of vector.
+ * @return the vector length
*/
public double getVectorLength() {
return sqrt(((x * x) + (y * y)));
}
/**
- * Invert current point.
+ * Inverts this point's coordinates (negates both x and y).
*
- * @return current point.
+ * @return this point (for chaining)
*/
public Point2D invert() {
x = -x;
}
/**
- * Round current point coordinates to integer.
+ * Rounds this point's coordinates to integer values.
*/
public void roundToInteger() {
x = (int) x;
}
/**
- * Subtract other point from current point. Value of other point will not be changed.
+ * Subtracts another point from this point. The other point is not modified.
*
- * @return current point.
+ * @param otherPoint the point to subtract
+ * @return this point (for chaining)
*/
public Point2D subtract(final Point2D otherPoint) {
x -= otherPoint.x;
}
/**
- * Convert current point to 3D point.
- * Value of the z coordinate will be set to zero.
+ * Converts this 2D point to a 3D point with z = 0.
*
- * @return 3D point.
+ * @return a new 3D point with the same x, y and z = 0
*/
public Point3D to3D() {
return new Point3D(x, y, 0);
}
/**
- * Set current point to zero.
+ * Resets this point's coordinates to (0, 0).
*
- * @return current point.
+ * @return this point (for chaining)
*/
public Point2D zero() {
x = 0;
/**
- * Creates new current point by cloning coordinates from parent point.
+ * Creates a new point by cloning coordinates from the parent point.
+ *
+ * @param parent the point to copy coordinates from
*/
public Point3D(final Point3D parent) {
x = parent.x;
}
/**
- * Add coordinates of current point to other point. Value of current point will not be changed.
+ * Adds coordinates of current point to one or more other points.
+ * The current point's coordinates are added to each target point.
*
- * @return current point.
+ * @param otherPoints the points to add this point's coordinates to
+ * @return this point (for chaining)
*/
public Point3D addTo(final Point3D... otherPoints) {
for (final Point3D otherPoint : otherPoints) otherPoint.add(this);
}
/**
- * Copy coordinates from other point to current point. Value of other point will not be changed.
+ * Copies coordinates from another point into this point.
+ *
+ * @param otherPoint the point to copy coordinates from
+ * @return this point (for chaining)
*/
public Point3D clone(final Point3D otherPoint) {
x = otherPoint.x;
}
/**
- * @return true if current point coordinates are equal to zero.
+ * Checks if all coordinates are zero.
+ *
+ * @return {@code true} if current point coordinates are equal to zero
*/
public boolean isZero() {
return (x == 0) && (y == 0) && (z == 0);
}
/**
- * @return length of current vector.
+ * Computes the length (magnitude) of this vector.
+ *
+ * @return the vector length
*/
public double getVectorLength() {
return sqrt(((x * x) + (y * y) + (z * z)));
}
/**
- * Rotate current point around center point by angleXZ and angleYZ.
+ * Rotates this point around a center point by the given XZ and YZ angles.
* <p>
* See also: <a href="https://marctenbosch.com/quaternions/">Let's remove Quaternions from every 3D Engine</a>
*
- * @param center center point.
- * @param angleXZ angle around XZ axis.
- * @param angleYZ angle around YZ axis.
+ * @param center the center point to rotate around
+ * @param angleXZ the angle in the XZ plane (yaw) in radians
+ * @param angleYZ the angle in the YZ plane (pitch) in radians
+ * @return this point (for chaining)
*/
public Point3D rotate(final Point3D center, final double angleXZ,
final double angleYZ) {
}
/**
- * Subtract other point from current point. Value of other point will not be changed.
+ * Subtracts another point from this point.
*
- * @return current point.
+ * @param otherPoint the point to subtract
+ * @return this point (for chaining)
*/
public Point3D subtract(final Point3D otherPoint) {
x -= otherPoint.x;
}
/**
- * Translate current point along X axis by given increment.
+ * Translates this point along the X axis.
*
- * @return current point.
+ * @param xIncrement the amount to add to the X coordinate
+ * @return this point (for chaining)
*/
public Point3D translateX(final double xIncrement) {
x += xIncrement;
}
/**
- * Translate current point along Y axis by given increment.
+ * Translates this point along the Y axis.
*
- * @return current point.
+ * @param yIncrement the amount to add to the Y coordinate
+ * @return this point (for chaining)
*/
public Point3D translateY(final double yIncrement) {
y += yIncrement;
}
/**
- * Translate current point along Z axis by given increment.
+ * Translates this point along the Z axis.
*
- * @return current point.
+ * @param zIncrement the amount to add to the Z coordinate
+ * @return this point (for chaining)
*/
public Point3D translateZ(final double zIncrement) {
z += zIncrement;
package eu.svjatoslav.sixth.e3d.geometry;
/**
- * Utility class for polygon operations.
+ * Utility class for polygon operations, primarily point-in-polygon testing.
+ *
+ * <p>Provides static methods for geometric computations on triangles and other polygons.</p>
+ *
+ * @see Point2D
*/
public class Polygon {
+ /**
+ * Creates a new Polygon utility instance.
+ */
+ public Polygon() {
+ }
+
/**
- * Checks if point is on the right side of the line.
- * @param point point to check
- * @param lineP1 line start point
- * @param lineP2 line end point
- * @return true if point is on the right side of the line
+ * Checks if a point is on the right side of a directed line segment.
+ * Used internally for ray-casting in point-in-polygon tests.
+ *
+ * @param point the point to test
+ * @param lineP1 the start point of the line segment
+ * @param lineP2 the end point of the line segment
+ * @return {@code true} if the point is on the right side of the line
*/
private static boolean intersectsLine(final Point2D point, Point2D lineP1,
Point2D lineP2) {
return point.x >= crossX;
}
+ /**
+ * Tests whether a point lies inside a triangle using the ray-casting algorithm.
+ *
+ * <p>This method casts a horizontal ray from the test point and counts intersections
+ * with the triangle edges. If the number of intersections is odd, the point is inside.</p>
+ *
+ * @param point the point to test
+ * @param p1 the first vertex of the triangle
+ * @param p2 the second vertex of the triangle
+ * @param p3 the third vertex of the triangle
+ * @return {@code true} if the point is inside the triangle
+ */
public static boolean pointWithinPolygon(final Point2D point,
final Point2D p1, final Point2D p2, final Point2D p3) {
import static java.lang.Math.min;
/**
- * Rectangle class.
+ * A 2D axis-aligned rectangle defined by two corner points.
+ *
+ * <p>The rectangle is defined by two points ({@link #p1} and {@link #p2}) that represent
+ * opposite corners. The rectangle does not enforce ordering of these points.</p>
+ *
+ * @see Point2D
+ * @see Box the 3D equivalent
*/
public class Rectangle {
/**
- * Rectangle points.
+ * The corner points of the rectangle (opposite corners).
*/
public Point2D p1, p2;
/**
- * Creates new rectangle with given size.
- * The rectangle will be centered at the origin.
- * The rectangle will be square.
+ * Creates a square rectangle centered at the origin with the specified size.
*
- * @param size The size of the rectangle.
+ * @param size the width and height of the square
*/
public Rectangle(final double size) {
p2 = new Point2D(size / 2, size / 2);
}
/**
- * @param p1 The first point of the rectangle.
- * @param p2 The second point of the rectangle.
+ * Creates a rectangle with the specified corner points.
+ *
+ * @param p1 the first corner point
+ * @param p2 the second corner point (opposite corner)
*/
public Rectangle(final Point2D p1, final Point2D p2) {
this.p1 = p1;
this.p2 = p2;
}
+ /**
+ * Returns the height of the rectangle (distance along the Y-axis).
+ *
+ * @return the height (always positive)
+ */
public double getHeight() {
return abs(p1.y - p2.y);
}
/**
- * @return The leftmost x coordinate of the rectangle.
+ * Returns the leftmost X coordinate of the rectangle.
+ *
+ * @return the minimum X value
*/
public double getLowerX() {
return min(p1.x, p2.x);
}
+ /**
+ * Returns the topmost Y coordinate of the rectangle.
+ *
+ * @return the minimum Y value
+ */
public double getLowerY() {
return min(p1.y, p2.y);
}
/**
- * @return rectangle width.
+ * Returns the width of the rectangle (distance along the X-axis).
+ *
+ * @return the width (always positive)
*/
public double getWidth() {
return abs(p1.x - p2.x);
-package eu.svjatoslav.sixth.e3d.geometry;
-
/**
- * Goal is to provide basic geometry classes.
- */
\ No newline at end of file
+ * Provides basic geometry classes for 2D and 3D coordinates and shapes.
+ *
+ * @see eu.svjatoslav.sixth.e3d.geometry.Point2D
+ * @see eu.svjatoslav.sixth.e3d.geometry.Point3D
+ */
+package eu.svjatoslav.sixth.e3d.geometry;
\ No newline at end of file
*/
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.
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;
}
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.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 with optional stdout passthrough.
+ *
+ * <p>Always captures log messages to a fixed-size circular buffer.
+ * When {@link #passthrough} is enabled, messages are also printed to stdout.</p>
+ *
+ * <p>This allows capturing early initialization logs before the user opens
+ * the {@link DeveloperToolsPanel}. When the panel is opened, the buffered history
+ * becomes immediately visible.</p>
+ *
+ * @see DeveloperToolsPanel
+ */
+public class DebugLogBuffer {
+
+ private static final DateTimeFormatter TIME_FORMATTER =
+ DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
+
+ private final String[] buffer;
+ private final int capacity;
+ private volatile int head = 0;
+ private volatile int count = 0;
+ private volatile boolean passthrough = false;
+
+ /**
+ * 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.
+ *
+ * <p>If passthrough is enabled, also prints to stdout.</p>
+ *
+ * @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++;
+ }
+ }
+
+ if (passthrough) {
+ System.out.println(timestamped);
+ }
+ }
+
+ /**
+ * Returns all buffered log entries in chronological order.
+ *
+ * @return a list of timestamped log entries
+ */
+ public synchronized List<String> getEntries() {
+ final List<String> entries = new ArrayList<>(count);
+
+ if (count < capacity) {
+ for (int i = 0; i < count; i++) {
+ entries.add(buffer[i]);
+ }
+ } else {
+ for (int i = 0; i < capacity; i++) {
+ final int index = (head + i) % capacity;
+ entries.add(buffer[index]);
+ }
+ }
+
+ return entries;
+ }
+
+ /**
+ * Clears all buffered log entries.
+ */
+ public synchronized void clear() {
+ head = 0;
+ count = 0;
+ }
+
+ /**
+ * Returns whether passthrough to stdout is enabled.
+ *
+ * @return {@code true} if logs are also printed to stdout
+ */
+ public boolean isPassthrough() {
+ return passthrough;
+ }
+
+ /**
+ * Enables or disables passthrough to stdout.
+ *
+ * <p>When enabled, all subsequent log messages will be printed
+ * to stdout in addition to being captured in the buffer.</p>
+ *
+ * @param passthrough {@code true} to enable passthrough
+ */
+ public void setPassthrough(final boolean passthrough) {
+ this.passthrough = passthrough;
+ }
+
+ /**
+ * Returns the current number of log entries in the buffer.
+ *
+ * @return the number of entries
+ */
+ public synchronized int size() {
+ return count;
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui;
+
+/**
+ * Per-ViewPanel developer tools that control diagnostic features.
+ *
+ * <p>Each {@link ViewPanel} has its own DeveloperTools instance, allowing
+ * different views to have independent debug configurations.</p>
+ *
+ * <p>Settings can be toggled at runtime via the {@link DeveloperToolsPanel}
+ * (opened with F12 key).</p>
+ *
+ * @see ViewPanel#getDeveloperTools()
+ * @see DeveloperToolsPanel
+ */
+public class DeveloperTools {
+
+ /**
+ * If {@code true}, textured polygon borders are drawn in yellow.
+ * Useful for visualizing polygon slicing for perspective-correct rendering.
+ */
+ public volatile boolean showPolygonBorders = false;
+
+ /**
+ * If {@code true}, only render even-numbered horizontal segments (0, 2, 4, 6).
+ * Odd segments (1, 3, 5, 7) will remain black. Useful for detecting
+ * if threads render outside their allocated screen area (overdraw detection).
+ */
+ public volatile boolean renderAlternateSegments = false;
+
+ /**
+ * Creates a new DeveloperTools instance with all debug features disabled.
+ */
+ public DeveloperTools() {
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.List;
+
+/**
+ * Developer tools panel for toggling diagnostic features and viewing logs.
+ *
+ * <p>Opens as a popup dialog when F12 is pressed. Provides:</p>
+ * <ul>
+ * <li>Checkboxes to toggle debug settings</li>
+ * <li>A scrollable log viewer showing captured debug output</li>
+ * <li>A button to clear the log buffer</li>
+ * </ul>
+ *
+ * @see DeveloperTools
+ * @see DebugLogBuffer
+ */
+public class DeveloperToolsPanel extends JDialog {
+
+ private static final int LOG_UPDATE_INTERVAL_MS = 500;
+
+ /** The developer tools being controlled. */
+ private final DeveloperTools developerTools;
+ /** The log buffer being displayed. */
+ private final DebugLogBuffer debugLogBuffer;
+ /** The text area showing log messages. */
+ private final JTextArea logArea;
+ /** Timer for periodic log updates. */
+ private final Timer updateTimer;
+ /** Flag to prevent concurrent log updates. */
+ private volatile boolean updating = false;
+
+ /**
+ * Creates and displays a developer tools panel.
+ *
+ * @param parent the parent frame (for centering)
+ * @param developerTools the developer tools to control
+ * @param debugLogBuffer the log buffer to display
+ */
+ public DeveloperToolsPanel(final Frame parent, final DeveloperTools developerTools,
+ final DebugLogBuffer debugLogBuffer) {
+ super(parent, "Developer Tools", false);
+ this.developerTools = developerTools;
+ this.debugLogBuffer = debugLogBuffer;
+
+ setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+ setLayout(new BorderLayout(8, 8));
+
+ final JPanel settingsPanel = createSettingsPanel();
+ add(settingsPanel, BorderLayout.NORTH);
+
+ logArea = new JTextArea(20, 60);
+ logArea.setEditable(false);
+ logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+ logArea.setBackground(Color.BLACK);
+ logArea.setForeground(Color.GREEN);
+ final JScrollPane scrollPane = new JScrollPane(logArea);
+ scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
+ add(scrollPane, BorderLayout.CENTER);
+
+ final JPanel buttonPanel = createButtonPanel();
+ add(buttonPanel, BorderLayout.SOUTH);
+
+ pack();
+ setLocationRelativeTo(parent);
+
+ updateTimer = new Timer(LOG_UPDATE_INTERVAL_MS, new ActionListener() {
+ @Override
+ public void actionPerformed(final ActionEvent e) {
+ updateLogDisplay();
+ }
+ });
+
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowOpened(final WindowEvent e) {
+ debugLogBuffer.setPassthrough(true);
+ updateLogDisplay();
+ updateTimer.start();
+ }
+
+ @Override
+ public void windowClosed(final WindowEvent e) {
+ updateTimer.stop();
+ debugLogBuffer.setPassthrough(false);
+ }
+ });
+ }
+
+ private JPanel createSettingsPanel() {
+ final JPanel panel = new JPanel();
+ panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+ panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
+
+ final JCheckBox showBordersCheckbox = new JCheckBox("Show polygon borders");
+ showBordersCheckbox.setSelected(developerTools.showPolygonBorders);
+ showBordersCheckbox.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(final ChangeEvent e) {
+ developerTools.showPolygonBorders = showBordersCheckbox.isSelected();
+ }
+ });
+
+ final JCheckBox alternateSegmentsCheckbox = new JCheckBox("Render alternate segments (overdraw debug)");
+ alternateSegmentsCheckbox.setSelected(developerTools.renderAlternateSegments);
+ alternateSegmentsCheckbox.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(final ChangeEvent e) {
+ developerTools.renderAlternateSegments = alternateSegmentsCheckbox.isSelected();
+ }
+ });
+
+ panel.add(showBordersCheckbox);
+ panel.add(alternateSegmentsCheckbox);
+
+ return panel;
+ }
+
+ private JPanel createButtonPanel() {
+ final JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+
+ final JButton clearButton = new JButton("Clear Logs");
+ clearButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(final ActionEvent e) {
+ debugLogBuffer.clear();
+ logArea.setText("");
+ }
+ });
+
+ panel.add(clearButton);
+
+ return panel;
+ }
+
+ private void updateLogDisplay() {
+ if (updating) {
+ return;
+ }
+ updating = true;
+ try {
+ final List<String> entries = debugLogBuffer.getEntries();
+ final StringBuilder sb = new StringBuilder();
+ for (final String entry : entries) {
+ sb.append(entry).append('\n');
+ }
+ logArea.setText(sb.toString());
+
+ final JScrollBar vertical = ((JScrollPane) logArea.getParent().getParent())
+ .getVerticalScrollBar();
+ vertical.setValue(vertical.getMaximum());
+ } finally {
+ updating = false;
+ }
+ }
+}
\ No newline at end of file
return true;
}
+ /**
+ * Returns the wireframe border box for this component.
+ *
+ * @return the border wireframe box
+ */
public WireframeBox getBorders() {
if (borders == null)
borders = createBorder();
return borders;
}
+ /**
+ * Returns the depth of this component's bounding box.
+ *
+ * @return the depth in pixels
+ */
public int getDepth() {
return (int) containingBox.getDepth();
}
+ /**
+ * Returns the height of this component's bounding box.
+ *
+ * @return the height in pixels
+ */
public int getHeight() {
return (int) containingBox.getHeight();
}
+ /**
+ * Returns the width of this component's bounding box.
+ *
+ * @return the width in pixels
+ */
public int getWidth() {
return (int) containingBox.getWidth();
}
+ /**
+ * Hides the focus border around this component.
+ */
public void hideBorder() {
if (!borderShown)
return;
* UI component that mouse is currently hovering over.
*/
private MouseInteractionController currentObjectUnderMouseCursor;
+ /**
+ * Developer tools for this rendering context.
+ * Controls diagnostic features like logging and visualization.
+ */
+ public DeveloperTools developerTools;
/**
* Creates a new rendering context for full-screen rendering.
this.bufferedImage = parent.bufferedImage;
this.pixels = parent.pixels;
this.graphics = parent.graphics;
+ this.developerTools = parent.developerTools;
}
/**
* Resets per-frame state in preparation for rendering a new frame.
- * Clears the mouse event and the current object under the mouse cursor.
+ * Increments the frame number and clears the mouse event state.
*/
public void prepareForNewFrameRendering() {
+ frameNumber++;
mouseEvent = null;
currentObjectUnderMouseCursor = null;
}
* 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) {
this.currentObjectUnderMouseCursor = currentObjectUnderMouseCursor;
}
/**
- * @return <code>true</code> if view update is needed as a consequence of this mouse event.
+ * 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;
*/
public int column;
+ /**
+ * Creates a text pointer at position (0, 0).
+ */
public TextPointer() {
this(0, 0);
}
+ /**
+ * Creates a text pointer at the specified row and column.
+ *
+ * @param row the row index (0-based)
+ * @param column the column index (0-based)
+ */
public TextPointer(final int row, final int column) {
this.row = row;
this.column = column;
}
+ /**
+ * Creates a text pointer by copying another text pointer.
+ *
+ * @param parent the text pointer to copy
+ */
public TextPointer(final TextPointer parent) {
this(parent.row, parent.column);
}
private static final long serialVersionUID = -7037635097739548470L;
+ /** The embedded 3D view panel. */
private final ViewPanel viewPanel;
/**
setExtendedState(JFrame.MAXIMIZED_BOTH);
}
setVisible(true);
+ validate();
- getViewPanel().ensureRenderThreadStarted();
+ // Render thread will be started by ViewPanel's componentShown/componentResized listeners
+ // after all frame listeners have been registered by the caller
addResizeListener();
addWindowListener(this);
private static final int NUM_BUFFERS = 2;
private static final int NUM_RENDER_SEGMENTS = 8;
+ /** The input manager handling mouse and keyboard events. */
private final InputManager inputManager = new InputManager(this);
+ /** The stack managing keyboard focus for GUI components. */
private final KeyboardFocusStack keyboardFocusStack;
+ /** The camera representing the viewer's position and orientation. */
private final Camera camera = new Camera();
+ /** The root shape collection containing all 3D shapes in the scene. */
private final ShapeCollection rootShapeCollection = new ShapeCollection();
+ /** The set of frame listeners notified before each frame. */
private final Set<FrameListener> frameListeners = ConcurrentHashMap.newKeySet();
+ /** The executor service for parallel rendering. */
private final ExecutorService renderExecutor = Executors.newFixedThreadPool(NUM_RENDER_SEGMENTS);
+ /** The background color of the view. */
public Color backgroundColor = Color.BLACK;
+ /** Developer tools for this view panel. */
+ private final DeveloperTools developerTools = new DeveloperTools();
+ /** Debug log buffer for capturing diagnostic output. */
+ private final DebugLogBuffer debugLogBuffer = new DebugLogBuffer(10000);
+ /** The developer tools panel popup, or null if not currently shown. */
+ private DeveloperToolsPanel developerToolsPanel = null;
+
/**
* Stores milliseconds when the last frame was updated. This is needed to calculate the time delta between frames.
* Time delta is used to calculate smooth animation.
*/
private long lastUpdateMillis = 0;
+ /** The current rendering context for the active frame. */
private RenderingContext renderingContext = null;
/**
*/
private volatile boolean renderThreadRunning = false;
+ /** Timestamp for the next scheduled frame. */
private long nextFrameTime;
+ /** The buffer strategy for page-flipping rendering. */
private BufferStrategy bufferStrategy;
+ /** Whether the buffer strategy has been initialized. */
private boolean bufferStrategyInitialized = false;
+ /**
+ * Creates a new view panel with default settings.
+ */
public ViewPanel() {
frameListeners.add(camera);
frameListeners.add(inputManager);
@Override
public void componentResized(final ComponentEvent e) {
viewRepaintNeeded = true;
- maybeStartRenderThread();
}
@Override
public void componentShown(final ComponentEvent e) {
viewRepaintNeeded = true;
- maybeStartRenderThread();
}
});
}
maybeStartRenderThread();
}
+ /**
+ * Returns the camera representing the viewer's position and orientation.
+ *
+ * @return the camera
+ */
public Camera getCamera() {
return camera;
}
return getPreferredSize();
}
+ /**
+ * Returns the current rendering context for the active frame.
+ *
+ * @return the rendering context, or null if no frame is being rendered
+ */
public RenderingContext getRenderingContext() {
return renderingContext;
}
+ /**
+ * Returns the developer tools for this view panel.
+ *
+ * @return the developer tools
+ */
+ public DeveloperTools getDeveloperTools() {
+ return developerTools;
+ }
+
+ /**
+ * Returns the debug log buffer for this view panel.
+ *
+ * @return the debug log buffer
+ */
+ public DebugLogBuffer getDebugLogBuffer() {
+ return debugLogBuffer;
+ }
+
+ /**
+ * Shows the developer tools panel, toggling it if already open.
+ * Called when F12 is pressed.
+ */
+ public void showDeveloperToolsPanel() {
+ if (developerToolsPanel != null && developerToolsPanel.isVisible()) {
+ developerToolsPanel.dispose();
+ developerToolsPanel = null;
+ return;
+ }
+
+ Frame parentFrame = null;
+ Container parent = getParent();
+ while (parent != null) {
+ if (parent instanceof Frame) {
+ parentFrame = (Frame) parent;
+ break;
+ }
+ parent = parent.getParent();
+ }
+
+ developerToolsPanel = new DeveloperToolsPanel(parentFrame, developerTools, debugLogBuffer);
+ developerToolsPanel.setVisible(true);
+ }
+
@Override
public void paint(final Graphics g) {
}
try {
createBufferStrategy(NUM_BUFFERS);
bufferStrategy = getBufferStrategy();
- if (bufferStrategy != null)
+ if (bufferStrategy != null) {
bufferStrategyInitialized = true;
+ // Prime the buffer strategy with an initial show() to ensure it's ready
+ Graphics2D g = null;
+ try {
+ g = (Graphics2D) bufferStrategy.getDrawGraphics();
+ if (g != null) {
+ g.setColor(java.awt.Color.BLACK);
+ g.fillRect(0, 0, getWidth(), getHeight());
+ }
+ } finally {
+ if (g != null) g.dispose();
+ }
+ bufferStrategy.show();
+ java.awt.Toolkit.getDefaultToolkit().sync();
+ }
} catch (final Exception e) {
bufferStrategy = null;
bufferStrategyInitialized = false;
}
}
+ private static int renderFrameCount = 0;
+
private void renderFrame() {
ensureBufferStrategy();
- if (bufferStrategy == null || renderingContext == null)
+ if (bufferStrategy == null || renderingContext == null) {
+ debugLogBuffer.log("[VIEWPANEL] renderFrame ABORT: bufferStrategy=" + bufferStrategy + ", renderingContext=" + renderingContext);
return;
+ }
+
+ renderFrameCount++;
+ debugLogBuffer.log("[VIEWPANEL] renderFrame #" + renderFrameCount
+ + " START, shapes=" + rootShapeCollection.getShapes().size()
+ + ", frameNumber=" + renderingContext.frameNumber
+ + ", bufferSize=" + renderingContext.width + "x" + renderingContext.height);
try {
- do {
- // Phase 1: Clear all segments
- clearCanvasAllSegments();
-
- // Phase 2: Transform shapes (single-threaded)
- rootShapeCollection.transformShapes(this, renderingContext);
-
- // Phase 3: Sort shapes (single-threaded)
- rootShapeCollection.sortShapes();
-
- // Phase 4: Paint segments in parallel
- final int height = renderingContext.height;
- final int segmentHeight = height / NUM_RENDER_SEGMENTS;
- final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[NUM_RENDER_SEGMENTS];
- final CountDownLatch latch = new CountDownLatch(NUM_RENDER_SEGMENTS);
-
- for (int i = 0; i < NUM_RENDER_SEGMENTS; i++) {
- final int segmentIndex = i;
- final int minY = i * segmentHeight;
- final int maxY = (i == NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight;
-
- segmentContexts[i] = new SegmentRenderingContext(renderingContext, minY, maxY);
-
- renderExecutor.submit(() -> {
- try {
- rootShapeCollection.paintShapes(segmentContexts[segmentIndex]);
- } finally {
- latch.countDown();
- }
- });
+ // === Render ONCE to offscreen buffer ===
+ // The offscreen bufferedImage is unaffected by BufferStrategy contentsRestored(),
+ // so we only need to render once, then retry the blit if needed.
+ clearCanvasAllSegments();
+ debugLogBuffer.log("[VIEWPANEL] Phase 1 done: canvas cleared");
+
+ rootShapeCollection.transformShapes(this, renderingContext);
+ debugLogBuffer.log("[VIEWPANEL] Phase 2 done: shapes transformed, queued=" + rootShapeCollection.getQueuedShapeCount());
+
+ rootShapeCollection.sortShapes();
+ debugLogBuffer.log("[VIEWPANEL] Phase 3 done: shapes sorted");
+
+ // Phase 4: Paint segments in parallel
+ final int height = renderingContext.height;
+ final int segmentHeight = height / NUM_RENDER_SEGMENTS;
+ final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[NUM_RENDER_SEGMENTS];
+ final CountDownLatch latch = new CountDownLatch(NUM_RENDER_SEGMENTS);
+
+ for (int i = 0; i < NUM_RENDER_SEGMENTS; i++) {
+ final int segmentIndex = i;
+ final int minY = i * segmentHeight;
+ final int maxY = (i == NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight;
+
+ segmentContexts[i] = new SegmentRenderingContext(renderingContext, minY, maxY);
+
+ // Skip odd segments when renderAlternateSegments is enabled for overdraw debugging
+ if (developerTools.renderAlternateSegments && (i % 2 == 1)) {
+ latch.countDown();
+ continue;
}
- // Wait for all segments to complete
- try {
- latch.await();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- return;
- }
+ renderExecutor.submit(() -> {
+ try {
+ rootShapeCollection.paintShapes(segmentContexts[segmentIndex]);
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ // Wait for all segments to complete
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ debugLogBuffer.log("[VIEWPANEL] Phase 4 done: segments painted");
- // Phase 5: Combine mouse results
- combineMouseResults(segmentContexts);
+ // Phase 5: Combine mouse results
+ combineMouseResults(segmentContexts);
+ debugLogBuffer.log("[VIEWPANEL] Phase 5 done: mouse results combined");
- // Phase 6: Blit to screen
+ // === Blit loop — only re-blit, never re-render ===
+ // contentsRestored() can trigger when the OS recreates the back buffer
+ // (common during window creation). Since our offscreen bufferedImage still
+ // contains the correct frame data, we only need to re-blit, not re-render.
+ int blitCount = 0;
+ do {
Graphics2D g = null;
try {
g = (Graphics2D) bufferStrategy.getDrawGraphics();
if (g != null) {
- g.drawImage(renderingContext.bufferedImage, 0, 0, null);
- g.dispose();
+ debugLogBuffer.log("[VIEWPANEL] Blit attempt #" + (blitCount + 1) +
+ ", image=" + renderingContext.bufferedImage.getWidth() + "x" + renderingContext.bufferedImage.getHeight() +
+ ", type=" + renderingContext.bufferedImage.getType() +
+ ", g=" + g.getClass().getSimpleName());
+ // Use image observer to ensure proper image loading
+ g.drawImage(renderingContext.bufferedImage, 0, 0, this);
+ blitCount++;
}
} catch (final Exception e) {
- if (g != null)
- g.dispose();
+ debugLogBuffer.log("[VIEWPANEL] Blit exception: " + e.getMessage());
break;
+ } finally {
+ if (g != null) g.dispose();
}
} while (bufferStrategy.contentsRestored());
+ debugLogBuffer.log("[VIEWPANEL] Phase 6 done: blit complete (x" + blitCount + "), contentsRestored=" + bufferStrategy.contentsRestored() + ", contentsLost=" + bufferStrategy.contentsLost());
if (bufferStrategy.contentsLost()) {
+ debugLogBuffer.log("[VIEWPANEL] Buffer contents LOST, reinitializing");
bufferStrategyInitialized = false;
bufferStrategy = null;
} else {
+ debugLogBuffer.log("[VIEWPANEL] Calling bufferStrategy.show()");
bufferStrategy.show();
java.awt.Toolkit.getDefaultToolkit().sync();
+ debugLogBuffer.log("[VIEWPANEL] show() completed");
}
} catch (final Exception e) {
+ debugLogBuffer.log("[VIEWPANEL] renderFrame exception: " + e.getMessage());
+ e.printStackTrace();
bufferStrategyInitialized = false;
bufferStrategy = null;
}
private void clearCanvasAllSegments() {
final int rgb = (backgroundColor.r << 16) | (backgroundColor.g << 8) | backgroundColor.b;
+ debugLogBuffer.log("[VIEWPANEL] Clearing canvas with color: 0x" + Integer.toHexString(rgb) + " (black=0x0)");
final int width = renderingContext.width;
+ final int height = renderingContext.height;
final int[] pixels = renderingContext.pixels;
- Arrays.fill(pixels, 0, width * renderingContext.height, rgb);
+
+ if (developerTools.renderAlternateSegments) {
+ // Clear only even segments (0, 2, 4, 6), leave odd segments black
+ final int segmentHeight = height / NUM_RENDER_SEGMENTS;
+ for (int seg = 0; seg < NUM_RENDER_SEGMENTS; seg += 2) {
+ final int minY = seg * segmentHeight;
+ final int maxY = (seg == NUM_RENDER_SEGMENTS - 1) ? height : (seg + 1) * segmentHeight;
+ Arrays.fill(pixels, minY * width, maxY * width, rgb);
+ }
+ } else {
+ Arrays.fill(pixels, 0, width * height, rgb);
+ }
}
private void combineMouseResults(final SegmentRenderingContext[] segmentContexts) {
nextFrameTime = System.currentTimeMillis();
while (renderThreadRunning) {
- ensureThatViewIsUpToDate();
+ try {
+ ensureThatViewIsUpToDate();
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
if (maintainTargetFps()) break;
}
|| (renderingContext.width != panelWidth)
|| (renderingContext.height != panelHeight)) {
renderingContext = new RenderingContext(panelWidth, panelHeight);
+ renderingContext.developerTools = developerTools;
}
renderingContext.prepareForNewFrameRendering();
*/
public Vertex down;
+ /**
+ * Creates a new view space tracker.
+ */
public ViewSpaceTracker() {
}
*/
public class ViewUpdateTimerTask extends java.util.TimerTask {
+ /** The view panel to update. */
public ViewPanel viewPanel;
+ /**
+ * Creates a new timer task for the given view panel.
+ *
+ * @param viewPanel the view panel to update
+ */
public ViewUpdateTimerTask(final ViewPanel viewPanel) {
this.viewPanel = viewPanel;
}
public class Connexion3D {
+ /**
+ * Creates a new Connexion3D instance.
+ */
+ public Connexion3D() {
+ }
+
+ /**
+ * Reads raw data from the 3Dconnexion device for testing purposes.
+ *
+ * @param args command line arguments (ignored)
+ * @throws IOException if the device cannot be read
+ */
public static void main(final String[] args) throws IOException {
final BufferedReader in = new BufferedReader(new FileReader(
private boolean mouseMoved;
private boolean mouseWithinWindow = false;
+ /**
+ * Creates an input manager attached to the given view panel.
+ *
+ * @param viewPanel the view panel to receive input from
+ */
public InputManager(final ViewPanel viewPanel) {
this.viewPanel = viewPanel;
bind(viewPanel);
}
+ /**
+ * Processes accumulated input events and updates camera based on mouse drag/wheel.
+ *
+ * @param viewPanel the view panel
+ * @param millisecondsSinceLastFrame time since last frame (unused)
+ * @return {@code true} if a view repaint is needed
+ */
@Override
public boolean onFrame(final ViewPanel viewPanel, final int millisecondsSinceLastFrame) {
boolean viewUpdateNeeded = handleKeyboardEvents();
return viewUpdateNeeded;
}
+ /**
+ * Binds this input manager to listen for events on the given component.
+ *
+ * @param component the component to attach listeners to
+ */
private void bind(final Component component) {
component.addMouseMotionListener(this);
component.addKeyListener(this);
component.addMouseWheelListener(this);
}
+ /**
+ * Processes all accumulated keyboard events and forwards them to the current focus owner.
+ *
+ * @return {@code true} if any event handler requested a repaint
+ */
private boolean handleKeyboardEvents() {
final KeyboardInputHandler currentFocusOwner = viewPanel.getKeyboardFocusStack().getCurrentFocusOwner();
return viewUpdateNeeded;
}
+ /**
+ * Processes a single keyboard event by dispatching to the focus owner.
+ *
+ * @param currentFocusOwner the component that currently has keyboard focus
+ * @param keyEvent the keyboard event to process
+ * @return {@code true} if the handler requested a repaint
+ */
private boolean processKeyEvent(KeyboardInputHandler currentFocusOwner, KeyEvent keyEvent) {
switch (keyEvent.getID()) {
case KeyEvent.KEY_PRESSED:
return false;
}
+ /**
+ * Handles mouse clicks and hover detection.
+ * Sets up the mouse event in the rendering context for shape hit testing.
+ *
+ * @param viewPanel the view panel
+ * @return {@code true} if a repaint is needed
+ */
private synchronized boolean handleMouseClicksAndHover(final ViewPanel viewPanel) {
boolean rerenderNeeded = false;
MouseEvent event = findClickLocationToTrace();
@Override
public void keyPressed(final KeyEvent evt) {
+ if (evt.getKeyCode() == java.awt.event.KeyEvent.VK_F12) {
+ viewPanel.showDeveloperToolsPanel();
+ return;
+ }
synchronized (detectedKeyEvents) {
pressedKeysToPressedTimeMap.put(evt.getKeyCode(), System.currentTimeMillis());
detectedKeyEvents.add(evt);
*/
public class KeyboardHelper {
+ /**
+ * Private constructor to prevent instantiation of this utility class.
+ */
+ private KeyboardHelper() {
+ }
+
/** Key code for the Tab key. */
public static final int TAB = 9;
/** Key code for the Down arrow key. */
public interface KeyboardInputHandler {
/**
- * @return <code>true</code> if view needs to be re-rendered.
+ * Called when the component loses keyboard focus.
+ *
+ * @param viewPanel the view panel that owns this handler
+ * @return {@code true} if view needs to be re-rendered
*/
boolean focusLost(ViewPanel viewPanel);
/**
- * @return <code>true</code> if view needs to be re-rendered.
+ * Called when the component receives keyboard focus.
+ *
+ * @param viewPanel the view panel that owns this handler
+ * @return {@code true} if view needs to be re-rendered
*/
boolean focusReceived(ViewPanel viewPanel);
/**
- * @return <code>true</code> if view needs to be re-rendered.
+ * Called when a key is pressed while the component has focus.
+ *
+ * @param event the key event
+ * @param viewPanel the view panel that owns this handler
+ * @return {@code true} if view needs to be re-rendered
*/
boolean keyPressed(KeyEvent event, ViewPanel viewPanel);
/**
- * @return <code>true</code> if view needs to be re-rendered.
+ * Called when a key is released while the component has focus.
+ *
+ * @param event the key event
+ * @param viewPanel the view panel that owns this handler
+ * @return {@code true} if view needs to be re-rendered
*/
boolean keyReleased(KeyEvent event, ViewPanel viewPanel);
/**
* Called when mouse is clicked on component.
*
- * @return <code>true</code> if view update is needed as a consequence of this mouse click.
+ * @param button the mouse button that was clicked (1 = left, 2 = middle, 3 = right)
+ * @return {@code true} if view update is needed as a consequence of this mouse click
*/
boolean mouseClicked(int button);
*/
public class WorldNavigationUserInputTracker implements KeyboardInputHandler, FrameListener {
+ /**
+ * Creates a new world navigation input tracker.
+ */
+ public WorldNavigationUserInputTracker() {
+ }
+
@Override
public boolean onFrame(final ViewPanel viewPanel,
final int millisecondsSinceLastFrame) {
-package eu.svjatoslav.sixth.e3d.gui.humaninput;
-
/**
- * This package is responsible for tracking human input devices (keyboard, mouse, etc.) and
- * forwarding those inputs to subsequent virtual components.
- */
\ No newline at end of file
+ * Provides input device tracking (keyboard, mouse) and event forwarding to virtual components.
+ *
+ * @see eu.svjatoslav.sixth.e3d.gui.humaninput.InputManager
+ * @see eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack
+ */
+package eu.svjatoslav.sixth.e3d.gui.humaninput;
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Graphical user interface components for the Sixth 3D engine.
+ *
+ * <p>This package provides the primary integration points for embedding 3D rendering
+ * into Java applications using Swing/AWT.</p>
+ *
+ * <p>Key classes:</p>
+ * <ul>
+ * <li>{@link eu.svjatoslav.sixth.e3d.gui.ViewPanel} - The main rendering surface (JPanel)</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.gui.ViewFrame} - A JFrame with embedded ViewPanel</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.gui.Camera} - Represents the viewer's position and orientation</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.gui.DeveloperTools} - Debugging and profiling utilities</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.gui.ViewPanel
+ * @see eu.svjatoslav.sixth.e3d.gui.Camera
+ */
+
+package eu.svjatoslav.sixth.e3d.gui;
\ No newline at end of file
*/
char value;
+ /**
+ * Creates a character with the given value.
+ *
+ * @param value the character value
+ */
public Character(final char value) {
this.value = value;
}
*/
public class LookAndFeel {
+ /** Default foreground (text) color. */
public Color foreground = new Color(255, 255, 255);
+
+ /** Default background color. */
public Color background = new Color(20, 20, 20, 255);
+ /** Background color for tab stop positions. */
public Color tabStopBackground = new Color(25, 25, 25, 255);
+ /** Cursor foreground color. */
public Color cursorForeground = new Color(255, 255, 255);
+
+ /** Cursor background color. */
public Color cursorBackground = new Color(255, 0, 0);
+ /** Selection foreground color. */
public Color selectionForeground = new Color(255, 255, 255);
+
+ /** Selection background color. */
public Color selectionBackground = new Color(0, 80, 80);
+ /**
+ * Creates a look and feel with default colors.
+ */
+ public LookAndFeel() {
+ }
+
}
*/
public List<TextLine> rows = new ArrayList<>();
+ /**
+ * Creates a new empty page.
+ */
+ public Page() {
+ }
+
+ /**
+ * Ensures that the page has at least the specified number of lines.
+ *
+ * @param row the minimum number of lines required
+ */
public void ensureMaxTextLine(final int row) {
while (rows.size() <= row)
rows.add(new TextLine());
* Returns the character at the specified location.
* If the location is out of bounds, returns a space.
*
- * @return The character at the specified location.
+ * @param row the row index
+ * @param column the column index
+ * @return the character at the specified location
*/
public char getChar(final int row, final int column) {
if (rows.size() <= row)
return result.toString();
}
+ /**
+ * Inserts a character at the specified position.
+ *
+ * @param row the row index
+ * @param col the column index
+ * @param value the character to insert
+ */
public void insertCharacter(final int row, final int col, final char value) {
getLine(row).insertCharacter(col, value);
}
+ /**
+ * Inserts a line at the specified row.
+ *
+ * @param row the row index where to insert
+ * @param textLine the text line to insert
+ */
public void insertLine(final int row, final TextLine textLine) {
rows.add(row, textLine);
}
* Selection start and end pointers.
*/
public TextPointer selectionStart = new TextPointer(0, 0);
+
+ /**
+ * The end position of the text selection.
+ */
public TextPointer selectionEnd = new TextPointer(0, 0);
/**
-package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
-
/**
- * This package contains a simple text editor component.
- */
\ No newline at end of file
+ * Provides a simple text editor component rendered in 3D space.
+ *
+ * @see eu.svjatoslav.sixth.e3d.gui.textEditorComponent.TextEditComponent
+ */
+package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
\ No newline at end of file
*/
private double angleYZ = 0;
+ /**
+ * Creates a rotation with no rotation (zero angles).
+ */
public Rotation() {
computeMultipliers();
}
computeMultipliers();
}
+ /**
+ * Creates a copy of this rotation with the same angles.
+ *
+ * @return a new rotation with the same angle values
+ */
@Override
public Rotation clone() {
return new Rotation(angleXZ, angleYZ);
computeMultipliers();
}
+ /**
+ * Copies the angles from another rotation into this rotation.
+ *
+ * @param rotation the rotation to copy angles from
+ */
public void setAngles(Rotation rotation) {
this.angleXZ = rotation.angleXZ;
this.angleYZ = rotation.angleYZ;
*/
private final Rotation rotation;
+ /**
+ * Creates a transform with no translation or rotation (identity transform).
+ */
public Transform() {
translation = new Point3D();
rotation = new Rotation();
this.rotation = rotation;
}
+ /**
+ * Creates a copy of this transform with cloned translation and rotation.
+ *
+ * @return a new transform with the same translation and rotation values
+ */
@Override
public Transform clone() {
return new Transform(translation, rotation);
}
+ /**
+ * Returns the rotation component of this transform.
+ *
+ * @return the rotation (mutable reference)
+ */
public Rotation getRotation() {
return rotation;
}
+ /**
+ * Returns the translation component of this transform.
+ *
+ * @return the translation point (mutable reference)
+ */
public Point3D getTranslation() {
return translation;
}
*/
public class TransformStack {
+ /**
+ * Creates a new empty transform stack.
+ */
+ public TransformStack() {
+ }
+
/**
* Array of transforms in the stack.
* Fixed size for efficiency to avoid memory allocation during rendering.
import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
/**
- * Vertex is a point where two or more lines, line segments, or rays come together.
- * In other words, it's a corner of a polygon, polyhedron, or other geometric shape.
- * For example, a triangle has three vertices, a square has four, and a cube has eight.
+ * A vertex in 3D space with transformation and screen projection support.
+ *
+ * <p>A vertex represents a corner point of a polygon or polyhedron. In addition to
+ * the 3D coordinate, it stores the transformed position (relative to viewer) and
+ * the projected screen coordinates for rendering.</p>
+ *
+ * <p><b>Coordinate spaces:</b></p>
+ * <ul>
+ * <li>{@link #coordinate} - Original position in local/model space</li>
+ * <li>{@link #transformedCoordinate} - Position relative to viewer (camera space)</li>
+ * <li>{@link #onScreenCoordinate} - 2D screen position after perspective projection</li>
+ * </ul>
+ *
+ * <p><b>Example:</b></p>
+ * <pre>{@code
+ * Vertex v = new Vertex(new Point3D(10, 20, 30));
+ * v.calculateLocationRelativeToViewer(transformStack, renderContext);
+ * if (v.transformedCoordinate.z > 0) {
+ * // Vertex is in front of the camera
+ * }
+ * }</pre>
+ *
+ * @see Point3D
+ * @see TransformStack
*/
public class Vertex {
/**
- * Vertex coordinate in 3D space.
+ * Vertex coordinate in local/model 3D space.
*/
public Point3D coordinate;
/**
- * Vertex coordinate relative to the viewer after transformation.
- * Visible vertices have positive z coordinate.
- * Viewer is located at (0, 0, 0).
+ * Vertex coordinate relative to the viewer after transformation (camera space).
+ * Visible vertices have positive z coordinate (in front of the viewer).
* No perspective correction is applied.
*/
public Point3D transformedCoordinate;
/**
- * Vertex coordinate in pixels relative to the top left corner of the screen after transformation
- * and perspective correction.
+ * Vertex position on screen in pixels, relative to top-left corner.
+ * Calculated after transformation and perspective projection.
*/
public Point2D onScreenCoordinate;
/**
- * Coordinate within texture.
+ * Texture coordinate for UV mapping (optional).
*/
public Point2D textureCoordinate;
/**
- * The frame number when this vertex was last transformed.
+ * The frame number when this vertex was last transformed (for caching).
*/
- private int lastTransformedFrame;
+ private int lastTransformedFrame = -1; // Start at -1 so first frame (frameNumber=1) will transform
+ /**
+ * 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 location the 3D position of this vertex
+ */
public Vertex(final Point3D location) {
this(location, null);
}
+ /**
+ * Creates a vertex at the specified position with an optional texture coordinate.
+ *
+ * @param location the 3D position of this vertex
+ * @param textureCoordinate the UV texture coordinate, or {@code null} for none
+ */
public Vertex(final Point3D location, Point2D textureCoordinate) {
coordinate = location;
transformedCoordinate = new Point3D();
/**
- * Transforms vertex coordinate to calculate its location relative to the viewer.
- * It also calculates its location on the screen.
+ * Transforms this vertex from model space to screen space.
+ *
+ * <p>This method applies the transform stack to compute the vertex position
+ * relative to the viewer, then projects it to 2D screen coordinates.
+ * Results are cached per-frame to avoid redundant calculations.</p>
*
- * @param transforms Transforms pipeline.
- * @param renderContext Rendering context.
+ * @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) {
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
package eu.svjatoslav.sixth.e3d.renderer.octree;
/**
- * Point in 3D space. Used for octree. All coordinates are integers.
+ * Point in 3D space with integer coordinates. Used for octree voxel positions.
*/
public class IntegerPoint
{
- public int x, y, z = 0;
+ /** 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;
import static java.lang.Integer.min;
/**
- * <pre>
- * There are 3 cell types:
+ * Sparse voxel octree for 3D volume storage and ray tracing.
*
- * UNUSED
+ * <p>The octree represents a 3D volume with three cell types:</p>
+ * <ul>
+ * <li><b>UNUSED</b> - Empty cell, not yet allocated</li>
+ * <li><b>SOLID</b> - Contains color and illumination data</li>
+ * <li><b>CLUSTER</b> - Contains pointers to 8 child cells (for subdivision)</li>
+ * </ul>
*
- * SOLID
- * contains:
- * original color
- * visible color, after being illuminated by nearby light sources
+ * <p>Cell data is stored in parallel arrays ({@code cell1} through {@code cell8})
+ * for memory efficiency. Each array stores different aspects of cell data.</p>
*
- * CLUSTER
- * contains pointers to 8 sub cells
- * </pre>
+ * @see eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.RayTracer
+ * @see eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.Ray
*/
-
public class OctreeVolume {
- // cell is not hit by the ray
+ /** Return value indicating no intersection during ray tracing. */
public static final int TRACE_NO_HIT = -1;
- // solid cell (contains color and illumination)
+ /** Cell state marker for solid cells. */
private static final int CELL_STATE_SOLID = -2;
- // unused cell
+ /** Cell state marker for unused/empty cells. */
private static final int CELL_STATE_UNUSED = -1;
+ /** Cell data array 1: stores cell state and first child pointer. */
public int[] cell1;
+ /** Cell data array 2: stores color values. */
public int[] cell2;
+ /** Cell data array 3: stores illumination values. */
public int[] cell3;
+ /** Cell data array 4: stores child pointer 4. */
public int[] cell4;
+ /** Cell data array 5: stores child pointer 5. */
public int[] cell5;
+ /** Cell data array 6: stores child pointer 6. */
public int[] cell6;
+ /** Cell data array 7: stores child pointer 7. */
public int[] cell7;
+ /** Cell data array 8: stores child pointer 8. */
public int[] cell8;
/**
- * Pointer to the first unused cell.
+ * Pointer to the next unused cell in the allocation buffer.
*/
public int cellAllocationPointer = 0;
+
+ /** Number of currently allocated cells. */
public int usedCellsCount = 0;
+
+ /** Size of the root (master) cell in world units. */
public int masterCellSize;
+ /**
+ * Creates a new octree volume with default buffer size (1.5M cells)
+ * and master cell size of 256*64 units.
+ */
public OctreeVolume() {
initWorld(1500000, 256 * 64);
}
+ /**
+ * Subdivides a solid cell into 8 child cells, each with the same color and illumination.
+ *
+ * @param pointer the cell to break up
+ */
public void breakSolidCell(final int pointer) {
final int color = getCellColor(pointer);
final int illumination = getCellIllumination(pointer);
cell8[pointer] = 0;
}
+ /**
+ * Marks a cell as deleted and returns it to the unused pool.
+ *
+ * @param cellPointer the cell to delete
+ */
public void deleteCell(final int cellPointer) {
clearCell(cellPointer);
cell1[cellPointer] = CELL_STATE_UNUSED;
usedCellsCount--;
}
+ /**
+ * Tests whether a ray intersects with a cubic region.
+ *
+ * @param cubeX the X center of the cube
+ * @param cubeY the Y center of the cube
+ * @param cubeZ the Z center of the cube
+ * @param cubeSize the half-size of the cube
+ * @param r the ray to test
+ * @return intersection type code, or 0 if no intersection
+ */
public int doesIntersect(final int cubeX, final int cubeY, final int cubeZ,
final int cubeSize, final Ray r) {
}
/**
- * Fill 3D rectangle.
+ * Fills a 3D rectangular region with solid cells of the given color.
+ *
+ * @param p1 one corner of the rectangle
+ * @param p2 the opposite corner of the rectangle
+ * @param color the color to fill with
*/
public void fillRectangle(IntegerPoint p1, IntegerPoint p2, Color color) {
putCell(x, y, z, 0, 0, 0, masterCellSize, 0, color);
}
+ /**
+ * Returns the color value stored in a solid cell.
+ *
+ * @param pointer the cell pointer
+ * @return the packed RGB color value
+ */
public int getCellColor(final int pointer) {
return cell2[pointer];
}
+ /**
+ * Returns the illumination value stored in a solid cell.
+ *
+ * @param pointer the cell pointer
+ * @return the packed RGB illumination value
+ */
public int getCellIllumination(final int pointer) {
return cell3[pointer];
}
+ /**
+ * Initializes the octree storage arrays with the specified buffer size and root cell size.
+ *
+ * @param bufferLength the number of cells to allocate space for
+ * @param masterCellSize the size of the root cell in world units
+ */
public void initWorld(final int bufferLength, final int masterCellSize) {
// System.out.println("Initializing new world");
clearCell(0);
}
+ /**
+ * Checks if the cell at the given pointer is a solid (leaf) cell.
+ *
+ * @param pointer the cell pointer to check
+ * @return {@code true} if the cell is solid
+ */
public boolean isCellSolid(final int pointer) {
return cell1[pointer] == CELL_STATE_SOLID;
}
}
}
+ /**
+ * Allocates a new solid cell with the given color and illumination.
+ *
+ * @param color the color value for the new cell
+ * @param illumination the illumination value for the new cell
+ * @return the pointer to the newly allocated cell
+ */
public int makeNewCell(final int color, final int illumination) {
final int pointer = getNewCellPointer();
markCellAsSolid(pointer);
cell1[pointer] = CELL_STATE_SOLID;
}
+ /**
+ * Stores a voxel at the given world coordinates with the specified color.
+ *
+ * @param x the X coordinate
+ * @param y the Y coordinate
+ * @param z the Z coordinate
+ * @param color the color of the voxel
+ */
public void putCell(final int x, final int y, final int z, final Color color) {
putCell(x, y, z, 0, 0, 0, masterCellSize, 0, color);
}
}
}
+ /**
+ * Sets the color value for the cell at the given pointer.
+ *
+ * @param pointer the cell pointer
+ * @param color the color value to set
+ */
public void setCellColor(final int pointer, final int color) {
cell2[pointer] = color;
}
+ /**
+ * Sets the illumination value for the cell at the given pointer.
+ *
+ * @param pointer the cell pointer
+ * @param illumination the illumination value to set
+ */
public void setCellIllumination(final int pointer, final int illumination) {
cell3[pointer] = illumination;
}
/**
- * Trace ray through the world and return pointer to intersecting cell.
+ * Traces a ray through the octree to find an intersecting solid cell.
*
- * @return pointer to intersecting cell or TRACE_NO_HIT if no intersection.
+ * @param cellX the X coordinate of the current cell center
+ * @param cellY the Y coordinate of the current cell center
+ * @param cellZ the Z coordinate of the current cell center
+ * @param cellSize the size of the current cell
+ * @param pointer the pointer to the current cell
+ * @param ray the ray to trace
+ * @return pointer to intersecting cell or TRACE_NO_HIT if no intersection
*/
public int traceCell(final int cellX, final int cellY, final int cellZ,
final int cellSize, final int pointer, final Ray ray) {
*/
Point3D cameraCenter, topLeft, topRight, bottomLeft, bottomRight;
+ /**
+ * Creates a camera view for ray tracing from the given camera and zoom level.
+ *
+ * @param camera the camera to create a view for
+ * @param zoom the zoom level (scales the view frustum)
+ */
public CameraView(final Camera camera, final double zoom) {
// compute camera view coordinates as if camera is at (0,0,0) and look at (0,0,1)
final float viewAngle = (float) .6;
*/
Point3D location;
+ /**
+ * Creates a light source at the given location with the specified color and brightness.
+ *
+ * @param location the position of the light source in world space
+ * @param color the color of the light
+ * @param Brightness the brightness multiplier (0.0 = off, 1.0 = full)
+ */
public LightSource(final Point3D location, final Color color,
final float Brightness) {
this.location = location;
*/
public class RaytracingCamera extends TexturedRectangle {
+ /** Size of the camera view in world units. */
public static final int SIZE = 100;
+ /** Size of the rendered image in pixels. */
public static final int IMAGE_SIZE = 500;
private final CameraView cameraView;
+ /**
+ * Creates a raytracing camera at the specified camera position.
+ *
+ * @param camera the camera to use for the view
+ * @param zoom the zoom level
+ */
public RaytracingCamera(final Camera camera, final double zoom) {
super(new Transform(camera.getTransform().getTranslation().clone()));
cameraView = new CameraView(camera, zoom);
}
+ /**
+ * Returns the camera view used for ray tracing.
+ *
+ * @return the camera view
+ */
public CameraView getCameraView() {
return cameraView;
}
+ /**
+ * Loads a sprite image from the classpath.
+ *
+ * @param ref the resource path
+ * @return the loaded image
+ * @throws IOException if the image cannot be loaded
+ */
public BufferedImage getSprite(final String ref) throws IOException {
final URL url = this.getClass().getClassLoader().getResource(ref);
return ImageIO.read(url);
}
/**
+ * Creates a color from a hexadecimal string.
+ *
* @param colorHexCode color code in hex format.
* Supported formats are:
* <pre>
* <p>This class is used internally by {@link ShapeCollection} during the render pipeline.
* You typically do not need to interact with it directly.</p>
*
- * @see ShapeCollection#paint(eu.svjatoslav.sixth.e3d.gui.ViewPanel, RenderingContext)
+ * @see ShapeCollection#paintShapes(RenderingContext)
* @see AbstractCoordinateShape#onScreenZ
*/
public class RenderAggregator {
+ /**
+ * Creates a new render aggregator.
+ */
+ public RenderAggregator() {
+ }
+
private final ArrayList<AbstractCoordinateShape> shapes = new ArrayList<>();
private final ShapesZIndexComparator comparator = new ShapesZIndexComparator();
private boolean sorted = false;
}
/**
- * Adds a transformed shape to the queue for rendering in this frame.
+ * Queues a shape for rendering. Called during the transform phase.
*
- * @param shape the shape to render, with its screen-space coordinates already computed
+ * @param shape the shape to queue
*/
public void queueShapeForRendering(final AbstractCoordinateShape shape) {
shapes.add(shape);
*/
public class ShapeCollection {
+ /**
+ * Creates a new empty shape collection.
+ */
+ public ShapeCollection() {
+ }
+
private final RenderAggregator aggregator = new RenderAggregator();
private final TransformStack transformStack = new TransformStack();
private final List<AbstractShape> shapes = new ArrayList<>();
}
/**
- * Removes all shapes from this collection.
+ * Removes all shapes from this collection. This method is thread-safe.
*/
- public void clear() {
+ public synchronized void clear() {
shapes.clear();
}
public synchronized void transformShapes(final ViewPanel viewPanel,
final RenderingContext renderingContext) {
- renderingContext.frameNumber++;
-
aggregator.reset();
transformStack.clear();
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Lighting system for flat-shaded polygon rendering.
+ *
+ * <p>This package implements a simple Lambertian lighting model for shading
+ * solid polygons based on their surface normals relative to light sources.</p>
+ *
+ * <p>Key classes:</p>
+ * <ul>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager} - Manages lights and calculates shading</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightSource} - Represents a point light source</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.lighting;
\ No newline at end of file
*/
public abstract class AbstractShape {
+ /**
+ * Default constructor for abstract shape.
+ */
+ public AbstractShape() {
+ }
+
/**
* Optional controller that receives mouse interaction events (click, enter, exit)
* when the user interacts with this shape in the 3D view.
import eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureBitmap;
/**
- * Base class for textures always facing the viewer.
- * <p>
- * This class implements the "billboard" rendering technique where the texture
+ * A billboard: a texture that always faces the viewer.
+ *
+ * <p>This class implements the "billboard" rendering technique where the texture
* remains oriented towards the camera regardless of 3D position. The visible size
- * is calculated based on distance from viewer (z-coordinate) and scale factor.
- * <p>
- * The texture mapping algorithm:
- * 1. Calculates screen coverage based on perspective
- * 2. Clips to viewport boundaries
- * 3. Maps texture pixels to screen pixels using proportional scaling
+ * is calculated based on distance from viewer (z-coordinate) and scale factor.</p>
+ *
+ * <p><b>Texture mapping algorithm:</b></p>
+ * <ol>
+ * <li>Calculates screen coverage based on perspective</li>
+ * <li>Clips to viewport boundaries</li>
+ * <li>Maps texture pixels to screen pixels using proportional scaling</li>
+ * </ol>
+ *
+ * @see GlowingPoint a billboard with a circular gradient texture
+ * @see Texture
*/
public class Billboard extends AbstractCoordinateShape {
private static final double SCALE_MULTIPLIER = 0.005;
+
+ /**
+ * The texture to display on this billboard.
+ */
public final Texture texture;
/**
- * Scale of the texture object.
- * <p>
- * Object rendered visible size on the screen depends on underlying texture size and scale.
- * <p>
- * 0 means that object will be infinitely small.
- * 1 in recommended value to maintain sharpness of the texture as seen by the viewer.
+ * Scale factor for the billboard's visible size.
+ * <ul>
+ * <li>0 means infinitely small</li>
+ * <li>1 is recommended to maintain texture sharpness</li>
+ * </ul>
*/
private double scale;
+ /**
+ * Creates a billboard at the specified position with the given scale and texture.
+ *
+ * @param point the 3D position of the billboard center
+ * @param scale the scale factor (1.0 is recommended for sharpness)
+ * @param texture the texture to display
+ */
public Billboard(final Point3D point, final double scale,
final Texture texture) {
super(new Vertex(point));
}
/**
- * Paint the texture on the screen (targetRenderingArea)
+ * Renders this billboard to the screen.
+ *
+ * <p>The billboard is rendered as a screen-aligned quad centered on the projected
+ * position. The size is computed based on distance and scale factor.</p>
*
- * @param targetRenderingArea the screen to paint on
+ * @param targetRenderingArea the rendering context containing the pixel buffer
*/
@Override
public void paint(final RenderingContext targetRenderingArea) {
}
/**
- * Set the scale of the texture
+ * Sets the scale factor for this billboard.
*
- * @param scale the scale of the texture
+ * @param scale the scale factor (1.0 is recommended for sharpness)
*/
public void setScale(final double scale) {
this.scale = scale * SCALE_MULTIPLIER;
}
+ /**
+ * Returns the 3D position of this billboard.
+ *
+ * @return the center position in world coordinates
+ */
public Point3D getLocation() {
return coordinates[0].coordinate;
}
/**
* A glowing 3D point rendered with a circular gradient texture.
- * <p>
- * This class creates and reuses textures for glowing points of the same color.
+ *
+ * <p>This class creates and reuses textures for glowing points of the same color.
* The texture is a circle with an alpha gradient from center to edge, ensuring
- * a consistent visual appearance regardless of viewing angle.
- * <p>
- * The static set of glowing points enables texture sharing and garbage
- * collection of unused textures via WeakHashMap.
+ * a consistent visual appearance regardless of viewing angle.</p>
+ *
+ * <p><b>Texture sharing:</b> Glowing points of the same color share textures
+ * to reduce memory usage. Textures are garbage collected via WeakHashMap when
+ * no longer referenced.</p>
+ *
+ * @see Billboard the parent class
+ * @see Color
*/
public class GlowingPoint extends Billboard {
private static final int TEXTURE_RESOLUTION_PIXELS = 100;
+
/**
- * A set of all existing glowing points.
- * Used to reuse textures of glowing points of the same color.
+ * Set of all existing glowing points, used for texture sharing.
*/
private static final Set<GlowingPoint> glowingPoints = Collections.newSetFromMap(new WeakHashMap<>());
private final Color color;
+ /**
+ * Creates a glowing point at the specified position with the given size and color.
+ *
+ * @param point the 3D position of the point
+ * @param pointSize the visible size of the point
+ * @param color the color of the glow
+ */
public GlowingPoint(final Point3D point, final double pointSize,
final Color color) {
super(point, computeScale(pointSize), getTexture(color));
}
+ /**
+ * Computes the scale factor from point size.
+ *
+ * @param pointSize the desired visible size
+ * @return the scale factor for the billboard
+ */
private static double computeScale(double pointSize) {
return pointSize / ((double) (TEXTURE_RESOLUTION_PIXELS / 50f));
}
/**
* Returns a texture for a glowing point of the given color.
- * The texture is a circle with a gradient from transparent to the given color.
+ *
+ * <p>Attempts to reuse an existing texture from another glowing point of the
+ * same color. If none exists, creates a new texture.</p>
+ *
+ * @param color the color of the glow
+ * @return a texture with a circular alpha gradient
*/
private static Texture getTexture(final Color color) {
// attempt to reuse texture from existing glowing point of the same color
*/
public Color color;
+ /**
+ * Creates a copy of an existing line with cloned coordinates and color.
+ *
+ * @param parentLine the line to copy
+ */
public Line(final Line parentLine) {
this(parentLine.coordinates[0].coordinate.clone(),
parentLine.coordinates[1].coordinate.clone(),
new Color(parentLine.color), parentLine.width);
}
+ /**
+ * Creates a line between two points with the specified color and width.
+ *
+ * @param point1 the starting point of the line
+ * @param point2 the ending point of the line
+ * @param color the color of the line
+ * @param width the width of the line in world units
+ */
public Line(final Point3D point1, final Point3D point2, final Color color,
final double width) {
}
+ /**
+ * Draws a horizontal scanline between two interpolators with alpha blending.
+ *
+ * @param line1 the left edge interpolator
+ * @param line2 the right edge interpolator
+ * @param y the Y coordinate of the scanline
+ * @param renderBuffer the rendering context to draw into
+ */
private void drawHorizontalLine(final LineInterpolator line1,
final LineInterpolator line2, final int y,
final RenderingContext renderBuffer) {
}
+ /**
+ * Draws a thin line as single pixels with alpha-adjusted color.
+ * Used for lines that appear thin on screen (below minimum width threshold).
+ *
+ * @param buffer the rendering context to draw into
+ * @param alpha the alpha value for the entire line
+ */
private void drawSinglePixelHorizontalLine(final RenderingContext buffer,
final int alpha) {
}
+ /**
+ * Draws a thin vertical line as single pixels with alpha-adjusted color.
+ * Used for lines that appear thin on screen and are more vertical than horizontal.
+ *
+ * @param buffer the rendering context to draw into
+ * @param alpha the alpha value for the entire line
+ */
private void drawSinglePixelVerticalLine(final RenderingContext buffer,
final int alpha) {
}
}
+ /**
+ * Finds the index of the first interpolator (starting from startPointer) that contains the given Y coordinate.
+ *
+ * @param startPointer the index to start searching from
+ * @param y the Y coordinate to search for
+ * @return the index of the interpolator, or -1 if not found
+ */
private int getLineInterpolator(final int startPointer, final int y) {
for (int i = startPointer; i < lineInterpolators.length; i++)
return -1;
}
+ /**
+ * Renders this line to the screen using perspective-correct width and alpha blending.
+ *
+ * <p>This method handles two rendering modes:</p>
+ * <ul>
+ * <li>Thin lines: When the projected width is below threshold, draws single-pixel
+ * lines with alpha adjusted for sub-pixel appearance.</li>
+ * <li>Thick lines: Creates four edge interpolators and fills the rectangular area
+ * scanline by scanline with perspective-correct alpha fading at edges.</li>
+ * </ul>
+ *
+ * @param buffer the rendering context containing the pixel buffer
+ */
@Override
public void paint(final RenderingContext buffer) {
* This class encapsulates common line styling parameters (width and color) to
* avoid redundant configuration. It provides multiple constructors for
* flexibility and ensures default values are used when not specified.
+ *
+ * <p><b>Example usage:</b></p>
+ * <pre>{@code
+ * // Create a line appearance with default color and width 2.0
+ * LineAppearance appearance = new LineAppearance(2.0, Color.RED);
+ *
+ * // Create multiple lines with the same appearance
+ * Line line1 = appearance.getLine(new Point3D(0, 0, 100), new Point3D(10, 0, 100));
+ * Line line2 = appearance.getLine(new Point3D(0, 10, 100), new Point3D(10, 10, 100));
+ *
+ * // Override color for a specific line
+ * Line blueLine = appearance.getLine(p1, p2, Color.BLUE);
+ * }</pre>
*/
public class LineAppearance {
private Color color = new Color(100, 100, 255, 255);
+ /**
+ * Creates a line appearance with default width (1.0) and default color (light blue).
+ */
public LineAppearance() {
lineWidth = 1;
}
+ /**
+ * Creates a line appearance with the specified width and default color (light blue).
+ *
+ * @param lineWidth the line width in world units
+ */
public LineAppearance(final double lineWidth) {
this.lineWidth = lineWidth;
}
+ /**
+ * Creates a line appearance with the specified width and color.
+ *
+ * @param lineWidth the line width in world units
+ * @param color the line color
+ */
public LineAppearance(final double lineWidth, final Color color) {
this.lineWidth = lineWidth;
this.color = color;
}
+ /**
+ * Creates a line between two points using this appearance's width and color.
+ *
+ * @param point1 the starting point of the line
+ * @param point2 the ending point of the line
+ * @return a new Line instance
+ */
public Line getLine(final Point3D point1, final Point3D point2) {
return new Line(point1, point2, color, lineWidth);
}
+ /**
+ * Creates a line between two points using this appearance's width and a custom color.
+ *
+ * @param point1 the starting point of the line
+ * @param point2 the ending point of the line
+ * @param color the color for this specific line (overrides the default)
+ * @return a new Line instance
+ */
public Line getLine(final Point3D point1, final Point3D point2,
final Color color) {
return new Line(point1, point2, color, lineWidth);
}
+ /**
+ * Returns the line width configured for this appearance.
+ *
+ * @return the line width in world units
+ */
public double getLineWidth() {
return lineWidth;
}
private int width;
private double dinc;
+ /**
+ * Creates a new line interpolator with uninitialized endpoints.
+ */
+ public LineInterpolator() {
+ }
+
+ /**
+ * Checks if the given Y coordinate falls within the vertical span of this line.
+ *
+ * @param y the Y coordinate to test
+ * @return {@code true} if y is between y1 and y2 (inclusive)
+ */
public boolean containsY(final int y) {
if (y1 < y2) {
return false;
}
+ /**
+ * Returns the depth value (d) at the current Y position.
+ *
+ * @return the interpolated depth value
+ */
public double getD() {
return d;
}
+ /**
+ * Computes the X coordinate for the given Y position.
+ *
+ * @param y the Y coordinate
+ * @return the interpolated X coordinate
+ */
public int getX(final int y) {
if (height == 0)
return (int) (x2 + x1) / 2;
return (int) x1 + ((width * distanceFromY1) / height);
}
+ /**
+ * Sets the endpoints and depth values for this line interpolator.
+ *
+ * @param x1 the X coordinate of the first point
+ * @param y1 the Y coordinate of the first point
+ * @param d1 the depth value at the first point
+ * @param x2 the X coordinate of the second point
+ * @param y2 the Y coordinate of the second point
+ * @param d2 the depth value at the second point
+ */
public void setPoints(final double x1, final double y1, final double d1,
final double x2, final double y2, final double d2) {
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * 3D line segment rendering with perspective-correct width and alpha blending.
+ *
+ * <p>Lines are rendered with width that adjusts based on distance from the viewer.
+ * The rendering uses interpolators for smooth edges and proper alpha blending.</p>
+ *
+ * <p>Key classes:</p>
+ * <ul>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line} - The line shape</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance} - Color and width configuration</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineInterpolator} - Scanline edge interpolation</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line;
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Primitive shape implementations for the rasterization pipeline.
+ *
+ * <p>Basic shapes are the building blocks of 3D scenes. Each can be rendered
+ * independently and combined to create more complex objects.</p>
+ *
+ * <p>Subpackages:</p>
+ * <ul>
+ * <li>{@code line} - 3D line segments with perspective-correct width</li>
+ * <li>{@code solidpolygon} - Solid-color triangles with flat shading</li>
+ * <li>{@code texturedpolygon} - Triangles with UV-mapped textures</li>
+ * </ul>
+ *
+ * <p>Additional basic shapes:</p>
+ * <ul>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.Billboard} - Textures that always face the camera</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.GlowingPoint} - Circular gradient billboards</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.Billboard
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic;
\ No newline at end of file
*/
private int absoluteHeight;
+ /**
+ * Creates a new line interpolator with uninitialized endpoints.
+ */
+ public LineInterpolator() {
+ }
+
@Override
public boolean equals(final Object o) {
if (o == null) return false;
private LightingManager lightingManager;
private boolean backfaceCulling = false;
+ /**
+ * Creates a solid triangle with the specified vertices and color.
+ *
+ * @param point1 the first vertex position
+ * @param point2 the second vertex position
+ * @param point3 the third vertex position
+ * @param color the fill color of the triangle
+ */
public SolidPolygon(final Point3D point1, final Point3D point2,
final Point3D point3, final Color color) {
super(
this.color = color;
}
+ /**
+ * Draws a horizontal scanline between two edge interpolators with alpha blending.
+ *
+ * @param line1 the left edge interpolator
+ * @param line2 the right edge interpolator
+ * @param y the Y coordinate of the scanline
+ * @param renderBuffer the rendering context to draw into
+ * @param color the color to draw with
+ */
public static void drawHorizontalLine(final LineInterpolator line1,
final LineInterpolator line2, final int y,
final RenderingContext renderBuffer, final Color color) {
}
+ /**
+ * Renders a triangle with mouse interaction support and optional backface culling.
+ *
+ * <p>This static method handles:</p>
+ * <ul>
+ * <li>Rounding vertices to integer screen coordinates</li>
+ * <li>Mouse hover detection via point-in-polygon test</li>
+ * <li>Viewport clipping</li>
+ * <li>Scanline rasterization with alpha blending</li>
+ * </ul>
+ *
+ * @param context the rendering context
+ * @param onScreenPoint1 the first vertex in screen coordinates
+ * @param onScreenPoint2 the second vertex in screen coordinates
+ * @param onScreenPoint3 the third vertex in screen coordinates
+ * @param mouseInteractionController optional controller for mouse events, or null
+ * @param color the fill color
+ */
public static void drawPolygon(final RenderingContext context,
final Point2D onScreenPoint1, final Point2D onScreenPoint2,
final Point2D onScreenPoint3,
drawHorizontalLine(b, c, y, context, color);
}
+ /**
+ * Returns the fill color of this polygon.
+ *
+ * @return the polygon color
+ */
public Color getColor() {
return color;
}
+ /**
+ * Sets the fill color of this polygon.
+ *
+ * @param color the new color
+ */
public void setColor(final Color color) {
this.color = color;
}
this.lightingManager = lightingManager;
}
+ /**
+ * Checks if backface culling is enabled for this polygon.
+ *
+ * @return {@code true} if backface culling is enabled
+ */
public boolean isBackfaceCullingEnabled() {
return backfaceCulling;
}
+ /**
+ * Enables or disables backface culling for this polygon.
+ *
+ * <p>When enabled, polygons facing away from the camera (determined by
+ * screen-space winding order) are not rendered.</p>
+ *
+ * @param backfaceCulling {@code true} to enable backface culling
+ */
public void setBackfaceCulling(final boolean backfaceCulling) {
this.backfaceCulling = backfaceCulling;
}
+ /**
+ * Calculates the unit normal vector of this triangle.
+ *
+ * @param result the point to store the normal vector in
+ */
private void calculateNormal(final Point3D result) {
final Point3D v1 = coordinates[0].coordinate;
final Point3D v2 = coordinates[1].coordinate;
result.z = nz;
}
+ /**
+ * Calculates the centroid (geometric center) of this triangle.
+ *
+ * @param result the point to store the center in
+ */
private void calculateCenter(final Point3D result) {
final Point3D v1 = coordinates[0].coordinate;
final Point3D v2 = coordinates[1].coordinate;
result.z = (v1.z + v2.z + v3.z) / 3.0;
}
+ /**
+ * Renders this triangle to the screen.
+ *
+ * <p>This method performs:</p>
+ * <ul>
+ * <li>Backface culling check (if enabled)</li>
+ * <li>Flat shading calculation (if lighting is enabled)</li>
+ * <li>Triangle rasterization using the static drawPolygon method</li>
+ * </ul>
+ *
+ * @param renderBuffer the rendering context containing the pixel buffer
+ */
@Override
public void paint(final RenderingContext renderBuffer) {
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Solid-color triangle rendering with scanline rasterization.
+ *
+ * <p>Solid polygons are the primary building blocks for opaque 3D surfaces.
+ * The rasterizer handles perspective-correct interpolation, alpha blending,
+ * viewport clipping, and optional flat shading.</p>
+ *
+ * <p>Key classes:</p>
+ * <ul>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon} - The solid triangle shape</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.LineInterpolator} - Edge interpolation for scanlines</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon;
\ No newline at end of file
private Point2D texturePoint1;
private Point2D texturePoint2;
+ /**
+ * Creates a new polygon border interpolator.
+ */
+ public PolygonBorderInterpolator() {
+ }
@Override
public boolean equals(final Object o) {
return 0;
}
+ /**
+ * Checks if the given Y coordinate falls within the vertical span of this edge.
+ *
+ * @param y the Y coordinate to test
+ * @return {@code true} if y is within the edge's vertical range
+ */
public boolean containsY(final int y) {
if (onScreenPoint1.y < onScreenPoint2.y) {
return false;
}
+ /**
+ * Returns the interpolated texture X coordinate at the current Y position.
+ *
+ * @return the texture X coordinate
+ */
public double getTX() {
if (onScreenHeight == 0)
return texturePoint1.x + ((textureWidth * distanceFromY1) / onScreenHeight);
}
+ /**
+ * Returns the interpolated texture Y coordinate at the current Y position.
+ *
+ * @return the texture Y coordinate
+ */
public double getTY() {
if (onScreenHeight == 0)
return texturePoint1.y + ((textureHeight * distanceFromY1) / onScreenHeight);
}
+ /**
+ * Returns the interpolated screen X coordinate at the current Y position.
+ *
+ * @return the screen X coordinate
+ */
public int getX() {
if (onScreenHeight == 0)
return (int) (onScreenPoint1.x + ((onScreenWidth * distanceFromY1) / onScreenHeight));
}
+ /**
+ * Sets the current Y coordinate for interpolation.
+ *
+ * @param y the current Y coordinate
+ */
public void setCurrentY(final int y) {
distanceFromY1 = y - onScreenPoint1.y;
}
+ /**
+ * Sets the screen and texture coordinates for this edge.
+ *
+ * @param onScreenPoint1 the first screen-space endpoint
+ * @param onScreenPoint2 the second screen-space endpoint
+ * @param texturePoint1 the texture coordinate for the first endpoint
+ * @param texturePoint2 the texture coordinate for the second endpoint
+ */
public void setPoints(final Point2D onScreenPoint1, final Point2D onScreenPoint2,
final Point2D texturePoint1, final Point2D texturePoint2) {
import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon;
/**
- * Textured polygon.
- * <p>
+ * A textured triangle renderer with perspective-correct texture mapping.
*
- * <pre>
- * This is how perspective-correct texture rendering is implemented:
- * If polygon is sufficiently small, it is rendered without perspective correction.
- * Otherwise, it is sliced into smaller polygons.
- * </pre>
+ * <p>This class renders triangles with UV-mapped textures. For large triangles,
+ * the rendering may be sliced into smaller pieces for better perspective correction.</p>
+ *
+ * <p><b>Perspective-correct texture rendering:</b></p>
+ * <ul>
+ * <li>Small polygons are rendered without perspective correction</li>
+ * <li>Larger polygons are sliced into smaller pieces for accurate perspective</li>
+ * </ul>
+ *
+ * @see Texture
+ * @see Vertex#textureCoordinate
*/
-
public class TexturedPolygon extends AbstractCoordinateShape {
private static final ThreadLocal<PolygonBorderInterpolator[]> INTERPOLATORS =
new PolygonBorderInterpolator(), new PolygonBorderInterpolator(), new PolygonBorderInterpolator()
});
- public final Texture texture;
-
/**
- * If <code>true</code> then polygon borders will be drawn.
- * It is used for debugging purposes.
+ * The texture to apply to this polygon.
*/
- public boolean showBorders = false;
+ public final Texture texture;
+
private boolean backfaceCulling = false;
private double totalTextureDistance = -1;
+ /**
+ * Creates a textured triangle with the specified vertices and texture.
+ *
+ * @param p1 the first vertex (must have textureCoordinate set)
+ * @param p2 the second vertex (must have textureCoordinate set)
+ * @param p3 the third vertex (must have textureCoordinate set)
+ * @param texture the texture to apply
+ */
public TexturedPolygon(Vertex p1, Vertex p2, Vertex p3, final Texture texture) {
super(p1, p2, p3);
this.texture = texture;
}
+ /**
+ * Computes the total UV distance between all texture coordinate pairs.
+ * Used to determine appropriate mipmap level.
+ */
private void computeTotalTextureDistance() {
// compute total texture distance
totalTextureDistance = coordinates[0].textureCoordinate.getDistanceTo(coordinates[1].textureCoordinate);
totalTextureDistance += coordinates[1].textureCoordinate.getDistanceTo(coordinates[2].textureCoordinate);
}
+ /**
+ * Draws a horizontal scanline between two edge interpolators with texture sampling.
+ *
+ * @param line1 the left edge interpolator
+ * @param line2 the right edge interpolator
+ * @param y the Y coordinate of the scanline
+ * @param renderBuffer the rendering context to draw into
+ * @param textureBitmap the texture bitmap to sample from
+ */
private void drawHorizontalLine(final PolygonBorderInterpolator line1,
final PolygonBorderInterpolator line2, final int y,
final RenderingContext renderBuffer,
}
+ /**
+ * Renders this textured triangle to the screen.
+ *
+ * <p>This method performs:</p>
+ * <ul>
+ * <li>Backface culling check (if enabled)</li>
+ * <li>Mouse interaction detection</li>
+ * <li>Mipmap level selection based on screen coverage</li>
+ * <li>Scanline rasterization with texture sampling</li>
+ * </ul>
+ *
+ * @param renderBuffer the rendering context containing the pixel buffer
+ */
@Override
public void paint(final RenderingContext renderBuffer) {
renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController);
// Show polygon boundaries (for debugging)
- if (showBorders)
+ if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders)
showBorders(renderBuffer);
// find top-most point
}
+ /**
+ * Checks if backface culling is enabled for this polygon.
+ *
+ * @return {@code true} if backface culling is enabled
+ */
public boolean isBackfaceCullingEnabled() {
return backfaceCulling;
}
+ /**
+ * Enables or disables backface culling for this polygon.
+ *
+ * @param backfaceCulling {@code true} to enable backface culling
+ */
public void setBackfaceCulling(final boolean backfaceCulling) {
this.backfaceCulling = backfaceCulling;
}
+ /**
+ * Draws the polygon border edges in yellow (for debugging).
+ *
+ * @param renderBuffer the rendering context
+ */
private void showBorders(final RenderingContext renderBuffer) {
final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate;
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Textured triangle rendering with perspective-correct UV mapping.
+ *
+ * <p>Textured polygons apply 2D textures to 3D triangles using UV coordinates.
+ * Large polygons may be sliced into smaller pieces for accurate perspective correction.</p>
+ *
+ * <p>Key classes:</p>
+ * <ul>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon} - The textured triangle shape</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.PolygonBorderInterpolator} - Edge interpolation with UVs</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon;
\ No newline at end of file
/**
* This method should be overridden by anyone wanting to customize shape
* before it is rendered.
+ *
+ * @param transformPipe the current transform stack
+ * @param context the rendering context for the current frame
*/
public void beforeTransformHook(final TransformStack transformPipe,
final RenderingContext context) {
/**
* Paint solid elements of this composite shape into given color.
+ *
+ * @param color the color to apply to all solid sub-shapes
*/
public void setColor(final Color color) {
for (final SubShape subShape : getOriginalSubShapes()) {
}
}
+ /**
+ * Enables or disables backface culling for all SolidPolygon and TexturedPolygon sub-shapes.
+ *
+ * @param backfaceCulling {@code true} to enable backface culling, {@code false} to disable
+ */
public void setBackfaceCulling(final boolean backfaceCulling) {
for (final SubShape subShape : getOriginalSubShapes()) {
final AbstractShape shape = subShape.getShape();
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Base class and utilities for composite shapes.
+ *
+ * <p>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape}
+ * is the foundation for building complex 3D objects by grouping primitives.</p>
+ *
+ * <p>Features:</p>
+ * <ul>
+ * <li>Position and rotation in 3D space</li>
+ * <li>Named groups for selective visibility</li>
+ * <li>Automatic sub-shape management</li>
+ * <li>Integration with lighting and slicing</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.SubShape
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base;
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Composite shapes that group multiple primitives into compound 3D objects.
+ *
+ * <p>Composite shapes allow building complex objects from simpler primitives.
+ * They support grouping, visibility toggling, and hierarchical transformations.</p>
+ *
+ * <p>Subpackages:</p>
+ * <ul>
+ * <li>{@code base} - Base class for all composite shapes</li>
+ * <li>{@code solid} - Solid objects (cubes, spheres, cylinders)</li>
+ * <li>{@code wireframe} - Wireframe objects (boxes, grids, spheres)</li>
+ * <li>{@code textcanvas} - 3D text rendering canvas</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite;
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Solid composite shapes built from SolidPolygon primitives.
+ *
+ * <p>These shapes render as filled surfaces with optional flat shading.
+ * Useful for creating opaque 3D objects like boxes, spheres, and cylinders.</p>
+ *
+ * <p>Key classes:</p>
+ * <ul>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube} - A solid cube</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonRectangularBox} - A solid box</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonSphere} - A solid sphere</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCylinder} - A solid cylinder</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonPyramid} - A solid pyramid</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid;
\ No newline at end of file
*/
private eu.svjatoslav.sixth.e3d.renderer.raster.Color backgroundColor;
+ /**
+ * Creates a canvas character at the specified location with given colors.
+ *
+ * @param centerLocation the center position in 3D space
+ * @param character the character to render
+ * @param foregroundColor the foreground (text) color
+ * @param backgroundColor the background color
+ */
public CanvasCharacter(final Point3D centerLocation, final char character,
final eu.svjatoslav.sixth.e3d.renderer.raster.Color foregroundColor,
final eu.svjatoslav.sixth.e3d.renderer.raster.Color backgroundColor) {
}
/**
- * Returns color of the background.
+ * Returns the background color of the character.
+ *
+ * @return the background color
*/
public eu.svjatoslav.sixth.e3d.renderer.raster.Color getBackgroundColor() {
return backgroundColor;
}
/**
- * Sets color of the background.
+ * Sets the background color of the character.
+ *
+ * @param backgroundColor the new background color
*/
public void setBackgroundColor(
final eu.svjatoslav.sixth.e3d.renderer.raster.Color backgroundColor) {
}
+ /**
+ * Sets the character value to render.
+ *
+ * @param value the new character value
+ */
public void setValue(final char value) {
this.value = value;
}
public static final int FONT_CHAR_HEIGHT_TEXTURE_PIXELS = 32;
+ /**
+ * The default font used for rendering text on the canvas.
+ */
public static final Font FONT = CanvasCharacter.getFont((int) (FONT_CHAR_HEIGHT_TEXTURE_PIXELS / 1.066));
private static final String GROUP_TEXTURE = "texture";
private static final String GROUP_CHARACTERS = "characters";
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Wireframe composite shapes built from Line primitives.
+ *
+ * <p>These shapes render as edge-only outlines, useful for visualization,
+ * debugging, and architectural-style rendering.</p>
+ *
+ * <p>Key classes:</p>
+ * <ul>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.WireframeBox} - A wireframe box</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.WireframeCube} - A wireframe cube</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.WireframeSphere} - A wireframe sphere</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.Grid2D} - A 2D grid plane</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.Grid3D} - A 3D grid volume</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.WireframeBox
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe;
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Renderable shape classes for the rasterization pipeline.
+ *
+ * <p>This package contains the shape hierarchy used for 3D rendering:</p>
+ * <ul>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape} - Base class for all shapes</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape} - Base for shapes with vertices</li>
+ * </ul>
+ *
+ * <p>Subpackages organize shapes by type:</p>
+ * <ul>
+ * <li>{@code basic} - Primitive shapes (lines, polygons, billboards)</li>
+ * <li>{@code composite} - Compound shapes built from primitives (boxes, grids, text)</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes;
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Geometry slicing for perspective-correct texture rendering.
+ *
+ * <p>Large textured polygons are subdivided into smaller triangles to ensure
+ * accurate perspective correction. This package provides the recursive subdivision
+ * algorithm used by composite shapes.</p>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.slicer.BorderLine
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.slicer;
\ No newline at end of file
}
/**
- * A helper class that accumulates color values for a given area of a bitmap
+ * A helper class that accumulates color values for a given area of a bitmap.
*/
public static class ColorAccumulator {
- public int r, g, b, a;
-
+ /** Accumulated red component. */
+ public int r;
+ /** Accumulated green component. */
+ public int g;
+ /** Accumulated blue component. */
+ public int b;
+ /** Accumulated alpha component. */
+ public int a;
+
+ /** Number of pixels accumulated. */
public int pixelCount = 0;
+ /**
+ * Creates a new color accumulator with zero values.
+ */
+ public ColorAccumulator() {
+ }
+
/**
* Accumulates the color values of the given pixel
*
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Texture support with mipmap chains for level-of-detail rendering.
+ *
+ * <p>Textures provide 2D image data that can be mapped onto polygons. The mipmap
+ * system automatically generates scaled versions for efficient rendering at
+ * various distances.</p>
+ *
+ * <p>Key classes:</p>
+ * <ul>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture} - Main texture class with mipmap support</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureBitmap} - Raw pixel data for a single mipmap level</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.texture;
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+/**
+ * Unit tests for the text editor component.
+ *
+ * <p>Tests for {@link eu.svjatoslav.sixth.e3d.gui.textEditorComponent.TextLine}
+ * and related text processing functionality.</p>
+ */
+
+package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
\ No newline at end of file