-# Project Overview
+# Sixth 3D Engine - Quick Reference
+
+Software-based 3D rendering engine (no OpenGL/DirectX). Pure Java rasterizer with texture support, lighting, CSG operations, and camera navigation.
+
+---
+
+# Quick Lookup: "I Want To..."
+
+| Task | Class (path) | Key Constructor/Method |
+|---------------------------------|-----------------------------------------------------|-----------------------------------------------------------|
+| **Create a window** | `ViewFrame` (`gui/ViewFrame.java`) | `new ViewFrame()` → `.getViewPanel()` |
+| **Add shapes to scene** | `ShapeCollection` (`raster/ShapeCollection.java`) | `viewPanel.getRootShapeCollection().addShape(shape)` |
+| **Position camera** | `Camera` (`gui/Camera.java`) | `camera.getTransform().setTranslation(Point3D)` |
+| **Create a wireframe cube** | `WireframeCube` (`shapes/composite/wireframe/`) | `new WireframeCube(center, halfSize, appearance)` |
+| **Create a solid cube** | `SolidPolygonCube` (`shapes/composite/solid/`) | `new SolidPolygonCube(center, halfSize, color)` |
+| **Create a line** | `Line` (`shapes/basic/line/`) | `new Line(p1, p2, color, width)` |
+| **Create a polygon** | `SolidPolygon` (`shapes/basic/solidpolygon/`) | `SolidPolygon.triangle(...)` or `.quad(...)` |
+| **Create a sphere (wireframe)** | `WireframeSphere` (`shapes/composite/wireframe/`) | `new WireframeSphere(center, radius, appearance)` |
+| **Create a sphere (solid)** | `SolidPolygonSphere` (`shapes/composite/solid/`) | `new SolidPolygonSphere(center, radius, segments, color)` |
+| **Create text in 3D** | `TextCanvas` (`shapes/composite/textcanvas/`) | `new TextCanvas(transform, text, fgColor, bgColor)` |
+| **Add a light source** | `LightSource` (`raster/lighting/`) | `lighting.addLight(new LightSource(pos, color))` |
+| **Enable shading** | `AbstractCompositeShape` (`shapes/composite/base/`) | `shape.setShadingEnabled(true)` |
+| **CSG: subtract** | `AbstractCompositeShape` (`shapes/composite/base/`) | `shape.subtract(otherShape)` |
+| **CSG: union** | `AbstractCompositeShape` (`shapes/composite/base/`) | `shape.union(otherShape)` |
+| **CSG: intersect** | `AbstractCompositeShape` (`shapes/composite/base/`) | `shape.intersect(otherShape)` |
+| **Animate per-frame** | `FrameListener` (`gui/FrameListener.java`) | `viewPanel.addFrameListener((panel, deltaMs) -> {...})` |
+| **Handle mouse clicks** | `MouseInteractionController` (`gui/humaninput/`) | `shape.setMouseInteractionController(controller)` |
+| **Position/rotate shape** | `AbstractCompositeShape` (`shapes/composite/base/`) | `new AbstractCompositeShape(location)` |
+| **Hide/show shape groups** | `ShapeCollection` (`raster/ShapeCollection.java`) | `.hideGroup("debug")` / `.showGroup("debug")` |
+| **Create a billboard** | `Billboard` (`shapes/basic/`) | `new Billboard(position, scale, texture)` |
+| **Create a glowing point** | `GlowingPoint` (`shapes/basic/`) | `new GlowingPoint(position, scale, color)` |
+
+---
+
+# Code Examples
+
+## Basic Scene Setup
-sixth-3d-engine is a Java-based 3D rendering engine. It provides:
+```java
+import eu.svjatoslav.sixth.e3d.gui.ViewFrame;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+// Create window with 3D view
+ViewFrame frame = new ViewFrame();
+ViewPanel viewPanel = frame.getViewPanel();
+ShapeCollection scene = viewPanel.getRootShapeCollection();
+
+// Position camera (behind origin, looking forward)
+viewPanel.getCamera().getTransform().setTranslation(new Point3D(0, 0, -200));
+
+// Add shapes here...
+// scene.addShape(...);
+```
+
+## Creating Shapes
+
+```java
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.*;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.*;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.*;
+
+// Wireframe shapes (use LineAppearance for color/width)
+LineAppearance appearance = new LineAppearance(2.0, Color.CYAN);
+scene.addShape(new WireframeCube(new Point3D(0, 0, 200), 50, appearance));
+scene.addShape(new WireframeSphere(new Point3D(100, 0, 300), 40, appearance));
+scene.addShape(new WireframeBox(p1, p2, appearance));
+
+// Solid shapes (use Color directly)
+scene.addShape(new SolidPolygonCube(new Point3D(0, 0, 200), 50, Color.GREEN));
+scene.addShape(new SolidPolygonSphere(new Point3D(100, 0, 300), 40, 16, Color.RED));
+
+// Simple line
+scene.addShape(new Line(
+ new Point3D(-50, 0, 100),
+ new Point3D(50, 0, 100),
+ Color.YELLOW, 3.0
+));
+
+// Polygon (triangle or quad)
+scene.addShape(SolidPolygon.triangle(
+ new Point3D(0, 0, 0),
+ new Point3D(50, 0, 0),
+ new Point3D(25, 50, 0),
+ Color.BLUE
+));
+scene.addShape(SolidPolygon.quad(
+ new Point3D(-50, -50, 0),
+ new Point3D(50, -50, 0),
+ new Point3D(50, 50, 0),
+ new Point3D(-50, 50, 0),
+ Color.WHITE
+));
+```
+
+## Custom Composite Shape
+
+```java
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+AbstractCompositeShape myShape = new AbstractCompositeShape(new Point3D(0, 0, 200));
+myShape.addShape(new Line(p1, p2, Color.RED, 2.0));
+myShape.addShape(new SolidPolygonCube(Point3D.origin(), 10, Color.BLUE));
+scene.addShape(myShape);
+```
+
+## Lighting and Shading
+
+```java
+import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.*;
-- 3D geometry primitives (points, boxes, circles, polygons)
-- A rasterization-based renderer with texture support
-- An octree-based volume renderer with ray tracing
-- A GUI framework built on Java Swing (JPanel) with camera navigation
-- Composite and primitive shape rendering (lines, solid polygons, textured polygons, wireframes)
-- A text editor component rendered in 3D space
-- Human input device (HID) tracking for mouse and keyboard
+LightingManager lighting = viewPanel.getLightingManager();
+lighting.addLight(new LightSource(new Point3D(100, -100, 200), Color.YELLOW));
+lighting.setAmbientLight(new Color(20, 20, 20));
-# Documentation
+// Enable shading on solid shapes
+SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 50, 16, Color.RED);
+sphere.setShadingEnabled(true);
+scene.addShape(sphere);
+```
-Extensive documentation is available in Org mode format under `doc/`:
+## CSG Operations (Boolean Operations)
-| Path | Topic |
-|----------------------------------------------|-------------------------------------------------------------------------------------------------|
-| `doc/index.org` | Main documentation: engine intro, coordinate system, shapes, CSG overview, developer tools |
-| `doc/rendering-loop/index.org` | Rendering pipeline: 5 phases (transform→sort→paint→blit), multi-threaded paint, frame listeners |
-| `doc/shading/index.org` | Lighting: Lambert cosine law, light sources, ambient light, distance attenuation |
-| `doc/csg/index.org` | Boolean operations: subtract/union/intersect via BSP trees, polygon clipping |
-| `doc/perspective-correct-textures/index.org` | Tessellation for perspective-correct texture mapping (adaptive subdivision) |
-| `doc/frustum-culling/index.org` | View frustum culling: 6 planes, AABB intersection tests, scene design tips |
-| `TODO.org` | Project roadmap: planned features, performance improvements, demo ideas |
+```java
+// Create two shapes
+SolidPolygonCube box = new SolidPolygonCube(new Point3D(0, 0, 200), 50, Color.GREEN);
+SolidPolygonSphere sphere = new SolidPolygonSphere(new Point3D(0, 0, 200), 35, 16, Color.RED);
-# Repository Structure
+// Subtract sphere from box (creates a box with spherical hole)
+box.subtract(sphere);
+scene.addShape(box);
- src/main/java/eu/svjatoslav/sixth/e3d/
- ├── geometry/ — Core geometry: Point2D, Point3D, Box, Circle, Polygon
- ├── math/ — Math utilities: Rotation, Transform, TransformStack, Vertex, DiamondSquare
- ├── gui/ — GUI framework: ViewPanel (Swing), Camera, keyboard/mouse input
- │ ├── humaninput/ — Mouse/keyboard event handling
- │ └── textEditorComponent/ — 3D text editor widget
- └── renderer/
- ├── octree/ — Octree volume representation and ray tracer
- └── raster/ — Rasterization pipeline
- ├── shapes/
- │ ├── basic/ — Primitive shapes: Line, SolidPolygon, TexturedTriangle
- │ └── composite/ — Composite shapes: AbstractCompositeShape, TextCanvas,
- │ WireframeBox, SolidPolygonRectangularBox
- ├── tessellation/ — Triangle tessellation for perspective-correct rendering
- └── texture/ — Texture and TextureBitmap with mipmap support
+// Union: combine shapes
+// box.union(sphere);
-# Build & Test Commands
+// Intersect: keep only overlapping parts
+// box.intersect(sphere);
+```
-## Build System
+## Text in 3D
-- **Build tool:** Maven
-- **Java version:** 21
-- **Build command:** `mvn clean install`
+```java
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas;
+import eu.svjatoslav.sixth.e3d.gui.TextPointer;
+import eu.svjatoslav.sixth.e3d.math.Transform;
+
+// Create text canvas
+Transform location = new Transform(new Point3D(0, 0, 500));
+TextCanvas canvas = new TextCanvas(location, "Hello World!", Color.WHITE, Color.BLACK);
+scene.addShape(canvas);
+
+// Or create blank canvas and write to it
+TextCanvas blank = new TextCanvas(location, new TextPointer(10, 40), Color.GREEN, Color.BLACK);
+blank.locate(0, 0); // row 0, column 0
+blank.print("Line 1");
+blank.locate(1, 0);
+blank.print("Line 2");
+blank.setForegroundColor(Color.YELLOW);
+blank.putChar('X');
+```
+
+## Animation with FrameListener
-## Testing
+```java
+viewPanel.addFrameListener((panel, deltaMs) -> {
+ double rotationIncrement = deltaMs * 0.001; // radians per ms
+
+ // Update shape transform
+ currentAngle += rotationIncrement;
+ myShape.setTransform(new Transform(
+ myShape.getLocation(),
+ currentAngle, 0 // yaw, pitch
+ ));
+
+ return true; // return true to request repaint
+});
+```
-- **Test framework:** JUnit 4
-- **Run all tests:** `mvn test`
-- **Run single test class:** `mvn test -Dtest=TextLineTest`
-- **Run specific test method:** `mvn test -Dtest=TextLineTest#testAddIdent`
+## Camera Control
-Test files are located in `src/test/java/` following the same package structure as main code.
+```java
+import eu.svjatoslav.sixth.e3d.math.Quaternion;
-## No Linting
+Camera camera = viewPanel.getCamera();
-- No Checkstyle, PMD, or SpotBugs configured
-- No `.editorconfig` or formatting configuration files present
-- Code formatting follows manual conventions (see below)
+// Set position
+camera.getTransform().setTranslation(new Point3D(100, -50, -300));
-# Code Style Guidelines
+// Set orientation (quaternion from yaw/pitch angles)
+camera.getTransform().getRotation().set(Quaternion.fromAngles(0.5, -0.3));
-## License Header
+// Look at a specific point (convenience method)
+// camera.lookAt(new Point3D(0, 0, 200));
+```
-All Java files must start with this exact header:
+## Mouse Interaction on Shapes
```java
-/*
- * Sixth 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
+import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseEvent;
+
+SolidPolygonCube clickableCube = new SolidPolygonCube(Point3D.origin(), 50, Color.BLUE);
+clickableCube.setMouseInteractionController(new MouseInteractionController() {
+ @Override
+ public void mouseClicked(final MouseEvent event) {
+ System.out.println("Cube clicked at: " + event.coordinate);
+ }
+
+ @Override
+ public void mouseEntered(final MouseEvent event) {
+ clickableCube.setColor(Color.RED);
+ }
+
+ @Override
+ public void mouseExited(final MouseEvent event) {
+ clickableCube.setColor(Color.BLUE);
+ }
+});
+scene.addShape(clickableCube);
```
-## Formatting
+---
-- **Indentation:** 4 spaces (no tabs)
-- **Braces:** K&R style (opening brace on same line)
-- **Line length:** No strict limit, but keep reasonable (~120 chars preferred)
-- **Blank lines:** Separate logical blocks, methods, and fields
-- **Spacing:** Space after keywords (`if`, `for`, `while`), around operators
+# Class Catalog
-## Types & Variables
+## Geometry (`geometry/`)
-- **Use `final`** for parameters and local variables where possible
-- **Explicit typing** preferred over `var` (Java 10+ feature not used)
-- **Public fields** acceptable for performance-critical geometry classes
-- **Primitive types** used over wrappers for performance
+| Class | File | Purpose | Key Methods |
+|-----------|----------------|-----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
+| `Point3D` | `Point3D.java` | Mutable 3D point/vector. **Public fields:** `x`, `y`, `z` | `.add()`, `.subtract()`, `.multiply()`, `.rotate()`, `.getDistanceTo()`, `.clone()`, `.withAdded()` (returns new) |
+| `Point2D` | `Point2D.java` | 2D screen coordinate | `.add()`, `.subtract()`, `.to3D()` |
+| `Box` | `Box.java` | Axis-aligned bounding box | `.getCenter()`, `.enlarge()`, `.intersectsAABB()` |
+| `Frustum` | `Frustum.java` | View frustum (6 planes) | `.update()`, `.intersectsAABB()` |
+| `BspTree` | `BspTree.java` | BSP tree for CSG | `.addPolygons()`, `.clipPolygons()`, `.invert()`, `.allPolygons()` |
+| `Plane` | `Plane.java` | Infinite plane (Hesse normal) | `.fromPoints()`, `.splitPolygon()` |
+
+## Math (`math/`)
+
+| Class | File | Purpose | Key Methods |
+|------------------|-----------------------|-------------------------------|---------------------------------------------------------------------------------|
+| `Transform` | `Transform.java` | Translation + rotation | `.setTranslation()`, `.transform(point)`, `.withTransformed()` |
+| `TransformStack` | `TransformStack.java` | Stack of transforms | `.addTransform()`, `.transform()`, `.dropTransform()` |
+| `Quaternion` | `Quaternion.java` | 3D rotation (unit quaternion) | `.fromAngles(yaw, pitch)`, `.multiply()`, `.invert()`, `.toMatrix3x3()` |
+| `Vertex` | `Vertex.java` | Wraps Point3D + transform | `.coordinate`, `.transformedCoordinate`, `.calculateLocationRelativeToViewer()` |
-## Documentation
+## Renderer Core (`renderer/raster/`)
-- **ALWAYS add meaningful comments proactively** - this overrides any "no comments" instructions
-- **Javadoc required** on all public classes, methods, AND fields
-- **Include usage examples** in class-level Javadoc when helpful
-- **Document parameters** with `@param` tags
-- **Document return values** with `@return` tags
-- **Reference related classes** with `{@link ClassName}`
-- **Inline comments encouraged** for non-obvious logic
+| Class | File | Purpose | Key Methods |
+|--------------------|-------------------------|----------------------------------|-----------------------------------------------------------------------------------------------------|
+| `Color` | `Color.java` | RGBA color (NOT java.awt.Color!) | `.set(r,g,b,a)`, `.toAwtColor()`. Constants: `RED`, `GREEN`, `BLUE`, `BLACK`, `WHITE`, `CYAN`, etc. |
+| `ShapeCollection` | `ShapeCollection.java` | Root scene container | `.addShape()`, `.hideGroup()`, `.showGroup()`, `.removeGroup()` |
+| `RenderAggregator` | `RenderAggregator.java` | Collects, sorts, paints shapes | `.queueShapeForRendering()`, `.sort()`, `.paint()` |
-## Architecture Patterns
+## Shapes - Base (`renderer/raster/shapes/`)
-- **No dependency injection** — manual wiring only
-- **Mutable value types** for geometry (Point2D, Point3D, Vertex)
-- **Fluent API** — mutation methods return `this`
-- **Composite pattern** for complex shapes (AbstractCompositeShape)
-- **Strategy pattern** for rendering (RenderAggregator)
+| Class | File | Purpose |
+|---------------------------|---------------------------------------|-----------------------------------------------------------------|
+| `AbstractShape` | `shapes/AbstractShape.java` | Base class for all shapes. Bounding box caching. |
+| `AbstractCoordinateShape` | `shapes/AbstractCoordinateShape.java` | Base for shapes with vertices. Has `List<Vertex>`, `onScreenZ`. |
-# Architecture & Key Concepts
+## Shapes - Basic (`shapes/basic/`)
+
+| Class | File | Purpose | Constructor |
+|--------------------|-----------------------------------------------|------------------------------|---------------------------------------------------------|
+| `Line` | `basic/line/Line.java` | 3D line segment | `new Line(p1, p2, color, width)` |
+| `LineAppearance` | `basic/line/LineAppearance.java` | Factory for consistent lines | `new LineAppearance(width, color)` → `.getLine(p1, p2)` |
+| `SolidPolygon` | `basic/solidpolygon/SolidPolygon.java` | Solid convex N-gon | `SolidPolygon.triangle(...)` or `.quad(...)` |
+| `TexturedTriangle` | `basic/texturedpolygon/TexturedTriangle.java` | Textured triangle with UV | `new TexturedTriangle(v1,v2,v3,texture)` |
+| `Billboard` | `basic/Billboard.java` | Texture facing camera | `new Billboard(position, scale, texture)` |
+| `GlowingPoint` | `basic/GlowingPoint.java` | Glowing circular point | `new GlowingPoint(position, scale, color)` |
+
+## Shapes - Composite Wireframe (`shapes/composite/wireframe/`)
+
+| Class | Constructor |
+|---------------------|-------------------------------------------------------------|
+| `WireframeCube` | `new WireframeCube(center, halfSize, appearance)` |
+| `WireframeBox` | `new WireframeBox(corner1, corner2, appearance)` |
+| `WireframeSphere` | `new WireframeSphere(center, radius, appearance)` |
+| `WireframeCylinder` | `new WireframeCylinder(center, radius, height, appearance)` |
+| `WireframeCone` | `new WireframeCone(center, radius, height, appearance)` |
+| `WireframeArrow` | `new WireframeArrow(start, end, appearance)` |
+| `Grid2D` | 2D grid plane |
+| `Grid3D` | 3D grid in space |
-## Coordinate System
+## Shapes - Composite Solid (`shapes/composite/solid/`)
-Sixth 3D uses a **left-handed coordinate system** matching standard 2D screen coordinates:
+| Class | Constructor |
+|------------------------------|-----------------------------------------------------------|
+| `SolidPolygonCube` | `new SolidPolygonCube(center, halfSize, color)` |
+| `SolidPolygonRectangularBox` | `new SolidPolygonRectangularBox(corner1, corner2, color)` |
+| `SolidPolygonSphere` | `new SolidPolygonSphere(center, radius, segments, color)` |
+| `SolidPolygonCylinder` | Solid cylinder |
+| `SolidPolygonCone` | Solid cone |
+| `SolidPolygonArrow` | Solid arrow |
-| Axis | Positive Direction | Meaning |
-|------|--------------------|----------------------------------|
-| X | RIGHT | Larger X = further right |
-| Y | DOWN | Smaller Y = higher visually (up) |
-| Z | AWAY from viewer | Negative Z = closer to camera |
+## Shapes - Composite Base (`shapes/composite/base/`)
-**Important positioning rules:**
+| Class | File | Purpose | Key Methods |
+|--------------------------|------------------------------------|-----------------------|-------------------------------------------------------------------------------------------------|
+| `AbstractCompositeShape` | `base/AbstractCompositeShape.java` | Group shapes, CSG ops | `.addShape()`, `.subtract()`, `.union()`, `.intersect()`, `.setShadingEnabled()`, `.setColor()` |
-- To place object A **above** object B, give A a **smaller Y value** (`y - offset`)
-- To place object A **below** object B, give A a **larger Y value** (`y + offset`)
-- This is the opposite of many 3D engines (OpenGL, Unity, Blender) which use Y-up
+## Text (`shapes/composite/textcanvas/`)
-**Common mistake:** If you're used to Y-up engines, you may accidentally place elements above when you intend below (or
-vice versa). Always verify: positive Y = down in Sixth 3D.
+| Class | File | Purpose | Key Methods |
+|--------------|------------------------------|-----------------|-----------------------------------------------------------------------------------|
+| `TextCanvas` | `textcanvas/TextCanvas.java` | Text grid in 3D | `.print()`, `.locate(row,col)`, `.clear()`, `.setForegroundColor()`, `.putChar()` |
-- `Point2D` and `Point3D` are mutable value types with public fields (`x`, `y`, `z`)
-- Points support fluent/chaining API — mutation methods return `this`
-- `Vertex` wraps a `Point3D` and adds `transformedCoordinate` for viewer-relative positioning
+## Texture (`renderer/raster/texture/`)
-## Transform Pipeline
+| Class | File | Purpose |
+|--------------------|---------------------------------|----------------------------------------------|
+| `Texture` | `texture/Texture.java` | 2D texture with mipmaps |
+| `TextureBitmap` | `texture/TextureBitmap.java` | Raw pixel array for one mipmap level |
+| `TextureGenerator` | `texture/TextureGenerator.java` | Factory for common textures (glows, borders) |
-- `TransformStack` holds an array of `Transform` objects (translation + orientation)
-- `Rotation` stores XZ and YZ rotation angles with precomputed sin/cos
-- Shapes implement `transform(TransformStack, RenderAggregator)` to project themselves
+## Lighting (`renderer/raster/lighting/`)
+
+| Class | File | Purpose | Key Methods |
+|-------------------|---------------------------------|----------------|-----------------------------------------------------------|
+| `LightingManager` | `lighting/LightingManager.java` | Manages lights | `.addLight()`, `.setAmbientLight()`, `.computeLighting()` |
+| `LightSource` | `lighting/LightSource.java` | Point light | `new LightSource(position, color)` → `.setIntensity()` |
+
+## GUI (`gui/`)
+
+| Class | File | Purpose | Key Methods |
+|-----------------|--------------------------|------------------------------|--------------------------------------------------------------------------------------------------------|
+| `ViewPanel` | `gui/ViewPanel.java` | AWT Canvas, render loop | `.getRootShapeCollection()`, `.getCamera()`, `.getLightingManager()`, `.addFrameListener()`, `.stop()` |
+| `ViewFrame` | `gui/ViewFrame.java` | JFrame wrapper | `new ViewFrame()` → `.getViewPanel()` |
+| `Camera` | `gui/Camera.java` | Viewer position/orientation | `.getTransform()`, `.setTransform()` |
+| `FrameListener` | `gui/FrameListener.java` | Per-frame callback interface | `.onFrame(panel, deltaMs)` → return true to repaint |
+
+## Input (`gui/humaninput/`)
+
+| Class | File | Purpose |
+|------------------------------|----------------------------------------------|--------------------------------|
+| `InputManager` | `humaninput/InputManager.java` | Mouse/keyboard tracking |
+| `MouseInteractionController` | `humaninput/MouseInteractionController.java` | Interface for clickable shapes |
+| `KeyboardFocusStack` | `humaninput/KeyboardFocusStack.java` | Focus management for widgets |
+
+## Octree Renderer (`renderer/octree/`)
+
+Alternative rendering path for voxel volumes with ray tracing.
+
+| Class | File | Purpose |
+|----------------|-----------------------------------|-----------------------------|
+| `OctreeVolume` | `octree/OctreeVolume.java` | Sparse voxel octree storage |
+| `RayTracer` | `octree/raytracer/RayTracer.java` | Ray tracing renderer |
+
+---
+
+# Architecture & Key Concepts
+
+## Coordinate System (CRITICAL)
+
+Sixth 3D uses **left-handed coordinates** matching 2D screen space:
+
+| Axis | Positive = | Example |
+|------|-------------|----------------------------------|
+| X | RIGHT | Larger X = further right |
+| Y | DOWN | Smaller Y = higher (up visually) |
+| Z | INTO screen | Negative Z = closer to camera |
+
+**To place A ABOVE B:** give A a **smaller Y** (`y - offset`)
+**To place A BELOW B:** give A a **larger Y** (`y + offset`)
+
+This is opposite to Y-up engines (OpenGL, Unity, Blender).
## Shape Hierarchy
-- `AbstractShape` — base class with optional `MouseInteractionController`
-- `AbstractCoordinateShape` — has `List<Vertex>` coordinates and `onScreenZ` for depth sorting
-- `AbstractCompositeShape` — groups sub-shapes with group IDs and visibility toggles
-- Concrete shapes: `Line`, `SolidPolygon`, `TexturedTriangle`, `TextCanvas`, `WireframeBox`
+```
+AbstractShape (base)
+ ├── AbstractCoordinateShape (has vertices)
+ │ ├── Line
+ │ ├── SolidPolygon
+ │ ├── TexturedTriangle
+ │ ├── Billboard
+ │ └── GlowingPoint
+ └── AbstractCompositeShape (groups shapes)
+ ├── Wireframe shapes (WireframeCube, WireframeSphere, ...)
+ ├── Solid shapes (SolidPolygonCube, SolidPolygonSphere, ...)
+ └── TextCanvas
+```
+
+## Render Pipeline
+
+```
+ViewPanel.renderFrame()
+ 1. ShapeCollection.transformShapes() — apply camera transform
+ 2. ShapeCollection.sortShapes() — sort by Z (back-to-front)
+ 3. ShapeCollection.paintShapes() — painter's algorithm
+ 4. BufferStrategy.show() — page flip to display
+```
+
+- Shapes implement `transform()` to project from world to screen space
+- Shapes implement `paint()` to rasterize to pixel buffer
+- `onScreenZ` determines render order (set during transform phase)
-## Rendering
+## Backface Culling
-- `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 signed area in screen space: `signedArea < 0` = front-facing
+Uses signed area in screen space:
+- `signedArea < 0` → front-facing (CCW winding)
+- `signedArea > 0` → back-facing (CW winding)
-## Color
+Vertex order for front face: **top → lower-left → lower-right** (as seen from camera)
-- Use project's `eu.svjatoslav.sixth.e3d.renderer.raster.Color` (NOT `java.awt.Color`)
-- RGBA with int components (0–255), predefined constants (RED, GREEN, BLUE, etc.)
-- Provides `toAwtColor()` for AWT interop
+---
+
+# Build & Test
+
+```bash
+# Build
+mvn clean install
+
+# Run all tests
+mvn test
+
+# Run single test class
+mvn test -Dtest=TextLineTest
+
+# Run specific test method
+mvn test -Dtest=TextLineTest#testAddIdent
+```
-## GUI / Input
+Test files: `src/test/java/` (JUnit 4)
-- `Camera` represents viewer position and orientation
-- `InputManager` processes mouse/keyboard events
-- `MouseInteractionController` interface allows shapes to respond to input
-- `KeyboardFocusStack` manages keyboard input focus
+---
# Tips for AI Agents
-1. **Creating shapes:** Extend `AbstractCoordinateShape` for simple geometry or `AbstractCompositeShape` for compounds
-2. **Always use project Color:** `eu.svjatoslav.sixth.e3d.renderer.raster.Color`, never `java.awt.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 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
+1. **Always use project Color:** `eu.svjatoslav.sixth.e3d.renderer.raster.Color` (NOT `java.awt.Color`)
+2. **Point3D is mutable:** Clone before storing references: `point.clone()`
+3. **Y is down:** Remember coordinate system when positioning elements
+4. **SolidPolygon works for quads:** Use `SolidPolygon.quad(p1,p2,p3,p4,color)` - automatically triangulated
+5. **CSG on AbstractCompositeShape:** Only composite shapes support `subtract()`, `union()`, `intersect()`
+6. **Animations via FrameListener:** Return `true` from `onFrame()` to trigger repaint
+7. **Shading needs lighting:** `setShadingEnabled(true)` + add `LightSource` to `LightingManager`
+8. **Wireframe shapes need LineAppearance:** `new LineAppearance(width, Color)` for consistent line styling
+9. **Group visibility:** Use `.addShape(shape, "groupName")` then `.hideGroup()` / `.showGroup()`
+
+---
+
+# Documentation (Org Mode)
+
+| Path | Topic |
+|---------------------------------|-------------------------------------------------------|
+| `doc/index.org` | Main: coordinate system, shapes, CSG, developer tools |
+| `doc/rendering-loop/index.org` | 5-phase pipeline, multi-threaded paint |
+| `doc/shading/index.org` | Lambert shading, lights, distance attenuation |
+| `doc/csg/index.org` | Boolean ops via BSP trees |
+| `doc/frustum-culling/index.org` | View frustum culling |
* Documentation
-:PROPERTIES:
-:CUSTOM_ID: documentation
-:END:
-** Document shading
-
-Make separate demo about that with shaded spheres and some light
-sources.
-
-Make dedicated tutorial about shading algorithm with screenshot and
-what are available parameters.
-
** Document boolean operations
* Add 3D mouse support
+ When there are fast-paced scenes, dynamically and temporarily reduce
image resolution if needed to maintain desired FPS.
+** Make it possible to configure field of view (FOV)
** Explore possibility for implementing better perspective correct textured polygons
** Add X, Y, Z axis indicators
:ID: 8c5e2a1f-9d3b-4f6a-b8e7-1c4d5f7a9b2e
:END:
+Press *F12* anywhere in the application to open the Developer Tools
+panel:
+
#+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 three diagnostic toggles, camera position display, frustum
-culling statistics, and a live log viewer that's always recording.
-
-** Render frame logging (always on)
-:PROPERTIES:
-:CUSTOM_ID: render-frame-logging
-:END:
-
-Render frame diagnostics are always logged to a circular buffer. When you
-open the Developer Tools panel, you can see the complete rendering history.
-
-Log entries include:
-- Abort conditions (bufferStrategy or renderingContext not available)
-- Blit exceptions
-- Buffer contents lost (triggers reinitialization)
-- Render frame exceptions
-
-Use this for:
-- Diagnosing buffer strategy issues (screen tearing, blank frames)
-- Debugging rendering failures
-
** Show polygon borders
:PROPERTIES:
:CUSTOM_ID: show-polygon-borders
:END:
-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.
+When enabled, each [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] draws yellow outlines around its
+three edges after rendering its texture content. This overlays the
+geometric structure of tessellated textured surfaces onto the final
+image.
+
+The feature exists primarily to visualize [[file:perspective-correct-textures/index.org::#visualizing-tessellation][adaptive tessellation]] — the
+recursive subdivision of large triangles into smaller pieces for
+perspective-correct texture mapping. When you enable this option on a
+scene with tessellation active, you can see how the
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/TexturedPolygonTessellator.html][TexturedPolygonTessellator]] has split each original triangle. Dense
+yellow lines indicate high tessellation (many small triangles), while
+sparse lines show low tessellation or triangles that were small enough
+to render without subdivision.
+
+Use this visualization when investigating:
+- Tessellation behavior: verify that near surfaces are subdivided more
+ densely
+- Texture distortion: compare the tessellation density against visible
+ warping in the texture
** Render alternate segments (overdraw debug)
:PROPERTIES:
*Example:*
#+BEGIN_EXAMPLE
-Total: 473 Culled: 425 Culled %: 89.9%
+Total: 473 Culled: 425 (89.9%)
#+END_EXAMPLE
This means 473 composite shapes were tested, 425 were outside the view
Use the *Clear Logs* button to reset the log buffer for fresh
diagnostic captures.
-** API access
-:PROPERTIES:
-:CUSTOM_ID: api-access
-:END:
-
-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;
-tools.showSegmentBoundaries = true;
-#+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
-:PROPERTIES:
-:CUSTOM_ID: technical-details
-:END:
-
-The Developer Tools panel is implemented as a =JFrame= that:
-- Centers on the parent =ViewPanel= window
-- Runs on the Event Dispatch Thread (EDT)
-- Does not block the render loop
-- Automatically closes when parent window closes
-- Updates statistics every 200ms while open
-
-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
:CUSTOM_ID: how-tessellation-works
:END:
-The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/TexturedPolygonTessellator.html][TexturedPolygonTessellator]] class recursively splits triangles:
+The engine uses *screen-space adaptive tessellation* via
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/ScreenSpaceTessellator.html][ScreenSpaceTessellator]]:
#+BEGIN_SRC java
void tessellate(TexturedTriangle polygon) {
- // Find the longest edge
- TessellationEdge longest = findLongestEdge(polygon);
+ // Find the longest edge in screen-space pixels
+ ScreenSpaceEdge longest = findLongestScreenEdge(polygon);
- if (longest.length < maxDistance) {
- // Small enough: add to result
+ if (longest.screenLength <= maxScreenPixels) {
+ // Small enough on screen: add to result
result.add(polygon);
} else {
- // Split at midpoint
- Vertex middle = longest.getMiddlePoint();
+ // Split at midpoint (interpolate world, texture, screen coords)
+ Vertex middle = longest.interpolateMidpoint();
// Recurse on two sub-triangles
tessellate(subTriangle1);
tessellate(subTriangle2);
#+INCLUDE: "triangle-tessellation.svg" export html
-The midpoint is computed by averaging both 3D coordinates *and* texture
-coordinates.
+The midpoint is computed by averaging both 3D coordinates, texture
+coordinates, *and* screen coordinates. This ensures each sub-triangle
+has correct depth sorting.
+** Adaptive Threshold
+
+The tessellation threshold is *adaptive* - it adjusts dynamically based
+on polygon budget:
+
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/AdaptiveTessellationController.html][AdaptiveTessellationController]] tracks polygon count per frame
+- If count exceeds ~1000 polygons, threshold increases (less tessellation)
+- If count is low and threshold above minimum, threshold decreases (better quality)
+- Threshold range: 30-500 pixels
+
+** Safeguards Against Excessive Tessellation
+
+When the camera is very close to a polygon, screen-space coordinates
+can become extremely large (100,000+ pixels). Without safeguards, this
+would produce millions of triangles. The tessellator includes three
+limits:
+
+- Maximum recursion depth (8 levels = max 256 triangles per polygon)
+- Maximum triangles per polygon (hard limit of 200)
+- Maximum edge length for tessellation (10,000 pixels - skip tessellation for huge polygons)
* Visualizing the Tessellation
:PROPERTIES:
:CUSTOM_ID: visualizing-tessellation
+:ID: f4e03f70-c84c-4eaa-90e2-e7d87a68e517
:END:
-Press *F12* to open Developer Tools and enable "Show polygon borders".
+Press *F12* to open [[file:~/data/sync2/workspace/sixth-3d/doc/index.org::#developer-tools][Developer tools]] and enable "Show polygon borders".
This draws yellow outlines around all textured polygons, making the
tessellation visible:
| Class | Purpose |
|-----------------+--------------------------------------|
| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | Textured triangle shape |
-| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/TexturedPolygonTessellator.html][TexturedPolygonTessellator]] | Triangle tessellation for perspective correction |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/ScreenSpaceTessellator.html][ScreenSpaceTessellator]] | Screen-space tessellation for perspective correction |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/AdaptiveTessellationController.html][AdaptiveTessellationController]] | Dynamic threshold adjustment based on polygon budget |
| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.html][Texture]] | Mipmap container with Graphics2D |
| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.html][TextureBitmap]] | Raw pixel array for one mipmap level |
[[file:../index.org][Back to main documentation]]
-* Shading & Lighting
+* Overview
:PROPERTIES:
:CUSTOM_ID: shading-lighting
:END:
[[file:../index.org::#normal-vector][Normal Vector]] section for more details on how normals are computed and used
throughout the engine.
-** Light Sources
+* Light Sources
:PROPERTIES:
:CUSTOM_ID: light-sources
:END:
complex lighting setups like the screenshot above showing a sphere lit
by two lights from the right.
-** Ambient Light
-:PROPERTIES:
-:CUSTOM_ID: ambient-light
-:END:
-
-#+INCLUDE: "ambient-light-comparison.svg" export html
-
-*Ambient light* provides base illumination that affects all surfaces
-equally, regardless of orientation. Without ambient light, surfaces not
-directly facing a light source would be pure black.
-
-- Default ambient: =Color(50, 50, 50)= (dim gray)
-- Configurable via =lightingManager.setAmbientLight()=
-- Too much ambient: flat appearance (no contrast)
-- Too little ambient: harsh shadows (pure black areas)
-
-#+BEGIN_SRC java
-// Increase ambient for softer shadows
-viewPanel.getLightingManager().setAmbientLight(new Color(80, 80, 80));
-
-// Reduce ambient for dramatic contrast
-viewPanel.getLightingManager().setAmbientLight(new Color(20, 20, 20));
-#+END_SRC
-
** Distance Attenuation
:PROPERTIES:
:CUSTOM_ID: distance-attenuation
distance-based dimming. The =0.0001= coefficient was tuned for typical
scene scales in Sixth 3D.
-** Integration with the Render Pipeline
+* Ambient Light
:PROPERTIES:
-:CUSTOM_ID: render-pipeline-integration
+:CUSTOM_ID: ambient-light
:END:
-#+INCLUDE: "shading-pipeline.svg" export html
+#+INCLUDE: "ambient-light-comparison.svg" export html
-Lighting is computed during *Phase 2* (transform phase) of the
-[[file:../rendering-loop/][rendering loop]]:
+*Ambient light* provides base illumination that affects all surfaces
+equally, regardless of orientation. Without ambient light, surfaces not
+directly facing a light source would be pure black.
-1. Each shaded polygon calculates its center point and surface normal
-2. [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes lighting from all sources
-3. Result stored in reusable =shadedColor= field
-4. During *Phase 4* (paint), the cached color is used directly
+- Default ambient: =Color(50, 50, 50)= (dim gray)
+- Configurable via =lightingManager.setAmbientLight()=
+- Too much ambient: flat appearance (no contrast)
+- Too little ambient: harsh shadows (pure black areas)
-**Why during transform phase?**
+#+BEGIN_SRC java
+// Increase ambient for softer shadows
+viewPanel.getLightingManager().setAmbientLight(new Color(80, 80, 80));
-- Transform phase is *single-threaded* — no race conditions
-- Lighting computed *once per polygon per frame* — not per pixel
-- Result reused during multi-threaded paint phase — efficient
+// Reduce ambient for dramatic contrast
+viewPanel.getLightingManager().setAmbientLight(new Color(20, 20, 20));
+#+END_SRC
-** Using Shading in Your Scene
+* Using Shading in Your Scene
:PROPERTIES:
:CUSTOM_ID: using-shading
:END:
[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setShadingEnabled(boolean)][setShadingEnabled(true)]] on a composite enables shading for all its
sub-polygons.
+** Related Classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class | Purpose |
+|-------+---------|
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] | Manages light sources and computes shading |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightSource.html][LightSource]] | Individual light with position, color, intensity |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape with shading support |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Composite shape with shading propagation |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html][ViewPanel]] | Provides access to LightingManager |
+* Implementation details
+:PROPERTIES:
+:CUSTOM_ID: implementation-details
+:END:
+
+#+INCLUDE: "shading-pipeline.svg" export html
+
+Lighting is computed during *Phase 2* (transform phase) of the
+[[file:../rendering-loop/][rendering loop]]:
+
+1. Each shaded polygon calculates its center point and surface normal
+2. [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes lighting from all sources
+3. Result stored in reusable =shadedColor= field
+4. During *Phase 4* (paint), the cached color is used directly
+
+**Why during transform phase?**
+
+- Transform phase is *single-threaded* — no race conditions
+- Lighting computed *once per polygon per frame* — not per pixel
+- Result reused during multi-threaded paint phase — efficient
+
** Performance Characteristics
:PROPERTIES:
:CUSTOM_ID: performance
This approach trades visual fidelity (no per-pixel lighting) for
performance — essential for software rendering where per-pixel lighting
would be prohibitively expensive.
-
-** Related Classes
-:PROPERTIES:
-:CUSTOM_ID: related-classes
-:END:
-
-| Class | Purpose |
-|-------+---------|
-| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] | Manages light sources and computes shading |
-| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightSource.html][LightSource]] | Individual light with position, color, intensity |
-| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape with shading support |
-| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Composite shape with shading propagation |
-| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html][ViewPanel]] | Provides access to LightingManager |
package eu.svjatoslav.sixth.e3d.gui;
import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.AdaptiveTessellationController;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
* <ul>
* <li>Checkboxes to toggle debug settings</li>
* <li>Camera position display with copy button</li>
+ * <li>Composite shape frustum culling statistics</li>
+ * <li>Adaptive tessellation statistics (threshold and polygon count)</li>
* <li>A scrollable log viewer showing captured debug output</li>
* <li>A button to clear the log buffer</li>
* <li>Resizable window with native maximize support</li>
*
* @see DeveloperTools
* @see DebugLogBuffer
+ * @see AdaptiveTessellationController
*/
public class DeveloperToolsPanel extends JFrame {
* The label showing culled percentage.
*/
private final JLabel culledPercentLabel;
+ /**
+ * The label showing current tessellation threshold.
+ */
+ private final JLabel tessellationThresholdLabel;
+ /**
+ * The label showing tessellated polygon count.
+ */
+ private final JLabel tessellationPolygonCountLabel;
/**
* Timer for periodic updates.
*/
culledPercentLabel = new JLabel("0.0%");
culledPercentLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+ // Initialize tessellation statistics labels
+ tessellationThresholdLabel = new JLabel("30.0");
+ tessellationThresholdLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+ tessellationPolygonCountLabel = new JLabel("0");
+ tessellationPolygonCountLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+
final JPanel topPanel = new JPanel();
topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
topPanel.add(createSettingsPanel());
topPanel.add(createCameraPanel());
topPanel.add(createCullingPanel());
+ topPanel.add(createTessellationPanel());
add(topPanel, BorderLayout.NORTH);
logArea = new JTextArea(15, 60);
return panel;
}
+ private JPanel createTessellationPanel() {
+ final JPanel panel = new JPanel();
+ panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+ panel.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createEmptyBorder(0, 8, 8, 8),
+ BorderFactory.createTitledBorder("Adaptive Tessellation")
+ ));
+
+ // Single row: threshold, polygon count
+ final JPanel statsRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2));
+ statsRow.add(new JLabel("Threshold (px):"));
+ statsRow.add(tessellationThresholdLabel);
+ statsRow.add(new JLabel(" Polygons:"));
+ statsRow.add(tessellationPolygonCountLabel);
+
+ panel.add(statsRow);
+
+ return panel;
+ }
+
private JPanel createButtonPanel() {
final JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
try {
updateCameraLabel();
updateCullingStatistics();
+ updateTessellationStatistics();
updateLogDisplay();
} finally {
updating = false;
culledPercentLabel.setText(String.format(" (%.1f%%)", stats.getCulledPercentage()));
}
+ private void updateTessellationStatistics() {
+ final AdaptiveTessellationController controller = AdaptiveTessellationController.getInstance();
+ tessellationThresholdLabel.setText(String.format("%.1f", controller.getThreshold()));
+ tessellationPolygonCountLabel.setText(String.valueOf(controller.getLastPolygonCount()));
+ }
+
private void updateLogDisplay() {
final List<String> entries = debugLogBuffer.getEntries();
final StringBuilder sb = new StringBuilder();
return result;
}
+ /**
+ * Creates a deep copy of this vertex including screen-space coordinates.
+ *
+ * <p>Unlike {@link #clone()}, this method also copies the
+ * {@link #transformedCoordinate} and {@link #onScreenCoordinate}.
+ * Used by the tessellation system to create sub-triangles with pre-computed screen positions.</p>
+ *
+ * @return a new Vertex with cloned data including screen coordinates
+ */
+ public Vertex cloneWithScreenCoords() {
+ final Vertex result = clone();
+
+ result.transformedCoordinate = new Point3D(transformedCoordinate);
+ result.onScreenCoordinate = new Point2D(onScreenCoordinate);
+
+ return result;
+ }
+
/**
* Flips the orientation of this vertex by negating the normal vector.
* Called when the orientation of a polygon is flipped during CSG operations.
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape;
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.SubShape;
+import eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.AdaptiveTessellationController;
import java.util.ArrayList;
import java.util.Collection;
aggregator.reset();
transformStack.clear();
+ // Reset adaptive tessellation counter for this frame
+ AdaptiveTessellationController.getInstance().resetFrameCount();
+
final Camera camera = viewPanel.getCamera();
// Update frustum for this frame (used for frustum culling)
*/
public void paintShapes(final RenderingContext renderingContext) {
aggregator.paintSorted(renderingContext);
+
+ // Update adaptive tessellation threshold based on this frame's polygon count
+ // If threshold changed significantly, invalidate caches to retessellate with new threshold
+ if (AdaptiveTessellationController.getInstance().endFrame()) {
+ rootComposite.setCacheNeedsRebuild(true);
+ }
}
/**
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
+
+/**
+ * A textured triangle with pre-computed screen-space coordinates.
+ *
+ * <p>This class is created by the {@link eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.ScreenSpaceTessellator}
+ * during tessellation. Unlike regular {@link TexturedTriangle}, it skips the vertex transformation
+ * step during rendering because its screen coordinates and Z-depth are already computed.</p>
+ *
+ * <p>This optimization avoids redundant coordinate transformation for tessellated sub-triangles,
+ * improving performance when large polygons are split into many smaller pieces.</p>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.ScreenSpaceTessellator
+ * @see TexturedTriangle
+ */
+public class TessellatedTexturedTriangle extends TexturedTriangle {
+
+ /**
+ * Creates a tessellated textured triangle with pre-computed screen coordinates.
+ *
+ * <p>The vertices' {@link Vertex#onScreenCoordinate} and {@link Vertex#transformedCoordinate}
+ * are already populated, so the {@link #transform} method will skip coordinate calculation.</p>
+ *
+ * @param v1 the first vertex (world coords, texture coords, screen coords pre-computed)
+ * @param v2 the second vertex (world coords, texture coords, screen coords pre-computed)
+ * @param v3 the third vertex (world coords, texture coords, screen coords pre-computed)
+ * @param texture the texture to apply
+ */
+ public TessellatedTexturedTriangle(final Vertex v1, final Vertex v2, final Vertex v3,
+ final Texture texture) {
+ super(v1, v2, v3, texture);
+ }
+
+ /**
+ * Transforms this triangle for rendering by transforming vertices normally.
+ *
+ * <p>Unlike the original design that skipped transformation, this method now
+ * transforms vertices normally using their world coordinates. The world coordinates
+ * were correctly interpolated during tessellation, so the resulting screen coordinates
+ * will be fresh for each frame.</p>
+ *
+ * @param transforms the transform stack to apply
+ * @param aggregator the aggregator to queue this shape for rendering
+ * @param renderingContext the rendering context
+ */
+ @Override
+ public void transform(final eu.svjatoslav.sixth.e3d.math.TransformStack transforms,
+ final RenderAggregator aggregator,
+ final RenderingContext renderingContext) {
+
+ // Transform vertices normally using world coordinates
+ // (world coords were interpolated correctly during tessellation)
+ double accumulatedZ = 0;
+ boolean paint = true;
+
+ for (final Vertex v : vertices) {
+ v.calculateLocationRelativeToViewer(transforms, renderingContext);
+ accumulatedZ += v.transformedCoordinate.z;
+
+ if (!v.transformedCoordinate.isVisible()) {
+ paint = false;
+ }
+ }
+
+ if (paint) {
+ onScreenZ = accumulatedZ / vertices.size();
+ aggregator.queueShapeForRendering(this);
+ }
+ }
+
+ /**
+ * Creates a tessellated triangle by interpolating between two vertices.
+ *
+ * <p>This factory method creates a new vertex at the midpoint of two vertices,
+ * interpolating world coordinates, texture coordinates, screen coordinates,
+ * and Z-depth. Used during tessellation to split edges.</p>
+ *
+ * @param v1 the first vertex endpoint
+ * @param v2 the second vertex endpoint
+ * @param screen1 the screen coordinate of the first vertex
+ * @param screen2 the screen coordinate of the second vertex
+ * @return a new vertex at the midpoint with interpolated properties
+ */
+ public static Vertex interpolateMidpoint(final Vertex v1, final Vertex v2,
+ final Point2D screen1, final Point2D screen2) {
+ final Point3D worldMid = new Point3D().computeMiddlePoint(v1.coordinate, v2.coordinate);
+ final Point2D texMid = new Point2D().setToMiddle(v1.textureCoordinate, v2.textureCoordinate);
+
+ final Vertex result = new Vertex(worldMid, texMid);
+
+ // Interpolate screen coordinates
+ result.onScreenCoordinate = new Point2D(
+ (screen1.x + screen2.x) / 2,
+ (screen1.y + screen2.y) / 2
+ );
+
+ // Interpolate transformed Z
+ result.transformedCoordinate = new Point3D(
+ (v1.transformedCoordinate.x + v2.transformedCoordinate.x) / 2,
+ (v1.transformedCoordinate.y + v2.transformedCoordinate.y) / 2,
+ (v1.transformedCoordinate.z + v2.transformedCoordinate.z) / 2
+ );
+
+ return result;
+ }
+}
\ No newline at end of file
this.backfaceCulling = backfaceCulling;
}
+ /**
+ * The screen-space edge length used for the last tessellation decision.
+ * Used to cache tessellation results - only retessellate when size changes significantly.
+ * A value of 0 indicates this triangle has never been tessellated based on screen-space metrics.
+ */
+ private double lastScreenSpaceSize = 0;
+
+ /**
+ * Returns the screen-space edge length from the last tessellation decision.
+ * Used by AbstractCompositeShape to determine if retessellation is needed.
+ *
+ * @return the last recorded screen-space size, or 0 if never tessellated
+ */
+ public double getLastScreenSpaceSize() {
+ return lastScreenSpaceSize;
+ }
+
+ /**
+ * Updates the recorded screen-space size after tessellation.
+ *
+ * @param size the screen-space edge length that was used for tessellation
+ */
+ public void setLastScreenSpaceSize(final double size) {
+ this.lastScreenSpaceSize = size;
+ }
+
+ /**
+ * Checks if this triangle has valid screen coordinates from a previous frame.
+ * Used to determine if screen-space tessellation can be applied.
+ *
+ * @return {@code true} if at least one vertex has non-origin screen coordinates
+ */
+ public boolean hasPreviousScreenCoordinates() {
+ // Check if any vertex has been transformed (screen coords not at origin)
+ final Point2D screen1 = vertices.get(0).onScreenCoordinate;
+ return screen1.x != 0 || screen1.y != 0;
+ }
+
+ /**
+ * Computes the maximum screen-space edge length from current vertex screen coordinates.
+ * Used to determine tessellation level based on visual size.
+ *
+ * @return the longest edge length in screen pixels
+ */
+ public double computeMaxScreenEdgeLength() {
+ final Point2D s1 = vertices.get(0).onScreenCoordinate;
+ final Point2D s2 = vertices.get(1).onScreenCoordinate;
+ final Point2D s3 = vertices.get(2).onScreenCoordinate;
+
+ final double d1 = s1.getDistanceTo(s2);
+ final double d2 = s2.getDistanceTo(s3);
+ final double d3 = s3.getDistanceTo(s1);
+
+ return Math.max(d1, Math.max(d2, d3));
+ }
+
+ /**
+ * Determines if retessellation is needed based on screen-space size change.
+ * Uses the same 1.5x ratio threshold as AbstractCompositeShape uses for LOD factors.
+ *
+ * @param currentScreenSize the current screen-space edge length
+ * @return {@code true} if retessellation should be performed
+ */
+ public boolean needsRetessellation(final double currentScreenSize) {
+ if (lastScreenSpaceSize == 0) {
+ // Never tessellated - need to tessellate now
+ return true;
+ }
+
+ final double larger = Math.max(currentScreenSize, lastScreenSpaceSize);
+ final double smaller = Math.min(currentScreenSize, lastScreenSpaceSize);
+
+ // Retessellate if size changed by more than 1.5x ratio
+ return (larger / smaller) > 1.5;
+ }
+
/**
* Draws the triangle border edges in yellow (for debugging).
*
* Textured triangle rendering with perspective-correct UV mapping.
*
* <p>Textured triangles apply 2D textures to 3D triangles using UV coordinates.
- * Large triangles may be tessellated into smaller pieces for accurate perspective correction.</p>
+ * Screen-space tessellation is applied automatically during the transform phase
+ * for better perspective correction on large triangles.</p>
*
* <p>Key classes:</p>
* <ul>
- * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle} - The textured triangle shape</li>
- * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.PolygonBorderInterpolator} - Edge interpolation with UVs</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle} -
+ * The base textured triangle with screen-space tessellation</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TessellatedTexturedTriangle} -
+ * Pre-transformed sub-triangle created during tessellation</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.TexturedTriangle
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TessellatedTexturedTriangle
* @see eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture
*/
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line;
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TessellatedTexturedTriangle;
+import eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.AdaptiveTessellationController;
import eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.TexturedPolygonTessellator;
+import eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.ScreenSpaceTessellator;
import java.util.ArrayList;
import java.util.Iterator;
*/
double currentTessellationFactor = 5;
+ /**
+ * Thread-local screen-space tessellator for per-polygon tessellation based on visual size.
+ * Reused across frames to avoid object allocation overhead.
+ * The threshold is updated via setMaxScreenPixels() before each tessellation.
+ */
+ private static final ThreadLocal<ScreenSpaceTessellator> SCREEN_SPACE_TESSELLATOR =
+ ThreadLocal.withInitial(() -> new ScreenSpaceTessellator(
+ AdaptiveTessellationController.MIN_THRESHOLD));
+
/**
* Frame-optimized cache of shapes ready for rendering, derived from {@link #subShapesRegistry}.
*
*/
private List<AbstractShape> cachedRenderList = new ArrayList<>();
+ /**
+ * Number of original TexturedTriangle that were tessellated to produce TessellatedTexturedTriangle in cachedRenderList.
+ * Used to compute "polygons created by tessellation" = TessellatedTexturedTriangle count - this count.
+ */
+ private int originalTessellationInputCount = 0;
+
/**
* Flag indicating whether {@link #cachedRenderList} needs to be rebuilt from {@link #subShapesRegistry}.
*
*
* @param context the rendering context for logging
*/
- private void retessellateIfNeeded(final RenderingContext context) {
+ private void retessellateIfNeeded(final TransformStack transformPipe, final RenderingContext context) {
if (isRootComposite) {
if (cacheNeedsRebuild)
- retessellate(context);
+ retessellate(transformPipe, context);
+ else {
+ // For root composites with TexturedTriangles, check if screen-space sizes changed
+ // This enables per-polygon LOD to adapt when camera moves
+ boolean needsRetessellation = false;
+ for (int i = 0; i < subShapesRegistry.size() && !needsRetessellation; i++) {
+ final SubShape subShape = subShapesRegistry.get(i);
+ if (!subShape.isVisible())
+ continue;
+ final AbstractShape shape = subShape.getShape();
+ if (shape instanceof TexturedTriangle triangle) {
+ // Compute screen coords for this triangle to check size
+ computeScreenCoordsForTriangle(triangle, transformPipe, context);
+ if (triangle.needsRetessellation(triangle.computeMaxScreenEdgeLength())) {
+ needsRetessellation = true;
+ }
+ }
+ }
+ if (needsRetessellation)
+ retessellate(transformPipe, context);
+ }
return;
}
if (isRetessellationNeeded(proposedTessellationFactor, currentTessellationFactor)) {
currentTessellationFactor = proposedTessellationFactor;
- retessellate(context);
+ retessellate(transformPipe, context);
}
}
return this;
}
- /**
- * Sets the cache rebuild flag, forcing {@link #cachedRenderList} to be regenerated.
+/**
+ * Sets the cache rebuild flag on this composite and all nested composites recursively.
*
* <p>Used by {@code ShapeCollection} to trigger retessellate when clearing the scene
* or for other advanced use cases.</p>
*/
public void setCacheNeedsRebuild(final boolean needsRebuild) {
this.cacheNeedsRebuild = needsRebuild;
+ // Propagate to nested composites
+ for (final SubShape subShape : subShapesRegistry) {
+ final AbstractShape shape = subShape.getShape();
+ if (shape instanceof AbstractCompositeShape composite) {
+ composite.setCacheNeedsRebuild(needsRebuild);
+ }
+ }
}
/**
*
* @param context the rendering context for logging, may be {@code null}
*/
- private void retessellate(final RenderingContext context) {
+ private void retessellate(final TransformStack transformPipe, final RenderingContext context) {
cacheNeedsRebuild = false;
final List<AbstractShape> result = new ArrayList<>();
+ originalTessellationInputCount = 0; // Reset before counting
final TexturedPolygonTessellator tessellator = new TexturedPolygonTessellator(currentTessellationFactor);
int texturedPolygonCount = 0;
final AbstractShape shape = subShape.getShape();
- if (shape instanceof TexturedTriangle) {
- tessellator.tessellate((TexturedTriangle) shape);
+ if (shape instanceof TexturedTriangle triangle) {
+ // Compute screen coords before tessellation so ScreenSpaceTessellator
+ // can make accurate decisions based on actual visual size
+ computeScreenCoordsForTriangle(triangle, transformPipe, context);
+
+ final double tessellationThreshold = AdaptiveTessellationController.getInstance().getThreshold();
+
+ final ScreenSpaceTessellator ssTessellator = SCREEN_SPACE_TESSELLATOR.get();
+ ssTessellator.setMaxScreenPixels(tessellationThreshold);
+ ssTessellator.tessellate(triangle);
+ final List<TessellatedTexturedTriangle> ssResult = ssTessellator.getResult();
+ result.addAll(ssResult);
texturedPolygonCount++;
} else if (shape instanceof SolidPolygon polygon) {
final int vertexCount = polygon.getVertexCount();
result.addAll(tessellator.getResult());
+ originalTessellationInputCount = texturedPolygonCount;
cachedRenderList = result;
if (context != null && context.debugLogBuffer != null) {
beforeTransformHook(transformPipe, context);
- retessellateIfNeeded(context);
+ retessellateIfNeeded(transformPipe, context);
// transform rendered subshapes
- for (final AbstractShape shape : cachedRenderList)
+ // Count tessellated polygons in cached render list and subtract originals to get "created by tessellation"
+ int tessellatedPolygonCount = 0;
+ for (final AbstractShape shape : cachedRenderList) {
shape.transform(transformPipe, aggregator, context);
+ if (shape instanceof TessellatedTexturedTriangle) {
+ tessellatedPolygonCount++;
+ }
+ }
+ // Report polygons CREATED by tessellation (output - input originals)
+ final int createdByTessellation = tessellatedPolygonCount - originalTessellationInputCount;
+ if (createdByTessellation > 0) {
+ AdaptiveTessellationController.getInstance().addTessellatedPolygons(createdByTessellation);
+ }
transformPipe.dropTransform();
}
return result;
}
+ /**
+ * Computes screen coordinates for a TexturedTriangle's vertices.
+ * Called before ScreenSpaceTessellator to ensure accurate screen-space size decisions.
+ *
+ * <p>This method transforms each vertex's world coordinates through the transform stack
+ * and projects to screen coordinates, populating the vertex's transformedCoordinate
+ * and onScreenCoordinate fields.</p>
+ *
+ * @param triangle the triangle to compute screen coords for
+ * @param transformPipe the current transform stack
+ * @param context the rendering context (for screen projection parameters)
+ */
+ private void computeScreenCoordsForTriangle(final TexturedTriangle triangle,
+ final TransformStack transformPipe,
+ final RenderingContext context) {
+ for (final Vertex v : triangle.vertices) {
+ // Transform world coords to view space
+ transformPipe.transform(v.coordinate, v.transformedCoordinate);
+ // Project to screen coords
+ v.onScreenCoordinate.x = (v.transformedCoordinate.x / v.transformedCoordinate.z) * context.projectionScale;
+ v.onScreenCoordinate.y = (v.transformedCoordinate.y / v.transformedCoordinate.z) * context.projectionScale;
+ v.onScreenCoordinate.add(context.centerCoordinate);
+ }
+ }
+
}
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.tessellation;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Controls adaptive tessellation thresholds based on polygon budget.
+ *
+ * <p>This controller dynamically adjusts the screen-space tessellation threshold
+ * to keep the number of tessellated polygons approximately within a target budget.
+ * When polygon count exceeds the budget, the threshold is increased (less tessellation,
+ * larger polygons). When below budget and threshold is above minimum, it's decreased
+ * (better quality, smaller polygons).</p>
+ *
+ * <p><b>Frame lifecycle:</b></p>
+ * <ol>
+ * <li>{@link #resetFrameCount()} called at frame start to clear counter</li>
+ * <li>{@link #addTessellatedPolygons(int)} called during transform for each tessellated polygon</li>
+ * <li>{@link #endFrame()} called after painting to adjust threshold based on count</li>
+ * </ol>
+ *
+* <p><b>Adjustment logic:</b></p>
+ * <ul>
+ * <li>If polygonCount exceeds TARGET: increase threshold by INCREASE_FACTOR</li>
+ * <li>If polygonCount is below 80% of TARGET and threshold above MIN: decrease threshold</li>
+ * <li>Threshold is clamped to MIN_THRESHOLD as the lower bound</li>
+ * </ul>
+ *
+ * <p><b>Thread safety:</b> Uses volatile for threshold and AtomicInteger for count.
+ * Safe for multi-threaded rendering pipelines.</p>
+ *
+ * @see ScreenSpaceTessellator
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle
+ */
+public class AdaptiveTessellationController {
+
+ /**
+ * Singleton instance.
+ */
+ private static final AdaptiveTessellationController INSTANCE = new AdaptiveTessellationController();
+
+ /**
+ * Minimum tessellation threshold in pixels.
+ * This is the base value from {@code TexturedTriangle.TESSELLATION_THRESHOLD_PIXELS}.
+ * Threshold will never go below this.
+ */
+ public static final double MIN_THRESHOLD = 30.0;
+
+ /**
+ * Target polygon count for tessellated triangles.
+ * The controller tries to keep actual count close to this value.
+ */
+ public static final int TARGET_POLYGON_COUNT = 1000;
+
+ /**
+ * Factor for increasing threshold when over budget.
+ * A value of 1.2 means threshold increases by 20% when too many polygons.
+ */
+ private static final double INCREASE_FACTOR = 1.2;
+
+ /**
+ * Factor for decreasing threshold when under budget.
+ * A value of 0.9 means threshold decreases by 10% when below target.
+ */
+ private static final double DECREASE_FACTOR = 0.9;
+
+ /**
+ * Minimum decrease threshold - only decrease if below 80% of target.
+ * This prevents rapid oscillation near the target boundary.
+ */
+ private static final double DECREASE_THRESHOLD_RATIO = 0.8;
+
+ /**
+ * Maximum threshold cap to prevent unbounded growth.
+ * At this threshold, polygons would be very large and texture distortion visible.
+ */
+ private static final double MAX_THRESHOLD = 300.0;
+
+ /**
+ * Current tessellation threshold in pixels.
+ * Volatile for thread-safe reads from multiple rendering threads.
+ */
+ private volatile double currentThreshold = MIN_THRESHOLD;
+
+ /**
+ * Counter for tessellated polygons during the current frame.
+ * AtomicInteger for thread-safe increments during parallel transform phase.
+ */
+ private final AtomicInteger framePolygonCount = new AtomicInteger(0);
+
+ /**
+ * Polygon count from the last frame.
+ * Used for diagnostics and debugging.
+ */
+ private volatile int lastPolygonCount = 0;
+
+ /**
+ * Returns the singleton instance.
+ *
+ * @return the adaptive tessellation controller
+ */
+ public static AdaptiveTessellationController getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Returns the current tessellation threshold in screen pixels.
+ *
+ * <p>This value is read by tessellators during the transform phase
+ * to determine when to subdivide triangles.</p>
+ *
+ * @return the current threshold in pixels
+ */
+ public double getThreshold() {
+ return currentThreshold;
+ }
+
+ /**
+ * Resets the polygon counter at the start of a new frame.
+ *
+ * <p>Called from {@code ShapeCollection.transformShapes()} before
+ * processing shapes.</p>
+ */
+ public void resetFrameCount() {
+ framePolygonCount.set(0);
+ }
+
+ /**
+ * Adds tessellated polygons to the current frame's count.
+ *
+ * <p>Called from {@code TexturedTriangle.transform()} after tessellation
+ * completes, with the number of sub-triangles produced.</p>
+ *
+ * @param count the number of tessellated polygons to add
+ */
+ public void addTessellatedPolygons(final int count) {
+ framePolygonCount.addAndGet(count);
+ }
+
+ /**
+ * Called at the end of each frame to adjust threshold based on polygon count.
+ *
+ * <p><b>Adjustment rules:</b></p>
+ * <ul>
+ * <li>Adjustment is proportional to how far count is from target (smoother convergence)</li>
+ * <li>Minimum change per frame is 2% to ensure gradual adjustment</li>
+ * <li>Maximum change per frame is 10% to prevent sudden jumps</li>
+ * <li>Threshold is always clamped between MIN_THRESHOLD and MAX_THRESHOLD</li>
+ * </ul>
+ *
+ * @return true if threshold changed significantly (should invalidate caches)
+ */
+ public boolean endFrame() {
+ final int count = framePolygonCount.get();
+ lastPolygonCount = count;
+
+ final double oldThreshold = currentThreshold;
+
+ if (count > TARGET_POLYGON_COUNT) {
+ // Too many polygons - increase threshold proportionally
+ final double ratio = (double) count / TARGET_POLYGON_COUNT;
+ // Map ratio 1.0-3.0+ to adjustment 2%-10%
+ final double adjustment = Math.min(0.10, Math.max(0.02, (ratio - 1.0) * 0.05));
+ currentThreshold = Math.min(MAX_THRESHOLD, currentThreshold * (1.0 + adjustment));
+ } else if (count < TARGET_POLYGON_COUNT * DECREASE_THRESHOLD_RATIO
+ && currentThreshold > MIN_THRESHOLD) {
+ // Below 80% of target - decrease threshold proportionally
+ final double ratio = (double) count / TARGET_POLYGON_COUNT;
+ // Map ratio 0.0-0.8 to adjustment 2%-10%
+ final double adjustment = Math.min(0.10, Math.max(0.02, (0.8 - ratio) * 0.05));
+ currentThreshold = Math.max(MIN_THRESHOLD, currentThreshold * (1.0 - adjustment));
+ }
+
+ // Return true if threshold changed (should invalidate caches)
+ return currentThreshold != oldThreshold;
+ }
+
+ /**
+ * Returns the polygon count from the last frame.
+ *
+ * @return the last frame's polygon count
+ */
+ public int getLastPolygonCount() {
+ return lastPolygonCount;
+ }
+
+ /**
+ * Returns the current frame's polygon count (so far).
+ *
+ * @return the current frame's polygon count
+ */
+ public int getCurrentFrameCount() {
+ return framePolygonCount.get();
+ }
+
+ /**
+ * Resets the threshold to the minimum value and clears counters.
+ *
+ * <p>Useful when starting a new scene or resetting renderer state.</p>
+ */
+ public void reset() {
+ currentThreshold = MIN_THRESHOLD;
+ lastPolygonCount = 0;
+ framePolygonCount.set(0);
+ }
+}
\ 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.renderer.raster.tessellation;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TessellatedTexturedTriangle;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tessellates textured polygons based on screen-space edge lengths.
+ *
+ * <p>This tessellator splits triangles recursively based on how large they appear
+ * on screen (in pixels), rather than world-space dimensions. This provides
+ * per-polygon level-of-detail tessellation that adapts to camera distance naturally.</p>
+ *
+ * <p><b>Tessellation algorithm:</b></p>
+ * <ol>
+ * <li>Compute screen-space lengths of all three edges (pixels)</li>
+ * <li>If longest edge ≤ maxScreenPixels, emit the triangle as-is</li>
+ * <li>Otherwise, split the longest edge at its midpoint</li>
+ * <li>Interpolate: world coordinates, texture coordinates, screen coordinates, Z-depth</li>
+ * <li>Recurse on the two resulting sub-triangles</li>
+ * </ol>
+ *
+ * <p><b>Safeguards against excessive tessellation:</b></p>
+ * <ul>
+ * <li>Maximum recursion depth (prevents infinite/very deep recursion)</li>
+ * <li>Maximum triangles per original polygon (hard limit on output)</li>
+ * <li>Maximum screen-space edge length (skip tessellation for huge off-screen polygons)</li>
+ * </ul>
+ *
+ * <p>The resulting {@link TessellatedTexturedTriangle} instances have pre-computed screen
+ * coordinates, avoiding redundant transformation during the render phase.</p>
+ *
+ * @see TessellatedTexturedTriangle
+ * @see TexturedTriangle
+ */
+public class ScreenSpaceTessellator {
+
+ private static final ScreenSpaceEdge edge1 = new ScreenSpaceEdge();
+ private static final ScreenSpaceEdge edge2 = new ScreenSpaceEdge();
+ private static final ScreenSpaceEdge edge3 = new ScreenSpaceEdge();
+
+ /**
+ * Maximum recursion depth for tessellation.
+ * Prevents stack overflow and excessive subdivision.
+ * 8 levels = 256 triangles max per original polygon.
+ */
+ private static final int MAX_RECURSION_DEPTH = 8;
+
+ /**
+ * Maximum number of tessellated triangles to produce per original polygon.
+ * Acts as a hard safety limit to prevent memory explosion.
+ */
+ private static final int MAX_TRIANGLES_PER_POLYGON = 600;
+
+ /**
+ * Maximum screen-space edge length to even consider tessellation.
+ * Edges larger than this indicate the polygon is mostly off-screen or
+ * the camera is extremely close - tessellation would produce invisible triangles.
+ * Set to 10000 pixels (roughly 4K screen diagonal * 2).
+ */
+ private static final double MAX_EDGE_LENGTH_FOR_TESSELLATION = 10000.0;
+
+ /**
+ * Maximum screen-space edge length in pixels.
+ * Edges longer than this will be subdivided.
+ *
+ * This value is mutable to support adaptive tessellation - it can be
+ * updated from {@link AdaptiveTessellationController} before each use.
+ */
+ private double maxScreenPixels;
+
+ /**
+ * Current recursion depth (reset for each tessellate() call).
+ */
+ private int currentDepth = 0;
+
+ /**
+ * Mouse interaction controller to propagate to tessellated triangles.
+ * Set from the original triangle during tessellate() and passed to all sub-triangles.
+ */
+ private MouseInteractionController mouseInteractionController;
+
+ /**
+ * Resulting tessellated triangles.
+ */
+ private final List<TessellatedTexturedTriangle> result = new ArrayList<>();
+
+ /**
+ * Creates a tessellator with the specified maximum screen-space edge length.
+ *
+ * @param maxScreenPixels the maximum allowed edge length in screen pixels;
+ * edges longer than this will be subdivided
+ */
+ public ScreenSpaceTessellator(final double maxScreenPixels) {
+ this.maxScreenPixels = maxScreenPixels;
+ }
+
+ /**
+ * Updates the maximum screen-space edge length threshold.
+ *
+ * <p>This method allows adaptive tessellation control by updating the threshold
+ * before each tessellation operation. Called from {@link AdaptiveTessellationController}.</p>
+ *
+ * @param maxScreenPixels the new maximum allowed edge length in screen pixels
+ */
+ public void setMaxScreenPixels(final double maxScreenPixels) {
+ this.maxScreenPixels = maxScreenPixels;
+ }
+
+ /**
+ * Returns the list of tessellated triangles.
+ *
+ * @return the resulting tessellated triangles
+ */
+ public List<TessellatedTexturedTriangle> getResult() {
+ return result;
+ }
+
+ /**
+ * Tessellates the given textured triangle into smaller triangles.
+ *
+ * <p>After calling this method, retrieve the resulting sub-triangles via
+ * {@link #getResult()}. The original triangle's texture reference and
+ * backface culling settings are preserved on all sub-triangles.</p>
+ *
+ * <p>Safeguards prevent excessive tessellation when the polygon is extremely
+ * large on screen (e.g., when camera is very close).</p>
+ *
+ * @param original the triangle to tessellate (must have screen coords computed)
+ */
+ public void tessellate(final TexturedTriangle original) {
+ result.clear();
+ currentDepth = 0;
+ mouseInteractionController = original.mouseInteractionController;
+
+ final Vertex v1 = original.vertices.get(0);
+ final Vertex v2 = original.vertices.get(1);
+ final Vertex v3 = original.vertices.get(2);
+
+ tessellateRecursively(
+ v1, v2, v3,
+ v1.onScreenCoordinate, v2.onScreenCoordinate, v3.onScreenCoordinate,
+ original.texture,
+ original.isBackfaceCullingEnabled()
+ );
+ }
+
+ /**
+ * Recursively tessellates a triangle based on screen-space edge lengths.
+ *
+ * @param c1 first vertex (world + texture coords)
+ * @param c2 second vertex (world + texture coords)
+ * @param c3 third vertex (world + texture coords)
+ * @param screen1 screen coordinate of first vertex
+ * @param screen2 screen coordinate of second vertex
+ * @param screen3 screen coordinate of third vertex
+ * @param texture the texture to apply
+ * @param backfaceCull whether to enable backface culling on sub-triangles
+ */
+ private void tessellateRecursively(final Vertex c1, final Vertex c2, final Vertex c3,
+ final Point2D screen1, final Point2D screen2,
+ final Point2D screen3,
+ final Texture texture, final boolean backfaceCull) {
+
+ // Safety check: maximum triangles reached - emit as-is and stop
+ if (result.size() >= MAX_TRIANGLES_PER_POLYGON) {
+ emitTriangle(c1, c2, c3, texture, backfaceCull);
+ return;
+ }
+
+ edge1.set(c1, c2, screen1, screen2, 1);
+ edge2.set(c2, c3, screen2, screen3, 2);
+ edge3.set(c3, c1, screen3, screen1, 3);
+
+ // Inline sort of 3 edges by screen-space length
+ ScreenSpaceEdge a = edge1;
+ ScreenSpaceEdge b = edge2;
+ ScreenSpaceEdge c = edge3;
+ ScreenSpaceEdge t;
+
+ if (a.getScreenLength() > b.getScreenLength()) {
+ t = a;
+ a = b;
+ b = t;
+ }
+ if (b.getScreenLength() > c.getScreenLength()) {
+ t = b;
+ b = c;
+ c = t;
+ }
+ if (a.getScreenLength() > b.getScreenLength()) {
+ t = a;
+ a = b;
+ b = t;
+ }
+
+ final ScreenSpaceEdge longestEdge = c;
+
+ // Safety check: polygon is way too large (off-screen or extreme close-up)
+ // Skip tessellation entirely to prevent generating millions of invisible triangles
+ if (longestEdge.getScreenLength() > MAX_EDGE_LENGTH_FOR_TESSELLATION) {
+ emitTriangle(c1, c2, c3, texture, backfaceCull);
+ return;
+ }
+
+ // If longest edge is within threshold, emit triangle
+ if (longestEdge.getScreenLength() <= maxScreenPixels) {
+ emitTriangle(c1, c2, c3, texture, backfaceCull);
+ return;
+ }
+
+ // Safety check: max recursion depth reached - emit and stop
+ if (currentDepth >= MAX_RECURSION_DEPTH) {
+ emitTriangle(c1, c2, c3, texture, backfaceCull);
+ return;
+ }
+
+ // Split longest edge at midpoint
+ final Vertex midpoint = longestEdge.interpolateMidpoint();
+ final Point2D midpointScreen = longestEdge.interpolateScreenMidpoint();
+
+ currentDepth++;
+
+ // Recurse on two sub-triangles based on which edge was longest
+ switch (longestEdge.edgeId) {
+ case 1:
+ // Edge 1 was c1→c2
+ tessellateRecursively(c1, midpoint, c3, screen1, midpointScreen, screen3,
+ texture, backfaceCull);
+ tessellateRecursively(midpoint, c2, c3, midpointScreen, screen2, screen3,
+ texture, backfaceCull);
+ break;
+ case 2:
+ // Edge 2 was c2→c3
+ tessellateRecursively(c1, c2, midpoint, screen1, screen2, midpointScreen,
+ texture, backfaceCull);
+ tessellateRecursively(c1, midpoint, c3, screen1, midpointScreen, screen3,
+ texture, backfaceCull);
+ break;
+ case 3:
+ // Edge 3 was c3→c1
+ tessellateRecursively(c1, c2, midpoint, screen1, screen2, midpointScreen,
+ texture, backfaceCull);
+ tessellateRecursively(midpoint, c2, c3, midpointScreen, screen2, screen3,
+ texture, backfaceCull);
+ break;
+ }
+
+ currentDepth--;
+ }
+
+ /**
+ * Emits a tessellated triangle to the result list.
+ */
+ private void emitTriangle(final Vertex c1, final Vertex c2, final Vertex c3,
+ final Texture texture, final boolean backfaceCull) {
+ final TessellatedTexturedTriangle triangle = new TessellatedTexturedTriangle(
+ c1.cloneWithScreenCoords(),
+ c2.cloneWithScreenCoords(),
+ c3.cloneWithScreenCoords(),
+ texture
+ );
+ triangle.setBackfaceCulling(backfaceCull);
+ triangle.setMouseInteractionController(mouseInteractionController);
+
+ // Compute average Z-depth for proper depth sorting
+ triangle.onScreenZ = (c1.transformedCoordinate.z + c2.transformedCoordinate.z + c3.transformedCoordinate.z) / 3.0;
+
+ result.add(triangle);
+ }
+
+ /**
+ * Represents an edge with screen-space length for tessellation decisions.
+ */
+ private static class ScreenSpaceEdge {
+
+ Vertex v1;
+ Vertex v2;
+ Point2D screen1;
+ Point2D screen2;
+ int edgeId;
+
+ void set(final Vertex v1, final Vertex v2,
+ final Point2D screen1, final Point2D screen2,
+ final int edgeId) {
+ this.v1 = v1;
+ this.v2 = v2;
+ this.screen1 = screen1;
+ this.screen2 = screen2;
+ this.edgeId = edgeId;
+ }
+
+ /**
+ * Returns the screen-space length of this edge in pixels.
+ */
+ double getScreenLength() {
+ final double dx = screen2.x - screen1.x;
+ final double dy = screen2.y - screen1.y;
+ return Math.sqrt(dx * dx + dy * dy);
+ }
+
+ /**
+ * Interpolates a vertex at the midpoint of this edge.
+ *
+ * <p>Interpolates world coordinates, texture coordinates, transformed coordinates,
+ * and screen coordinates.</p>
+ *
+ * @return a new vertex at the midpoint with all interpolated properties
+ */
+ Vertex interpolateMidpoint() {
+ final Point3D worldMid = new Point3D().computeMiddlePoint(v1.coordinate, v2.coordinate);
+ final Point2D texMid = new Point2D().setToMiddle(v1.textureCoordinate, v2.textureCoordinate);
+
+ final Vertex result = new Vertex(worldMid, texMid);
+
+ result.transformedCoordinate = new Point3D(
+ (v1.transformedCoordinate.x + v2.transformedCoordinate.x) / 2,
+ (v1.transformedCoordinate.y + v2.transformedCoordinate.y) / 2,
+ (v1.transformedCoordinate.z + v2.transformedCoordinate.z) / 2
+ );
+
+ result.onScreenCoordinate = new Point2D(
+ (screen1.x + screen2.x) / 2,
+ (screen1.y + screen2.y) / 2
+ );
+
+ return result;
+ }
+
+ /**
+ * Returns the screen-space midpoint of this edge.
+ */
+ Point2D interpolateScreenMidpoint() {
+ return new Point2D(
+ (screen1.x + screen2.x) / 2,
+ (screen1.y + screen2.y) / 2
+ );
+ }
+ }
+}
\ No newline at end of file
* Triangle tessellation for perspective-correct texture rendering.
*
* <p>Large textured triangles are tessellated into smaller triangles to ensure
- * accurate perspective correction. This package provides the recursive tessellation
- * algorithm used by composite shapes.</p>
+ * accurate perspective correction. This package provides two tessellation algorithms:</p>
*
+ * <ul>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.ScreenSpaceTessellator} -
+ * Screen-space tessellation based on pixel edge lengths (per-polygon LOD)</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.TexturedPolygonTessellator} -
+ * World-space tessellation based on distance (legacy, used for CSG operations)</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.ScreenSpaceTessellator
* @see eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.TexturedPolygonTessellator
* @see eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.TessellationEdge
*/
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.texture;
+
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.lang.Math.pow;
+import static java.lang.Math.sqrt;
+
+/**
+ * Factory class for generating reusable textures with configurable borders and glow effects.
+ *
+ * <p>Provides static factory methods to create common texture patterns:</p>
+ * <ul>
+ * <li>{@link #solidWithBorder} - solid fill color with opaque border (for bordered polygons)</li>
+ * <li>{@link #glowingBorder} - transparent center with glowing edges (for wireframe-effect shapes)</li>
+ * <li>{@link #radialGlow} - circular radial gradient (for point/billboard glows)</li>
+ * </ul>
+ *
+ * <p><b>Texture caching:</b> Textures are cached by their generation parameters using a
+ * {@link WeakReference}-based cache. When textures are no longer referenced elsewhere,
+ * they are automatically garbage collected. This reduces memory usage when many shapes
+ * share identical textures.</p>
+ *
+ * <p><b>Example usage:</b></p>
+ * <pre>{@code
+ * // RGB cube plate with black border
+ * Texture tex1 = TextureGenerator.solidWithBorder(64, new Color(255, 0, 0), new Color(0, 0, 0), 3);
+ *
+ * // Wireframe-effect texture (glowing cyan edges, transparent center)
+ * Texture tex2 = TextureGenerator.glowingBorder(64, new Color(0, 255, 255), 6, 120, true);
+ *
+ * // Circular glow point
+ * Texture tex3 = TextureGenerator.radialGlow(100, new Color(255, 200, 100));
+ * }</pre>
+ *
+ * @see Texture
+ * @see Color
+ */
+public final class TextureGenerator {
+
+ /**
+ * Cache of generated textures, keyed by configuration parameters.
+ * Uses WeakReference so textures are GC'd when no longer referenced elsewhere.
+ */
+ private static final Map<TextureKey, WeakReference<Texture>> textureCache = new HashMap<>();
+
+ /**
+ * Private constructor to prevent instantiation.
+ * This class only provides static factory methods.
+ */
+ private TextureGenerator() {
+ }
+
+ /**
+ * Creates a texture with a solid fill color and an opaque border.
+ *
+ * <p>The fill color fills the entire texture except for the border region.
+ * The border is drawn as an opaque rectangle inset from the edges.</p>
+ *
+ * <p><b>Caching:</b> Identical parameters produce the same cached texture instance.</p>
+ *
+ * @param size the texture width and height in pixels
+ * @param fillColor the color filling the interior
+ * @param borderColor the color of the border
+ * @param borderWidth the width of the border in pixels
+ * @param maxUpscale the maximum number of upscaled mipmap levels (0 = no upscale, 1 = 2x upscale, 2 = 4x upscale)
+ * @return a texture with solid fill and opaque border
+ */
+ public static Texture solidWithBorder(final int size, final Color fillColor,
+ final Color borderColor, final int borderWidth,
+ final int maxUpscale) {
+ final TextureKey key = new TextureKey(size, fillColor, borderColor, borderWidth,
+ TextureType.SOLID_WITH_BORDER, 0, false, maxUpscale);
+
+ return getOrCreate(key, () -> generateSolidWithBorder(size, fillColor, borderColor, borderWidth, maxUpscale));
+ }
+
+ /**
+ * Creates a texture with a transparent center and glowing border edges.
+ *
+ * <p>This texture is useful for creating wireframe-effect shapes: the polygon
+ * appears to have only edges, with the interior being transparent. The border
+ * has a glow effect where intensity decreases from the edge toward the center.</p>
+ *
+ * <p><b>Glow effect:</b> Multiple concentric border lines are drawn with decreasing
+ * alpha values, creating a luminous edge appearance. The glow intensity controls
+ * how bright the innermost edge appears.</p>
+ *
+ * <p><b>Caching:</b> Identical parameters produce the same cached texture instance.</p>
+ *
+ * @param size the texture width and height in pixels
+ * @param borderColor the color of the glowing border
+ * @param borderWidth the width of the border region in pixels (where glow appears)
+ * @param glowIntensity the base intensity added to the border color (0-255 range)
+ * @param transparent if true, center is fully transparent; if false, has slight tint
+ * @param maxUpscale the maximum number of upscaled mipmap levels (0 = no upscale, 1 = 2x upscale, 2 = 4x upscale)
+ * @return a texture with glowing edges and transparent center
+ */
+ public static Texture glowingBorder(final int size, final Color borderColor,
+ final int borderWidth, final int glowIntensity,
+ final boolean transparent, final int maxUpscale) {
+ final TextureKey key = new TextureKey(size, borderColor, Color.TRANSPARENT, borderWidth,
+ TextureType.GLOWING_BORDER, glowIntensity, transparent, maxUpscale);
+
+ return getOrCreate(key, () -> generateGlowingBorder(size, borderColor, borderWidth,
+ glowIntensity, transparent, maxUpscale));
+ }
+
+ /**
+ * Creates a texture with a circular radial gradient glow.
+ *
+ * <p>The texture has a circular alpha gradient: fully opaque at the center,
+ * transitioning to fully transparent at the edges. The color intensity
+ * remains constant while alpha decreases radially.</p>
+ *
+ * <p>This is suitable for rendering glowing points or circular billboards.
+ * The center of the texture is the brightest point, fading outward.</p>
+ *
+ * <p><b>Caching:</b> Identical parameters produce the same cached texture instance.</p>
+ *
+ * @param size the texture width and height in pixels (should be even)
+ * @param color the color of the glow (alpha is overridden by radial gradient)
+ * @return a texture with circular radial alpha gradient
+ */
+ public static Texture radialGlow(final int size, final Color color) {
+ final TextureKey key = new TextureKey(size, color, Color.TRANSPARENT, 0,
+ TextureType.RADIAL_GLOW, 0, false, 1);
+
+ return getOrCreate(key, () -> generateRadialGlow(size, color));
+ }
+
+ /**
+ * Retrieves a cached texture or creates a new one if not cached.
+ *
+ * @param key the cache key identifying the texture configuration
+ * @param generator the function to create the texture if not cached
+ * @return the cached or newly created texture
+ */
+ private static Texture getOrCreate(final TextureKey key, final TextureSupplier generator) {
+ synchronized (textureCache) {
+ final WeakReference<Texture> ref = textureCache.get(key);
+ if (ref != null) {
+ final Texture cached = ref.get();
+ if (cached != null) {
+ return cached;
+ }
+ // Reference was cleared, remove stale entry
+ textureCache.remove(key);
+ }
+
+ final Texture texture = generator.create();
+ textureCache.put(key, new WeakReference<>(texture));
+ return texture;
+ }
+ }
+
+ /**
+ * Generates a texture with solid fill and opaque border.
+ */
+ private static Texture generateSolidWithBorder(final int size, final Color fillColor,
+ final Color borderColor, final int borderWidth,
+ final int maxUpscale) {
+ final Texture texture = new Texture(size, size, maxUpscale);
+
+ // Fill interior with fill color
+ texture.primaryBitmap.drawRectangle(borderWidth, borderWidth,
+ size - borderWidth, size - borderWidth, fillColor);
+
+ // Draw border regions (top, bottom, left, right edges)
+ // Top border
+ texture.primaryBitmap.drawRectangle(0, 0, size, borderWidth, borderColor);
+ // Bottom border
+ texture.primaryBitmap.drawRectangle(0, size - borderWidth, size, size, borderColor);
+ // Left border (excluding top/bottom corners already filled)
+ texture.primaryBitmap.drawRectangle(0, borderWidth, borderWidth, size - borderWidth, borderColor);
+ // Right border
+ texture.primaryBitmap.drawRectangle(size - borderWidth, borderWidth, size, size - borderWidth, borderColor);
+
+ texture.resetResampledBitmapCache();
+ return texture;
+ }
+
+ /**
+ * Generates a texture with glowing border and transparent center.
+ */
+ private static Texture generateGlowingBorder(final int size, final Color borderColor,
+ final int borderWidth, final int glowIntensity,
+ final boolean transparent, final int maxUpscale) {
+ final Texture texture = new Texture(size, size, maxUpscale);
+
+ // Clear to transparent or slight tint
+ final int centerAlpha = transparent ? 0 : 30;
+ final java.awt.Color bgColor = new java.awt.Color(borderColor.r, borderColor.g, borderColor.b, centerAlpha);
+ texture.graphics.setBackground(bgColor);
+ texture.graphics.clearRect(0, 0, size, size);
+
+ // Draw concentric glow lines from outer to inner
+ for (int i = 0; i < borderWidth; i++) {
+ final int intensity = (int) (glowIntensity * (borderWidth - i) / borderWidth);
+ final int alpha = Math.max(0, 200 - i * 30);
+
+ final java.awt.Color glowColor = new java.awt.Color(
+ Math.min(255, borderColor.r + intensity),
+ Math.min(255, borderColor.g + intensity),
+ Math.min(255, borderColor.b + intensity),
+ alpha
+ );
+
+ texture.graphics.setColor(glowColor);
+ texture.graphics.drawRect(i, i, size - 1 - 2 * i, size - 1 - 2 * i);
+ }
+
+ texture.graphics.dispose();
+ texture.resetResampledBitmapCache();
+ return texture;
+ }
+
+ /**
+ * Generates a texture with circular radial alpha gradient.
+ */
+ private static Texture generateRadialGlow(final int size, final Color color) {
+ final Texture texture = new Texture(size, size, 1);
+ final int halfSize = size / 2;
+
+ for (int x = 0; x < size; x++) {
+ for (int y = 0; y < size; y++) {
+ final int distanceFromCenter = (int) sqrt(pow(halfSize - x, 2) + pow(halfSize - y, 2));
+
+ int alpha = 255 - ((270 * distanceFromCenter) / halfSize);
+ if (alpha < 0) {
+ alpha = 0;
+ }
+
+ texture.primaryBitmap.pixels[texture.primaryBitmap.getAddress(x, y)] =
+ (alpha << 24) | (color.r << 16) | (color.g << 8) | color.b;
+ }
+ }
+
+ texture.resetResampledBitmapCache();
+ return texture;
+ }
+
+ /**
+ * Functional interface for texture creation.
+ */
+ @FunctionalInterface
+ private interface TextureSupplier {
+ Texture create();
+ }
+
+ /**
+ * Enumeration of texture generation types for cache key differentiation.
+ */
+ private enum TextureType {
+ SOLID_WITH_BORDER,
+ GLOWING_BORDER,
+ RADIAL_GLOW
+ }
+
+ /**
+ * Cache key for texture lookup based on generation parameters.
+ *
+ * <p>Two textures with identical parameters should produce the same key,
+ * enabling cache reuse. The key includes all parameters that affect
+ * the visual appearance of the generated texture.</p>
+ */
+ private static final class TextureKey {
+ private final int size;
+ private final Color primaryColor;
+ private final Color secondaryColor;
+ private final int borderWidth;
+ private final TextureType type;
+ private final int glowIntensity;
+ private final boolean transparent;
+ private final int maxUpscale;
+
+ TextureKey(final int size, final Color primaryColor, final Color secondaryColor,
+ final int borderWidth, final TextureType type, final int glowIntensity,
+ final boolean transparent, final int maxUpscale) {
+ this.size = size;
+ this.primaryColor = primaryColor;
+ this.secondaryColor = secondaryColor;
+ this.borderWidth = borderWidth;
+ this.type = type;
+ this.glowIntensity = glowIntensity;
+ this.transparent = transparent;
+ this.maxUpscale = maxUpscale;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final TextureKey that = (TextureKey) o;
+
+ if (size != that.size) return false;
+ if (borderWidth != that.borderWidth) return false;
+ if (glowIntensity != that.glowIntensity) return false;
+ if (transparent != that.transparent) return false;
+ if (maxUpscale != that.maxUpscale) return false;
+ if (type != that.type) return false;
+ if (!primaryColor.equals(that.primaryColor)) return false;
+ return secondaryColor.equals(that.secondaryColor);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = size;
+ result = 31 * result + primaryColor.hashCode();
+ result = 31 * result + secondaryColor.hashCode();
+ result = 31 * result + borderWidth;
+ result = 31 * result + type.hashCode();
+ result = 31 * result + glowIntensity;
+ result = 31 * result + (transparent ? 1 : 0);
+ result = 31 * result + maxUpscale;
+ return result;
+ }
+ }
+}
\ No newline at end of file