From: Svjatoslav Date: Sat, 25 Apr 2026 13:40:24 +0000 (+0300) Subject: feat(tessellation): add screen-space adaptive tessellation and texture generator X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;p=sixth-3d.git feat(tessellation): add screen-space adaptive tessellation and texture generator Add screen-space tessellation system that subdivides textured triangles based on pixel edge lengths, providing per-polygon LOD that adapts to camera distance naturally. Includes safeguards against excessive subdivision with recursion depth limits and triangle count caps. Add TextureGenerator factory for reusable textures with configurable borders and glow effects (solidWithBorder, glowingBorder, radialGlow). Textures are cached with WeakReference for automatic GC when unused. Enhance DeveloperToolsPanel with tessellation quality controls and real-time stats display. Propagate mouse interaction controllers through tessellated triangles to preserve clickability. Rewrite AGENTS.md as comprehensive quick-reference guide with task lookup table, code examples, and class catalog organized by package. --- diff --git a/AGENTS.md b/AGENTS.md index 537e168..93cb2fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,182 +1,452 @@ -# 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`, `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` 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 | diff --git a/TODO.org b/TODO.org index 2eab58f..dbdd8cc 100644 --- a/TODO.org +++ b/TODO.org @@ -1,15 +1,4 @@ * 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 @@ -120,6 +109,7 @@ shadows. + 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 diff --git a/doc/index.org b/doc/index.org index 0043675..02c11c0 100644 --- a/doc/index.org +++ b/doc/index.org @@ -606,54 +606,40 @@ line's appearance. :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: @@ -746,7 +732,7 @@ children) are skipped. *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 @@ -771,51 +757,6 @@ The scrollable text area shows captured debug output in real-time: 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 diff --git a/doc/perspective-correct-textures/index.org b/doc/perspective-correct-textures/index.org index 1aa4bf8..beb870e 100644 --- a/doc/perspective-correct-textures/index.org +++ b/doc/perspective-correct-textures/index.org @@ -43,19 +43,20 @@ negligible. :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); @@ -65,16 +66,38 @@ void tessellate(TexturedTriangle polygon) { #+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: @@ -95,6 +118,7 @@ This visualization helps you: | 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 | diff --git a/doc/shading/index.org b/doc/shading/index.org index 3a7c96b..79ba647 100644 --- a/doc/shading/index.org +++ b/doc/shading/index.org @@ -12,7 +12,7 @@ [[file:../index.org][Back to main documentation]] -* Shading & Lighting +* Overview :PROPERTIES: :CUSTOM_ID: shading-lighting :END: @@ -62,7 +62,7 @@ memory allocation during the subsequent multi-threaded paint phase. See the [[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: @@ -99,30 +99,6 @@ Multiple light sources add their contributions together, allowing for 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 @@ -146,28 +122,31 @@ This simplified formula prevents harsh cutoffs while still providing 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: @@ -226,6 +205,39 @@ Shading propagates through composite shapes — calling [[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 @@ -248,16 +260,3 @@ The shading implementation is optimized for CPU rendering: 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 | diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java index 5e5535c..68949b5 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java @@ -5,6 +5,7 @@ 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; @@ -24,6 +25,8 @@ import java.util.List; *
    *
  • Checkboxes to toggle debug settings
  • *
  • Camera position display with copy button
  • + *
  • Composite shape frustum culling statistics
  • + *
  • Adaptive tessellation statistics (threshold and polygon count)
  • *
  • A scrollable log viewer showing captured debug output
  • *
  • A button to clear the log buffer
  • *
  • Resizable window with native maximize support
  • @@ -31,6 +34,7 @@ import java.util.List; * * @see DeveloperTools * @see DebugLogBuffer + * @see AdaptiveTessellationController */ public class DeveloperToolsPanel extends JFrame { @@ -68,6 +72,14 @@ 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. */ @@ -107,11 +119,18 @@ public class DeveloperToolsPanel extends JFrame { 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); @@ -235,6 +254,26 @@ public class DeveloperToolsPanel extends JFrame { 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)); @@ -260,6 +299,7 @@ public class DeveloperToolsPanel extends JFrame { try { updateCameraLabel(); updateCullingStatistics(); + updateTessellationStatistics(); updateLogDisplay(); } finally { updating = false; @@ -299,6 +339,12 @@ public class DeveloperToolsPanel extends JFrame { 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 entries = debugLogBuffer.getEntries(); final StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java index 8f83a50..913bd10 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java @@ -146,6 +146,24 @@ public class Vertex { return result; } + /** + * Creates a deep copy of this vertex including screen-space coordinates. + * + *

    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.

    + * + * @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. diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java index 524538a..824693b 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java @@ -15,6 +15,7 @@ import eu.svjatoslav.sixth.e3d.math.TransformStack; 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; @@ -227,6 +228,9 @@ public class ShapeCollection { 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) @@ -276,6 +280,12 @@ public class ShapeCollection { */ 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); + } } /** diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TessellatedTexturedTriangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TessellatedTexturedTriangle.java new file mode 100644 index 0000000..867eaca --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TessellatedTexturedTriangle.java @@ -0,0 +1,117 @@ +/* + * 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. + * + *

    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.

    + * + *

    This optimization avoids redundant coordinate transformation for tessellated sub-triangles, + * improving performance when large polygons are split into many smaller pieces.

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

    The vertices' {@link Vertex#onScreenCoordinate} and {@link Vertex#transformedCoordinate} + * are already populated, so the {@link #transform} method will skip coordinate calculation.

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

    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.

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

    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.

    + * + * @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 diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java index 7291ea0..3d21bb4 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java @@ -296,6 +296,82 @@ public class TexturedTriangle extends AbstractCoordinateShape { 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). * diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java index e436db7..a23f2b2 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java @@ -7,15 +7,21 @@ * Textured triangle rendering with perspective-correct UV mapping. * *

    Textured triangles apply 2D textures to 3D triangles using UV coordinates. - * Large triangles may be tessellated into smaller pieces for accurate perspective correction.

    + * Screen-space tessellation is applied automatically during the transform phase + * for better perspective correction on large triangles.

    * *

    Key classes:

    *
      - *
    • {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle} - The textured triangle shape
    • - *
    • {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.PolygonBorderInterpolator} - Edge interpolation with UVs
    • + *
    • {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle} - + * The base textured triangle with screen-space tessellation
    • + *
    • {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TessellatedTexturedTriangle} - + * Pre-transformed sub-triangle created during tessellation
    • + *
    • {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.PolygonBorderInterpolator} - + * Edge interpolation with UVs
    • *
    * * @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 */ diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java index 10652c5..bf1bf77 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java @@ -20,7 +20,10 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape; 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; @@ -103,6 +106,15 @@ public class AbstractCompositeShape extends AbstractShape { */ 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 SCREEN_SPACE_TESSELLATOR = + ThreadLocal.withInitial(() -> new ScreenSpaceTessellator( + AdaptiveTessellationController.MIN_THRESHOLD)); + /** * Frame-optimized cache of shapes ready for rendering, derived from {@link #subShapesRegistry}. * @@ -122,6 +134,12 @@ public class AbstractCompositeShape extends AbstractShape { */ private List 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}. * @@ -430,11 +448,31 @@ public class AbstractCompositeShape extends AbstractShape { * * @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; } @@ -442,7 +480,7 @@ public class AbstractCompositeShape extends AbstractShape { if (isRetessellationNeeded(proposedTessellationFactor, currentTessellationFactor)) { currentTessellationFactor = proposedTessellationFactor; - retessellate(context); + retessellate(transformPipe, context); } } @@ -525,8 +563,8 @@ public class AbstractCompositeShape extends AbstractShape { 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. * *

    Used by {@code ShapeCollection} to trigger retessellate when clearing the scene * or for other advanced use cases.

    @@ -535,6 +573,13 @@ public class AbstractCompositeShape extends AbstractShape { */ 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); + } + } } /** @@ -826,10 +871,11 @@ public class AbstractCompositeShape extends AbstractShape { * * @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 result = new ArrayList<>(); + originalTessellationInputCount = 0; // Reset before counting final TexturedPolygonTessellator tessellator = new TexturedPolygonTessellator(currentTessellationFactor); int texturedPolygonCount = 0; @@ -844,8 +890,18 @@ public class AbstractCompositeShape extends AbstractShape { 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 ssResult = ssTessellator.getResult(); + result.addAll(ssResult); texturedPolygonCount++; } else if (shape instanceof SolidPolygon polygon) { final int vertexCount = polygon.getVertexCount(); @@ -865,6 +921,7 @@ public class AbstractCompositeShape extends AbstractShape { result.addAll(tessellator.getResult()); + originalTessellationInputCount = texturedPolygonCount; cachedRenderList = result; if (context != null && context.debugLogBuffer != null) { @@ -991,11 +1048,22 @@ public class AbstractCompositeShape extends AbstractShape { 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(); } @@ -1018,4 +1086,29 @@ public class AbstractCompositeShape extends AbstractShape { return result; } + /** + * Computes screen coordinates for a TexturedTriangle's vertices. + * Called before ScreenSpaceTessellator to ensure accurate screen-space size decisions. + * + *

    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.

    + * + * @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); + } + } + } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/AdaptiveTessellationController.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/AdaptiveTessellationController.java new file mode 100644 index 0000000..fc5087a --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/AdaptiveTessellationController.java @@ -0,0 +1,209 @@ +/* + * 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. + * + *

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

    + * + *

    Frame lifecycle:

    + *
      + *
    1. {@link #resetFrameCount()} called at frame start to clear counter
    2. + *
    3. {@link #addTessellatedPolygons(int)} called during transform for each tessellated polygon
    4. + *
    5. {@link #endFrame()} called after painting to adjust threshold based on count
    6. + *
    + * +*

    Adjustment logic:

    + *
      + *
    • If polygonCount exceeds TARGET: increase threshold by INCREASE_FACTOR
    • + *
    • If polygonCount is below 80% of TARGET and threshold above MIN: decrease threshold
    • + *
    • Threshold is clamped to MIN_THRESHOLD as the lower bound
    • + *
    + * + *

    Thread safety: Uses volatile for threshold and AtomicInteger for count. + * Safe for multi-threaded rendering pipelines.

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

    This value is read by tessellators during the transform phase + * to determine when to subdivide triangles.

    + * + * @return the current threshold in pixels + */ + public double getThreshold() { + return currentThreshold; + } + + /** + * Resets the polygon counter at the start of a new frame. + * + *

    Called from {@code ShapeCollection.transformShapes()} before + * processing shapes.

    + */ + public void resetFrameCount() { + framePolygonCount.set(0); + } + + /** + * Adds tessellated polygons to the current frame's count. + * + *

    Called from {@code TexturedTriangle.transform()} after tessellation + * completes, with the number of sub-triangles produced.

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

    Adjustment rules:

    + *
      + *
    • Adjustment is proportional to how far count is from target (smoother convergence)
    • + *
    • Minimum change per frame is 2% to ensure gradual adjustment
    • + *
    • Maximum change per frame is 10% to prevent sudden jumps
    • + *
    • Threshold is always clamped between MIN_THRESHOLD and MAX_THRESHOLD
    • + *
    + * + * @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. + * + *

    Useful when starting a new scene or resetting renderer state.

    + */ + public void reset() { + currentThreshold = MIN_THRESHOLD; + lastPolygonCount = 0; + framePolygonCount.set(0); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/ScreenSpaceTessellator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/ScreenSpaceTessellator.java new file mode 100644 index 0000000..7b48a64 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/ScreenSpaceTessellator.java @@ -0,0 +1,351 @@ +/* + * 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. + * + *

    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.

    + * + *

    Tessellation algorithm:

    + *
      + *
    1. Compute screen-space lengths of all three edges (pixels)
    2. + *
    3. If longest edge ≤ maxScreenPixels, emit the triangle as-is
    4. + *
    5. Otherwise, split the longest edge at its midpoint
    6. + *
    7. Interpolate: world coordinates, texture coordinates, screen coordinates, Z-depth
    8. + *
    9. Recurse on the two resulting sub-triangles
    10. + *
    + * + *

    Safeguards against excessive tessellation:

    + *
      + *
    • Maximum recursion depth (prevents infinite/very deep recursion)
    • + *
    • Maximum triangles per original polygon (hard limit on output)
    • + *
    • Maximum screen-space edge length (skip tessellation for huge off-screen polygons)
    • + *
    + * + *

    The resulting {@link TessellatedTexturedTriangle} instances have pre-computed screen + * coordinates, avoiding redundant transformation during the render phase.

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

    This method allows adaptive tessellation control by updating the threshold + * before each tessellation operation. Called from {@link AdaptiveTessellationController}.

    + * + * @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 getResult() { + return result; + } + + /** + * Tessellates the given textured triangle into smaller triangles. + * + *

    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.

    + * + *

    Safeguards prevent excessive tessellation when the polygon is extremely + * large on screen (e.g., when camera is very close).

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

    Interpolates world coordinates, texture coordinates, transformed coordinates, + * and screen coordinates.

    + * + * @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 diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/package-info.java index 81d45cb..402627e 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/package-info.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/tessellation/package-info.java @@ -7,9 +7,16 @@ * Triangle tessellation for perspective-correct texture rendering. * *

    Large textured triangles are tessellated into smaller triangles to ensure - * accurate perspective correction. This package provides the recursive tessellation - * algorithm used by composite shapes.

    + * accurate perspective correction. This package provides two tessellation algorithms:

    * + *
      + *
    • {@link eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.ScreenSpaceTessellator} - + * Screen-space tessellation based on pixel edge lengths (per-polygon LOD)
    • + *
    • {@link eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.TexturedPolygonTessellator} - + * World-space tessellation based on distance (legacy, used for CSG operations)
    • + *
    + * + * @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 */ diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureGenerator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureGenerator.java new file mode 100644 index 0000000..0e318e6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureGenerator.java @@ -0,0 +1,327 @@ +/* + * 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. + * + *

    Provides static factory methods to create common texture patterns:

    + *
      + *
    • {@link #solidWithBorder} - solid fill color with opaque border (for bordered polygons)
    • + *
    • {@link #glowingBorder} - transparent center with glowing edges (for wireframe-effect shapes)
    • + *
    • {@link #radialGlow} - circular radial gradient (for point/billboard glows)
    • + *
    + * + *

    Texture caching: 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.

    + * + *

    Example usage:

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

    The fill color fills the entire texture except for the border region. + * The border is drawn as an opaque rectangle inset from the edges.

    + * + *

    Caching: Identical parameters produce the same cached texture instance.

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

    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.

    + * + *

    Glow effect: 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.

    + * + *

    Caching: Identical parameters produce the same cached texture instance.

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

    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.

    + * + *

    This is suitable for rendering glowing points or circular billboards. + * The center of the texture is the brightest point, fading outward.

    + * + *

    Caching: Identical parameters produce the same cached texture instance.

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

    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.

    + */ + 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