From 023800d9f03fadd4b048d7ec1628469a10a21ebb Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Thu, 12 Mar 2026 20:07:52 +0200 Subject: [PATCH 1/1] Initial commit --- .gitignore | 9 + AGENTS.md | 148 +++ COPYING | 121 ++ Tools/Open with IntelliJ IDEA | 54 + Tools/Update web site | 101 ++ doc/example.png | Bin 0 -> 67796 bytes doc/index.org | 556 +++++++++ pom.xml | 145 +++ .../eu/svjatoslav/sixth/e3d/geometry/Box.java | 115 ++ .../svjatoslav/sixth/e3d/geometry/Circle.java | 22 + .../sixth/e3d/geometry/Point2D.java | 160 +++ .../sixth/e3d/geometry/Point3D.java | 419 +++++++ .../sixth/e3d/geometry/Polygon.java | 61 + .../sixth/e3d/geometry/Rectangle.java | 63 ++ .../sixth/e3d/geometry/package-info.java | 5 + .../eu/svjatoslav/sixth/e3d/gui/Camera.java | 223 ++++ .../sixth/e3d/gui/FrameListener.java | 52 + .../sixth/e3d/gui/GuiComponent.java | 161 +++ .../sixth/e3d/gui/RenderingContext.java | 187 +++ .../svjatoslav/sixth/e3d/gui/TextPointer.java | 109 ++ .../svjatoslav/sixth/e3d/gui/ViewFrame.java | 203 ++++ .../svjatoslav/sixth/e3d/gui/ViewPanel.java | 407 +++++++ .../sixth/e3d/gui/ViewSpaceTracker.java | 126 +++ .../sixth/e3d/gui/ViewUpdateTimerTask.java | 25 + .../sixth/e3d/gui/humaninput/Connexion3D.java | 36 + .../e3d/gui/humaninput/InputManager.java | 236 ++++ .../gui/humaninput/KeyboardFocusStack.java | 107 ++ .../e3d/gui/humaninput/KeyboardHelper.java | 118 ++ .../gui/humaninput/KeyboardInputHandler.java | 40 + .../sixth/e3d/gui/humaninput/MouseEvent.java | 46 + .../MouseInteractionController.java | 33 + .../WorldNavigationUserInputTracker.java | 87 ++ .../e3d/gui/humaninput/package-info.java | 6 + .../gui/textEditorComponent/Character.java | 24 + .../gui/textEditorComponent/LookAndFeel.java | 25 + .../e3d/gui/textEditorComponent/Page.java | 136 +++ .../TextEditComponent.java | 911 +++++++++++++++ .../e3d/gui/textEditorComponent/TextLine.java | 410 +++++++ .../gui/textEditorComponent/package-info.java | 5 + .../svjatoslav/sixth/e3d/math/Rotation.java | 150 +++ .../svjatoslav/sixth/e3d/math/Transform.java | 112 ++ .../sixth/e3d/math/TransformStack.java | 81 ++ .../eu/svjatoslav/sixth/e3d/math/Vertex.java | 86 ++ .../sixth/e3d/math/package-info.java | 9 + .../eu/svjatoslav/sixth/e3d/package-info.java | 7 + .../e3d/renderer/octree/IntegerPoint.java | 20 + .../e3d/renderer/octree/OctreeVolume.java | 1005 +++++++++++++++++ .../e3d/renderer/octree/package-info.java | 20 + .../renderer/octree/raytracer/CameraView.java | 42 + .../octree/raytracer/LightSource.java | 35 + .../e3d/renderer/octree/raytracer/Ray.java | 71 ++ .../e3d/renderer/octree/raytracer/RayHit.java | 56 + .../renderer/octree/raytracer/RayTracer.java | 411 +++++++ .../octree/raytracer/RaytracingCamera.java | 99 ++ .../octree/raytracer/package-info.java | 21 + .../sixth/e3d/renderer/package-info.java | 11 + .../sixth/e3d/renderer/raster/Color.java | 273 +++++ .../e3d/renderer/raster/RenderAggregator.java | 78 ++ .../e3d/renderer/raster/ShapeCollection.java | 131 +++ .../renderer/raster/lighting/LightSource.java | 136 +++ .../raster/lighting/LightingManager.java | 180 +++ .../e3d/renderer/raster/package-info.java | 26 + .../shapes/AbstractCoordinateShape.java | 150 +++ .../renderer/raster/shapes/AbstractShape.java | 81 ++ .../raster/shapes/basic/Billboard.java | 141 +++ .../raster/shapes/basic/GlowingPoint.java | 99 ++ .../raster/shapes/basic/line/Line.java | 342 ++++++ .../shapes/basic/line/LineAppearance.java | 50 + .../shapes/basic/line/LineInterpolator.java | 68 ++ .../basic/solidpolygon/LineInterpolator.java | 126 +++ .../basic/solidpolygon/SolidPolygon.java | 296 +++++ .../PolygonBorderInterpolator.java | 124 ++ .../texturedpolygon/TexturedPolygon.java | 249 ++++ .../composite/ForwardOrientedTextBlock.java | 90 ++ .../raster/shapes/composite/Graph.java | 180 +++ .../shapes/composite/LightSourceMarker.java | 43 + .../shapes/composite/TexturedRectangle.java | 185 +++ .../base/AbstractCompositeShape.java | 399 +++++++ .../shapes/composite/base/SubShape.java | 93 ++ .../composite/solid/SolidPolygonCube.java | 45 + .../composite/solid/SolidPolygonCylinder.java | 98 ++ .../composite/solid/SolidPolygonPyramid.java | 68 ++ .../solid/SolidPolygonRectangularBox.java | 92 ++ .../composite/solid/SolidPolygonSphere.java | 84 ++ .../composite/textcanvas/CanvasCharacter.java | 188 +++ .../composite/textcanvas/RenderMode.java | 28 + .../composite/textcanvas/TextCanvas.java | 463 ++++++++ .../composite/textcanvas/package-info.java | 9 + .../shapes/composite/wireframe/Grid2D.java | 80 ++ .../shapes/composite/wireframe/Grid3D.java | 99 ++ .../composite/wireframe/WireframeBox.java | 89 ++ .../composite/wireframe/WireframeCube.java | 45 + .../composite/wireframe/WireframeDrawing.java | 75 ++ .../composite/wireframe/WireframeSphere.java | 87 ++ .../renderer/raster/slicer/BorderLine.java | 81 ++ .../e3d/renderer/raster/slicer/Slicer.java | 141 +++ .../e3d/renderer/raster/texture/Texture.java | 383 +++++++ .../raster/texture/TextureBitmap.java | 253 +++++ .../sixth/e3d/examples/hourglass.png | Bin 0 -> 2161 bytes .../gui/textEditorComponent/TextLineTest.java | 116 ++ 100 files changed, 13992 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 COPYING create mode 100755 Tools/Open with IntelliJ IDEA create mode 100755 Tools/Update web site create mode 100644 doc/example.png create mode 100644 doc/index.org create mode 100644 pom.xml create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/geometry/Circle.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/geometry/package-info.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/FrameListener.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/GuiComponent.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/gui/TextPointer.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewFrame.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewSpaceTracker.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewUpdateTimerTask.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/Connexion3D.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardFocusStack.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardHelper.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardInputHandler.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseEvent.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseInteractionController.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/WorldNavigationUserInputTracker.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/package-info.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Character.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/LookAndFeel.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Page.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextEditComponent.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLine.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/package-info.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/math/TransformStack.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/math/package-info.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/package-info.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/IntegerPoint.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/OctreeVolume.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/package-info.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/LightSource.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/Ray.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayHit.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayTracer.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/package-info.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/package-info.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/RenderAggregator.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightSource.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/package-info.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractShape.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/GlowingPoint.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineAppearance.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineInterpolator.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/LightSourceMarker.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCube.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/RenderMode.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/package-info.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid2D.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid3D.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeDrawing.java create mode 100755 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeSphere.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/BorderLine.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.java create mode 100644 src/main/resources/eu/svjatoslav/sixth/e3d/examples/hourglass.png create mode 100644 src/test/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLineTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..31378ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/.idea/ +/target/ +/.classpath +/.project +/.settings/ +/doc/graphs/ +/doc/apidocs/ +/*.iml +*.html diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3ad2276 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,148 @@ +# Project Overview + +sixth-3d-engine is a Java-based 3D rendering engine. It provides: + +- 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 + +# Repository Structure + + src/main/java/eu/svjatoslav/sixth/e3d/ + ├── geometry/ — Core geometry: Point2D, Point3D, Box, Circle, Polygon + ├── math/ — Math utilities: Rotation, Transform, TransformStack, Vertex + ├── 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, TexturedPolygon + │ └── composite/ — Composite shapes: AbstractCompositeShape, TextCanvas, + │ WireframeBox, SolidPolygonRectangularBox + ├── slicer/ — Geometry slicing for level-of-detail + └── texture/ — Texture and TextureBitmap with mipmap support + +# Build & Test Commands + +## Build System + +- **Build tool:** Maven +- **Java version:** 21 +- **Build command:** `mvn clean install` + +## Testing + +- **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` + +Test files are located in `src/test/java/` following the same package structure as main code. + +## No Linting + +- No Checkstyle, PMD, or SpotBugs configured +- No `.editorconfig` or formatting configuration files present +- Code formatting follows manual conventions (see below) + +# Code Style Guidelines + +## License Header + +All Java files must start with this exact header: + +```java +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +``` + +## 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 + +## Types & Variables + +- **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 + +## Documentation + +- **Javadoc required** on all public classes and methods +- **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}` + +## Architecture Patterns + +- **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) + +# Architecture & Key Concepts + +## Coordinate System + +- `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 + +## Transform Pipeline + +- `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 + +## Shape Hierarchy + +- `AbstractShape` — base class with optional `MouseInteractionController` +- `AbstractCoordinateShape` — has `Vertex[]` coordinates and `onScreenZ` for depth sorting +- `AbstractCompositeShape` — groups sub-shapes with group IDs and visibility toggles +- Concrete shapes: `Line`, `SolidPolygon`, `TexturedPolygon`, `TextCanvas`, `WireframeBox` + +## Rendering + +- `ShapeCollection` is the root container with `RenderAggregator` and `TransformStack` +- `RenderAggregator` collects projected shapes, sorts by Z-index, paints back-to-front +- `ViewPanel` (extends `JPanel`) drives render loop, notifies `FrameListener` per frame +- Backface culling uses screen-space normal Z-component + +## Color + +- 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 + +## GUI / Input + +- `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 screen-space Z-component of normal; positive = front-facing +7. **Polygon winding:** Counter-clockwise winding for front-facing polygons (when viewed from outside) +8. **Testing:** Write JUnit 4 tests in `src/test/java/` with matching package structure diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/Tools/Open with IntelliJ IDEA b/Tools/Open with IntelliJ IDEA new file mode 100755 index 0000000..304bf94 --- /dev/null +++ b/Tools/Open with IntelliJ IDEA @@ -0,0 +1,54 @@ +#!/bin/bash + +# This script launches IntelliJ IDEA with the current project +# directory. The script is designed to be run by double-clicking it in +# the GNOME Nautilus file manager. + +# First, we change the current working directory to the directory of +# the script. + +# "${0%/*}" gives us the path of the script itself, without the +# script's filename. + +# This command basically tells the system "change the current +# directory to the directory containing this script". + +cd "${0%/*}" + +# Then, we move up one directory level. +# The ".." tells the system to go to the parent directory of the current directory. +# This is done because we assume that the project directory is one level up from the script. +cd .. + +# Now, we use the 'setsid' command to start a new session and run +# IntelliJ IDEA in the background. 'setsid' is a UNIX command that +# runs a program in a new session. + +# The command 'idea .' opens IntelliJ IDEA with the current directory +# as the project directory. The '&' at the end is a UNIX command that +# runs the process in the background. The '> /dev/null' part tells +# the system to redirect all output (both stdout and stderr, denoted +# by '&') that would normally go to the terminal to go to /dev/null +# instead, which is a special file that discards all data written to +# it. + +setsid idea . &>/dev/null & + +# The 'disown' command is a shell built-in that removes a shell job +# from the shell's active list. Therefore, the shell will not send a +# SIGHUP to this particular job when the shell session is terminated. + +# '-h' option specifies that if the shell receives a SIGHUP, it also +# doesn't send a SIGHUP to the job. + +# '$!' is a shell special parameter that expands to the process ID of +# the most recent background job. +disown -h $! + + +sleep 2 + +# Finally, we use the 'exit' command to terminate the shell script. +# This command tells the system to close the terminal window after +# IntelliJ IDEA has been opened. +exit diff --git a/Tools/Update web site b/Tools/Update web site new file mode 100755 index 0000000..9daf5a4 --- /dev/null +++ b/Tools/Update web site @@ -0,0 +1,101 @@ +#!/bin/bash +cd "${0%/*}"; if [ "$1" != "T" ]; then gnome-terminal -e "'$0' T"; exit; fi; + +cd .. + +# Function to export org to html using emacs in batch mode +export_org_to_html() { + local org_file=$1 + local dir=$(dirname "$org_file") + local base=$(basename "$org_file" .org) + ( + cd "$dir" || return 1 + local html_file="${base}.html" + if [ -f "$html_file" ]; then + rm -f "$html_file" + fi + echo "Exporting: $org_file → $dir/$html_file" + emacs --batch -l ~/.emacs --visit="${base}.org" --funcall=org-html-export-to-html --kill + if [ $? -eq 0 ]; then + echo "✓ Successfully exported $org_file" + else + echo "✗ Failed to export $org_file" + return 1 + fi + ) +} + +export_org_files_to_html() { + echo "🔍 Searching for .org files in doc/ ..." + echo "=======================================" + + mapfile -t ORG_FILES < <(find doc -type f -name "*.org" | sort) + + if [ ${#ORG_FILES[@]} -eq 0 ]; then + echo "❌ No .org files found!" + return 1 + fi + + echo "Found ${#ORG_FILES[@]} .org file(s):" + printf '%s\n' "${ORG_FILES[@]}" + echo "=======================================" + + SUCCESS_COUNT=0 + FAILED_COUNT=0 + + for org_file in "${ORG_FILES[@]}"; do + export_org_to_html "$org_file" + if [ $? -eq 0 ]; then + ((SUCCESS_COUNT++)) + else + ((FAILED_COUNT++)) + fi + done + + echo "=======================================" + echo "📊 SUMMARY:" + echo " ✓ Successful: $SUCCESS_COUNT" + echo " ✗ Failed: $FAILED_COUNT" + echo " Total: $((SUCCESS_COUNT + FAILED_COUNT))" + echo "" +} + +build_visualization_graphs() { + rm -rf doc/graphs/ + mkdir -p doc/graphs/ + + javainspect -j target/sixth-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "All classes" -t png -ho + javainspect -j target/sixth-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "GUI" -t png -w "eu.svjatoslav.sixth.e3d.gui.*" -ho + javainspect -j target/sixth-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "Raster engine" -t png -w "eu.svjatoslav.sixth.e3d.renderer.raster.*" -ho + + meviz index -w doc/graphs/ -t "Sixth 3D classes" +} + +# Build project jar file and JavaDocs +mvn clean package + +# Put generated JavaDoc HTML files to documentation directory +rm -rf doc/apidocs/ +cp -r target/apidocs/ doc/ + +# Publish Emacs org-mode files into HTML format +export_org_files_to_html + +# Generate nice looking code visualization diagrams +build_visualization_graphs + + +## Upload assembled documentation to server +echo "📤 Uploading to server..." +rsync -avz --delete -e 'ssh -p 10006' doc/ \ + n0@www3.svjatoslav.eu:/mnt/big/projects/sixth-3d/ + +if [ $? -eq 0 ]; then + echo "✓ Upload completed successfully!" +else + echo "✗ Upload failed!" +fi + +echo "" +echo "Press ENTER to close this window." +read diff --git a/doc/example.png b/doc/example.png new file mode 100644 index 0000000000000000000000000000000000000000..709424030b06354af0c4ac691eb441f119d853b1 GIT binary patch literal 67796 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 KMNUMnLSTZv%wjVD literal 0 HcmV?d00001 diff --git a/doc/index.org b/doc/index.org new file mode 100644 index 0000000..c882fd4 --- /dev/null +++ b/doc/index.org @@ -0,0 +1,556 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Sixth 3D - Realtime 3D engine +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +#+begin_export html + +#+end_export + + +* Introduction +:PROPERTIES: +:CUSTOM_ID: overview +:ID: a31a1f4d-5368-4fd9-aaf8-fa6d81851187 +:END: + +[[file:example.png]] + +*Sixth 3D* is a realtime 3D rendering engine written in pure Java. It +runs entirely on the CPU — no GPU required, no OpenGL, no Vulkan, no +native libraries. Just Java. + +The motivation is simple: GPU-based 3D is a minefield of accidental +complexity. Drivers are buggy or missing entirely. Features you need +aren't supported on your target hardware. You run out of GPU RAM. You +wrestle with platform-specific interop layers, shader compilation +quirks, and dependency hell. Every GPU API comes with its own +ecosystem of pain — version mismatches, incomplete implementations, +vendor-specific workarounds. I want a library that "just works". + +Sixth 3D takes a different path. By rendering everything in software +on the CPU, the entire GPU problem space simply disappears. You add a +Maven dependency, write some Java, and you have a 3D scene. It runs +wherever Java runs. + +This approach is quite practical for many use-cases. Modern systems +ship with many CPU cores, and those with unified memory architectures +offer high bandwidth between CPU and RAM. Software rendering that once +seemed wasteful is now a reasonable choice where you need good-enough +performance without the overhead of a full GPU pipeline. Java's JIT +compiler helps too, optimizing hot rendering paths at runtime. + +Beyond convenience, CPU rendering gives you complete control. You own +every pixel. You can freely experiment with custom rendering +algorithms, optimization strategies, and visual effects without being +constrained by what a GPU API exposes. Instead of brute-forcing +everything through a fixed GPU pipeline, you can implement clever, +application-specific optimizations. + +Sixth 3D is part of the larger [[https://www3.svjatoslav.eu/projects/sixth/][Sixth project]], with the long-term goal +of providing a platform for 3D user interfaces and interactive data +visualization. It can also be used as a standalone 3D engine in any +Java project. See the [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demos]] for examples of what it can do today. + +* Minimal example +:PROPERTIES: +:CUSTOM_ID: tutorial +:ID: 19a0e3f9-5225-404e-a48b-584b099fccf9 +:END: + +*Resources to help you understand the Sixth 3D library:* +- Read online [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/][JavaDoc]]. +- See [[https://www3.svjatoslav.eu/projects/sixth-3d/graphs/][Sixth 3D class diagrams]]. (Diagrams were generated by using + [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility) +- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]]. + + +*Brief tutorial:* + +Here we guide you through creating your first 3D scene with Sixth 3D +engine. + +Prerequisites: +- Java 21 or later installed +- Maven 3.x +- Basic Java knowledge + +** Add Dependency to Your Project +:PROPERTIES: +:CUSTOM_ID: add-dependency-to-your-project +:ID: 3fffc32e-ae66-40b7-ad7d-fab6093c778b +:END: + +Add Sixth 3D to your pom.xml: + +#+BEGIN_SRC xml + + + eu.svjatoslav + sixth-3d + 1.3 + + + + + + svjatoslav.eu + Svjatoslav repository + https://www3.svjatoslav.eu/maven/ + + +#+END_SRC + +** Create Your First 3D Scene +:PROPERTIES: +:CUSTOM_ID: create-your-first-3d-scene +:ID: 564fa596-9b2b-418a-9df9-baa46f0d0a66 +:END: + +Here is a minimal working example: + +#+BEGIN_SRC java + import eu.svjatoslav.sixth.e3d.geometry.Point3D; + import eu.svjatoslav.sixth.e3d.gui.ViewFrame; + import eu.svjatoslav.sixth.e3d.math.Transform; + import eu.svjatoslav.sixth.e3d.renderer.raster.Color; + import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection; + import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonRectangularBox; + + public class MyFirstScene { + public static void main(String[] args) { + // Create the application window + ViewFrame viewFrame = new ViewFrame(); + + // Get the collection where you add 3D shapes + ShapeCollection shapes = viewFrame.getViewPanel().getRootShapeCollection(); + + // Add a red box at position (0, 0, 0) + Transform boxTransform = new Transform(new Point3D(0, 0, 0), 0, 0); + SolidPolygonRectangularBox box = new SolidPolygonRectangularBox( + new Point3D(-50, -50, -50), + new Point3D(50, 50, 50), + Color.RED + ); + box.setTransform(boxTransform); + shapes.addShape(box); + + // Position your camera + viewFrame.getViewPanel().getCamera().setLocation(new Point3D(0, -100, -300)); + + // Update the screen + viewFrame.getViewPanel().repaintDuringNextViewUpdate(); + } + } +#+END_SRC + +Compile and run *MyFirstScene* class. New window should open that will +display 3D scene with red box. + +*Navigating the scene:* + +| Input | Action | +|---------------------+-------------------------------------| +| Arrow Up / W | Move forward | +| Arrow Down / S | Move backward | +| Arrow Left | Move left (strafe) | +| Arrow Right | Move right (strafe) | +| Mouse drag | Look around (rotate camera) | +| Mouse scroll wheel | Move up / down | + +Movement uses physics-based acceleration for smooth, natural +motion. The faster you're moving, the more acceleration builds up, +creating an intuitive flying experience. + +* In-depth understanding +** Vertex + +#+BEGIN_EXPORT html + + + + + + + + + + V + (x, y, z) + x + y + +#+END_EXPORT + +A *vertex* is a single point in 3D space, defined by three +coordinates: *x*, *y*, and *z*. Every 3D object is ultimately built +from vertices. A vertex can also carry additional data beyond +position. + +- Position: =(x, y, z)= +- Can also store: color, texture UV, normal vector +- A triangle = 3 vertices, a cube = 8 vertices +- Vertex maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Point3D.html][Point3D]] class in Sixth 3D engine. + + +** Edge + +#+BEGIN_EXPORT html + + + + + + + + V₁ + V₂ + V₃ + edge + +#+END_EXPORT + +An *edge* is a straight line segment connecting two vertices. Edges +define the wireframe skeleton of a 3D model. In rendering, edges +themselves are rarely drawn — they exist implicitly as boundaries of +faces. + +- Edge = line from V₁ to V₂ +- A triangle has 3 edges +- A cube has 12 edges +- Wireframe mode renders edges visibly +- Edge is related to and can be represented by the [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class in Sixth + 3D engine. + +** Face (Triangle) + +#+BEGIN_EXPORT html + + + + + + + + + + V₁ + V₂ + V₃ + FACE + +#+END_EXPORT + +A *face* is a flat surface enclosed by edges. In most 3D engines, the fundamental face is a *triangle* — defined by exactly 3 vertices. Triangles are preferred because they are always planar (flat) and trivially simple to rasterize. + +- Triangle = 3 vertices + 3 edges +- Always guaranteed to be coplanar +- Quads (4 vertices) = 2 triangles +- Complex shapes = many triangles (a "mesh") +- Face maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] or [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html][TexturedPolygon]] in Sixth 3D. + +** Coordinate System (X, Y, Z) + +#+BEGIN_EXPORT html + + + + + + X + right / left + + + Y + up / down + + + Z + depth (forward/back) + Origin + (0, 0, 0) + + + + (3, 4, 0) + +#+END_EXPORT + +Every point in 3D space is located using three perpendicular axes +originating from the *origin (0, 0, 0)*. The *X* axis runs left–right, +the *Y* axis runs up–down, and the *Z* axis represents depth. + +- Right-handed vs left-handed systems differ in which direction =+Z= points +- Right-handed: +Z towards viewer (OpenGL) +- Left-handed: +Z into screen (DirectX) + +** Normal Vector + +#+BEGIN_EXPORT html + + + + + + + + + N̂ + unit normal + (perpendicular + to surface) + + + Light + + L · N = brightness + +#+END_EXPORT + +A *normal* is a vector perpendicular to a surface. It tells the +renderer which direction a face is pointing. Normals are critical for +*lighting* — the angle between the light direction and the normal +determines how bright a surface appears. + +- *Face normal*: one normal per triangle +- *Vertex normal*: one normal per vertex (averaged from adjacent faces for smooth shading) +- =dot(L, N)= → surface brightness +- Flat shading → face normals +- Gouraud/Phong → vertex normals + interpolation + +** Mesh + +#+BEGIN_EXPORT html + + + + + + + + + + + + + + + + + + + triangulated + section + + +#+END_EXPORT + +A *mesh* is a collection of vertices, edges, and faces that together define the shape of a 3D object. Even curved surfaces like spheres are approximated by many small triangles — more triangles means a smoother appearance. + +- Mesh data = vertex array + index array +- Index array avoids duplicating shared vertices +- Cube: 8 vertices, 12 triangles +- Smooth sphere: hundreds–thousands of triangles +- =vertices[] + indices[]= → efficient storage +- In Sixth 3D engine: + - [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]]: base class for single shapes with vertices (triangles, lines). Use when creating one primitive. + - [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]: groups multiple shapes into one object. Use for complex models that move/rotate together. + +** Winding Order & Backface Culling + +#+BEGIN_EXPORT html + + + + + + CCW + + + + V₁ + V₂ + V₃ + FRONT FACE ✓ + + + + CW + + + BACK FACE ✗ + (culled — not drawn) + +#+END_EXPORT + +The order in which a triangle's vertices are listed determines its *winding order*. Counter-clockwise (CCW) typically means front-facing. *Backface culling* skips rendering triangles that face away from the camera — a major performance optimization. + +- CCW winding → front face (visible) +- CW winding → back face (culled) +- Saves ~50% of triangle rendering +- Normal direction derived from winding order via =cross(V₂-V₁, V₃-V₁)= + +In Sixth 3D, backface culling is *optional* and disabled by default. Enable it per-shape: +- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#setBackfaceCulling(boolean)][SolidPolygon.setBackfaceCulling(true)]] +- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html#setBackfaceCulling(boolean)][TexturedPolygon.setBackfaceCulling(true)]] +- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] (applies to all + sub-shapes) + +** Working with Colors +:PROPERTIES: +:CUSTOM_ID: working-with-colors +:ID: f2c9642a-a093-444f-8992-76c97ff28c16 +:END: + +Sixth 3D uses its own Color class (not java.awt.Color): + +#+BEGIN_SRC java +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; + +// Using predefined colors +Color red = Color.RED; +Color green = Color.GREEN; +Color blue = Color.BLUE; + +// Create custom color (R, G, B, A) +Color custom = new Color(255, 128, 64, 200); // semi-transparent orange + +// Or use hex string +Color hex = new Color("FF8040CC"); // same orange with alpha +#+END_SRC + +* Source code +:PROPERTIES: +:CUSTOM_ID: source-code +:ID: 978b7ea2-e246-45d0-be76-4d561308e9f3 +:END: + +*This program is free software: released under Creative Commons Zero +(CC0) license* + +*Program author:* +- Svjatoslav Agejenko +- Homepage: https://svjatoslav.eu +- Email: mailto://svjatoslav@svjatoslav.eu +- See also: [[https://www.svjatoslav.eu/projects/][Other software projects hosted at svjatoslav.eu]] + +*Getting the source code:* +- [[https://www2.svjatoslav.eu/gitweb/?p=sixth-3d.git;a=snapshot;h=HEAD;sf=tgz][Download latest source code snapshot in TAR GZ format]] +- [[https://www2.svjatoslav.eu/gitweb/?p=sixth-3d.git;a=summary][Browse Git repository online]] +- Clone Git repository using command: + : git clone https://www3.svjatoslav.eu/git/sixth-3d.git + +** Understanding the Sixth 3D source code + +- Read online [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/][JavaDoc]]. +- See [[https://www3.svjatoslav.eu/projects/sixth-3d/graphs/][Sixth 3D class diagrams]]. (Diagrams were generated by using + [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility) +- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]]. + +* Future ideas +:PROPERTIES: +:CUSTOM_ID: future-ideas +:ID: 2258231b-007d-42d3-9ba9-a9957a0dfc56 +:END: + ++ Read this as example, and apply improvements/fixes where applicable: + http://blog.rogach.org/2015/08/how-to-create-your-own-simple-3d-render.html + ++ Improve triangulation. Read: https://ianthehenry.com/posts/delaunay/ + ++ Partial region/frame repaint: when only one small object changed on + the scene, it would be faster to re-render that specific area. + + + Once partial rendering works, in would be easy to add multi-core + rendering support. So that each core renders it's own region of + the screen. + ++ Anti-aliasing. Would improve text readability. If antialiazing is + too expensive for every frame, it could be used only for last frame + before animations become still and waiting for user input starts. + +** Render only visible polygons +:PROPERTIES: +:CUSTOM_ID: render-only-visible-polygons +:ID: c32d839a-cfa8-4aec-a8e0-8c9e7ebb8bba +:END: + +Very high-level idea description: + ++ This would significantly reduce RAM <-> CPU traffic. + ++ General algorithm description: + + For each horizontal scanline: + + sort polygon edges from left to right + + while iterating and drawing pixels over screen X axis (left to + right) track next appearing/disappearing polygons. + + For each polygon edge update Z sorted active polygons list. + + Only draw pixel from the top-most polygon. + + Only if polygon area is transparent/half-transparent add + colors from the polygons below. + ++ As a bonus, this would allow to track which polygons are really + visible in the final scene for each frame. + + + Such information allows further optimizations: + + + Dynamic geometry simplification: + + Dynamically detect and replace invisible objects from the + scene with simplified bounding box. + + + Dynamically replace boudnig box with actual object once it + becomes visible. + + + Dynamically unload unused textures from RAM. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4ba6be5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,145 @@ + + 4.0.0 + eu.svjatoslav + sixth-3d + 1.4-SNAPSHOT + Sixth 3D + 3D engine + + + 21 + 21 + 21 + UTF-8 + UTF-8 + + + + svjatoslav.eu + https://svjatoslav.eu + + + + + junit + junit + 4.12 + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + true + UTF-8 + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.4 + + + attach-javadocs + + jar + + + + + + + + foo + bar + + + + ${java.home}/bin/javadoc + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4.3 + + UTF-8 + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.2 + + + org.apache.maven.scm + maven-scm-provider-gitexe + 1.9.4 + + + + + + + + org.apache.maven.wagon + wagon-ssh-external + 2.6 + + + + + + + + svjatoslav.eu + svjatoslav.eu + scpexe://svjatoslav.eu:10006/srv/maven + + + svjatoslav.eu + svjatoslav.eu + scpexe://svjatoslav.eu:10006/srv/maven + + + + + + svjatoslav.eu + Svjatoslav repository + https://www3.svjatoslav.eu/maven/ + + + + + scm:git:ssh://n0@svjatoslav.eu:10006/home/n0/git/sixth-3d.git + scm:git:ssh://n0@svjatoslav.eu:10006/home/n0/git/sixth-3d.git + HEAD + + + diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java new file mode 100644 index 0000000..e461531 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java @@ -0,0 +1,115 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.geometry; + +import static java.lang.Math.abs; + +/** + * Same as: 3D rectangle, rectangular box, rectangular parallelopiped, cuboid, + * rhumboid, hexahedron, rectangular prism. + */ +public class Box implements Cloneable { + + /** + * The first point of the box. + */ + public final Point3D p1; + /** + * The second point of the box. + */ + public final Point3D p2; + + /** + * Creates a new box with two points at the origin. + */ + public Box() { + p1 = new Point3D(); + p2 = new Point3D(); + } + + /** + * Creates a new box with two points at the specified coordinates. + */ + public Box(final Point3D p1, final Point3D p2) { + this.p1 = p1; + this.p2 = p2; + } + + + /** + * Enlarges the box by the specified border in all directions. + * + * @param border The border to enlarge the box by. + * If the border is negative, the box will be shrunk. + * @return The current box. + */ + public Box enlarge(final double border) { + + if (p1.x < p2.x) { + p1.translateX(-border); + p2.translateX(border); + } else { + p1.translateX(border); + p2.translateX(-border); + } + + if (p1.y < p2.y) { + p1.translateY(-border); + p2.translateY(border); + } else { + p1.translateY(border); + p2.translateY(-border); + } + + if (p1.z < p2.z) { + p1.translateZ(-border); + p2.translateZ(border); + } else { + p1.translateZ(border); + p2.translateZ(-border); + } + + return this; + } + + @Override + public Box clone() { + return new Box(p1.clone(), p2.clone()); + } + + /** + * @return The depth of the box. The depth is the distance between the two points on the z-axis. + */ + public double getDepth() { + return abs(p1.z - p2.z); + } + + /** + * @return The height of the box. The height is the distance between the two points on the y-axis. + */ + public double getHeight() { + return abs(p1.y - p2.y); + } + + /** + * @return The width of the box. The width is the distance between the two points on the x-axis. + */ + public double getWidth() { + return abs(p1.x - p2.x); + } + + + /** + * Sets the size of the box. The box will be centered at the origin. + * Previous size and position of the box will be lost. + * + * @param size {@link Point3D} specifies box size in x, y and z axis. + */ + public void setBoxSize(final Point3D size) { + p2.clone(size).scaleDown(2); + p1.clone(p2).invert(); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Circle.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Circle.java new file mode 100644 index 0000000..2ac177d --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Circle.java @@ -0,0 +1,22 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.geometry; + +/** + * Circle in 2D space. + */ +public class Circle { + + /** + * The center of the circle. + */ + Point2D location; + + /** + * The radius of the circle. + */ + double radius; + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java new file mode 100755 index 0000000..a6bb6a1 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java @@ -0,0 +1,160 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.geometry; + +import static java.lang.Math.sqrt; + +/** + * Used to represent point in a 2D space or vector. + * + * @see Point3D + */ +public class Point2D implements Cloneable { + + public double x, y; + + public Point2D() { + } + + public Point2D(final double x, final double y) { + this.x = x; + this.y = y; + } + + public Point2D(final Point2D parent) { + x = parent.x; + y = parent.y; + } + + + /** + * Add other point to current point. Value of other point will not be changed. + * + * @return current point. + */ + public Point2D add(final Point2D otherPoint) { + x += otherPoint.x; + y += otherPoint.y; + return this; + } + + /** + * @return true if current point coordinates are equal to zero. + */ + public boolean isZero() { + return (x == 0) && (y == 0); + } + + @Override + public Point2D clone() { + return new Point2D(this); + } + + /** + * Copy coordinates from other point to current point. Value of other point will not be changed. + */ + public void clone(final Point2D otherPoint) { + x = otherPoint.x; + y = otherPoint.y; + } + + /** + * Set current point to middle of two other points. + * + * @param p1 first point. + * @param p2 second point. + * @return current point. + */ + public Point2D setToMiddle(final Point2D p1, final Point2D p2) { + x = (p1.x + p2.x) / 2d; + y = (p1.y + p2.y) / 2d; + return this; + } + + public double getAngleXY(final Point2D anotherPoint) { + return Math.atan2(x - anotherPoint.x, y - anotherPoint.y); + } + + /** + * Compute distance to another point. + * + * @param anotherPoint point to compute distance to. + * @return distance from current point to another point. + */ + public double getDistanceTo(final Point2D anotherPoint) { + final double xDiff = x - anotherPoint.x; + final double yDiff = y - anotherPoint.y; + + return sqrt(((xDiff * xDiff) + (yDiff * yDiff))); + } + + /** + * Calculate length of vector. + * + * @return length of vector. + */ + public double getVectorLength() { + return sqrt(((x * x) + (y * y))); + } + + /** + * Invert current point. + * + * @return current point. + */ + public Point2D invert() { + x = -x; + y = -y; + return this; + } + + /** + * Round current point coordinates to integer. + */ + public void roundToInteger() { + x = (int) x; + y = (int) y; + } + + /** + * Subtract other point from current point. Value of other point will not be changed. + * + * @return current point. + */ + public Point2D subtract(final Point2D otherPoint) { + x -= otherPoint.x; + y -= otherPoint.y; + return this; + } + + /** + * Convert current point to 3D point. + * Value of the z coordinate will be set to zero. + * + * @return 3D point. + */ + public Point3D to3D() { + return new Point3D(x, y, 0); + } + + /** + * Set current point to zero. + * + * @return current point. + */ + public Point2D zero() { + x = 0; + y = 0; + return this; + } + + @Override + public String toString() { + return "Point2D{" + + "x=" + x + + ", y=" + y + + '}'; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java new file mode 100755 index 0000000..6f93616 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java @@ -0,0 +1,419 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.geometry; + +import eu.svjatoslav.sixth.e3d.renderer.octree.IntegerPoint; + +import static java.lang.Math.*; + +/** + * A mutable 3D point or vector with double-precision coordinates. + * + *

{@code Point3D} is the fundamental coordinate type used throughout the Sixth 3D engine. + * It represents either a position in 3D space or a directional vector, with public + * {@code x}, {@code y}, {@code z} fields for direct access.

+ * + *

All mutation methods return {@code this} for fluent chaining:

+ *
{@code
+ * Point3D p = new Point3D(10, 20, 30)
+ *     .scaleUp(2.0)
+ *     .translateX(5)
+ *     .add(new Point3D(1, 1, 1));
+ * // p is now (25, 41, 61)
+ * }
+ * + *

Common operations:

+ *
{@code
+ * // Create points
+ * Point3D origin = new Point3D();              // (0, 0, 0)
+ * Point3D pos = new Point3D(100, 200, 300);
+ * Point3D copy = new Point3D(pos);             // clone
+ *
+ * // Measure distance
+ * double dist = pos.getDistanceTo(origin);
+ *
+ * // Rotation
+ * pos.rotate(origin, Math.PI / 4, 0);  // rotate 45 degrees on XZ plane
+ *
+ * // Scale
+ * pos.scaleUp(2.0);   // double all coordinates
+ * pos.scaleDown(2.0);  // halve all coordinates
+ * }
+ * + *

Warning: This class is mutable with public fields. Clone before storing + * references that should not be shared:

+ *
{@code
+ * Point3D safeCopy = original.clone();
+ * }
+ * + * @see Point2D the 2D equivalent + * @see eu.svjatoslav.sixth.e3d.math.Vertex wraps a Point3D with transform support + */ +public class Point3D implements Cloneable { + + /** X coordinate (horizontal axis). */ + public double x; + /** Y coordinate (vertical axis, positive = down in screen space). */ + public double y; + /** Z coordinate (depth axis, positive = into the screen / away from viewer). */ + public double z; + + /** + * Creates a point at the origin (0, 0, 0). + */ + public Point3D() { + } + + /** + * Creates a point with the specified double-precision coordinates. + * + * @param x the X coordinate + * @param y the Y coordinate + * @param z the Z coordinate + */ + public Point3D(final double x, final double y, final double z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Creates a point with the specified float coordinates (widened to double). + * + * @param x the X coordinate + * @param y the Y coordinate + * @param z the Z coordinate + */ + public Point3D(final float x, final float y, final float z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Creates a point with the specified integer coordinates (widened to double). + * + * @param x the X coordinate + * @param y the Y coordinate + * @param z the Z coordinate + */ + public Point3D(final int x, final int y, final int z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Creates a point from an {@link IntegerPoint} (used by octree voxel coordinates). + * + * @param point the integer point to convert + */ + public Point3D(IntegerPoint point) { + this.x = point.x; + this.y = point.y; + this.z = point.z; + } + + + /** + * Creates new current point by cloning coordinates from parent point. + */ + public Point3D(final Point3D parent) { + x = parent.x; + y = parent.y; + z = parent.z; + } + + /** + * Add other point to current point. Value of other point will not be changed. + * + * @param otherPoint point to add. + * @return current point. + */ + public Point3D add(final Point3D otherPoint) { + x += otherPoint.x; + y += otherPoint.y; + z += otherPoint.z; + return this; + } + + /** + * Add coordinates of current point to other point. Value of current point will not be changed. + * + * @return current point. + */ + public Point3D addTo(final Point3D... otherPoints) { + for (final Point3D otherPoint : otherPoints) otherPoint.add(this); + return this; + } + + /** + * Create new point by cloning position of current point. + * + * @return newly created clone. + */ + public Point3D clone() { + return new Point3D(this); + } + + /** + * Copy coordinates from other point to current point. Value of other point will not be changed. + */ + public Point3D clone(final Point3D otherPoint) { + x = otherPoint.x; + y = otherPoint.y; + z = otherPoint.z; + return this; + } + + /** + * Set current point coordinates to the middle point between two other points. + * + * @param p1 first point. + * @param p2 second point. + * @return current point. + */ + public Point3D computeMiddlePoint(final Point3D p1, final Point3D p2) { + x = (p1.x + p2.x) / 2d; + y = (p1.y + p2.y) / 2d; + z = (p1.z + p2.z) / 2d; + return this; + } + + /** + * @return true if current point coordinates are equal to zero. + */ + public boolean isZero() { + return (x == 0) && (y == 0) && (z == 0); + } + + /** + * Computes the angle on the X-Z plane between this point and another point. + * + * @param anotherPoint the other point + * @return the angle in radians + */ + public double getAngleXZ(final Point3D anotherPoint) { + return Math.atan2(x - anotherPoint.x, z - anotherPoint.z); + } + + /** + * Computes the angle on the Y-Z plane between this point and another point. + * + * @param anotherPoint the other point + * @return the angle in radians + */ + public double getAngleYZ(final Point3D anotherPoint) { + return Math.atan2(y - anotherPoint.y, z - anotherPoint.z); + } + + /** + * Computes the angle on the X-Y plane between this point and another point. + * + * @param anotherPoint the other point + * @return the angle in radians + */ + public double getAngleXY(final Point3D anotherPoint) { + return Math.atan2(x - anotherPoint.x, y - anotherPoint.y); + } + + /** + * Compute distance to another point. + * + * @param anotherPoint point to compute distance to. + * @return distance to another point. + */ + public double getDistanceTo(final Point3D anotherPoint) { + final double xDelta = x - anotherPoint.x; + final double yDelta = y - anotherPoint.y; + final double zDelta = z - anotherPoint.z; + + return sqrt(((xDelta * xDelta) + (yDelta * yDelta) + (zDelta * zDelta))); + } + + /** + * @return length of current vector. + */ + public double getVectorLength() { + return sqrt(((x * x) + (y * y) + (z * z))); + } + + /** + * Invert current point coordinates. + * + * @return current point. + */ + public Point3D invert() { + x = -x; + y = -y; + z = -z; + return this; + } + + /** + * Rotate current point around center point by angleXZ and angleYZ. + *

The camera is the user's "eyes" in the 3D scene. It has a position (location), + * a looking direction (defined by XZ and YZ angles), and a movement system with + * velocity, acceleration, and friction for smooth camera navigation.

+ * + *

By default, the user can navigate using arrow keys (handled by + * {@link eu.svjatoslav.sixth.e3d.gui.humaninput.WorldNavigationUserInputTracker}), + * and the mouse controls the look direction (handled by + * {@link eu.svjatoslav.sixth.e3d.gui.humaninput.InputManager}).

+ * + *

Programmatic camera control:

+ *
{@code
+ * Camera camera = viewPanel.getCamera();
+ *
+ * // Set camera position
+ * camera.getTransform().setTranslation(new Point3D(0, -50, -200));
+ *
+ * // Set camera orientation (radians)
+ * camera.getTransform().setRotation(0, 0);  // angleXZ, angleYZ
+ *
+ * // Copy camera state from another camera
+ * Camera snapshot = new Camera(camera);
+ * }
+ * + * @see ViewPanel#getCamera() + * @see eu.svjatoslav.sixth.e3d.gui.humaninput.WorldNavigationUserInputTracker default keyboard navigation + */ +public class Camera implements FrameListener { + + /** + * Camera movement speed limit, relative to the world. When camera coordinates are + * updated within the world, camera orientation relative to the world is + * taken into account. + */ + public static final double SPEED_LIMIT = 30; + /** + * Just in case we want to adjust global speed for some reason. + */ + private static final double SPEED_MULTIPLIER = .02d; + /** + * Determines amount of friction user experiences every millisecond while moving around in space. + */ + private static final double MILLISECOND_FRICTION = 1.005; + /** + * Camera movement speed, relative to camera itself. When camera coordinates + * are updated within the world, camera orientation relative to the world is + * taken into account. + */ + private final Point3D movementVector = new Point3D(); + private final Point3D previousLocation = new Point3D(); + public double cameraAcceleration = 0.1; + /** + * The transform containing camera location and orientation. + */ + private final Transform transform; + + /** + * Creates a camera at the world origin with no rotation. + */ + public Camera() { + transform = new Transform(); + } + + /** + * Creates a copy of an existing camera, cloning its position and orientation. + * + * @param sourceView the camera to copy + */ + public Camera(final Camera sourceView) { + transform = sourceView.getTransform().clone(); + } + + public Camera(final Transform transform){ + this.transform = transform; + } + + @Override + public boolean onFrame(final ViewPanel viewPanel, final int millisecondsSinceLastFrame) { + + previousLocation.clone(transform.getTranslation()); + translateCameraLocationBasedOnMovementVector(millisecondsSinceLastFrame); + applyFrictionToMovement(millisecondsSinceLastFrame); + return isFrameRepaintNeeded(); + } + + private boolean isFrameRepaintNeeded() { + final double distanceMoved = transform.getTranslation().getDistanceTo(previousLocation); + return distanceMoved > 0.03; + } + + /** + * Clamps the camera's movement speed to {@link #SPEED_LIMIT}. + * Called after modifying the movement vector to prevent excessive velocity. + */ + public void enforceSpeedLimit() { + final double currentSpeed = movementVector.getVectorLength(); + + if (currentSpeed <= SPEED_LIMIT) + return; + + movementVector.scaleDown(currentSpeed / SPEED_LIMIT); + } + + /** + * Returns the current movement velocity vector, relative to the camera's orientation. + * Modify this vector to programmatically move the camera. + * + * @return the movement vector (mutable reference) + */ + public Point3D getMovementVector() { + return movementVector; + } + + /** + * Returns the current movement speed (magnitude of the movement vector). + * + * @return the scalar speed value + */ + public double getMovementSpeed() { + return movementVector.getVectorLength(); + } + + /** + * Apply friction to camera movement vector. + * + * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate. + * Therefore, we take frame rendering time into account when translating + * camera between consecutive frames. + */ + private void applyFrictionToMovement(int millisecondsPassedSinceLastFrame) { + for (int i = 0; i < millisecondsPassedSinceLastFrame; i++) + applyMillisecondFrictionToUserMovementVector(); + } + + /** + * Apply friction to camera movement vector. + */ + private void applyMillisecondFrictionToUserMovementVector() { + movementVector.x /= MILLISECOND_FRICTION; + movementVector.y /= MILLISECOND_FRICTION; + movementVector.z /= MILLISECOND_FRICTION; + } + + /** + * Translate coordinates based on camera movement vector and camera orientation in the world. + * + * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate. + * Therefore, we take frame rendering time into account when translating + * camera between consecutive frames. + */ + private void translateCameraLocationBasedOnMovementVector(int millisecondsPassedSinceLastFrame) { + final double sinXZ = transform.getRotation().getSinXZ(); + final double cosXZ = transform.getRotation().getCosXZ(); + + final Point3D location = transform.getTranslation(); + + location.x -= (float) sinXZ + * movementVector.z * SPEED_MULTIPLIER + * millisecondsPassedSinceLastFrame; + location.z += (float) cosXZ + * movementVector.z * SPEED_MULTIPLIER + * millisecondsPassedSinceLastFrame; + + location.x += (float) cosXZ + * movementVector.x * SPEED_MULTIPLIER + * millisecondsPassedSinceLastFrame; + location.z += (float) sinXZ + * movementVector.x * SPEED_MULTIPLIER + * millisecondsPassedSinceLastFrame; + + location.y += movementVector.y * SPEED_MULTIPLIER + * millisecondsPassedSinceLastFrame; + } + + /** + * Returns the transform containing this camera's location and orientation. + * + * @return the transform (mutable reference) + */ + public Transform getTransform() { + return transform; + } + + /** + * Orients the camera to look at a target point in world coordinates. + * + *

Calculates the required XZ and YZ rotation angles to point the camera + * from its current position toward the target. Useful for programmatic + * camera control, cinematic sequences, and following objects.

+ * + *

Example:

+ *
{@code
+     * Camera camera = viewPanel.getCamera();
+     * camera.getTransform().setTranslation(new Point3D(100, -50, -200));
+     * camera.lookAt(new Point3D(0, 0, 0));  // Point camera at origin
+     * }
+ * + * @param target the world-space point to look at + */ + public void lookAt(final Point3D target) { + final Point3D pos = transform.getTranslation(); + final double dx = target.x - pos.x; + final double dy = target.y - pos.y; + final double dz = target.z - pos.z; + + final double angleXZ = -Math.atan2(dx, dz); + final double horizontalDist = Math.sqrt(dx * dx + dz * dz); + final double angleYZ = -Math.atan2(dy, horizontalDist); + + transform.setRotation(angleXZ, angleYZ); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/FrameListener.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/FrameListener.java new file mode 100644 index 0000000..dcfe7a6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/FrameListener.java @@ -0,0 +1,52 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui; + +/** + * Listener interface for per-frame callbacks before the 3D scene is rendered. + * + *

Implement this interface and register it with + * {@link ViewPanel#addFrameListener(FrameListener)} to receive a callback + * before each frame. This is the primary mechanism for implementing animations, + * physics updates, and other time-dependent behavior.

+ * + *

Usage example - animating a shape:

+ *
{@code
+ * viewPanel.addFrameListener((panel, deltaMs) -> {
+ *     // Rotate the shape a little each frame
+ *     double angleIncrement = deltaMs * 0.001;  // radians per millisecond
+ *     myShape.setTransform(new Transform(
+ *         myShape.getLocation(),
+ *         currentAngle += angleIncrement, 0
+ *     ));
+ *     return true;  // request repaint since we changed something
+ * });
+ * }
+ * + *

The engine uses the return values to optimize rendering: if no listener + * returns {@code true} and no other changes occurred, the frame is skipped + * to save CPU and energy.

+ * + * @see ViewPanel#addFrameListener(FrameListener) + * @see ViewPanel#removeFrameListener(FrameListener) + */ +public interface FrameListener { + + /** + * Called before each frame render, allowing the listener to update state + * and indicate whether a repaint is needed. + * + *

Each registered listener is called exactly once per frame tick. + * The frame is only rendered if at least one listener returns {@code true} + * (or if the view was explicitly marked for repaint).

+ * + * @param viewPanel the view panel being rendered + * @param millisecondsSinceLastFrame time elapsed since the previous frame, + * for frame-rate-independent updates + * @return {@code true} if the view should be re-rendered this frame, + * {@code false} if this listener has no visual changes + */ + boolean onFrame(ViewPanel viewPanel, int millisecondsSinceLastFrame); +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/GuiComponent.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/GuiComponent.java new file mode 100644 index 0000000..cd3a357 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/GuiComponent.java @@ -0,0 +1,161 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui; + +import eu.svjatoslav.sixth.e3d.geometry.Box; +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardHelper; +import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardInputHandler; +import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.sixth.e3d.math.Transform; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.WireframeBox; + +import java.awt.event.KeyEvent; + +/** + * Base class for interactive GUI components rendered in 3D space. + * + *

{@code GuiComponent} combines a composite shape with keyboard and mouse interaction + * handling. When clicked, it acquires keyboard focus (via the {@link eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack}), + * and a red wireframe border is displayed to indicate focus. Pressing ESC releases focus.

+ * + *

This class is the foundation for interactive widgets like the + * {@link eu.svjatoslav.sixth.e3d.gui.textEditorComponent.TextEditComponent}.

+ * + *

Usage example - creating a custom GUI component:

+ *
{@code
+ * GuiComponent myWidget = new GuiComponent(
+ *     new Transform(new Point3D(0, 0, 300)),
+ *     viewPanel,
+ *     new Point3D(400, 300, 0)  // width, height, depth
+ * );
+ *
+ * // Add visual content to the widget
+ * myWidget.addShape(someTextCanvas);
+ *
+ * // Add to the scene
+ * viewPanel.getRootShapeCollection().addShape(myWidget);
+ * }
+ * + * @see eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack manages which component has keyboard focus + * @see eu.svjatoslav.sixth.e3d.gui.textEditorComponent.TextEditComponent a full text editor built on this class + */ +public class GuiComponent extends AbstractCompositeShape implements + KeyboardInputHandler, MouseInteractionController { + + private static final String GROUP_GUI_FOCUS = "gui.focus"; + + /** + * The view panel this component is attached to. + */ + public final ViewPanel viewPanel; + Box containingBox = new Box(); + private WireframeBox borders = null; + + private boolean borderShown = false; + + /** + * Creates a GUI component with the specified transform, view panel, and bounding box size. + * + * @param transform the position and orientation of the component in 3D space + * @param viewPanel the view panel this component belongs to + * @param size the bounding box dimensions (width, height, depth) + */ + public GuiComponent(final Transform transform, + final ViewPanel viewPanel, final Point3D size) { + super(transform); + this.viewPanel = viewPanel; + setDimensions(size); + } + + private WireframeBox createBorder() { + final LineAppearance appearance = new LineAppearance(10, + new eu.svjatoslav.sixth.e3d.renderer.raster.Color(255, 0, 0, 100)); + + final double borderSize = 10; + + final Box borderArea = containingBox.clone().enlarge(borderSize); + + return new WireframeBox(borderArea, appearance); + } + + @Override + public boolean focusLost(final ViewPanel viewPanel) { + hideBorder(); + return true; + } + + @Override + public boolean focusReceived(final ViewPanel viewPanel) { + showBorder(); + return true; + } + + public WireframeBox getBorders() { + if (borders == null) + borders = createBorder(); + return borders; + } + + public int getDepth() { + return (int) containingBox.getDepth(); + } + + public int getHeight() { + return (int) containingBox.getHeight(); + } + + public int getWidth() { + return (int) containingBox.getWidth(); + } + + public void hideBorder() { + if (!borderShown) + return; + borderShown = false; + removeGroup(GROUP_GUI_FOCUS); + } + + @Override + public boolean keyPressed(final KeyEvent event, final ViewPanel viewPanel) { + if (event.getKeyChar() == KeyboardHelper.ESC) + viewPanel.getKeyboardFocusStack().popFocusOwner(); + return true; + } + + @Override + public boolean keyReleased(final KeyEvent event, final ViewPanel viewPanel) { + return false; + } + + @Override + public boolean mouseClicked(int button) { + return viewPanel.getKeyboardFocusStack().pushFocusOwner(this); + } + + @Override + public boolean mouseEntered() { + return false; + } + + @Override + public boolean mouseExited() { + return false; + } + + private void setDimensions(final Point3D size) { + containingBox.setBoxSize(size); + } + + private void showBorder() { + if (borderShown) + return; + borderShown = true; + addShape(getBorders(), GROUP_GUI_FOCUS); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java new file mode 100644 index 0000000..d072874 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java @@ -0,0 +1,187 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui; + +import eu.svjatoslav.sixth.e3d.geometry.Point2D; +import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseEvent; +import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.awt.image.WritableRaster; + +/** + * Contains all state needed to render a single frame: the pixel buffer, graphics context, + * screen dimensions, and mouse event tracking. + * + *

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

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

The context also manages mouse interaction detection: as shapes are painted + * back-to-front, each shape can report itself as the object under the mouse cursor. + * After painting completes, the topmost shape receives the mouse event.

+ * + * @see ViewPanel the panel that creates and manages this context + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape#paint(RenderingContext) + */ +public class RenderingContext { + + /** + * The {@link BufferedImage} pixel format used for the rendering buffer (4-byte ABGR). + */ + public static final int bufferedImageType = BufferedImage.TYPE_4BYTE_ABGR; + + /** + * Java2D graphics context for drawing text, anti-aliased shapes, and other + * high-level graphics operations onto the render buffer. + */ + public final Graphics2D graphics; + + /** + * Pixels of the rendering area. + * Each pixel is represented by 4 bytes: alpha, blue, green, red. + */ + public final byte[] pixels; + + /** + * Width of the rendering area in pixels. + */ + public final int width; + + /** + * Height of the rendering area in pixels. + */ + public final int height; + + /** + * Center of the screen in screen space (pixels). + * This is the point where (0,0) coordinate of the world space is rendered. + */ + public final Point2D centerCoordinate; + + /** + * Scale factor for perspective projection, derived from screen width. + * Used to convert normalized device coordinates to screen pixels. + */ + public final double projectionScale; + final BufferedImage bufferedImage; + /** + * Number of frame that is currently being rendered. + * Every frame has its own number. + */ + public int frameNumber = 0; + + /** + * UI component that mouse is currently hovering over. + */ + private MouseInteractionController objectPreviouslyUnderMouseCursor; + /** + * Mouse click event that needs to be processed. + * This event is processed only once per frame. + * If there are multiple objects under mouse cursor, the top-most object will receive the event. + * If there are no objects under mouse cursor, the event will be ignored. + * If there is no event, this field will be null. + * This field is set to null after the event is processed. + */ + private MouseEvent mouseEvent; + /** + * UI component that mouse is currently hovering over. + */ + private MouseInteractionController currentObjectUnderMouseCursor; + + /** + * Creates a new rendering context with the specified pixel dimensions. + * + *

Initializes the offscreen image buffer, extracts the raw pixel byte array, + * and configures anti-aliasing on the Graphics2D context.

+ * + * @param width the rendering area width in pixels + * @param height the rendering area height in pixels + */ + public RenderingContext(final int width, final int height) { + this.width = width; + this.height = height; + this.centerCoordinate = new Point2D(width / 2d, height / 2d); + this.projectionScale = width / 3d; + + bufferedImage = new BufferedImage(width, height, bufferedImageType); + + final WritableRaster raster = bufferedImage.getRaster(); + final DataBufferByte dbi = (DataBufferByte) raster.getDataBuffer(); + pixels = dbi.getData(); + + graphics = (Graphics2D) bufferedImage.getGraphics(); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + } + + /** + * Resets per-frame state in preparation for rendering a new frame. + * Clears the mouse event and the current object under the mouse cursor. + */ + public void prepareForNewFrameRendering() { + mouseEvent = null; + currentObjectUnderMouseCursor = null; + } + + /** + * Returns the pending mouse event for this frame, or {@code null} if none. + * + * @return the mouse event to process, or {@code null} + */ + public MouseEvent getMouseEvent() { + return mouseEvent; + } + + /** + * Sets the mouse event to be processed during this frame's rendering. + * + * @param mouseEvent the mouse event with position and button information + */ + public void setMouseEvent(MouseEvent mouseEvent) { + this.mouseEvent = mouseEvent; + } + + /** + * Called when given object was detected under mouse cursor, while processing {@link #mouseEvent}. + * Because objects are rendered back to front. The last method caller will set the top-most object, if + * there are multiple objects under mouse cursor. + */ + public void setCurrentObjectUnderMouseCursor(MouseInteractionController currentObjectUnderMouseCursor) { + this.currentObjectUnderMouseCursor = currentObjectUnderMouseCursor; + } + + /** + * @return true if view update is needed as a consequence of this mouse event. + */ + public boolean handlePossibleComponentMouseEvent() { + if (mouseEvent == null) return false; + + boolean viewRepaintNeeded = false; + + if (objectPreviouslyUnderMouseCursor != currentObjectUnderMouseCursor) { + // Mouse cursor has just entered or left component. + viewRepaintNeeded = objectPreviouslyUnderMouseCursor != null && objectPreviouslyUnderMouseCursor.mouseExited(); + viewRepaintNeeded |= currentObjectUnderMouseCursor != null && currentObjectUnderMouseCursor.mouseEntered(); + objectPreviouslyUnderMouseCursor = currentObjectUnderMouseCursor; + } + + if (mouseEvent.button != 0 && currentObjectUnderMouseCursor != null) { + // Mouse button was clicked on some component. + viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseClicked(mouseEvent.button); + } + + return viewRepaintNeeded; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/TextPointer.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/TextPointer.java new file mode 100755 index 0000000..91cd6e6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/TextPointer.java @@ -0,0 +1,109 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui; + +import static java.lang.Integer.compare; + +/** + * A pointer to a character in a text using row and column. + *

+ * It can be used to represent a cursor position in a text. + * Also, it can be used to represent beginning and end of a selection. + */ +public class TextPointer implements Comparable { + + /** + * The row of the character. Starts from 0. + */ + public int row; + + /** + * The column of the character. Starts from 0. + */ + public int column; + + public TextPointer() { + this(0, 0); + } + + public TextPointer(final int row, final int column) { + this.row = row; + this.column = column; + } + + public TextPointer(final TextPointer parent) { + this(parent.row, parent.column); + } + + @Override + public boolean equals(final Object o) { + if (o == null) return false; + + return o instanceof TextPointer && compareTo((TextPointer) o) == 0; + } + + @Override + public int hashCode() { + int result = row; + result = 31 * result + column; + return result; + } + + /** + * Compares this pointer to another pointer. + * + * @param textPointer The pointer to compare to. + * @return

    + *
  • -1 if this pointer is smaller than the argument pointer.
  • + *
  • 0 if they are equal.
  • + *
  • 1 if this pointer is bigger than the argument pointer.
  • + *
+ */ + @Override + public int compareTo(final TextPointer textPointer) { + + if (row < textPointer.row) + return -1; + if (row > textPointer.row) + return 1; + + return compare(column, textPointer.column); + } + + /** + * Checks if this pointer is between the argument pointers. + *

+ * This pointer is considered to be between the pointers if it is bigger or equal to the start pointer + * and smaller than the end pointer. + * + * @param start The start pointer. + * @param end The end pointer. + * @return True if this pointer is between the specified pointers. + */ + public boolean isBetween(final TextPointer start, final TextPointer end) { + + if (start == null) + return false; + + if (end == null) + return false; + + // Make sure that start is smaller than end. + TextPointer smaller; + TextPointer bigger; + + if (end.compareTo(start) >= 0) { + smaller = start; + bigger = end; + } else { + smaller = end; + bigger = start; + } + + // Check if this pointer is between the specified pointers. + return (compareTo(smaller) >= 0) && (bigger.compareTo(this) > 0); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewFrame.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewFrame.java new file mode 100755 index 0000000..e4c084a --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewFrame.java @@ -0,0 +1,203 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; + +/** + * Convenience window (JFrame) that creates and hosts a {@link ViewPanel} for 3D rendering. + * + *

This is the simplest way to get a 3D view up and running. The frame starts + * maximized, enforces a minimum size of 400x400, and handles window lifecycle + * events (minimizing, restoring, closing) automatically.

+ * + *

Quick start:

+ *
{@code
+ * // Create a window with a 3D view
+ * ViewFrame frame = new ViewFrame();
+ *
+ * // Access the view panel to add shapes and configure the scene
+ * ViewPanel viewPanel = frame.getViewPanel();
+ * viewPanel.getRootShapeCollection().addShape(
+ *     new WireframeCube(new Point3D(0, 0, 200), 50,
+ *         new LineAppearance(5, Color.GREEN))
+ * );
+ *
+ * // To close programmatically:
+ * frame.exit();
+ * }
+ * + * @see ViewPanel the embedded 3D rendering panel + */ +public class ViewFrame extends JFrame implements WindowListener { + + private static final long serialVersionUID = -7037635097739548470L; + + private final ViewPanel viewPanel; + + /** + * Creates a new maximized window with a 3D view. + */ + public ViewFrame() { + this(-1, -1, true); + } + + /** + * Creates a new window with a 3D view at the specified size. + * + * @param width window width in pixels, or -1 for default + * @param height window height in pixels, or -1 for default + */ + public ViewFrame(final int width, final int height) { + this(width, height, false); + } + + private ViewFrame(final int width, final int height, final boolean maximize) { + setTitle("3D engine"); + + addWindowListener(new java.awt.event.WindowAdapter() { + @Override + public void windowClosing(final java.awt.event.WindowEvent e) { + exit(); + } + }); + + viewPanel = new ViewPanel(); + + add(getViewPanel()); + + if (width > 0 && height > 0) { + setSize(width, height); + } else { + setSize(800, 600); + } + + if (maximize) { + setExtendedState(JFrame.MAXIMIZED_BOTH); + } + setVisible(true); + + addResizeListener(); + addWindowListener(this); + } + + private void addResizeListener() { + addComponentListener(new ComponentListener() { + // This method is called after the component's size changes + @Override + public void componentHidden(final ComponentEvent e) { + } + + @Override + public void componentMoved(final ComponentEvent e) { + } + + @Override + public void componentResized(final ComponentEvent evt) { + + final Component c = (Component) evt.getSource(); + + // Get new size + final Dimension newSize = c.getSize(); + + boolean sizeFixed = false; + + if (newSize.width < 400) { + newSize.width = 400; + sizeFixed = true; + } + + if (newSize.height < 400) { + newSize.height = 400; + sizeFixed = true; + } + + if (sizeFixed) + setSize(newSize); + + } + + @Override + public void componentShown(final ComponentEvent e) { + viewPanel.repaintDuringNextViewUpdate(); + } + + }); + } + + /** + * Exit the application. + */ + public void exit() { + if (getViewPanel() != null) { + getViewPanel().stop(); + getViewPanel().setEnabled(false); + getViewPanel().setVisible(false); + } + dispose(); + } + + @Override + public java.awt.Dimension getPreferredSize() { + return new java.awt.Dimension(640, 480); + } + + /** + * Returns the embedded {@link ViewPanel} for adding shapes and configuring the scene. + * + * @return the view panel contained in this frame + */ + public ViewPanel getViewPanel() { + return viewPanel; + } + + @Override + public void windowActivated(final WindowEvent e) { + viewPanel.repaintDuringNextViewUpdate(); + } + + @Override + public void windowClosed(final WindowEvent e) { + } + + @Override + public void windowClosing(final WindowEvent e) { + } + + @Override + public void windowDeactivated(final WindowEvent e) { + } + + /** + * Repaint the view when the window is deiconified. + * + * Deiconified means that the window is restored from minimized state. + */ + @Override + public void windowDeiconified(final WindowEvent e) { + viewPanel.repaintDuringNextViewUpdate(); + } + + /** + * Do nothing when the window is iconified. + * + * Iconified means that the window is minimized. + * @param e the event to be processed + */ + @Override + public void windowIconified(final WindowEvent e) { + } + + @Override + public void windowOpened(final WindowEvent e) { + viewPanel.repaintDuringNextViewUpdate(); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java new file mode 100755 index 0000000..25a73f5 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java @@ -0,0 +1,407 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui; + +import eu.svjatoslav.sixth.e3d.gui.humaninput.InputManager; +import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack; +import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Java Swing panel that provides a 3D rendering canvas with built-in camera navigation. + * + *

{@code ViewPanel} is the primary entry point for embedding the Sixth 3D engine into + * a Java Swing application. It manages the render loop, maintains a scene graph + * ({@link ShapeCollection}), and handles user input for camera navigation.

+ * + *

Quick start - creating a 3D view in a window:

+ *
{@code
+ * // Option 1: Use ViewFrame (creates a maximized JFrame for you)
+ * ViewFrame frame = new ViewFrame();
+ * ViewPanel viewPanel = frame.getViewPanel();
+ *
+ * // Option 2: Embed ViewPanel in your own Swing layout
+ * JFrame frame = new JFrame("My 3D App");
+ * ViewPanel viewPanel = new ViewPanel();
+ * frame.add(viewPanel);
+ * frame.setSize(800, 600);
+ * frame.setVisible(true);
+ *
+ * // Add shapes to the scene
+ * ShapeCollection scene = viewPanel.getRootShapeCollection();
+ * scene.addShape(new WireframeCube(
+ *     new Point3D(0, 0, 200), 50,
+ *     new LineAppearance(5, Color.GREEN)
+ * ));
+ *
+ * // Position the camera
+ * viewPanel.getCamera().setLocation(new Point3D(0, 0, -100));
+ *
+ * // Listen for frame updates (e.g., for animations)
+ * viewPanel.addFrameListener((panel, deltaMs) -> {
+ *     // Called before each frame. Return true to force repaint.
+ *     return false;
+ * });
+ * }
+ * + *

Architecture:

+ *
    + *
  • A background render thread continuously generates frames at the target FPS
  • + *
  • The engine intelligently skips rendering when no visual changes are detected
  • + *
  • {@link FrameListener}s are notified before each potential frame, enabling animations
  • + *
  • Mouse/keyboard input is managed by {@link InputManager}
  • + *
  • Keyboard focus is managed by {@link KeyboardFocusStack}
  • + *
+ * + * @see ViewFrame convenience window wrapper + * @see ShapeCollection the scene graph + * @see Camera the camera/viewer + * @see FrameListener for per-frame callbacks + */ +public class ViewPanel extends JPanel implements ComponentListener { + private static final long serialVersionUID = 1683277888885045387L; + private final InputManager inputManager = new InputManager(this); + private final KeyboardFocusStack keyboardFocusStack; + private final Camera camera = new Camera(); + private final ShapeCollection rootShapeCollection = new ShapeCollection(); + private final Set frameListeners = ConcurrentHashMap.newKeySet(); + public Color backgroundColor = Color.BLACK; + + /** + * Stores milliseconds when the last frame was updated. This is needed to calculate the time delta between frames. + * Time delta is used to calculate smooth animation. + */ + private long lastUpdateMillis = 0; + + private RenderingContext renderingContext = null; + + /** + * Currently target frames per second rate for this view. Target FPS can be changed at runtime. + * 3D engine tries to be smart and only repaints screen when there are visible changes. + */ + private int targetFPS = 60; + + /** + * Set to true if it is known than next frame reeds to be painted. Flag is cleared + * immediately after frame got updated. + */ + private boolean viewRepaintNeeded = true; + + /** + * Render thread that runs the continuous frame generation loop. + */ + private Thread renderThread; + + /** + * Flag to control whether the render thread should keep running. + */ + private volatile boolean renderThreadRunning = false; + + private long nextFrameTime; + + public ViewPanel() { + frameListeners.add(camera); + frameListeners.add(inputManager); + + keyboardFocusStack = new KeyboardFocusStack(this); + + initializePanelLayout(); + + startRenderThread(); + + addComponentListener(this); + } + + /** + * Returns the camera that represents the viewer's position and + * orientation in the 3D world. Use this to programmatically move the camera. + * + * @return the camera for this view + */ + public Camera getCamera() { + return camera; + } + + /** + * Returns the keyboard focus stack, which manages which component receives + * keyboard input. + * + * @return the keyboard focus stack + */ + public KeyboardFocusStack getKeyboardFocusStack() { + return keyboardFocusStack; + } + + /** + * Returns the root shape collection (scene graph). Add your 3D shapes here + * to make them visible in the view. + * + *
{@code
+     * viewPanel.getRootShapeCollection().addShape(myShape);
+     * }
+ * + * @return the root shape collection + */ + public ShapeCollection getRootShapeCollection() { + return rootShapeCollection; + } + + /** + * Returns the human input device (mouse/keyboard) event tracker. + * + * @return the HID event tracker + */ + /** + * Returns the input manager handling mouse and keyboard events for this view. + * + * @return the input manager + */ + public InputManager getInputManager() { + return inputManager; + } + + /** + * Registers a listener that will be notified before each frame render. + * Listeners can trigger repaints by returning {@code true} from + * {@link FrameListener#onFrame}. + * + * @param listener the listener to add + * @see #removeFrameListener(FrameListener) + */ + public void addFrameListener(final FrameListener listener) { + frameListeners.add(listener); + } + + @Override + public void componentHidden(final ComponentEvent e) { + + } + + @Override + public void componentMoved(final ComponentEvent e) { + + } + + @Override + public void componentResized(final ComponentEvent e) { + viewRepaintNeeded = true; + } + + @Override + public void componentShown(final ComponentEvent e) { + viewRepaintNeeded = true; + } + + @Override + public Dimension getMaximumSize() { + return getPreferredSize(); + } + + @Override + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + @Override + public java.awt.Dimension getPreferredSize() { + return new java.awt.Dimension(640, 480); + } + + public RenderingContext getRenderingContext() { + return renderingContext; + } + + private void initializePanelLayout() { + setFocusCycleRoot(true); + setOpaque(true); + setFocusable(true); + setDoubleBuffered(false); + setVisible(true); + requestFocusInWindow(); + } + + private void renderFrame() { + // paint root geometry collection to the offscreen render buffer + clearCanvas(); + rootShapeCollection.paint(this, renderingContext); + + // draw rendered offscreen buffer to visible screen + final Graphics graphics = getGraphics(); + if (graphics != null) + graphics.drawImage(renderingContext.bufferedImage, 0, 0, null); + } + + private void clearCanvas() { + renderingContext.graphics.setColor(backgroundColor); + renderingContext.graphics.fillRect(0, 0, getWidth(), getHeight()); + } + + /** + * Calling these methods tells 3D engine that current 3D view needs to be + * repainted on first opportunity. + */ + public void repaintDuringNextViewUpdate() { + viewRepaintNeeded = true; + } + + /** + * Set target frames per second rate for this view. Target FPS can be changed at runtime. + * Use 0 or negative value for unlimited FPS (max performance mode for benchmarking). + * + * @param frameRate target frames per second rate for this view. + */ + public void setFrameRate(final int frameRate) { + targetFPS = frameRate; + } + + /** + * Stops rendering of this view. + */ + public void stop() { + renderThreadRunning = false; + if (renderThread != null) { + try { + renderThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + renderThread = null; + } + } + + /** + * Starts the render thread that continuously generates frames. + */ + private void startRenderThread() { + renderThreadRunning = true; + renderThread = new Thread(this::renderLoop, "RenderThread"); + renderThread.setDaemon(true); + renderThread.start(); + } + + /** + * Main render loop that generates frames continuously. + * Supports both unlimited FPS and fixed FPS modes with dynamic sleep adjustment. + */ + private void renderLoop() { + nextFrameTime = System.currentTimeMillis(); + + while (renderThreadRunning) { + ensureThatViewIsUpToDate(); + + if (maintainTargetFps()) break; + } + } + + /** + * Ensures that the rendering process maintains the target frames per second (FPS) + * by dynamically adjusting the thread sleep duration. + * + * @return {@code true} if the thread was interrupted while sleeping, otherwise {@code false}. + */ + private boolean maintainTargetFps() { + if (targetFPS <= 0) return false; + + long now = System.currentTimeMillis(); + + nextFrameTime += 1000L / targetFPS; + + // If we've fallen behind, reset to now instead of trying to catch up + if (nextFrameTime < now) + nextFrameTime = now; + + long sleepTime = nextFrameTime - now; + if (sleepTime > 0) { + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return true; + } + } + return false; + } + + /** + * This method is executed by periodic timer task, in frequency according to + * defined frame rate. + *

+ * It tells view to update itself. View can decide if actual re-rendering of + * graphics is needed. + */ + void ensureThatViewIsUpToDate() { + maintainRenderingContext(); + + final int millisecondsPassedSinceLastUpdate = getMillisecondsPassedSinceLastUpdate(); + + boolean renderFrame = notifyFrameListeners(millisecondsPassedSinceLastUpdate); + + if (viewRepaintNeeded) { + viewRepaintNeeded = false; + renderFrame = true; + } + + // abort rendering if window size is invalid + if ((getWidth() > 0) && (getHeight() > 0) && renderFrame) { + renderFrame(); + viewRepaintNeeded = renderingContext.handlePossibleComponentMouseEvent(); + } + + } + + private void maintainRenderingContext() { + int panelWidth = getWidth(); + int panelHeight = getHeight(); + + if (panelWidth <= 0 || panelHeight <= 0) { + renderingContext = null; + return; + } + + // create new rendering context if window size has changed + if ((renderingContext == null) + || (renderingContext.width != panelWidth) + || (renderingContext.height != panelHeight)) { + renderingContext = new RenderingContext(panelWidth, panelHeight); + } + + renderingContext.prepareForNewFrameRendering(); + } + + private boolean notifyFrameListeners(int millisecondsPassedSinceLastUpdate) { + boolean reRenderFrame = false; + for (final FrameListener listener : frameListeners) + if (listener.onFrame(this, millisecondsPassedSinceLastUpdate)) + reRenderFrame = true; + return reRenderFrame; + } + + private int getMillisecondsPassedSinceLastUpdate() { + final long currentTime = System.currentTimeMillis(); + + if (lastUpdateMillis == 0) + lastUpdateMillis = currentTime; + + final int millisecondsPassedSinceLastUpdate = (int) (currentTime - lastUpdateMillis); + lastUpdateMillis = currentTime; + return millisecondsPassedSinceLastUpdate; + } + + /** + * Removes a previously registered frame listener. + * + * @param frameListener the listener to remove + */ + public void removeFrameListener(FrameListener frameListener) { + frameListeners.remove(frameListener); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewSpaceTracker.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewSpaceTracker.java new file mode 100644 index 0000000..36cb220 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewSpaceTracker.java @@ -0,0 +1,126 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.math.TransformStack; +import eu.svjatoslav.sixth.e3d.math.Vertex; + +/** + * Tracks an object's position in view/camera space for distance and angle calculations. + * + *

Used primarily for level-of-detail (LOD) decisions based on how far and at what + * angle the viewer is from an object. The tracker maintains the object's center point + * transformed into view space, and optionally orientation axes for angle calculations.

+ * + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape + */ +public class ViewSpaceTracker { + + private final static int minimalSliceFactor = 5; + + /** + * The object's center point (0,0,0 in object space) transformed to view space. + */ + public Vertex center = new Vertex(); + + /** + * Point at (10,0,0) in object space, used for XZ angle calculation. + * Only initialized if orientation tracking is enabled. + */ + public Vertex right; + + /** + * Point at (0,10,0) in object space, used for YZ angle calculation. + * Only initialized if orientation tracking is enabled. + */ + public Vertex down; + + public ViewSpaceTracker() { + } + + /** + * Transforms the tracked points from object space to view space. + * + * @param transformPipe the current transform stack + * @param renderingContext the rendering context for frame info + */ + public void analyze(final TransformStack transformPipe, + final RenderingContext renderingContext) { + + center.calculateLocationRelativeToViewer(transformPipe, renderingContext); + + if (right != null) { + right.calculateLocationRelativeToViewer(transformPipe, renderingContext); + down.calculateLocationRelativeToViewer(transformPipe, renderingContext); + } + } + + /** + * Enables tracking of orientation axes for angle calculations. + * Disabled by default to save computation when angles are not needed. + */ + public void enableOrientationTracking() { + right = new Vertex(new Point3D(10, 0, 0)); + down = new Vertex(new Point3D(0, 10, 0)); + } + + /** + * Returns the angle between the viewer and object in the XY plane. + * + * @return the XY angle in radians + */ + public double getAngleXY() { + return center.transformedCoordinate + .getAngleXY(down.transformedCoordinate); + } + + /** + * Returns the angle between the viewer and object in the XZ plane. + * + * @return the XZ angle in radians + */ + public double getAngleXZ() { + return center.transformedCoordinate + .getAngleXZ(right.transformedCoordinate); + } + + /** + * Returns the angle between the viewer and object in the YZ plane. + * + * @return the YZ angle in radians + */ + public double getAngleYZ() { + return center.transformedCoordinate + .getAngleYZ(down.transformedCoordinate); + } + + /** + * Returns the distance from the camera to the object's center. + * Used for level-of-detail calculations. + * + * @return the distance in world units + */ + public double getDistanceToCamera() { + return center.transformedCoordinate.getVectorLength(); + } + + /** + * Proposes a slice factor for texture LOD based on distance to camera. + * + * @return the proposed slice factor + */ + public double proposeSliceFactor() { + final double distanceToCamera = getDistanceToCamera(); + + double proposedSliceFactor = distanceToCamera / 5; + + if (proposedSliceFactor < minimalSliceFactor) + proposedSliceFactor = minimalSliceFactor; + + return proposedSliceFactor; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewUpdateTimerTask.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewUpdateTimerTask.java new file mode 100755 index 0000000..8594a2c --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewUpdateTimerTask.java @@ -0,0 +1,25 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui; + +/** + * Timer task that updates view. + * + * Tries to keep constant FPS. + */ +public class ViewUpdateTimerTask extends java.util.TimerTask { + + public ViewPanel viewPanel; + + public ViewUpdateTimerTask(final ViewPanel viewPanel) { + this.viewPanel = viewPanel; + } + + @Override + public void run() { + viewPanel.ensureThatViewIsUpToDate(); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/Connexion3D.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/Connexion3D.java new file mode 100644 index 0000000..027dbf0 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/Connexion3D.java @@ -0,0 +1,36 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.humaninput; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; + +/** + * I have Space Mouse Compact 3D Connexion mouse: https://3dconnexion.com/us/product/spacemouse-compact/ + * + * I discovered that it is possible to read raw data from it by reading /dev/hidraw4 file. + * + * TODO: reverse engineer the data format and implement a driver for it. + */ + +public class Connexion3D { + + public static void main(final String[] args) throws IOException { + + final BufferedReader in = new BufferedReader(new FileReader( + "/dev/hidraw4")); + + + // for testing purposes + while (true) { + System.out.print(in.read() + " "); + System.out.println("\n"); + } + + // in.close(); + + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java new file mode 100644 index 0000000..9fb4e16 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java @@ -0,0 +1,236 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.humaninput; + +import eu.svjatoslav.sixth.e3d.geometry.Point2D; +import eu.svjatoslav.sixth.e3d.gui.Camera; +import eu.svjatoslav.sixth.e3d.gui.FrameListener; +import eu.svjatoslav.sixth.e3d.gui.ViewPanel; +import eu.svjatoslav.sixth.e3d.math.Rotation; + +import javax.swing.*; +import java.awt.event.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Manages mouse and keyboard input for the 3D view. + * + *

Handles Swing mouse/keyboard events, tracks pressed keys and mouse state, + * and forwards events to the appropriate handlers. Also provides default camera + * control via mouse dragging (look around) and mouse wheel (vertical movement).

+ * + * @see ViewPanel#getInputManager() + */ +public class InputManager implements + MouseMotionListener, KeyListener, MouseListener, MouseWheelListener, FrameListener { + + private final Map pressedKeysToPressedTimeMap = new HashMap<>(); + private final List detectedMouseEvents = new ArrayList<>(); + private final List detectedKeyEvents = new ArrayList<>(); + private final Point2D mouseDelta = new Point2D(); + private final ViewPanel viewPanel; + private int wheelMovedDirection = 0; + private Point2D oldMouseCoordinatesWhenDragging; + private Point2D currentMouseLocation; + private boolean mouseMoved; + private boolean mouseWithinWindow = false; + + public InputManager(final ViewPanel viewPanel) { + this.viewPanel = viewPanel; + bind(viewPanel); + } + + @Override + public boolean onFrame(final ViewPanel viewPanel, final int millisecondsSinceLastFrame) { + boolean viewUpdateNeeded = handleKeyboardEvents(); + viewUpdateNeeded |= handleMouseClicksAndHover(viewPanel); + viewUpdateNeeded |= handleMouseDragging(); + viewUpdateNeeded |= handleMouseVerticalScrolling(); + return viewUpdateNeeded; + } + + private void bind(final JPanel panel) { + panel.addMouseMotionListener(this); + panel.addKeyListener(this); + panel.addMouseListener(this); + panel.addMouseWheelListener(this); + } + + private boolean handleKeyboardEvents() { + final KeyboardInputHandler currentFocusOwner = viewPanel.getKeyboardFocusStack().getCurrentFocusOwner(); + ArrayList unprocessedKeyboardEvents = getUnprocessedKeyboardEvents(); + + return currentFocusOwner != null + && forwardKeyboardEventsToFocusOwner(currentFocusOwner, unprocessedKeyboardEvents); + } + + private ArrayList getUnprocessedKeyboardEvents() { + synchronized (detectedKeyEvents) { + ArrayList result = new ArrayList<>(detectedKeyEvents); + detectedKeyEvents.clear(); + return result; + } + } + + private boolean forwardKeyboardEventsToFocusOwner( + KeyboardInputHandler currentFocusOwner, ArrayList keyEvents) { + boolean viewUpdateNeeded = false; + + for (KeyEvent keyEvent : keyEvents) + viewUpdateNeeded |= processKeyEvent(currentFocusOwner, keyEvent); + + return viewUpdateNeeded; + } + + private boolean processKeyEvent(KeyboardInputHandler currentFocusOwner, KeyEvent keyEvent) { + switch (keyEvent.getID()) { + case KeyEvent.KEY_PRESSED: + return currentFocusOwner.keyPressed(keyEvent, viewPanel); + + case KeyEvent.KEY_RELEASED: + return currentFocusOwner.keyReleased(keyEvent, viewPanel); + } + return false; + } + + private synchronized boolean handleMouseClicksAndHover(final ViewPanel viewPanel) { + boolean rerenderNeeded = false; + MouseEvent event = findClickLocationToTrace(); + if (event != null) { + rerenderNeeded = true; + } else { + if (mouseMoved) { + mouseMoved = false; + rerenderNeeded = true; + } + + if (currentMouseLocation != null) { + event = new MouseEvent(currentMouseLocation, 0); + } + } + + if (viewPanel.getRenderingContext() != null) + viewPanel.getRenderingContext().setMouseEvent(event); + + return rerenderNeeded; + } + + private MouseEvent findClickLocationToTrace() { + synchronized (detectedMouseEvents) { + if (detectedMouseEvents.isEmpty()) + return null; + + return detectedMouseEvents.remove(0); + } + } + + /** + * Returns whether the specified key is currently pressed. + * + * @param keyCode the key code (from {@link java.awt.event.KeyEvent}) + * @return {@code true} if the key is currently pressed + */ + public boolean isKeyPressed(final int keyCode) { + return pressedKeysToPressedTimeMap.containsKey(keyCode); + } + + @Override + public void keyPressed(final KeyEvent evt) { + synchronized (detectedKeyEvents) { + pressedKeysToPressedTimeMap.put(evt.getKeyCode(), System.currentTimeMillis()); + detectedKeyEvents.add(evt); + } + } + + @Override + public void keyReleased(final KeyEvent evt) { + synchronized (detectedKeyEvents) { + pressedKeysToPressedTimeMap.remove(evt.getKeyCode()); + detectedKeyEvents.add(evt); + } + } + + @Override + public void keyTyped(final KeyEvent e) { + } + + @Override + public void mouseClicked(final java.awt.event.MouseEvent e) { + synchronized (detectedMouseEvents) { + detectedMouseEvents.add(new MouseEvent(e.getX(), e.getY(), e.getButton())); + } + } + + @Override + public void mouseDragged(final java.awt.event.MouseEvent evt) { + final Point2D mouseLocation = new Point2D(evt.getX(), evt.getY()); + + if (oldMouseCoordinatesWhenDragging == null) { + oldMouseCoordinatesWhenDragging = mouseLocation; + return; + } + + mouseDelta.add(mouseLocation.clone().subtract(oldMouseCoordinatesWhenDragging)); + + oldMouseCoordinatesWhenDragging = mouseLocation; + } + + @Override + public void mouseEntered(final java.awt.event.MouseEvent e) { + mouseWithinWindow = true; + } + + @Override + public synchronized void mouseExited(final java.awt.event.MouseEvent e) { + mouseWithinWindow = false; + currentMouseLocation = null; + } + + @Override + public synchronized void mouseMoved(final java.awt.event.MouseEvent e) { + currentMouseLocation = new Point2D(e.getX(), e.getY()); + mouseMoved = true; + } + + @Override + public void mousePressed(final java.awt.event.MouseEvent e) { + } + + @Override + public void mouseReleased(final java.awt.event.MouseEvent evt) { + oldMouseCoordinatesWhenDragging = null; + } + + @Override + public void mouseWheelMoved(final java.awt.event.MouseWheelEvent evt) { + wheelMovedDirection += evt.getWheelRotation(); + } + + private boolean handleMouseVerticalScrolling() { + final Camera camera = viewPanel.getCamera(); + final double actualAcceleration = 50 * camera.cameraAcceleration * (1 + (camera.getMovementSpeed() / 10)); + camera.getMovementVector().y += (wheelMovedDirection * actualAcceleration); + camera.enforceSpeedLimit(); + boolean repaintNeeded = wheelMovedDirection != 0; + wheelMovedDirection = 0; + return repaintNeeded; + } + + private boolean handleMouseDragging() { + final Camera camera = viewPanel.getCamera(); + Rotation rotation = camera.getTransform().getRotation(); + final double newXZ = rotation.getAngleXZ() - ((float) mouseDelta.x / 50); + final double newYZ = rotation.getAngleYZ() - ((float) mouseDelta.y / 50); + camera.getTransform().setRotation(newXZ, newYZ); + + boolean viewUpdateNeeded = !mouseDelta.isZero(); + mouseDelta.zero(); + return viewUpdateNeeded; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardFocusStack.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardFocusStack.java new file mode 100644 index 0000000..683dac3 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardFocusStack.java @@ -0,0 +1,107 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.humaninput; + +import eu.svjatoslav.sixth.e3d.gui.ViewPanel; + +import java.util.Stack; + +/** + * Manages a stack-based keyboard focus system for interactive 3D components. + * + *

The focus stack determines which {@link KeyboardInputHandler} currently receives + * keyboard events. When a component gains focus (e.g., by being clicked), it is pushed + * onto the stack and the previous focus owner is notified. When the component releases + * focus (e.g., pressing ESC), the previous handler is restored.

+ * + *

The default handler at the bottom of the stack is a + * {@link WorldNavigationUserInputTracker}, which handles WASD/arrow-key camera movement + * when no other component has focus.

+ * + *

Focus flow example:

+ *
{@code
+ * // Initial state: WorldNavigationUserInputTracker has focus (camera movement)
+ * // User clicks on a text editor:
+ * focusStack.pushFocusOwner(textEditor);
+ * // Now textEditor receives keyboard events
+ *
+ * // User presses ESC:
+ * focusStack.popFocusOwner();
+ * // Camera movement is restored
+ * }
+ * + * @see KeyboardInputHandler the interface that focus owners must implement + * @see WorldNavigationUserInputTracker default handler for camera navigation + */ +public class KeyboardFocusStack { + + private final ViewPanel viewPanel; + private final WorldNavigationUserInputTracker defaultInputHandler = new WorldNavigationUserInputTracker(); + private final Stack inputHandlers = new Stack<>(); + private KeyboardInputHandler currentUserInputHandler; + + /** + * Creates a new focus stack for the given view panel, with + * {@link WorldNavigationUserInputTracker} as the default focus owner. + * + * @param viewPanel the view panel this focus stack belongs to + */ + public KeyboardFocusStack(final ViewPanel viewPanel) { + this.viewPanel = viewPanel; + pushFocusOwner(defaultInputHandler); + } + + /** + * Returns the handler that currently has keyboard focus. + * + * @return the current focus owner + */ + public KeyboardInputHandler getCurrentFocusOwner() { + return currentUserInputHandler; + } + + /** + * Removes the current focus owner from the stack and restores focus to the + * previous handler. If the stack is empty, no action is taken. + */ + public void popFocusOwner() { + if (currentUserInputHandler != null) + currentUserInputHandler.focusLost(viewPanel); + + if (inputHandlers.isEmpty()) + return; + + currentUserInputHandler = inputHandlers.pop(); + currentUserInputHandler.focusReceived(viewPanel); + } + + /** + * Pushes a new handler onto the focus stack, making it the current focus owner. + * The previous focus owner is notified via {@link KeyboardInputHandler#focusLost} + * and preserved on the stack for later restoration. + * + *

If the given handler is already the current focus owner, this method does nothing + * and returns {@code false}.

+ * + * @param newInputHandler the handler to receive keyboard focus + * @return {@code true} if the view needs to be repainted as a result + */ + public boolean pushFocusOwner(final KeyboardInputHandler newInputHandler) { + boolean updateNeeded = false; + + if (currentUserInputHandler == newInputHandler) + return false; + + if (currentUserInputHandler != null) { + updateNeeded = currentUserInputHandler.focusLost(viewPanel); + inputHandlers.push(currentUserInputHandler); + } + + currentUserInputHandler = newInputHandler; + updateNeeded |= currentUserInputHandler.focusReceived(viewPanel); + + return updateNeeded; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardHelper.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardHelper.java new file mode 100644 index 0000000..fd4d5cf --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardHelper.java @@ -0,0 +1,118 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.humaninput; + +import java.awt.event.InputEvent; +import java.util.HashSet; +import java.util.Set; + +/** + * Utility class providing keyboard key code constants and modifier detection methods. + * + *

Provides named constants for common key codes and static helper methods + * to check whether modifier keys (Ctrl, Alt, Shift) are pressed in a given + * event modifier mask.

+ * + *

Usage example:

+ *
{@code
+ * public boolean keyPressed(KeyEvent event, ViewPanel viewPanel) {
+ *     if (event.getKeyCode() == KeyboardHelper.ENTER) {
+ *         // Handle Enter key
+ *     }
+ *     if (KeyboardHelper.isCtrlPressed(event.getModifiersEx())) {
+ *         // Handle Ctrl+key combination
+ *     }
+ *     return true;
+ * }
+ * }
+ * + * @see KeyboardInputHandler the interface for receiving keyboard events + */ +public class KeyboardHelper { + + /** Key code for the Tab key. */ + public static final int TAB = 9; + /** Key code for the Down arrow key. */ + public static final int DOWN = 40; + /** Key code for the Up arrow key. */ + public static final int UP = 38; + /** Key code for the Right arrow key. */ + public static final int RIGHT = 39; + /** Key code for the Left arrow key. */ + public static final int LEFT = 37; + /** Key code for the Page Down key. */ + public static final int PGDOWN = 34; + /** Key code for the Page Up key. */ + public static final int PGUP = 33; + /** Key code for the Home key. */ + public static final int HOME = 36; + /** Key code for the End key. */ + public static final int END = 35; + /** Key code for the Delete key. */ + public static final int DEL = 127; + /** Key code for the Enter/Return key. */ + public static final int ENTER = 10; + /** Key code for the Backspace key. */ + public static final int BACKSPACE = 8; + /** Key code for the Escape key. */ + public static final int ESC = 27; + /** Key code for the Shift key. */ + public static final int SHIFT = 16; + + private static final Set nonText; + + static { + nonText = new HashSet<>(); + nonText.add(DOWN); + nonText.add(UP); + nonText.add(LEFT); + nonText.add(RIGHT); + + nonText.add(SHIFT); + nonText.add(ESC); + } + + /** + * Checks if the Alt key is pressed in the given modifier mask. + * + * @param modifiersEx the extended modifier mask from {@link java.awt.event.KeyEvent#getModifiersEx()} + * @return {@code true} if Alt is pressed + */ + public static boolean isAltPressed(final int modifiersEx) { + return (modifiersEx | InputEvent.ALT_DOWN_MASK) == modifiersEx; + } + + /** + * Checks if the Ctrl key is pressed in the given modifier mask. + * + * @param modifiersEx the extended modifier mask from {@link java.awt.event.KeyEvent#getModifiersEx()} + * @return {@code true} if Ctrl is pressed + */ + public static boolean isCtrlPressed(final int modifiersEx) { + return (modifiersEx | InputEvent.CTRL_DOWN_MASK) == modifiersEx; + } + + /** + * Checks if the Shift key is pressed in the given modifier mask. + * + * @param modifiersEx the extended modifier mask from {@link java.awt.event.KeyEvent#getModifiersEx()} + * @return {@code true} if Shift is pressed + */ + public static boolean isShiftPressed(final int modifiersEx) { + return (modifiersEx | InputEvent.SHIFT_DOWN_MASK) == modifiersEx; + } + + /** + * Determines whether the given key code represents a text-producing key + * (as opposed to navigation or modifier keys like arrows, Shift, Escape). + * + * @param keyCode the key code to check + * @return {@code true} if the key produces text input + */ + public static boolean isText(final int keyCode) { + return !nonText.contains(keyCode); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardInputHandler.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardInputHandler.java new file mode 100644 index 0000000..ccdb962 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardInputHandler.java @@ -0,0 +1,40 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.humaninput; + +import eu.svjatoslav.sixth.e3d.gui.ViewPanel; + +import java.awt.event.KeyEvent; + +/** + * This is the process: + *

+ * 1. Component receives focus, perhaps because user clicked on it with the mouse. + * 2. Now component will receive user key press and release events from the keyboard. + * 3. Component loses focus. Perhaps user chose another component to interact with. + */ +public interface KeyboardInputHandler { + + /** + * @return true if view needs to be re-rendered. + */ + boolean focusLost(ViewPanel viewPanel); + + /** + * @return true if view needs to be re-rendered. + */ + boolean focusReceived(ViewPanel viewPanel); + + /** + * @return true if view needs to be re-rendered. + */ + boolean keyPressed(KeyEvent event, ViewPanel viewPanel); + + /** + * @return true if view needs to be re-rendered. + */ + boolean keyReleased(KeyEvent event, ViewPanel viewPanel); + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseEvent.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseEvent.java new file mode 100644 index 0000000..459d0d2 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseEvent.java @@ -0,0 +1,46 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.humaninput; + +import eu.svjatoslav.sixth.e3d.geometry.Point2D; + +/** + * Represents mouse event. + */ +public class MouseEvent { + + /** + * Mouse coordinate in screen space (pixels) relative to top left corner of the screen + * when mouse button was clicked. + */ + public Point2D coordinate; + + /** + *

+     * 0 - mouse over (no button pressed)
+     * 1 - left mouse button
+     * 2 - middle mouse button
+     * 3 - right mouse button
+     * 
+ */ + public int button; + + MouseEvent(final int x, final int y, final int button) { + this(new Point2D(x, y), button); + } + + MouseEvent(final Point2D coordinate, final int button) { + this.coordinate = coordinate; + this.button = button; + } + + @Override + public String toString() { + return "MouseEvent{" + + "coordinate=" + coordinate + + ", button=" + button + + '}'; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseInteractionController.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseInteractionController.java new file mode 100644 index 0000000..5b6d470 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseInteractionController.java @@ -0,0 +1,33 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.humaninput; + +/** + * Interface that allows to handle mouse events. + */ +public interface MouseInteractionController { + + /** + * Called when mouse is clicked on component. + * + * @return true if view update is needed as a consequence of this mouse click. + */ + boolean mouseClicked(int button); + + /** + * Called when mouse gets over given component. + * + * @return true if view update is needed as a consequence of this mouse enter. + */ + boolean mouseEntered(); + + /** + * Called when mouse leaves screen area occupied by component. + * + * @return true if view update is needed as a consequence of this mouse exit. + */ + boolean mouseExited(); + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/WorldNavigationUserInputTracker.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/WorldNavigationUserInputTracker.java new file mode 100644 index 0000000..f479a88 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/WorldNavigationUserInputTracker.java @@ -0,0 +1,87 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.humaninput; + +import eu.svjatoslav.sixth.e3d.gui.Camera; +import eu.svjatoslav.sixth.e3d.gui.ViewPanel; +import eu.svjatoslav.sixth.e3d.gui.FrameListener; + +import java.awt.event.KeyEvent; + +/** + * Default keyboard input handler that translates arrow key presses into camera (avatar) + * movement through the 3D world. + * + *

This handler is automatically registered as the default focus owner in the + * {@link KeyboardFocusStack}. It listens for arrow key presses on each frame and + * applies acceleration to the avatar's movement vector accordingly:

+ *
    + *
  • Up arrow - move forward (positive Z)
  • + *
  • Down arrow - move backward (negative Z)
  • + *
  • Right arrow - move right (positive X)
  • + *
  • Left arrow - move left (negative X)
  • + *
+ * + *

Movement acceleration scales with the time delta between frames for smooth, + * frame-rate-independent navigation. It also scales with current speed for a natural + * acceleration curve.

+ * + * @see KeyboardFocusStack the focus system that manages this handler + * @see Camera the camera/viewer that this handler moves + */ +public class WorldNavigationUserInputTracker implements KeyboardInputHandler, FrameListener { + + @Override + public boolean onFrame(final ViewPanel viewPanel, + final int millisecondsSinceLastFrame) { + + final InputManager inputManager = viewPanel.getInputManager(); + + final Camera camera = viewPanel.getCamera(); + + final double actualAcceleration = (long) millisecondsSinceLastFrame + * camera.cameraAcceleration + * (1 + (camera.getMovementSpeed() / 10)); + + if (inputManager.isKeyPressed(KeyboardHelper.UP)) + camera.getMovementVector().z += actualAcceleration; + + if (inputManager.isKeyPressed(KeyboardHelper.DOWN)) + camera.getMovementVector().z -= actualAcceleration; + + if (inputManager.isKeyPressed(KeyboardHelper.RIGHT)) + camera.getMovementVector().x += actualAcceleration; + + if (inputManager.isKeyPressed(KeyboardHelper.LEFT)) + camera.getMovementVector().x -= actualAcceleration; + + camera.enforceSpeedLimit(); + + return false; + } + + @Override + public boolean focusLost(final ViewPanel viewPanel) { + viewPanel.removeFrameListener(this); + return false; + } + + @Override + public boolean focusReceived(final ViewPanel viewPanel) { + viewPanel.addFrameListener(this); + return false; + } + + @Override + public boolean keyPressed(final KeyEvent event, final ViewPanel viewContext) { + return false; + } + + @Override + public boolean keyReleased(final KeyEvent event, final ViewPanel viewContext) { + return false; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/package-info.java new file mode 100644 index 0000000..dfbbd22 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/package-info.java @@ -0,0 +1,6 @@ +package eu.svjatoslav.sixth.e3d.gui.humaninput; + +/** + * This package is responsible for tracking human input devices (keyboard, mouse, etc.) and + * forwarding those inputs to subsequent virtual components. + */ \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Character.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Character.java new file mode 100644 index 0000000..27113b0 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Character.java @@ -0,0 +1,24 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.textEditorComponent; + +/** + * A character in a text editor. + */ +public class Character { + + /** + * The character value. + */ + char value; + + public Character(final char value) { + this.value = value; + } + + boolean hasValue() { + return value != ' '; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/LookAndFeel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/LookAndFeel.java new file mode 100644 index 0000000..b792779 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/LookAndFeel.java @@ -0,0 +1,25 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.textEditorComponent; + +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; + +/** + * A look and feel of a text editor. + */ +public class LookAndFeel { + + public Color foreground = new Color(255, 255, 255); + public Color background = new Color(20, 20, 20, 255); + + public Color tabStopBackground = new Color(25, 25, 25, 255); + + public Color cursorForeground = new Color(255, 255, 255); + public Color cursorBackground = new Color(255, 0, 0); + + public Color selectionForeground = new Color(255, 255, 255); + public Color selectionBackground = new Color(0, 80, 80); + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Page.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Page.java new file mode 100644 index 0000000..47c898f --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Page.java @@ -0,0 +1,136 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.textEditorComponent; + +import java.util.ArrayList; +import java.util.List; + +/** + * A page in a text editor. + */ +public class Page { + + /** + * The text lines. + */ + public List rows = new ArrayList<>(); + + public void ensureMaxTextLine(final int row) { + while (rows.size() <= row) + rows.add(new TextLine()); + } + + /** + * Returns the character at the specified location. + * If the location is out of bounds, returns a space. + * + * @return The character at the specified location. + */ + public char getChar(final int row, final int column) { + if (rows.size() <= row) + return ' '; + return rows.get(row).getCharForLocation(column); + } + + /** + * Returns the specified line. + * + * @param row The line number. + * @return The line. + */ + public TextLine getLine(final int row) { + ensureMaxTextLine(row); + return rows.get(row); + } + + /** + * Returns the length of the specified line. + * + * @param row The line number. + * @return The length of the line. + */ + public int getLineLength(final int row) { + if (rows.size() <= row) + return 0; + return rows.get(row).getLength(); + } + + /** + * Returns the number of lines in the page. + * + * @return The number of lines in the page. + */ + public int getLinesCount() { + pack(); + return rows.size(); + } + + /** + * Returns the text of the page. + * + * @return The text of the page. + */ + public String getText() { + pack(); + + final StringBuilder result = new StringBuilder(); + for (final TextLine textLine : rows) { + if (result.length() > 0) + result.append("\n"); + result.append(textLine.toString()); + } + return result.toString(); + } + + public void insertCharacter(final int row, final int col, final char value) { + getLine(row).insertCharacter(col, value); + } + + public void insertLine(final int row, final TextLine textLine) { + rows.add(row, textLine); + } + + /** + * Removes empty lines from the end of the page. + */ + private void pack() { + int newLength = 0; + + for (int i = rows.size() - 1; i >= 0; i--) + if (!rows.get(i).isEmpty()) { + newLength = i + 1; + break; + } + + if (newLength == rows.size()) + return; + + rows = rows.subList(0, newLength); + } + + /** + * Removes the specified character from the page. + * + * @param row The line number. + * @param col The character number. + */ + public void removeCharacter(final int row, final int col) { + if (rows.size() <= row) + return; + getLine(row).removeCharacter(col); + } + + /** + * Removes the specified line from the page. + * + * @param row The line number. + */ + public void removeLine(final int row) { + if (rows.size() <= row) + return; + rows.remove(row); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextEditComponent.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextEditComponent.java new file mode 100755 index 0000000..5ed7a76 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextEditComponent.java @@ -0,0 +1,911 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.textEditorComponent; + +import eu.svjatoslav.sixth.e3d.geometry.Point2D; +import eu.svjatoslav.sixth.e3d.gui.GuiComponent; +import eu.svjatoslav.sixth.e3d.gui.TextPointer; +import eu.svjatoslav.sixth.e3d.gui.ViewPanel; +import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardHelper; +import eu.svjatoslav.sixth.e3d.math.Transform; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas; + +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.event.KeyEvent; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +/** + * A full-featured text editor component rendered in 3D space. + * + *

Extends {@link GuiComponent} to integrate keyboard focus management and mouse + * interaction with a multi-line text editing surface. The editor is backed by a + * {@link Page} model containing {@link TextLine} instances and rendered via a + * {@link TextCanvas}.

+ * + *

Supported editing features:

+ *
    + *
  • Cursor navigation with arrow keys, Home, End, Page Up, and Page Down
  • + *
  • Text selection via Shift + arrow keys
  • + *
  • Clipboard operations: Ctrl+C (copy), Ctrl+X (cut), Ctrl+V (paste), Ctrl+A (select all)
  • + *
  • Word-level cursor movement with Ctrl+Left and Ctrl+Right
  • + *
  • Tab indentation and Shift+Tab dedentation for single lines and block selections
  • + *
  • Backspace dedentation of selected blocks (removes 4 spaces of indentation)
  • + *
  • Automatic scrolling when the cursor moves beyond the visible area
  • + *
+ * + *

Usage example:

+ *
{@code
+ * // Create a look and feel (or use defaults)
+ * LookAndFeel lookAndFeel = new LookAndFeel();
+ *
+ * // Create the text editor at a position in 3D space
+ * TextEditComponent editor = new TextEditComponent(
+ *     new Transform(new Point3D(0, 0, 500)),  // position in world
+ *     viewPanel,                                // the active ViewPanel
+ *     new Point2D(800, 600),                    // size in world coordinates
+ *     lookAndFeel
+ * );
+ *
+ * // Set initial content
+ * editor.setText("Hello, World!\nSecond line of text.");
+ *
+ * // Add to the scene
+ * viewPanel.getRootShapeCollection().addShape(editor);
+ * }
+ * + * @see GuiComponent the base class providing keyboard focus and mouse click handling + * @see Page the underlying text model holding all lines + * @see TextCanvas the rendering surface for character-based output + * @see LookAndFeel configurable colors for the editor's visual appearance + * @see TextPointer row/column pointer used for cursor and selection positions + */ +public class TextEditComponent extends GuiComponent implements ClipboardOwner { + + private static final long serialVersionUID = -7118833957783600630L; + + /** + * Text rows that need to be repainted. + */ + private final Set dirtyRows = new HashSet<>(); + + + /** + * The text canvas used to render characters on screen. + */ + private final TextCanvas textCanvas; + + /** + * The number of characters the view is scrolled horizontally. + */ + public int scrolledCharacters = 0; + + /** + * The number of lines the view is scrolled vertically. + */ + public int scrolledLines = 0; + + /** + * Whether the user is currently in selection mode (Shift key held during navigation). + */ + public boolean selecting = false; + + /** + * Selection start and end pointers. + */ + public TextPointer selectionStart = new TextPointer(0, 0); + public TextPointer selectionEnd = new TextPointer(0, 0); + + /** + * The current cursor position in the text (row and column). + */ + public TextPointer cursorLocation = new TextPointer(0, 0); + + /** + * The page model holding all text lines. + */ + Page page = new Page(); + + /** + * The look and feel configuration controlling editor colors. + */ + LookAndFeel lookAndFeel; + + /** + * If true, the page will be repainted on the next update. + */ + boolean repaintPage = false; + + /** + * Creates a new text editor component positioned in 3D space. + * + *

The editor dimensions in rows and columns are computed from the given world-coordinate + * size and the font character dimensions defined in {@link TextCanvas}. A {@link TextCanvas} + * is created internally and added as a child shape.

+ * + * @param transform the position and orientation of the editor in 3D space + * @param viewPanel the view panel this editor belongs to + * @param sizeInWorldCoordinates the editor size in world coordinates (width, height); + * determines the number of visible columns and rows + * @param lookAndFeel the color configuration for the editor's visual appearance + */ + public TextEditComponent(final Transform transform, + final ViewPanel viewPanel, + final Point2D sizeInWorldCoordinates, + LookAndFeel lookAndFeel) { + super(transform, viewPanel, sizeInWorldCoordinates.to3D()); + + this.lookAndFeel = lookAndFeel; + final int columns = (int) (sizeInWorldCoordinates.x / TextCanvas.FONT_CHAR_WIDTH); + final int rows = (int) (sizeInWorldCoordinates.y / TextCanvas.FONT_CHAR_HEIGHT); + + textCanvas = new TextCanvas( + new Transform(), + new TextPointer(rows, columns), + lookAndFeel.foreground, lookAndFeel.background); + + textCanvas.setMouseInteractionController(this); + + repaintPage(); + addShape(textCanvas); + } + + /** + * Ensures the cursor stays within the visible editor area by adjusting + * scroll offsets when the cursor moves beyond the visible boundaries. + * Also clamps the cursor position so that row and column are never negative. + */ + private void checkCursorBoundaries() { + if (cursorLocation.column < 0) + cursorLocation.column = 0; + if (cursorLocation.row < 0) + cursorLocation.row = 0; + + // ensure chat cursor stays within vertical editor boundaries by + // vertical scrolling + if ((cursorLocation.row - scrolledLines) < 0) + scroll(0, cursorLocation.row - scrolledLines); + + if ((((cursorLocation.row - scrolledLines) + 1)) > textCanvas.getSize().row) + scroll(0, + ((((((cursorLocation.row - scrolledLines) + 1) - textCanvas + .getSize().row))))); + + // ensure chat cursor stays within horizontal editor boundaries by + // horizontal scrolling + if ((cursorLocation.column - scrolledCharacters) < 0) + scroll(cursorLocation.column - scrolledCharacters, 0); + + if ((((cursorLocation.column - scrolledCharacters) + 1)) > textCanvas + .getSize().column) + scroll((((((cursorLocation.column - scrolledCharacters) + 1) - textCanvas + .getSize().column))), 0); + } + + /** + * Clears the current text selection by setting the selection end to match + * the selection start, effectively making the selection empty. + * + *

A full page repaint is scheduled to remove the visual selection highlight.

+ */ + public void clearSelection() { + selectionEnd = new TextPointer(selectionStart); + repaintPage = true; + } + + /** + * Copies the currently selected text to the system clipboard. + * + *

If no text is selected (i.e., selection start equals selection end), + * this method does nothing. Multi-line selections are joined with newline + * characters.

+ * + * @see #setClipboardContents(String) + * @see #cutToClipboard() + */ + public void copyToClipboard() { + if (selectionStart.compareTo(selectionEnd) == 0) + return; + // System.out.println("Copy action."); + final StringBuilder msg = new StringBuilder(); + + ensureSelectionOrder(); + + for (int row = selectionStart.row; row <= selectionEnd.row; row++) { + final TextLine textLine = page.getLine(row); + + if (row == selectionStart.row) { + if (row == selectionEnd.row) + msg.append(textLine.getSubString(selectionStart.column, + selectionEnd.column + 1)); + else + msg.append(textLine.getSubString(selectionStart.column, + textLine.getLength())); + } else { + msg.append('\n'); + if (row == selectionEnd.row) + msg.append(textLine + .getSubString(0, selectionEnd.column + 1)); + else + msg.append(textLine.toString()); + } + } + + setClipboardContents(msg.toString()); + } + + /** + * Cuts the currently selected text to the system clipboard. + * + *

This copies the selected text to the clipboard via {@link #copyToClipboard()}, + * then deletes the selection from the page and triggers a full repaint.

+ * + * @see #copyToClipboard() + * @see #deleteSelection() + */ + public void cutToClipboard() { + copyToClipboard(); + deleteSelection(); + repaintPage(); + } + + /** + * Deletes the currently selected text from the page. + * + *

After deletion, the selection is cleared and the cursor is moved to + * the position where the selection started.

+ * + * @see #ensureSelectionOrder() + */ + public void deleteSelection() { + ensureSelectionOrder(); + int ym = 0; + + for (int line = selectionStart.row; line <= selectionEnd.row; line++) { + final TextLine currentLine = page.getLine(line - ym); + + if (line == selectionStart.row) { + if (line == selectionEnd.row) + + currentLine.cutSubString(selectionStart.column, + selectionEnd.column); + else if (selectionStart.column == 0) { + page.removeLine(line - ym); + ym++; + } else + currentLine.cutSubString(selectionStart.column, + currentLine.getLength() + 1); + } else if (line == selectionEnd.row) + currentLine.cutSubString(0, selectionEnd.column); + else { + page.removeLine(line - ym); + ym++; + } + } + + clearSelection(); + cursorLocation = new TextPointer(selectionStart); + } + + /** + * Ensures that {@link #selectionStart} is smaller than + * {@link #selectionEnd}. + * + *

If the start pointer is after the end pointer (e.g., when the user + * selected text backwards), the two pointers are swapped so that + * subsequent operations can iterate from start to end.

+ */ + public void ensureSelectionOrder() { + if (selectionStart.compareTo(selectionEnd) > 0) { + final TextPointer temp = selectionEnd; + selectionEnd = selectionStart; + selectionStart = temp; + } + } + + /** + * Retrieves the current text contents of the system clipboard. + * + * @return the clipboard text content, or an empty string if the clipboard + * is empty or does not contain text + */ + public String getClipboardContents() { + String result = ""; + final Clipboard clipboard = Toolkit.getDefaultToolkit() + .getSystemClipboard(); + // odd: the Object param of getContents is not currently used + final Transferable contents = clipboard.getContents(null); + final boolean hasTransferableText = (contents != null) + && contents.isDataFlavorSupported(DataFlavor.stringFlavor); + if (hasTransferableText) + try { + result = (String) contents + .getTransferData(DataFlavor.stringFlavor); + } catch (final UnsupportedFlavorException | IOException ex) { + // highly unlikely since we are using a standard DataFlavor + System.out.println(ex); + } + // System.out.println(result); + return result; + } + + /** + * Places the given string into the system clipboard so that it can be + * pasted into other applications. + * + * @param contents the text to place on the clipboard + * @see #getClipboardContents() + * @see #copyToClipboard() + */ + public void setClipboardContents(final String contents) { + final StringSelection stringSelection = new StringSelection(contents); + final Clipboard clipboard = Toolkit.getDefaultToolkit() + .getSystemClipboard(); + clipboard.setContents(stringSelection, stringSelection); + } + + /** + * Scrolls to and positions the cursor at the beginning of the specified line. + * + *

The view is scrolled so the target line is visible, the cursor is placed + * at the start of that line (column 0), and a full repaint is triggered.

+ * + * @param Line the zero-based line number to navigate to + */ + public void goToLine(final int Line) { + // markNavigationLocation(Line); + scrolledLines = Line + 1; + cursorLocation.row = Line + 1; + cursorLocation.column = 0; + repaintPage(); + } + + /** + * Inserts the given text string at the current cursor position. + * + *

The text is processed character by character. Special characters are + * handled as editing operations:

+ *
    + *
  • {@code DEL} -- deletes the character at the cursor
  • + *
  • {@code ENTER} -- splits the current line at the cursor
  • + *
  • {@code BACKSPACE} -- deletes the character before the cursor
  • + *
+ *

All other printable characters are inserted at the cursor position, + * advancing the cursor column by one for each character.

+ * + * @param txt the text to insert; {@code null} values are silently ignored + */ + public void insertText(final String txt) { + if (txt == null) + return; + + for (final char c : txt.toCharArray()) { + + if (c == KeyboardHelper.DEL) { + processDel(); + continue; + } + + if (c == KeyboardHelper.ENTER) { + processEnter(); + continue; + } + + if (c == KeyboardHelper.BACKSPACE) { + processBackspace(); + continue; + } + + // type character + if (KeyboardHelper.isText(c)) { + page.insertCharacter(cursorLocation.row, cursorLocation.column, + c); + cursorLocation.column++; + } + } + } + + /** + * Handles a key press event by routing it through the editor's input processing + * pipeline. + * + *

This method delegates to the parent {@link GuiComponent#keyPressed(KeyEvent, ViewPanel)} + * (which handles ESC for focus release), then processes the key event for text editing, + * marks the affected row as dirty, adjusts scroll boundaries, and repaints as needed.

+ * + * @param event the keyboard event + * @param viewPanel the view panel that dispatched this event + * @return always {@code true}, indicating the event was consumed + */ + @Override + public boolean keyPressed(final KeyEvent event, final ViewPanel viewPanel) { + super.keyPressed(event, viewPanel); + + processKeyEvent(event); + + markRowDirty(); + + checkCursorBoundaries(); + + repaintWhatNeeded(); + return true; + } + + /** + * Called when this editor loses ownership of the system clipboard. + * + *

This is an empty implementation of the {@link ClipboardOwner} interface; + * no action is taken when clipboard ownership is lost.

+ * + * @param aClipboard the clipboard that this editor previously owned + * @param aContents the contents that were previously placed on the clipboard + */ + @Override + public void lostOwnership(final Clipboard aClipboard, + final Transferable aContents) { + // do nothing + } + + /** + * Marks the current cursor row as dirty, scheduling it for repaint on the + * next rendering cycle. + */ + public void markRowDirty() { + dirtyRows.add(cursorLocation.row); + } + + /** + * Pastes text from the system clipboard at the current cursor position. + * + * @see #getClipboardContents() + * @see #insertText(String) + */ + public void pasteFromClipboard() { + insertText(getClipboardContents()); + } + + /** + * Processes the backspace key action. + * + *

If there is no active selection, deletes the character before the cursor. + * If the cursor is at the beginning of a line, merges the current line with the + * previous one. If there is an active selection, dedents the selected lines by + * removing up to 4 leading spaces (block dedentation).

+ */ + private void processBackspace() { + if (selectionStart.compareTo(selectionEnd) == 0) { + // erase single character + if (cursorLocation.column > 0) { + cursorLocation.column--; + page.removeCharacter(cursorLocation.row, cursorLocation.column); + // System.out.println(lines.get(currentCursor.line).toString()); + } else if (cursorLocation.row > 0) { + cursorLocation.row--; + final int currentLineLength = page + .getLineLength(cursorLocation.row); + cursorLocation.column = currentLineLength; + page.getLine(cursorLocation.row) + .insertTextLine(currentLineLength, + page.getLine(cursorLocation.row + 1)); + page.removeLine(cursorLocation.row + 1); + repaintPage = true; + } + } else { + // dedent multiple lines + ensureSelectionOrder(); + // scan if enough space exists + for (int y = selectionStart.row; y < selectionEnd.row; y++) + if (page.getLine(y).getIdent() < 4) + return; + + for (int y = selectionStart.row; y < selectionEnd.row; y++) + page.getLine(y).cutFromBeginning(4); + + repaintPage = true; + } + } + + /** + * Processes keyboard shortcuts involving the Ctrl modifier key. + * + *

Supported combinations:

+ *
    + *
  • Ctrl+A -- select all text
  • + *
  • Ctrl+X -- cut selected text to clipboard
  • + *
  • Ctrl+C -- copy selected text to clipboard
  • + *
  • Ctrl+V -- paste from clipboard
  • + *
  • Ctrl+Right -- skip to the beginning of the next word
  • + *
  • Ctrl+Left -- skip to the beginning of the previous word
  • + *
+ * + * @param keyCode the key code of the pressed key (combined with Ctrl) + */ + private void processCtrlCombinations(final int keyCode) { + + if ((char) keyCode == 'A') { // CTRL + A -- select all + final int lastLineIndex = page.getLinesCount() - 1; + selectionStart = new TextPointer(0, 0); + selectionEnd = new TextPointer(lastLineIndex, + page.getLineLength(lastLineIndex)); + repaintPage(); + } + + // CTRL + X -- cut + if ((char) keyCode == 'X') + cutToClipboard(); + + // CTRL + C -- copy + if ((char) keyCode == 'C') + copyToClipboard(); + + // CTRL + V -- paste + if ((char) keyCode == 'V') + pasteFromClipboard(); + + if (keyCode == 39) { // RIGHT + // skip to the beginning of the next word + + for (int x = cursorLocation.column; x < (page + .getLineLength(cursorLocation.row) - 1); x++) + if ((page.getChar(cursorLocation.row, x) == ' ') + && (page.getChar(cursorLocation.row, x + 1) != ' ')) { + // beginning of the next word is found + cursorLocation.column = x + 1; + return; + } + + cursorLocation.column = page.getLineLength(cursorLocation.row); + return; + } + + if (keyCode == 37) { // Left + + // skip to the beginning of the previous word + for (int x = cursorLocation.column - 2; x >= 0; x--) + if ((page.getChar(cursorLocation.row, x) == ' ') + & (page.getChar(cursorLocation.row, x + 1) != ' ')) { + cursorLocation.column = x + 1; + return; + } + + cursorLocation.column = 0; + } + } + + /** + * Processes the Delete key action. + * + *

If there is no active selection, deletes the character at the cursor position. + * If the cursor is at the end of the line, the next line is merged into the current one. + * If there is an active selection, the entire selection is deleted.

+ */ + public void processDel() { + if (selectionStart.compareTo(selectionEnd) == 0) { + // is there still some text right to the cursor ? + if (cursorLocation.column < page.getLineLength(cursorLocation.row)) + page.removeCharacter(cursorLocation.row, cursorLocation.column); + else { + page.getLine(cursorLocation.row).insertTextLine( + cursorLocation.column, + page.getLine(cursorLocation.row + 1)); + page.removeLine(cursorLocation.row + 1); + repaintPage = true; + } + } else { + deleteSelection(); + repaintPage = true; + } + } + + /** + * Processes the Enter key action by splitting the current line at the cursor position. + * + *

Everything to the right of the cursor is moved to a new line inserted + * below. The cursor moves to the beginning of the new line.

+ */ + private void processEnter() { + final TextLine currentLine = page.getLine(cursorLocation.row); + // move everything right to the cursor into new line + final TextLine newLine = currentLine.getSubLine(cursorLocation.column, + currentLine.getLength()); + page.insertLine(cursorLocation.row + 1, newLine); + + // trim existing line + page.getLine(cursorLocation.row).cutUntilEnd(cursorLocation.column); + repaintPage = true; + + cursorLocation.row++; + cursorLocation.column = 0; + } + + /** + * Routes a keyboard event to the appropriate handler based on modifier keys + * and key codes. + * + *

Handles Ctrl combinations, Tab/Shift+Tab, text input, Shift-based selection, + * and cursor navigation keys (Home, End, arrows, Page Up/Down). Alt key events + * are ignored.

+ * + * @param event the keyboard event to process + */ + private void processKeyEvent(final KeyEvent event) { + final int modifiers = event.getModifiersEx(); + final int keyCode = event.getKeyCode(); + final char keyChar = event.getKeyChar(); + + // System.out.println("Keycode:" + keyCode s+ ", keychar:" + keyChar); + + if (KeyboardHelper.isAltPressed(modifiers)) + return; + + if (KeyboardHelper.isCtrlPressed(modifiers)) { + processCtrlCombinations(keyCode); + return; + } + + if (keyCode == KeyboardHelper.TAB) { + processTab(modifiers); + return; + } + + clearSelection(); + + if (KeyboardHelper.isText(keyCode)) { + insertText(String.valueOf(keyChar)); + return; + } + + if (KeyboardHelper.isShiftPressed(modifiers)) { + if (!selecting) + attemptSelectionStart:{ + + if (keyChar == 65535) + if (keyCode == 16) + break attemptSelectionStart; + if (((keyChar >= 32) & (keyChar <= 128)) | (keyChar == 10) + | (keyChar == 8) | (keyChar == 9)) + break attemptSelectionStart; + + selectionStart = new TextPointer(cursorLocation); + selectionEnd = selectionStart; + selecting = true; + repaintPage(); + } + } else + selecting = false; + + if (keyCode == KeyboardHelper.HOME) { + cursorLocation.column = 0; + return; + } + if (keyCode == KeyboardHelper.END) { + cursorLocation.column = page.getLineLength(cursorLocation.row); + return; + } + + // process cursor keys + if (keyCode == KeyboardHelper.DOWN) { + markRowDirty(); + cursorLocation.row++; + return; + } + + if (keyCode == KeyboardHelper.UP) { + markRowDirty(); + cursorLocation.row--; + return; + } + + if (keyCode == KeyboardHelper.RIGHT) { + cursorLocation.column++; + return; + } + + if (keyCode == KeyboardHelper.LEFT) { + cursorLocation.column--; + return; + } + + if (keyCode == KeyboardHelper.PGDOWN) { + cursorLocation.row += textCanvas.getSize().row; + repaintPage(); + return; + } + + if (keyCode == KeyboardHelper.PGUP) { + cursorLocation.row -= textCanvas.getSize().row; + repaintPage = true; + } + + } + + /** + * Processes the Tab key action for indentation and dedentation. + * + *

Behavior depends on modifiers and selection state:

+ *
    + *
  • Shift+Tab with selection: dedents all selected lines by + * removing up to 4 leading spaces, if all lines have sufficient indentation
  • + *
  • Shift+Tab without selection: dedents the current line by + * removing 4 leading spaces and moving the cursor back
  • + *
  • Tab with selection: indents all selected lines by adding + * 4 leading spaces
  • + *
+ * + * @param modifiers the keyboard modifier flags from the key event + */ + private void processTab(final int modifiers) { + if (KeyboardHelper.isShiftPressed(modifiers)) { + if (selectionStart.compareTo(selectionEnd) != 0) { + // dedent multiple lines + ensureSelectionOrder(); + + identSelection: + { + // check that indentation is possible + for (int y = selectionStart.row; y < selectionEnd.row; y++) { + final TextLine textLine = page.getLine(y); + + if (!textLine.isEmpty()) + if (textLine.getIdent() < 4) + break identSelection; + } + + for (int y = selectionStart.row; y < selectionEnd.row; y++) + page.getLine(y).cutFromBeginning(4); + } + } else { + // dedent current line + final TextLine textLine = page.getLine(cursorLocation.row); + + if (cursorLocation.column >= 4) + if (textLine.isEmpty()) + cursorLocation.column -= 4; + else if (textLine.getIdent() >= 4) { + cursorLocation.column -= 4; + textLine.cutFromBeginning(4); + } + + } + + repaintPage(); + + } else if (selectionStart.compareTo(selectionEnd) != 0) { + // ident multiple lines + ensureSelectionOrder(); + for (int y = selectionStart.row; y < selectionEnd.row; y++) + page.getLine(y).addIdent(4); + + repaintPage(); + } + } + + /** + * Repaints the entire visible page area onto the text canvas. + * + *

Iterates over every visible cell (row and column), applying the appropriate + * foreground and background colors based on whether the cell is the cursor position, + * part of a selection, or a tab stop margin. Characters are read from the underlying + * {@link Page} model with scroll offsets applied.

+ */ + public void repaintPage() { + + final int columnCount = textCanvas.getSize().column + 2; + final int rowCount = textCanvas.getSize().row + 2; + + for (int row = 0; row < rowCount; row++) + for (int column = 0; column < columnCount; column++) { + final boolean isTabMargin = ((column + scrolledCharacters) % 4) == 0; + + if ((column == (cursorLocation.column - scrolledCharacters)) + & (row == (cursorLocation.row - scrolledLines))) { + // cursor + textCanvas.setBackgroundColor(lookAndFeel.cursorBackground); + textCanvas.setForegroundColor(lookAndFeel.cursorForeground); + } else if (new TextPointer(row + scrolledLines, column).isBetween( + selectionStart, selectionEnd)) { + // selected text + textCanvas.setBackgroundColor(lookAndFeel.selectionBackground); + textCanvas.setForegroundColor(lookAndFeel.selectionForeground); + } else { + // normal text + textCanvas.setBackgroundColor(lookAndFeel.background); + textCanvas.setForegroundColor(lookAndFeel.foreground); + + if (isTabMargin) + textCanvas + .setBackgroundColor(lookAndFeel.tabStopBackground); + + } + + final char charUnderCursor = page.getChar(row + scrolledLines, + column + scrolledCharacters); + + textCanvas.putChar(row, column, charUnderCursor); + } + + } + + /** + * Repaints a single row of the editor. + * + *

Note: the current implementation delegates to + * {@link #repaintPage()} and repaints the entire page. This is a candidate + * for optimization.

+ * + * @param rowNumber the zero-based row index to repaint + */ + public void repaintRow(final int rowNumber) { + // TODO: Optimize this. No need to repaint entire page. + repaintPage(); + } + + /** + * Repaints only the portions of the editor that have been marked as dirty. + * + *

If {@link #repaintPage} is set, the entire page is repainted and all + * dirty row tracking is cleared. Otherwise, only the individually dirty rows + * are repainted.

+ */ + private void repaintWhatNeeded() { + if (repaintPage) { + dirtyRows.clear(); + repaintPage(); + return; + } + + dirtyRows.forEach(this::repaintRow); + dirtyRows.clear(); + } + + /** + * Scrolls the visible editor area by the specified number of characters and lines. + * + *

Scroll offsets are clamped so they never go below zero. A full page + * repaint is scheduled after scrolling.

+ * + * @param charactersToScroll the number of characters to scroll horizontally + * (positive = right, negative = left) + * @param linesToScroll the number of lines to scroll vertically + * (positive = down, negative = up) + */ + public void scroll(final int charactersToScroll, final int linesToScroll) { + scrolledLines += linesToScroll; + scrolledCharacters += charactersToScroll; + + if (scrolledLines < 0) + scrolledLines = 0; + + if (scrolledCharacters < 0) + scrolledCharacters = 0; + + repaintPage = true; + } + + /** + * Replaces the entire editor content with the given text. + * + *

Resets the cursor to position (0, 0), clears all scroll offsets and + * selections, creates a fresh {@link Page}, inserts the text, and triggers + * a full repaint.

+ * + * @param text the new text content for the editor; may contain newline + * characters to create multiple lines + */ + public void setText(final String text) { + // System.out.println("Set text:" + text); + cursorLocation = new TextPointer(0, 0); + scrolledCharacters = 0; + scrolledLines = 0; + selectionStart = new TextPointer(0, 0); + selectionEnd = new TextPointer(0, 0); + page = new Page(); + insertText(text); + repaintPage(); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLine.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLine.java new file mode 100755 index 0000000..1b83320 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLine.java @@ -0,0 +1,410 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui.textEditorComponent; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a single line of text in the text editor. + * + *

Internally stores a mutable list of {@link Character} objects, one per character in + * the line. Provides operations for inserting, cutting, and copying substrings, as well + * as indentation manipulation (adding or removing leading spaces).

+ * + *

Lines automatically trim trailing whitespace via the internal {@code pack()} method, + * which is invoked after most mutating operations. This ensures that lines never store + * unnecessary trailing space characters.

+ * + * @see Character the wrapper for individual character values in a line + * @see Page the container that holds multiple {@code TextLine} instances + * @see TextEditComponent the text editor component that uses lines for editing + */ +public class TextLine { + + private List chars = new ArrayList<>(); + + /** + * Creates an empty text line with no characters. + */ + public TextLine() { + } + + /** + * Creates a text line from an existing list of {@link Character} objects. + * + *

Trailing whitespace is automatically trimmed via {@code pack()}.

+ * + * @param value the list of characters to initialize this line with + */ + public TextLine(final List value) { + chars = value; + pack(); + } + + /** + * Creates a text line initialized with the given string. + * + *

Each character in the string is converted to a {@link Character} object. + * Trailing whitespace is automatically trimmed.

+ * + * @param value the string to initialize this line with + */ + public TextLine(final String value) { + setValue(value); + } + + /** + * Adds indentation (leading spaces) to the beginning of this line. + * + *

If the line is empty, no indentation is added. Otherwise, the specified + * number of space characters are prepended to the line.

+ * + * @param amount the number of space characters to prepend + */ + public void addIdent(final int amount) { + if (isEmpty()) + return; + + for (int i = 0; i < amount; i++) + chars.add(0, new Character(' ')); + } + + /** + * Removes characters from the specified range and returns them as a string. + * + *

This is a destructive operation: the characters in the range + * [{@code from}, {@code until}) are removed from this line. If the line is + * shorter than {@code until}, it is padded with spaces before extraction. + * Trailing whitespace is trimmed after removal.

+ * + * @param from the start index (inclusive) of the range to extract + * @param until the end index (exclusive) of the range to extract + * @return the extracted characters as a string + */ + public String copySubString(final int from, final int until) { + final StringBuilder result = new StringBuilder(); + + ensureLength(until); + + for (int i = from; i < until; i++) + result.append(chars.remove(from).value); + + pack(); + return result.toString(); + } + + + /** + * Removes the specified number of characters from the beginning of this line. + * + *

If {@code charactersToCut} exceeds the line length, the entire line is cleared. + * If {@code charactersToCut} is zero, no changes are made.

+ * + * @param charactersToCut the number of leading characters to remove + */ + public void cutFromBeginning(int charactersToCut) { + + if (charactersToCut > chars.size()) + charactersToCut = chars.size(); + + if (charactersToCut == 0) + return; + + chars = chars.subList(charactersToCut, chars.size()); + } + + /** + * Extracts a substring from this line, removing those characters and returning them. + * + *

Characters in the range [{@code from}, {@code until}) are removed from this + * line and returned as a string. Characters outside the range are retained. If the + * line is shorter than {@code until}, it is padded with spaces before extraction. + * Trailing whitespace is trimmed after the cut.

+ * + * @param from the start index (inclusive) of the range to cut + * @param until the end index (exclusive) of the range to cut + * @return the cut characters as a string + */ + public String cutSubString(final int from, final int until) { + final StringBuilder result = new StringBuilder(); + + final List reminder = new ArrayList<>(); + + ensureLength(until); + + for (int i = 0; i < chars.size(); i++) + if ((i >= from) && (i < until)) + result.append(chars.get(i).value); + else + reminder.add(chars.get(i)); + + chars = reminder; + + pack(); + return result.toString(); + } + + /** + * Truncates this line at the specified column, discarding all characters from + * that position to the end. + * + *

If {@code col} is greater than or equal to the current line length, + * no changes are made.

+ * + * @param col the column index at which to truncate (exclusive; characters at + * indices 0 through {@code col - 1} are kept) + */ + public void cutUntilEnd(final int col) { + if (col >= chars.size()) + return; + + chars = chars.subList(0, col); + } + + /** + * Ensures the internal character list is at least the given length, + * padding with space characters as needed. + */ + private void ensureLength(final int length) { + while (chars.size() < length) + chars.add(new Character(' ')); + } + + /** + * Returns the character at the specified column position. + * + *

If the column is beyond the end of this line, a space character is returned.

+ * + * @param col the zero-based column index + * @return the character at the given column, or {@code ' '} if out of bounds + */ + public char getCharForLocation(final int col) { + + if (col >= chars.size()) + return ' '; + + return chars.get(col).value; + } + + /** + * Returns the internal list of {@link Character} objects backing this line. + * + *

Note: the returned list is the live internal list. Modifications + * to the returned list will directly affect this line.

+ * + * @return the mutable list of characters in this line + */ + public List getChars() { + return chars; + } + + /** + * Returns the indentation level of this line, measured as the number of + * leading space characters before the first non-space character. + * + *

If the line is empty, returns {@code 0}.

+ * + * @return the number of leading space characters + * @throws RuntimeException if the line is non-empty but contains only spaces + * (should not occur due to trailing whitespace trimming by {@code pack()}) + */ + public int getIdent() { + if (isEmpty()) + return 0; + + for (int i = 0; i < chars.size(); i++) + if (chars.get(i).hasValue()) + return i; + + throw new RuntimeException("This code shall never execute"); + } + + /** + * Returns the length of this line (number of characters, excluding trimmed + * trailing whitespace). + * + * @return the number of characters in this line + */ + public int getLength() { + return chars.size(); + } + + /** + * Returns a new {@code TextLine} containing the characters from this line + * in the range [{@code from}, {@code until}). + * + *

If {@code until} exceeds the line length, only the available characters + * are included. The returned line is an independent copy.

+ * + * @param from the start index (inclusive) + * @param until the end index (exclusive) + * @return a new {@code TextLine} with the specified sub-range of characters + */ + public TextLine getSubLine(final int from, final int until) { + final List result = new ArrayList<>(); + + for (int i = from; i < until; i++) { + if (i >= chars.size()) + break; + result.add(chars.get(i)); + } + + return new TextLine(result); + } + + /** + * Returns a substring of this line from column {@code from} (inclusive) to + * column {@code until} (exclusive). + * + *

If the requested range extends beyond the line length, space characters + * are used for positions past the end of the line.

+ * + * @param from the start column (inclusive) + * @param until the end column (exclusive) + * @return the substring in the specified range + */ + public String getSubString(final int from, final int until) { + final StringBuilder result = new StringBuilder(); + + for (int i = from; i < until; i++) + result.append(getCharForLocation(i)); + + return result.toString(); + } + + /** + * Inserts a single character at the specified column position. + * + *

If the column is beyond the current line length, the line is padded + * with spaces up to that position. Trailing whitespace is trimmed after + * insertion.

+ * + * @param col the zero-based column at which to insert + * @param value the character to insert + */ + public void insertCharacter(final int col, final char value) { + ensureLength(col); + chars.add(col, new Character(value)); + pack(); + } + + /** + * Inserts a string at the specified column position. + * + *

Each character in the string is inserted sequentially starting at + * {@code col}. If the column is beyond the current line length, the line + * is padded with spaces. Trailing whitespace is trimmed after insertion.

+ * + * @param col the zero-based column at which to start inserting + * @param value the string to insert + */ + public void insertString(final int col, final String value) { + ensureLength(col); + int i = 0; + for (final char c : value.toCharArray()) { + chars.add(col + i, new Character(c)); + i++; + } + pack(); + } + + /** + * Inserts all characters from another {@code TextLine} at the specified column. + * + *

If the column is beyond the current line length, the line is padded with + * spaces. Trailing whitespace is trimmed after insertion.

+ * + * @param col the zero-based column at which to start inserting + * @param textLine the text line whose characters will be inserted + */ + public void insertTextLine(final int col, final TextLine textLine) { + ensureLength(col); + int i = 0; + for (final Character c : textLine.getChars()) { + chars.add(col + i, c); + i++; + } + pack(); + } + + /** + * Returns whether this line contains no characters. + * + *

Because trailing whitespace is trimmed, an empty line means there are + * no visible characters on this line.

+ * + * @return {@code true} if the line has no characters, {@code false} otherwise + */ + public boolean isEmpty() { + return chars.isEmpty(); + } + + /** + * Trims trailing whitespace from this line by removing trailing space + * characters that have no visible content. + */ + private void pack() { + int newLength = 0; + + for (int i = chars.size() - 1; i >= 0; i--) + if (chars.get(i).hasValue()) { + newLength = i + 1; + break; + } + + if (newLength == chars.size()) + return; + + chars = chars.subList(0, newLength); + } + + /** + * Removes the character at the specified column position. + * + *

If the column is beyond the end of the line, no changes are made.

+ * + * @param col the zero-based column of the character to remove + */ + public void removeCharacter(final int col) { + if (col >= chars.size()) + return; + + chars.remove(col); + } + + /** + * Replaces the entire contents of this line with the given string. + * + *

The existing characters are cleared, and each character from the string + * is added as a new {@link Character} object. Trailing whitespace is trimmed.

+ * + * @param string the new text content for this line + */ + public void setValue(final String string) { + chars.clear(); + for (final char c : string.toCharArray()) + chars.add(new Character(c)); + + pack(); + } + + /** + * Returns the string representation of this line by concatenating + * all character values. + * + * @return the text content of this line as a {@code String} + */ + @Override + public String toString() { + final StringBuilder buffer = new StringBuilder(); + + for (final Character character : chars) + buffer.append(character.value); + + return buffer.toString(); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/package-info.java new file mode 100644 index 0000000..ff8dff5 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/package-info.java @@ -0,0 +1,5 @@ +package eu.svjatoslav.sixth.e3d.gui.textEditorComponent; + +/** + * This package contains a simple text editor component. + */ \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java new file mode 100644 index 0000000..d921357 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java @@ -0,0 +1,150 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.math; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; + +import static java.lang.Math.cos; +import static java.lang.Math.sin; + +/** + * Represents a rotation in 3D space using two Euler angles (XZ and YZ). + * + *

Angles are stored with precomputed sine and cosine values for efficient + * repeated rotation operations without recalculating trigonometric functions.

+ * + * @see Transform + */ +public class Rotation implements Cloneable { + + /** + * Precomputed sine and cosine of the rotation angles. + */ + private double s1, c1, s2, c2; + + /** + * The angle of rotation around the XZ axis (yaw). + */ + private double angleXZ = 0; + + /** + * The angle of rotation around the YZ axis (pitch). + */ + private double angleYZ = 0; + + public Rotation() { + computeMultipliers(); + } + + /** + * Creates a rotation with the specified Euler angles. + * + * @param angleXZ the angle around the XZ axis (yaw) in radians + * @param angleYZ the angle around the YZ axis (pitch) in radians + */ + public Rotation(final double angleXZ, final double angleYZ) { + this.angleXZ = angleXZ; + this.angleYZ = angleYZ; + computeMultipliers(); + } + + @Override + public Rotation clone() { + return new Rotation(angleXZ, angleYZ); + } + + /** + * Recomputes the sine and cosine values from the current angles. + */ + private void computeMultipliers() { + s1 = sin(angleXZ); + c1 = cos(angleXZ); + + s2 = sin(angleYZ); + c2 = cos(angleYZ); + } + + /** + * Rotates a point around the origin using this rotation's angles. + * + * @param point3d the point to rotate (modified in place) + */ + public void rotate(final Point3D point3d) { + // Rotate around the XZ axis + final double z1 = (point3d.z * c1) - (point3d.x * s1); + point3d.x = (point3d.z * s1) + (point3d.x * c1); + + // Rotate around the YZ axis + point3d.z = (z1 * c2) - (point3d.y * s2); + point3d.y = (z1 * s2) + (point3d.y * c2); + } + + /** + * Adds the specified angles to this rotation and updates the trigonometric values. + * + * @param angleXZ the angle to add around the XZ axis in radians + * @param angleYZ the angle to add around the YZ axis in radians + */ + public void rotate(final double angleXZ, final double angleYZ) { + this.angleXZ += angleXZ; + this.angleYZ += angleYZ; + computeMultipliers(); + } + + /** + * Sets the rotation angles and recomputes the trigonometric values. + * + * @param angleXZ the angle around the XZ axis (yaw) in radians + * @param angleYZ the angle around the YZ axis (pitch) in radians + */ + public void setAngles(final double angleXZ, final double angleYZ) { + this.angleXZ = angleXZ; + this.angleYZ = angleYZ; + computeMultipliers(); + } + + public void setAngles(Rotation rotation) { + this.angleXZ = rotation.angleXZ; + this.angleYZ = rotation.angleYZ; + computeMultipliers(); + } + + /** + * Returns the angle around the XZ axis (yaw) in radians. + * + * @return the XZ angle + */ + public double getAngleXZ() { + return angleXZ; + } + + /** + * Returns the angle around the YZ axis (pitch) in radians. + * + * @return the YZ angle + */ + public double getAngleYZ() { + return angleYZ; + } + + /** + * Returns the precomputed sine of the XZ angle. + * + * @return sin(angleXZ) + */ + public double getSinXZ() { + return s1; + } + + /** + * Returns the precomputed cosine of the XZ angle. + * + * @return cos(angleXZ) + */ + public double getCosXZ() { + return c1; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java new file mode 100755 index 0000000..36a6202 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java @@ -0,0 +1,112 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.math; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; + +/** + * Represents a transformation in 3D space combining translation and rotation. + * + *

Transformations are applied in order: rotation first, then translation.

+ * + * @see Rotation + */ +public class Transform implements Cloneable { + + /** + * The translation applied after rotation. + */ + private final Point3D translation; + + /** + * The rotation applied before translation. + */ + private final Rotation rotation; + + public Transform() { + translation = new Point3D(); + rotation = new Rotation(); + } + + /** + * Creates a transform with the specified translation and no rotation. + * + * @param translation the translation + */ + public Transform(final Point3D translation) { + this.translation = translation; + rotation = new Rotation(); + } + + /** + * Creates a transform with the specified translation and rotation angles. + * + * @param translation the translation + * @param angleXZ the angle around the XZ axis (yaw) in radians + * @param angleYZ the angle around the YZ axis (pitch) in radians + */ + public Transform(final Point3D translation, final double angleXZ, + final double angleYZ) { + + this.translation = translation; + rotation = new Rotation(angleXZ, angleYZ); + } + + /** + * Creates a transform with the specified translation and rotation. + * + * @param translation the translation + * @param rotation the rotation + */ + public Transform(final Point3D translation, final Rotation rotation) { + this.translation = translation; + this.rotation = rotation; + } + + @Override + public Transform clone() { + return new Transform(translation, rotation); + } + + public Rotation getRotation() { + return rotation; + } + + public Point3D getTranslation() { + return translation; + } + + /** + * Applies this transform to a point: rotation followed by translation. + * + * @param point the point to transform (modified in place) + */ + public void transform(final Point3D point) { + rotation.rotate(point); + point.add(translation); + } + + /** + * Sets the rotation angles for this transform. + * + * @param angleXZ the angle around the XZ axis (yaw) in radians + * @param angleYZ the angle around the YZ axis (pitch) in radians + */ + public void setRotation(final double angleXZ, final double angleYZ) { + rotation.setAngles(angleXZ, angleYZ); + } + + /** + * Sets the translation for this transform by copying the values from the given point. + * + * @param translation the translation values to copy + */ + public void setTranslation(final Point3D translation) { + this.translation.x = translation.x; + this.translation.y = translation.y; + this.translation.z = translation.z; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/TransformStack.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/TransformStack.java new file mode 100644 index 0000000..1088dc4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/TransformStack.java @@ -0,0 +1,81 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.math; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; + +/** + * Stack of transforms applied to points during rendering. + * + *

Transforms are applied in reverse order (last added is applied first). + * This supports hierarchical scene graphs where child objects are positioned + * relative to their parent objects.

+ * + *

Example:

+ *
+ * There is a ship in the sea. The ship moves along the sea and every object
+ * on the ship moves with it. Inside the ship there is a car. The car moves
+ * along the ship and every object on the car moves with it.
+ *
+ * To calculate the world position of an object inside the car:
+ * 1. Apply object's position relative to the car
+ * 2. Apply car's position relative to the ship
+ * 3. Apply ship's position relative to the world
+ * 
+ * + * @see Transform + */ +public class TransformStack { + + /** + * Array of transforms in the stack. + * Fixed size for efficiency to avoid memory allocation during rendering. + */ + private final Transform[] transforms = new Transform[100]; + + /** + * Current number of transforms in the stack. + */ + private int transformsCount = 0; + + /** + * Pushes a transform onto the stack. + * + * @param transform the transform to push + */ + public void addTransform(final Transform transform) { + transforms[transformsCount] = transform; + transformsCount++; + } + + /** + * Clears all transforms from the stack. + */ + public void clear() { + transformsCount = 0; + } + + /** + * Pops the most recently added transform from the stack. + */ + public void dropTransform() { + transformsCount--; + } + + /** + * Transforms a point through all transforms in the stack. + * + * @param coordinate the input coordinate (not modified) + * @param result the output coordinate (receives transformed result) + */ + public void transform(final Point3D coordinate, final Point3D result) { + + result.clone(coordinate); + + // Apply transforms in reverse order (last added = first applied) + for (int i = transformsCount - 1; i >= 0; i--) + transforms[i].transform(result); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java new file mode 100644 index 0000000..4fedbb9 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java @@ -0,0 +1,86 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.math; + +import eu.svjatoslav.sixth.e3d.geometry.Point2D; +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.gui.RenderingContext; + +/** + * Vertex is a point where two or more lines, line segments, or rays come together. + * In other words, it's a corner of a polygon, polyhedron, or other geometric shape. + * For example, a triangle has three vertices, a square has four, and a cube has eight. + */ +public class Vertex { + + /** + * Vertex coordinate in 3D space. + */ + public Point3D coordinate; + + /** + * Vertex coordinate relative to the viewer after transformation. + * Visible vertices have positive z coordinate. + * Viewer is located at (0, 0, 0). + * No perspective correction is applied. + */ + public Point3D transformedCoordinate; + + /** + * Vertex coordinate in pixels relative to the top left corner of the screen after transformation + * and perspective correction. + */ + public Point2D onScreenCoordinate; + + + /** + * Coordinate within texture. + */ + public Point2D textureCoordinate; + + + /** + * The frame number when this vertex was last transformed. + */ + private int lastTransformedFrame; + + public Vertex() { + this(new Point3D()); + } + + public Vertex(final Point3D location) { + this(location, null); + } + + public Vertex(final Point3D location, Point2D textureCoordinate) { + coordinate = location; + transformedCoordinate = new Point3D(); + onScreenCoordinate = new Point2D(); + this.textureCoordinate = textureCoordinate; + } + + + /** + * Transforms vertex coordinate to calculate its location relative to the viewer. + * It also calculates its location on the screen. + * + * @param transforms Transforms pipeline. + * @param renderContext Rendering context. + */ + public void calculateLocationRelativeToViewer(final TransformStack transforms, + final RenderingContext renderContext) { + + if (lastTransformedFrame == renderContext.frameNumber) + return; + + lastTransformedFrame = renderContext.frameNumber; + + transforms.transform(coordinate, transformedCoordinate); + + onScreenCoordinate.x = ((transformedCoordinate.x / transformedCoordinate.z) * renderContext.projectionScale); + onScreenCoordinate.y = ((transformedCoordinate.y / transformedCoordinate.z) * renderContext.projectionScale); + onScreenCoordinate.add(renderContext.centerCoordinate); + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/package-info.java new file mode 100644 index 0000000..98dfff8 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/package-info.java @@ -0,0 +1,9 @@ +/** + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + *

+ * Math that is needed for the project. + */ + +package eu.svjatoslav.sixth.e3d.math; + diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/package-info.java new file mode 100644 index 0000000..fc7d743 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/package-info.java @@ -0,0 +1,7 @@ +/** + * This is root package for 3D engine. Since package name cannot start with a digit, it is named "e3d" instead, + * which stands for "Engine 3D". + */ + +package eu.svjatoslav.sixth.e3d; + diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/IntegerPoint.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/IntegerPoint.java new file mode 100644 index 0000000..7dce375 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/IntegerPoint.java @@ -0,0 +1,20 @@ +package eu.svjatoslav.sixth.e3d.renderer.octree; + +/** + * Point in 3D space. Used for octree. All coordinates are integers. + */ +public class IntegerPoint +{ + public int x, y, z = 0; + + public IntegerPoint() + { + } + + public IntegerPoint(final int x, final int y, final int z) + { + this.x = x; + this.y = y; + this.z = z; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/OctreeVolume.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/OctreeVolume.java new file mode 100755 index 0000000..a4f4382 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/OctreeVolume.java @@ -0,0 +1,1005 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.octree; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.Ray; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; + +import static java.lang.Integer.max; +import static java.lang.Integer.min; + +/** + *

+ * There are 3 cell types:
+ *
+ *      UNUSED
+ *
+ *      SOLID
+ *          contains:
+ *              original color
+ *              visible color, after being illuminated by nearby light sources
+ *
+ *      CLUSTER
+ *          contains pointers to 8 sub cells
+ * 
+ */ + +public class OctreeVolume { + + // cell is not hit by the ray + public static final int TRACE_NO_HIT = -1; + + // solid cell (contains color and illumination) + private static final int CELL_STATE_SOLID = -2; + + // unused cell + private static final int CELL_STATE_UNUSED = -1; + + public int[] cell1; + public int[] cell2; + public int[] cell3; + public int[] cell4; + public int[] cell5; + public int[] cell6; + public int[] cell7; + public int[] cell8; + + /** + * Pointer to the first unused cell. + */ + public int cellAllocationPointer = 0; + public int usedCellsCount = 0; + public int masterCellSize; + + public OctreeVolume() { + initWorld(1500000, 256 * 64); + } + + public void breakSolidCell(final int pointer) { + final int color = getCellColor(pointer); + final int illumination = getCellIllumination(pointer); + + cell1[pointer] = makeNewCell(color, illumination); + cell2[pointer] = makeNewCell(color, illumination); + cell3[pointer] = makeNewCell(color, illumination); + cell4[pointer] = makeNewCell(color, illumination); + cell5[pointer] = makeNewCell(color, illumination); + cell6[pointer] = makeNewCell(color, illumination); + cell7[pointer] = makeNewCell(color, illumination); + cell8[pointer] = makeNewCell(color, illumination); + } + + /** + * Clears the cell. + * @param pointer Pointer to the cell. + */ + public void clearCell(final int pointer) { + cell1[pointer] = 0; + cell2[pointer] = 0; + cell3[pointer] = 0; + cell4[pointer] = 0; + + cell5[pointer] = 0; + cell6[pointer] = 0; + cell7[pointer] = 0; + cell8[pointer] = 0; + } + + public void deleteCell(final int cellPointer) { + clearCell(cellPointer); + cell1[cellPointer] = CELL_STATE_UNUSED; + usedCellsCount--; + } + + public int doesIntersect(final int cubeX, final int cubeY, final int cubeZ, + final int cubeSize, final Ray r) { + + // ray starts inside the cube + if ((cubeX - cubeSize) < r.origin.x) + if ((cubeX + cubeSize) > r.origin.x) + if ((cubeY - cubeSize) < r.origin.y) + if ((cubeY + cubeSize) > r.origin.y) + if ((cubeZ - cubeSize) < r.origin.z) + if ((cubeZ + cubeSize) > r.origin.z) { + r.hitPoint = r.origin.clone(); + return 1; + } + // back face + if (r.direction.z > 0) + if ((cubeZ - cubeSize) > r.origin.z) { + final double mult = ((cubeZ - cubeSize) - r.origin.z) / r.direction.z; + final double hitX = (r.direction.x * mult) + r.origin.x; + if ((cubeX - cubeSize) < hitX) + if ((cubeX + cubeSize) > hitX) { + final double hitY = (r.direction.y * mult) + r.origin.y; + if ((cubeY - cubeSize) < hitY) + if ((cubeY + cubeSize) > hitY) { + r.hitPoint = new Point3D(hitX, hitY, cubeZ + - cubeSize); + return 2; + } + } + } + + // up face + if (r.direction.y > 0) + if ((cubeY - cubeSize) > r.origin.y) { + final double mult = ((cubeY - cubeSize) - r.origin.y) / r.direction.y; + final double hitX = (r.direction.x * mult) + r.origin.x; + if ((cubeX - cubeSize) < hitX) + if ((cubeX + cubeSize) > hitX) { + final double hitZ = (r.direction.z * mult) + r.origin.z; + if ((cubeZ - cubeSize) < hitZ) + if ((cubeZ + cubeSize) > hitZ) { + r.hitPoint = new Point3D(hitX, cubeY - cubeSize, + hitZ); + return 3; + } + } + } + + // left face + if (r.direction.x > 0) + if ((cubeX - cubeSize) > r.origin.x) { + final double mult = ((cubeX - cubeSize) - r.origin.x) / r.direction.x; + final double hitY = (r.direction.y * mult) + r.origin.y; + if ((cubeY - cubeSize) < hitY) + if ((cubeY + cubeSize) > hitY) { + final double hitZ = (r.direction.z * mult) + r.origin.z; + if ((cubeZ - cubeSize) < hitZ) + if ((cubeZ + cubeSize) > hitZ) { + r.hitPoint = new Point3D(cubeX - cubeSize, hitY, + hitZ); + return 4; + } + } + } + + // front face + if (r.direction.z < 0) + if ((cubeZ + cubeSize) < r.origin.z) { + final double mult = ((cubeZ + cubeSize) - r.origin.z) / r.direction.z; + final double hitX = (r.direction.x * mult) + r.origin.x; + if ((cubeX - cubeSize) < hitX) + if ((cubeX + cubeSize) > hitX) { + final double hitY = (r.direction.y * mult) + r.origin.y; + if ((cubeY - cubeSize) < hitY) + if ((cubeY + cubeSize) > hitY) { + r.hitPoint = new Point3D(hitX, hitY, cubeZ + + cubeSize); + return 5; + } + } + } + + // down face + if (r.direction.y < 0) + if ((cubeY + cubeSize) < r.origin.y) { + final double mult = ((cubeY + cubeSize) - r.origin.y) / r.direction.y; + final double hitX = (r.direction.x * mult) + r.origin.x; + if ((cubeX - cubeSize) < hitX) + if ((cubeX + cubeSize) > hitX) { + final double hitZ = (r.direction.z * mult) + r.origin.z; + if ((cubeZ - cubeSize) < hitZ) + if ((cubeZ + cubeSize) > hitZ) { + r.hitPoint = new Point3D(hitX, cubeY + cubeSize, + hitZ); + return 6; + } + } + } + + // right face + if (r.direction.x < 0) + if ((cubeX + cubeSize) < r.origin.x) { + final double mult = ((cubeX + cubeSize) - r.origin.x) / r.direction.x; + final double hitY = (r.direction.y * mult) + r.origin.y; + if ((cubeY - cubeSize) < hitY) + if ((cubeY + cubeSize) > hitY) { + final double hitZ = (r.direction.z * mult) + r.origin.z; + if ((cubeZ - cubeSize) < hitZ) + if ((cubeZ + cubeSize) > hitZ) { + r.hitPoint = new Point3D(cubeX + cubeSize, hitY, + hitZ); + return 7; + } + } + } + return 0; + } + + /** + * Fill 3D rectangle. + */ + public void fillRectangle(IntegerPoint p1, IntegerPoint p2, Color color) { + + int x1 = min(p1.x, p2.x); + int x2 = max(p1.x, p2.x); + int y1 = min(p1.y, p2.y); + int y2 = max(p1.y, p2.y); + int z1 = min(p1.z, p2.z); + int z2 = max(p1.z, p2.z); + + for (int x = x1; x <= x2; x++) + for (int y = y1; y <= y2; y++) + for (int z = z1; z <= z2; z++) + putCell(x, y, z, 0, 0, 0, masterCellSize, 0, color); + } + + public int getCellColor(final int pointer) { + return cell2[pointer]; + } + + public int getCellIllumination(final int pointer) { + return cell3[pointer]; + } + + public void initWorld(final int bufferLength, final int masterCellSize) { + // System.out.println("Initializing new world"); + + // initialize world storage buffer + this.masterCellSize = masterCellSize; + + cell1 = new int[bufferLength]; + cell2 = new int[bufferLength]; + cell3 = new int[bufferLength]; + cell4 = new int[bufferLength]; + + cell5 = new int[bufferLength]; + cell6 = new int[bufferLength]; + cell7 = new int[bufferLength]; + cell8 = new int[bufferLength]; + + for (int i = 0; i < bufferLength; i++) + cell1[i] = CELL_STATE_UNUSED; + + // initialize master cell + clearCell(0); + } + + public boolean isCellSolid(final int pointer) { + return cell1[pointer] == CELL_STATE_SOLID; + } + + /** + * Scans cells arrays and returns pointer to found unused cell. + * @return pointer to found unused cell + */ + public int getNewCellPointer() { + while (true) { + // ensure that cell allocation pointer is in bounds + if (cellAllocationPointer >= cell1.length) + cellAllocationPointer = 0; + + if (cell1[cellAllocationPointer] == CELL_STATE_UNUSED) { + // unused cell found + clearCell(cellAllocationPointer); + + usedCellsCount++; + return cellAllocationPointer; + } else + cellAllocationPointer++; + } + } + + public int makeNewCell(final int color, final int illumination) { + final int pointer = getNewCellPointer(); + markCellAsSolid(pointer); + setCellColor(pointer, color); + setCellIllumination(pointer, illumination); + return pointer; + } + + /** + * Mark cell as solid. + * + * @param pointer pointer to cell + */ + public void markCellAsSolid(final int pointer) { + cell1[pointer] = CELL_STATE_SOLID; + } + + public void putCell(final int x, final int y, final int z, final Color color) { + putCell(x, y, z, 0, 0, 0, masterCellSize, 0, color); + } + + private void putCell(final int x, final int y, final int z, + final int cellX, final int cellY, final int cellZ, + final int cellSize, final int cellPointer, final Color color) { + + if (cellSize > 1) { + + // if case of big cell + if (isCellSolid(cellPointer)) { + + // if cell is already a needed color, do notheing + if (getCellColor(cellPointer) == color.toInt()) + return; + + // otherwise break cell up + breakSolidCell(cellPointer); + + // continue, as if it is cluster now + } + + // decide witch subcube to use + int[] subCubeArray; + int subX, subY, subZ; + + if (x > cellX) { + subX = (cellSize / 2) + cellX; + if (y > cellY) { + subY = (cellSize / 2) + cellY; + if (z > cellZ) { + subZ = (cellSize / 2) + cellZ; + // 7 + subCubeArray = cell7; + } else { + subZ = (-cellSize / 2) + cellZ; + // 3 + subCubeArray = cell3; + } + } else { + subY = (-cellSize / 2) + cellY; + if (z > cellZ) { + subZ = (cellSize / 2) + cellZ; + // 6 + subCubeArray = cell6; + } else { + subZ = (-cellSize / 2) + cellZ; + // 2 + subCubeArray = cell2; + } + } + } else { + subX = (-cellSize / 2) + cellX; + if (y > cellY) { + subY = (cellSize / 2) + cellY; + if (z > cellZ) { + subZ = (cellSize / 2) + cellZ; + // 8 + subCubeArray = cell8; + } else { + subZ = (-cellSize / 2) + cellZ; + // 4 + subCubeArray = cell4; + } + } else { + subY = (-cellSize / 2) + cellY; + if (z > cellZ) { + subZ = (cellSize / 2) + cellZ; + // 5 + subCubeArray = cell5; + } else { + subZ = (-cellSize / 2) + cellZ; + // 1 + subCubeArray = cell1; + } + } + } + + int subCubePointer; + if (subCubeArray[cellPointer] == 0) { + // create empty cluster + subCubePointer = getNewCellPointer(); + subCubeArray[cellPointer] = subCubePointer; + } else + subCubePointer = subCubeArray[cellPointer]; + + putCell(x, y, z, subX, subY, subZ, cellSize / 2, subCubePointer, + color); + } else { + cell1[cellPointer] = CELL_STATE_SOLID; + cell2[cellPointer] = color.toInt(); + cell3[cellPointer] = CELL_STATE_UNUSED; + // System.out.println("Cell written!"); + } + } + + public void setCellColor(final int pointer, final int color) { + cell2[pointer] = color; + } + + public void setCellIllumination(final int pointer, final int illumination) { + cell3[pointer] = illumination; + } + + /** + * Trace ray through the world and return pointer to intersecting cell. + * + * @return pointer to intersecting cell or TRACE_NO_HIT if no intersection. + */ + public int traceCell(final int cellX, final int cellY, final int cellZ, + final int cellSize, final int pointer, final Ray ray) { + if (isCellSolid(pointer)) { + // solid cell + if (doesIntersect(cellX, cellY, cellZ, cellSize, ray) != 0) { + ray.hitCellSize = cellSize; + ray.hitCellX = cellX; + ray.hitCellY = cellY; + ray.hitCellZ = cellZ; + return pointer; + } + return TRACE_NO_HIT; + } else // cluster + if (doesIntersect(cellX, cellY, cellZ, cellSize, ray) != 0) { + final int halfOfCellSize = cellSize / 2; + int rayIntersectionResult; + + if (ray.origin.x > cellX) { + if (ray.origin.y > cellY) { + if (ray.origin.z > cellZ) { + // 7 + // 6 8 3 5 2 4 1 + + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell7[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell6[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell8[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell3[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell2[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell4[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell5[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell1[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } else { + // 3 + // 2 4 7 1 6 8 5 + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell3[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell2[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell4[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell7[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell6[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell8[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell1[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell5[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } + } else if (ray.origin.z > cellZ) { + // 6 + // 5 2 7 8 1 3 4 + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell6[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell7[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell2[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell5[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell8[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell3[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell1[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell4[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } else { + // 2 + // 1 3 6 5 4 7 8 + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell2[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell3[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell1[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell6[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell7[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell5[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell4[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell8[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } + } else if (ray.origin.y > cellY) { + if (ray.origin.z > cellZ) { + // 8 + // 5 7 4 1 6 3 2 + + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell8[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell7[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell5[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell4[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell3[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell1[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell6[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell2[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } else { + // 4 + // 1 3 8 5 7 2 6 + + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell4[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell8[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell3[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell1[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell7[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell5[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell2[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell6[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } + } else if (ray.origin.z > cellZ) { + // 5 + // 1 6 8 4 2 7 3 + + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY - halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell5[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY - halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell1[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY - halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell6[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY + halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell8[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY + halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell4[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY + halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell7[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY - halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell2[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY + halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell3[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } else { + // 1 + // 5 2 4 8 6 3 7 + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY - halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell1[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY - halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell5[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY - halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell2[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY + halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell4[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY - halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell6[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY + halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell8[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY + halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell3[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY + halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell7[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + } + } + return TRACE_NO_HIT; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/package-info.java new file mode 100755 index 0000000..821faf2 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/package-info.java @@ -0,0 +1,20 @@ +/** + * Octree-based voxel volume representation and rendering for the Sixth 3D engine. + * + *

This package provides a volumetric data structure based on an octree, which enables + * efficient storage and rendering of voxel data. The octree recursively subdivides 3D space + * into eight octants, achieving significant data compression for sparse or repetitive volumes.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume} - the main octree data structure + * for storing and querying voxel cells
  • + *
  • {@link eu.svjatoslav.sixth.e3d.renderer.octree.IntegerPoint} - integer 3D coordinate used + * for voxel addressing
  • + *
+ * + * @see eu.svjatoslav.sixth.e3d.renderer.octree.raytracer ray tracing through octree volumes + */ + +package eu.svjatoslav.sixth.e3d.renderer.octree; + diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java new file mode 100644 index 0000000..f0b214b --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java @@ -0,0 +1,42 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.gui.Camera; +import eu.svjatoslav.sixth.e3d.math.Rotation; + +import static eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.RaytracingCamera.SIZE; + +/** + * Represents camera view. Used to compute direction of rays during ray tracing. + */ +public class CameraView { + + /** + * Camera view coordinates. + */ + Point3D cameraCenter, topLeft, topRight, bottomLeft, bottomRight; + + public CameraView(final Camera camera, final double zoom) { + // compute camera view coordinates as if camera is at (0,0,0) and look at (0,0,1) + final float viewAngle = (float) .6; + cameraCenter = new Point3D(); + topLeft = new Point3D(0, 0, SIZE).rotate(-viewAngle, -viewAngle); + topRight = new Point3D(0, 0, SIZE).rotate(viewAngle, -viewAngle); + bottomLeft = new Point3D(0, 0, SIZE).rotate(-viewAngle, viewAngle); + bottomRight = new Point3D(0, 0, SIZE).rotate(viewAngle, viewAngle); + + Rotation rotation = camera.getTransform().getRotation(); + topLeft.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ()); + topRight.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ()); + bottomLeft.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ()); + bottomRight.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ()); + + // place camera view at camera location + camera.getTransform().getTranslation().clone().scaleDown(zoom).addTo(cameraCenter, topLeft, topRight, bottomLeft, bottomRight); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/LightSource.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/LightSource.java new file mode 100755 index 0000000..c0939d7 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/LightSource.java @@ -0,0 +1,35 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; + +/** + * Represents light source. + */ +public class LightSource { + + /** + * Light source color. + */ + public Color color; + /** + * Light source brightness. + */ + public float brightness; + /** + * Light source location. + */ + Point3D location; + + public LightSource(final Point3D location, final Color color, + final float Brightness) { + this.location = location; + this.color = color; + brightness = Brightness; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/Ray.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/Ray.java new file mode 100755 index 0000000..afbe4b1 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/Ray.java @@ -0,0 +1,71 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; + +/** + * Represents a ray used for tracing through an {@link eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume}. + * + *

A ray is defined by an {@link #origin} point and a {@link #direction} vector. + * After tracing through the octree, the intersection results are stored in the + * {@link #hitPoint}, {@link #hitCellSize}, and {@link #hitCellX}/{@link #hitCellY}/{@link #hitCellZ} + * fields, which are populated by the octree traversal algorithm.

+ * + * @see RayTracer + * @see eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume#traceCell(int, int, int, int, int, Ray) + */ +public class Ray { + + /** + * The origin point of the ray (the starting position in world space). + */ + public Point3D origin; + + /** + * The direction vector of the ray. Does not need to be normalized; + * the octree traversal handles arbitrary direction magnitudes. + */ + public Point3D direction; + + /** + * The point in world space where the ray intersected an octree cell. + * Set by the octree traversal algorithm after a successful intersection. + */ + public Point3D hitPoint; + + /** + * The size (side length) of the octree cell that was hit. + * A value of 1 indicates a leaf cell at the finest resolution. + */ + public int hitCellSize; + + /** + * The x coordinate of the octree cell that was hit. + */ + public int hitCellX; + + /** + * The y coordinate of the octree cell that was hit. + */ + public int hitCellY; + + /** + * The z coordinate of the octree cell that was hit. + */ + public int hitCellZ; + + /** + * Creates a new ray with the specified origin and direction. + * + * @param origin the starting point of the ray + * @param direction the direction vector of the ray + */ + public Ray(Point3D origin, Point3D direction) { + this.origin = origin; + this.direction = direction; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayHit.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayHit.java new file mode 100755 index 0000000..a1c8d41 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayHit.java @@ -0,0 +1,56 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer; + +/** + * Records the result of a ray-octree intersection test. + * + *

A {@code RayHit} stores the 3D world-space coordinates where a {@link Ray} + * intersected an octree cell, along with a pointer (index) to the intersected cell + * within the {@link eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume}'s internal + * cell arrays.

+ * + * @see Ray + * @see RayTracer + * @see eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume + */ +public class RayHit { + + /** + * The x coordinate of the intersection point in world space. + */ + float x; + + /** + * The y coordinate of the intersection point in world space. + */ + float y; + + /** + * The z coordinate of the intersection point in world space. + */ + float z; + + /** + * The index (pointer) into the octree's cell arrays identifying the cell that was hit. + */ + int cellPointer; + + /** + * Creates a new ray hit record. + * + * @param x the x coordinate of the intersection point + * @param y the y coordinate of the intersection point + * @param z the z coordinate of the intersection point + * @param cellPointer the index of the intersected cell in the octree's cell arrays + */ + public RayHit(final float x, final float y, final float z, + final int cellPointer) { + this.x = x; + this.y = y; + this.z = z; + this.cellPointer = cellPointer; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayTracer.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayTracer.java new file mode 100755 index 0000000..aa69a15 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayTracer.java @@ -0,0 +1,411 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.gui.ViewPanel; +import eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; + +import java.util.Vector; + +/** + * Ray tracing engine for rendering {@link OctreeVolume} scenes onto a {@link Texture}. + * + *

{@code RayTracer} implements {@link Runnable} and is designed to execute as a background + * task. It casts one ray per pixel through the camera's view frustum, tracing each ray + * into the octree volume to find intersections with solid cells. When a hit is found, the + * ray tracer computes per-pixel lighting by casting shadow rays from the hit point toward + * each {@link LightSource} along multiple surface-normal-offset directions (6 directions: + * +X, -X, +Y, -Y, +Z, -Z) to approximate diffuse illumination with soft shadows.

+ * + *

Rendering pipeline

+ *
    + *
  1. The camera's view frustum corners are obtained via {@link RaytracingCamera#getCameraView()}.
  2. + *
  3. For each pixel, a primary ray is constructed from the camera center through the + * interpolated position on the view plane.
  4. + *
  5. The ray is traced through the octree using + * {@link OctreeVolume#traceCell(int, int, int, int, int, Ray)}.
  6. + *
  7. If a solid cell is hit, up to 6 shadow rays are cast toward each light source. + * If no shadow ray is occluded, the light's contribution is accumulated.
  8. + *
  9. The final pixel color is the cell's base color modulated by the accumulated light.
  10. + *
  11. Computed lighting is cached in the octree cell data ({@code cell3}) for reuse.
  12. + *
+ * + *

Progress is reported periodically by invalidating the texture's mipmap cache and + * requesting a repaint on the {@link ViewPanel}, allowing partial results to be displayed + * while rendering continues.

+ * + * @see OctreeVolume + * @see Ray + * @see LightSource + * @see RaytracingCamera + */ +public class RayTracer implements Runnable { + + /** + * Minimum interval in milliseconds between progress updates (texture refresh and repaint). + */ + private static final int PROGRESS_UPDATE_FREQUENCY_MILLIS = 1000; + + /** + * The raytracing camera defining the viewpoint and view frustum for ray generation. + */ + private final RaytracingCamera raytracingCamera; + + /** + * The target texture where rendered pixels are written. + */ + private final Texture texture; + + /** + * The view panel used for triggering display repaints during progressive rendering. + */ + private final ViewPanel viewPanel; + + /** + * The octree volume to be ray-traced. + */ + private final OctreeVolume octreeVolume; + + /** + * The list of light sources used for illumination calculations. + */ + private final Vector lights; + + /** + * Counter tracking the number of light computations performed during the current render pass. + */ + private int computedLights; + + /** + * Creates a new ray tracer for the given scene configuration. + * + * @param texture the texture to render into; its primary bitmap dimensions + * determine the output resolution + * @param octreeVolume the octree volume containing the scene geometry + * @param lights the light sources to use for illumination + * @param raytracingCamera the raytracing camera defining the viewpoint + * @param viewPanel the view panel for triggering progress repaints + */ + public RayTracer(final Texture texture, final OctreeVolume octreeVolume, + final Vector lights, final RaytracingCamera raytracingCamera, + final ViewPanel viewPanel) { + + this.texture = texture; + this.octreeVolume = octreeVolume; + this.lights = lights; + this.raytracingCamera = raytracingCamera; + this.viewPanel = viewPanel; + } + + /** + * Executes the ray tracing render pass. + * + *

Iterates over every pixel of the target texture, constructs a primary ray + * from the camera center through the view plane, traces it into the octree volume, + * and writes the resulting color. The texture is periodically refreshed to show + * progressive results.

+ */ + @Override + public void run() { + computedLights = 0; + + // create camera + + // Camera cam = new Camera(camCenter, upLeft, upRight, downLeft, + // downRight); + + // add camera to the raytracing point + // Main.mainWorld.geometryCollection.addObject(cam); + // Main.mainWorld.compiledGeometry.compileGeometry(Main.mainWorld.geometryCollection); + + final int width = texture.primaryBitmap.width; + final int height = texture.primaryBitmap.height; + + final CameraView cameraView = raytracingCamera.getCameraView(); + + // calculate vertical vectors + final double x1p = cameraView.bottomLeft.x - cameraView.topLeft.x; + final double y1p = cameraView.bottomLeft.y - cameraView.topLeft.y; + final double z1p = cameraView.bottomLeft.z - cameraView.topLeft.z; + + final double x2p = cameraView.bottomRight.x - cameraView.topRight.x; + final double y2p = cameraView.bottomRight.y - cameraView.topRight.y; + final double z2p = cameraView.bottomRight.z - cameraView.topRight.z; + + long nextBitmapUpdate = System.currentTimeMillis() + + PROGRESS_UPDATE_FREQUENCY_MILLIS; + + for (int y = 0; y < height; y++) { + final double cx1 = cameraView.topLeft.x + ((x1p * y) / height); + final double cy1 = cameraView.topLeft.y + ((y1p * y) / height); + final double cz1 = cameraView.topLeft.z + ((z1p * y) / height); + + final double cx2 = cameraView.topRight.x + ((x2p * y) / height); + final double cy2 = cameraView.topRight.y + ((y2p * y) / height); + final double cz2 = cameraView.topRight.z + ((z2p * y) / height); + + // calculate horisontal vector + final double x3p = cx2 - cx1; + final double y3p = cy2 - cy1; + final double z3p = cz2 - cz1; + + for (int x = 0; x < width; x++) { + final double cx3 = cx1 + ((x3p * x) / width); + final double cy3 = cy1 + ((y3p * x) / width); + final double cz3 = cz1 + ((z3p * x) / width); + + final Ray r = new Ray( + new Point3D(cameraView.cameraCenter.x, + cameraView.cameraCenter.y, + cameraView.cameraCenter.z), + new Point3D( + cx3 - cameraView.cameraCenter.x, cy3 + - cameraView.cameraCenter.y, cz3 + - cameraView.cameraCenter.z) + ); + final int c = traceRay(r); + + final Color color = new Color(c); + texture.primaryBitmap.drawPixel(x, y, color); + } + + if (System.currentTimeMillis() > nextBitmapUpdate) { + nextBitmapUpdate = System.currentTimeMillis() + + PROGRESS_UPDATE_FREQUENCY_MILLIS; + texture.resetResampledBitmapCache(); + viewPanel.repaintDuringNextViewUpdate(); + } + } + + texture.resetResampledBitmapCache(); + viewPanel.repaintDuringNextViewUpdate(); + } + + /** + * Traces a single ray into the octree volume and computes the resulting pixel color. + * + *

If the ray intersects a solid cell, the method computes diffuse lighting by + * casting shadow rays from 6 surface-offset positions toward each light source. + * The lighting result is cached in the octree's {@code cell3} array to avoid + * redundant computation for the same cell.

+ * + * @param ray the ray to trace (origin and direction must be set) + * @return the packed RGB color value (0xRRGGBB), or 0 if the ray hits nothing + */ + private int traceRay(final Ray ray) { + + final int intersectingCell = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, ray); + + if (intersectingCell != -1) { + // if lightening not computed, compute it + if (octreeVolume.cell3[intersectingCell] == -1) + // if cell is larger than 1 + if (ray.hitCellSize > 1) { + // break it up + octreeVolume.breakSolidCell(intersectingCell); + return traceRay(ray); + } else { + computedLights++; + float red = 30, green = 30, blue = 30; + + for (final LightSource l : lights) { + final double xDist = (l.location.x - ray.hitCellX); + final double yDist = (l.location.y - ray.hitCellY); + final double zDist = (l.location.z - ray.hitCellZ); + + double newRed = 0, newGreen = 0, newBlue = 0; + double tempRed, tempGreen, tempBlue; + + double distance = Math.sqrt((xDist * xDist) + + (yDist * yDist) + (zDist * zDist)); + distance = (distance / 3) + 1; + + final Ray r1 = new Ray( + new Point3D( + ray.hitCellX, + ray.hitCellY - (float) 1.5, + ray.hitCellZ), + + new Point3D((float) l.location.x - (float) ray.hitCellX, l.location.y + - (ray.hitCellY - (float) 1.5), (float) l.location.z + - (float) ray.hitCellZ) + ); + + final int rt1 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r1); + + if (rt1 == -1) { + newRed = (l.color.r * l.brightness) / distance; + newGreen = (l.color.g * l.brightness) / distance; + newBlue = (l.color.b * l.brightness) / distance; + } + + final Ray r2 = new Ray( + new Point3D( + ray.hitCellX - (float) 1.5, + ray.hitCellY, ray.hitCellZ), + + new Point3D( + l.location.x - (ray.hitCellX - (float) 1.5), (float) l.location.y + - (float) ray.hitCellY, (float) l.location.z + - (float) ray.hitCellZ) + ); + + final int rt2 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r2); + + if (rt2 == -1) { + tempRed = (l.color.r * l.brightness) / distance; + tempGreen = (l.color.g * l.brightness) / distance; + tempBlue = (l.color.b * l.brightness) / distance; + + if (tempRed > newRed) + newRed = tempRed; + if (tempGreen > newGreen) + newGreen = tempGreen; + if (tempBlue > newBlue) + newBlue = tempBlue; + } + + final Ray r3 = new Ray( + new Point3D( + ray.hitCellX, ray.hitCellY, + ray.hitCellZ - (float) 1.5), + new Point3D( + (float) l.location.x - (float) ray.hitCellX, (float) l.location.y + - (float) ray.hitCellY, l.location.z + - (ray.hitCellZ - (float) 1.5)) + ); + + final int rt3 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r3); + + if (rt3 == -1) { + tempRed = (l.color.r * l.brightness) / distance; + tempGreen = (l.color.g * l.brightness) / distance; + tempBlue = (l.color.b * l.brightness) / distance; + if (tempRed > newRed) + newRed = tempRed; + if (tempGreen > newGreen) + newGreen = tempGreen; + if (tempBlue > newBlue) + newBlue = tempBlue; + } + + final Ray r4 = new Ray( + new Point3D( + ray.hitCellX, + ray.hitCellY + (float) 1.5, + ray.hitCellZ), + + new Point3D( + (float) l.location.x - (float) ray.hitCellX, l.location.y + - (ray.hitCellY + (float) 1.5), (float) l.location.z + - (float) ray.hitCellZ) + ); + + final int rt4 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r4); + + if (rt4 == -1) { + tempRed = (l.color.r * l.brightness) / distance; + tempGreen = (l.color.g * l.brightness) / distance; + tempBlue = (l.color.b * l.brightness) / distance; + if (tempRed > newRed) + newRed = tempRed; + if (tempGreen > newGreen) + newGreen = tempGreen; + if (tempBlue > newBlue) + newBlue = tempBlue; + } + + final Ray r5 = new Ray( + new Point3D( + ray.hitCellX + (float) 1.5, + ray.hitCellY, ray.hitCellZ), + + new Point3D( + l.location.x - (ray.hitCellX + (float) 1.5), (float) l.location.y + - (float) ray.hitCellY, (float) l.location.z + - (float) ray.hitCellZ) + ); + + final int rt5 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r5); + + if (rt5 == -1) { + tempRed = (l.color.r * l.brightness) / distance; + tempGreen = (l.color.g * l.brightness) / distance; + tempBlue = (l.color.b * l.brightness) / distance; + if (tempRed > newRed) + newRed = tempRed; + if (tempGreen > newGreen) + newGreen = tempGreen; + if (tempBlue > newBlue) + newBlue = tempBlue; + } + + final Ray r6 = new Ray( + new Point3D( + ray.hitCellX, ray.hitCellY, + ray.hitCellZ + (float) 1.5), + + new Point3D( + + (float) l.location.x - (float) ray.hitCellX, (float) l.location.y + - (float) ray.hitCellY, l.location.z + - (ray.hitCellZ + (float) 1.5))); + + final int rt6 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r6); + + if (rt6 == -1) { + tempRed = (l.color.r * l.brightness) / distance; + tempGreen = (l.color.g * l.brightness) / distance; + tempBlue = (l.color.b * l.brightness) / distance; + if (tempRed > newRed) + newRed = tempRed; + if (tempGreen > newGreen) + newGreen = tempGreen; + if (tempBlue > newBlue) + newBlue = tempBlue; + } + red += newRed; + green += newGreen; + blue += newBlue; + + } + + final int cellColor = octreeVolume.cell2[intersectingCell]; + + red = (red * ((cellColor & 0xFF0000) >> 16)) / 255; + green = (green * ((cellColor & 0xFF00) >> 8)) / 255; + blue = (blue * (cellColor & 0xFF)) / 255; + + if (red > 255) + red = 255; + if (green > 255) + green = 255; + if (blue > 255) + blue = 255; + + octreeVolume.cell3[intersectingCell] = (((int) red) << 16) + + (((int) green) << 8) + ((int) blue); + + } + if (octreeVolume.cell3[intersectingCell] == 0) + return octreeVolume.cell2[intersectingCell]; + return octreeVolume.cell3[intersectingCell]; + } + + // return (200 << 16) + (200 << 8) + 255; + return 0; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java new file mode 100755 index 0000000..e8237a9 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java @@ -0,0 +1,99 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.gui.Camera; +import eu.svjatoslav.sixth.e3d.math.Rotation; +import eu.svjatoslav.sixth.e3d.math.Transform; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.TexturedRectangle; +import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URL; + +/** + * Raytracing camera that renders a scene to a texture. + * It is represented on the scene as a textured rectangle showing the raytraced view. + */ +public class RaytracingCamera extends TexturedRectangle { + + public static final int SIZE = 100; + public static final int IMAGE_SIZE = 500; + private final CameraView cameraView; + + public RaytracingCamera(final Camera camera, final double zoom) { + super(new Transform(camera.getTransform().getTranslation().clone())); + cameraView = new CameraView(camera, zoom); + + computeCameraCoordinates(camera); + + addWaitNotification(getTexture()); + } + + private void addWaitNotification(final Texture texture) { + // add hourglass icon + try { + final BufferedImage sprite = getSprite("eu/svjatoslav/sixth/e3d/examples/hourglass.png"); + texture.graphics.drawImage(sprite, IMAGE_SIZE / 2, + (IMAGE_SIZE / 2) - 30, null); + } catch (final Exception ignored) { + } + + // add "Please wait..." message + texture.graphics.setColor(java.awt.Color.WHITE); + texture.graphics.setFont(new Font("Monospaced", Font.PLAIN, 10)); + texture.graphics.drawString("Please wait...", (IMAGE_SIZE / 2) - 20, + (IMAGE_SIZE / 2) + 30); + } + + private void computeCameraCoordinates(final Camera camera) { + initialize(SIZE, SIZE, IMAGE_SIZE, IMAGE_SIZE, 3); + + Point3D cameraCenter = new Point3D(); + + topLeft.setValues(cameraCenter.x, cameraCenter.y, cameraCenter.z + SIZE); + topRight.clone(topLeft); + bottomLeft.clone(topLeft); + bottomRight.clone(topLeft); + + final float viewAngle = (float) .6; + + topLeft.rotate(cameraCenter, -viewAngle, -viewAngle); + topRight.rotate(cameraCenter, viewAngle, -viewAngle); + bottomLeft.rotate(cameraCenter, -viewAngle, viewAngle); + bottomRight.rotate(cameraCenter, viewAngle, viewAngle); + + Rotation rotation = camera.getTransform().getRotation(); + topLeft.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ()); + topRight.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ()); + bottomLeft.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ()); + bottomRight.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ()); + + final Color cameraColor = new Color(255, 255, 0, 255); + final LineAppearance appearance = new LineAppearance(2, cameraColor); + + addShape(appearance.getLine(topLeft, topRight)); + addShape(appearance.getLine(bottomLeft, bottomRight)); + addShape(appearance.getLine(topLeft, bottomLeft)); + addShape(appearance.getLine(topRight, bottomRight)); + + } + + public CameraView getCameraView() { + return cameraView; + } + + public BufferedImage getSprite(final String ref) throws IOException { + final URL url = this.getClass().getClassLoader().getResource(ref); + return ImageIO.read(url); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/package-info.java new file mode 100755 index 0000000..b82ab4f --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/package-info.java @@ -0,0 +1,21 @@ +/** + * Ray tracer for rendering voxel data stored in an octree structure. + * + *

This package implements a ray tracing renderer that casts rays through an + * {@link eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume} to produce rendered images + * of volumetric data. The ray tracer traverses the octree hierarchy for efficient + * intersection testing, skipping empty regions of space.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.RayTracer} - main ray tracing engine
  • + *
  • {@link eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.RaytracingCamera} - camera configuration for ray generation
  • + *
  • {@link eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.Ray} - represents a single ray cast through the volume
  • + *
  • {@link eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.LightSource} - defines a light source for shading
  • + *
+ * + * @see eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume the voxel data structure + */ + +package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer; + diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/package-info.java new file mode 100755 index 0000000..249622e --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/package-info.java @@ -0,0 +1,11 @@ +/** + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + *

+ * + * Various 3D renderers utilizing different rendering approaches. + * + */ + +package eu.svjatoslav.sixth.e3d.renderer; + diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java new file mode 100644 index 0000000..e7cb25f --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java @@ -0,0 +1,273 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster; + +/** + * Immutable RGBA color representation for the Sixth 3D engine. + * + *

This is the engine's own color class (not {@link java.awt.Color}). All color values + * use integer components in the range 0-255. The class provides predefined constants + * for common colors and several constructors for creating colors from different formats.

+ * + *

Usage examples:

+ *
{@code
+ * // Use predefined color constants
+ * Color red = Color.RED;
+ * Color semiTransparent = new Color(255, 0, 0, 128);
+ *
+ * // Create from integer RGBA components (0-255)
+ * Color custom = new Color(100, 200, 50, 255);
+ *
+ * // Create from floating-point components (0.0-1.0)
+ * Color half = new Color(0.5, 0.5, 0.5, 1.0);
+ *
+ * // Create from hex string
+ * Color hex6 = new Color("FF8800");     // RGB, fully opaque
+ * Color hex8 = new Color("FF880080");   // RGBA with alpha
+ * Color hex3 = new Color("F80");        // Short RGB format
+ *
+ * // Create from packed RGB integer
+ * Color packed = new Color(0xFF8800);
+ *
+ * // Convert to AWT for interop with Java Swing
+ * java.awt.Color awtColor = custom.toAwtColor();
+ * }
+ * + *

Important: Always use this class instead of {@link java.awt.Color} when + * working with the Sixth 3D engine's rendering pipeline.

+ * + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line + */ +public final class Color { + + /** Fully opaque red (255, 0, 0). */ + public static final Color RED = new Color(255, 0, 0, 255); + /** Fully opaque green (0, 255, 0). */ + public static final Color GREEN = new Color(0, 255, 0, 255); + /** Fully opaque blue (0, 0, 255). */ + public static final Color BLUE = new Color(0, 0, 255, 255); + /** Fully opaque yellow (255, 255, 0). */ + public static final Color YELLOW = new Color(255, 255, 0, 255); + /** Fully opaque cyan (0, 255, 255). */ + public static final Color CYAN = new Color(0, 255, 255, 255); + /** Fully opaque magenta/purple (255, 0, 255). */ + public static final Color MAGENTA = new Color(255, 0, 255, 255); + /** Fully opaque white (255, 255, 255). */ + public static final Color WHITE = new Color(255, 255, 255, 255); + /** Fully opaque black (0, 0, 0). */ + public static final Color BLACK = new Color(0, 0, 0, 255); + /** Fully opaque purple/magenta (255, 0, 255). */ + public static final Color PURPLE = new Color(255, 0, 255, 255); + /** Fully transparent (alpha = 0). */ + public static final Color TRANSPARENT = new Color(0, 0, 0, 0); + + /** + * Red component. 0-255. + */ + public final int r; + + /** + * Green component. 0-255. + */ + public final int g; + + /** + * Blue component. 0-255. + */ + public final int b; + + /** + * Alpha component. + * 0 - transparent. + * 255 - opaque. + */ + public int a; + + private java.awt.Color cachedAwtColor; + + /** + * Creates a copy of the given color. + * + * @param parentColor the color to copy + */ + public Color(final Color parentColor) { + r = parentColor.r; + g = parentColor.g; + b = parentColor.b; + a = parentColor.a; + } + + /** + * Creates a color from floating-point RGBA components in the range 0.0 to 1.0. + * Values are internally converted to 0-255 integer range and clamped. + * + * @param r red component (0.0 = none, 1.0 = full) + * @param g green component (0.0 = none, 1.0 = full) + * @param b blue component (0.0 = none, 1.0 = full) + * @param a alpha component (0.0 = transparent, 1.0 = opaque) + */ + public Color(final double r, final double g, final double b, final double a) { + this.r = ensureByteLimit((int) (r * 255d)); + this.g = ensureByteLimit((int) (g * 255d)); + this.b = ensureByteLimit((int) (b * 255d)); + this.a = ensureByteLimit((int) (a * 255d)); + } + + /** + * @param colorHexCode color code in hex format. + * Supported formats are: + *
+     *                     RGB
+     *                     RGBA
+     *                     RRGGBB
+     *                     RRGGBBAA
+     *                     
+ */ + public Color(String colorHexCode) { + switch (colorHexCode.length()) { + case 3: + r = parseHexSegment(colorHexCode, 0, 1) * 16; + g = parseHexSegment(colorHexCode, 1, 1) * 16; + b = parseHexSegment(colorHexCode, 2, 1) * 16; + a = 255; + return; + + case 4: + r = parseHexSegment(colorHexCode, 0, 1) * 16; + g = parseHexSegment(colorHexCode, 1, 1) * 16; + b = parseHexSegment(colorHexCode, 2, 1) * 16; + a = parseHexSegment(colorHexCode, 3, 1) * 16; + return; + + case 6: + r = parseHexSegment(colorHexCode, 0, 2); + g = parseHexSegment(colorHexCode, 2, 2); + b = parseHexSegment(colorHexCode, 4, 2); + a = 255; + return; + + case 8: + r = parseHexSegment(colorHexCode, 0, 2); + g = parseHexSegment(colorHexCode, 2, 2); + b = parseHexSegment(colorHexCode, 4, 2); + a = parseHexSegment(colorHexCode, 6, 2); + return; + default: + throw new IllegalArgumentException("Unsupported color code: " + colorHexCode); + } + } + + /** + * Creates a fully opaque color from a packed RGB integer. + * + *

The integer is interpreted as {@code 0xRRGGBB}, where the upper 8 bits + * are the red channel, the middle 8 bits are green, and the lower 8 bits are blue.

+ * + * @param rgb packed RGB value (e.g. {@code 0xFF8800} for orange) + */ + public Color(final int rgb) { + r = (rgb & 0xFF0000) >> 16; + g = (rgb & 0xFF00) >> 8; + b = rgb & 0xFF; + a = 255; + } + + /** + * Creates a fully opaque color from RGB integer components (0-255). + * + * @param r red component (0-255) + * @param g green component (0-255) + * @param b blue component (0-255) + */ + public Color(final int r, final int g, final int b) { + this(r, g, b, 255); + } + + /** + * Creates a color from RGBA integer components (0-255). + * Values outside 0-255 are clamped. + * + * @param r red component (0-255) + * @param g green component (0-255) + * @param b blue component (0-255) + * @param a alpha component (0 = transparent, 255 = opaque) + */ + public Color(final int r, final int g, final int b, final int a) { + this.r = ensureByteLimit(r); + this.g = ensureByteLimit(g); + this.b = ensureByteLimit(b); + this.a = ensureByteLimit(a); + } + + private int parseHexSegment(String hexString, int start, int length) { + return Integer.parseInt(hexString.substring(start, start + length), 16); + } + + /** + * Ensure that color values are within allowed limits of 0 to 255. + */ + private int ensureByteLimit(final int value) { + if (value < 0) + return 0; + + if (value > 255) + return 255; + + return value; + } + + /** + * Returns {@code true} if this color is fully transparent (alpha = 0). + * + * @return {@code true} if the alpha component is zero + */ + public boolean isTransparent() { + return a == 0; + } + + /** + * Converts this color to a {@link java.awt.Color} instance for use with + * Java AWT/Swing graphics APIs. + * + * @return the equivalent {@link java.awt.Color} + */ + public java.awt.Color toAwtColor() { + if (cachedAwtColor == null) + cachedAwtColor = new java.awt.Color(r, g, b, a); + return cachedAwtColor; + } + + /** + * Converts this color to a packed ARGB integer as used by {@link java.awt.Color#getRGB()}. + * + * @return packed ARGB integer representation + */ + public int toInt() { + return (a << 24) | (r << 16) | (g << 8) | b; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Color color = (Color) o; + + if (r != color.r) return false; + if (g != color.g) return false; + if (b != color.b) return false; + return a == color.a; + } + + @Override + public int hashCode() { + int result = r; + result = 31 * result + g; + result = 31 * result + b; + result = 31 * result + a; + return result; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/RenderAggregator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/RenderAggregator.java new file mode 100644 index 0000000..c22f37c --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/RenderAggregator.java @@ -0,0 +1,78 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster; + +import eu.svjatoslav.sixth.e3d.gui.RenderingContext; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; + +/** + * Collects transformed shapes during a render frame and paints them in depth-sorted order. + * + *

The {@code RenderAggregator} implements the painter's algorithm: shapes are sorted + * from back to front (highest Z-depth first) and then painted sequentially. This ensures + * that closer shapes correctly occlude those behind them.

+ * + *

When two shapes have the same Z-depth, their unique {@link AbstractCoordinateShape#shapeId} + * is used as a tiebreaker to guarantee deterministic rendering order.

+ * + *

This class is used internally by {@link ShapeCollection} during the render pipeline. + * You typically do not need to interact with it directly.

+ * + * @see ShapeCollection#paint(eu.svjatoslav.sixth.e3d.gui.ViewPanel, RenderingContext) + * @see AbstractCoordinateShape#onScreenZ + */ +public class RenderAggregator { + + private final ArrayList shapes = new ArrayList<>(); + private final ShapesZIndexComparator comparator = new ShapesZIndexComparator(); + + /** + * Sorts all queued shapes by Z-depth (back to front) and paints them. + * + * @param renderBuffer the rendering context to paint shapes into + */ + public void paint(final RenderingContext renderBuffer) { + shapes.sort(comparator); + for (int i = 0; i < shapes.size(); i++) + shapes.get(i).paint(renderBuffer); + } + + /** + * Adds a transformed shape to the queue for rendering in this frame. + * + * @param shape the shape to render, with its screen-space coordinates already computed + */ + public void queueShapeForRendering(final AbstractCoordinateShape shape) { + shapes.add(shape); + } + + /** + * Clears all queued shapes, preparing for a new render frame. + */ + public void reset() { + shapes.clear(); + } + + /** + * Comparator that sorts shapes by Z-depth in descending order (farthest first) + * for the painter's algorithm. Uses shape ID as a tiebreaker. + */ + static class ShapesZIndexComparator implements Comparator, Serializable { + + @Override + public int compare(final AbstractCoordinateShape o1, final AbstractCoordinateShape o2) { + if (o1.getZ() < o2.getZ()) + return 1; + else if (o1.getZ() > o2.getZ()) + return -1; + + return Integer.compare(o1.shapeId, o2.shapeId); + } + } +} 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 new file mode 100755 index 0000000..ad0be61 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java @@ -0,0 +1,131 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.gui.Camera; +import eu.svjatoslav.sixth.e3d.gui.RenderingContext; +import eu.svjatoslav.sixth.e3d.gui.ViewPanel; +import eu.svjatoslav.sixth.e3d.math.Transform; +import eu.svjatoslav.sixth.e3d.math.TransformStack; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Root container that holds all 3D shapes in a scene and orchestrates their rendering. + * + *

{@code ShapeCollection} is the top-level scene graph. You add shapes to it, and during + * each render frame it transforms all shapes from world space to screen space (relative to the + * camera), sorts them by depth, and paints them back-to-front.

+ * + *

Usage example:

+ *
{@code
+ * // Get the root shape collection from the view panel
+ * ShapeCollection scene = viewPanel.getRootShapeCollection();
+ *
+ * // Add shapes to the scene
+ * scene.addShape(new Line(
+ *     new Point3D(0, 0, 100),
+ *     new Point3D(100, 0, 100),
+ *     Color.RED, 2.0
+ * ));
+ *
+ * scene.addShape(new WireframeCube(
+ *     new Point3D(0, 0, 200), 50,
+ *     new LineAppearance(5, Color.GREEN)
+ * ));
+ * }
+ * + *

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

+ * + * @see ViewPanel#getRootShapeCollection() + * @see AbstractShape the base class for all shapes + * @see RenderAggregator handles depth sorting and painting + */ +public class ShapeCollection { + + private final RenderAggregator aggregator = new RenderAggregator(); + private final TransformStack transformStack = new TransformStack(); + private final List shapes = new ArrayList<>(); + + // Camera rotation. We reuse this object for every frame render to avoid garbage collections. + private final Transform cameraRotationTransform = new Transform(); + + // Camera rotation. We reuse this object for every frame render to avoid garbage collections. + private final Transform cameraTranslationTransform = new Transform(); + + /** + * Adds a shape to this collection. This method is thread-safe. + * + * @param shape the shape to add to the scene + */ + public synchronized void addShape(final AbstractShape shape) { + shapes.add(shape); + } + + /** + * Returns the list of all shapes currently in this collection. + * + * @return unmodifiable view would be safer, but currently returns the internal list + */ + public Collection getShapes() { + return shapes; + } + + /** + * Removes all shapes from this collection. + */ + public void clear() { + shapes.clear(); + } + + /** + * Renders all shapes in this collection for the current frame. + * + *

This method performs the full render pipeline:

+ *
    + *
  1. Resets the aggregator and transform stack
  2. + *
  3. Applies the camera rotation (avatar's viewing direction)
  4. + *
  5. Applies the camera translation (avatar's position in the world)
  6. + *
  7. Transforms all shapes to screen space
  8. + *
  9. Sorts shapes by depth and paints them back-to-front
  10. + *
+ * + * @param viewPanel the view panel providing the camera state + * @param renderingContext the rendering context with pixel buffer and frame metadata + */ + public synchronized void paint(final ViewPanel viewPanel, + final RenderingContext renderingContext) { + + renderingContext.frameNumber++; + + aggregator.reset(); + transformStack.clear(); + + // Translate the scene according to camera current location. + final Camera camera = viewPanel.getCamera(); + + // Rotate the scene according to camera looking direction + + cameraRotationTransform.getRotation().setAngles(camera.getTransform().getRotation()); + transformStack.addTransform(cameraRotationTransform); + + // translate scene according to camera location in the world + final Point3D cameraLocation = camera.getTransform().getTranslation(); + cameraTranslationTransform.getTranslation().x = -cameraLocation.x; + cameraTranslationTransform.getTranslation().y = -cameraLocation.y; + cameraTranslationTransform.getTranslation().z = -cameraLocation.z; + transformStack.addTransform(cameraTranslationTransform); + + for (final AbstractShape shape : shapes) + shape.transform(transformStack, aggregator, renderingContext); + + aggregator.paint(renderingContext); + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightSource.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightSource.java new file mode 100644 index 0000000..7425ca9 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightSource.java @@ -0,0 +1,136 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.lighting; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; + +/** + * Represents a light source in the 3D scene with position, color, and intensity. + * + *

Light sources emit colored light that illuminates polygons based on their + * orientation relative to the light. The intensity of illumination follows the + * Lambert cosine law - surfaces facing the light receive full intensity, while + * surfaces at an angle receive proportionally less light.

+ * + *

Usage example:

+ *
{@code
+ * // Create a yellow light source at position (100, -50, 200)
+ * LightSource light = new LightSource(
+ *     new Point3D(100, -50, 200),
+ *     Color.YELLOW,
+ *     1.5
+ * );
+ *
+ * // Move the light source
+ * light.setPosition(new Point3D(0, 0, 300));
+ *
+ * // Change the light color
+ * light.setColor(new Color(255, 100, 50));
+ *
+ * // Adjust intensity
+ * light.setIntensity(2.0);
+ * }
+ * + * @see LightingManager manages multiple light sources and calculates shading + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon + */ +public class LightSource { + + /** + * Position of the light source in 3D world space. + */ + private Point3D position; + + /** + * Color of the light emitted by this source. + */ + private Color color; + + /** + * Intensity multiplier for this light source. + * Values greater than 1.0 make the light brighter, values less than 1.0 make it dimmer. + * High intensity values can cause surfaces to appear white (clamped at 255). + */ + private double intensity; + + /** + * Creates a new light source at the specified position with the given color and intensity. + * + * @param position the position of the light in world space + * @param color the color of the light + * @param intensity the intensity multiplier (1.0 = normal brightness) + */ + public LightSource(final Point3D position, final Color color, final double intensity) { + this.position = position; + this.color = color; + this.intensity = intensity; + } + + /** + * Creates a new light source at the specified position with the given color. + * Default intensity is 1.0. + * + * @param position the position of the light in world space + * @param color the color of the light + */ + public LightSource(final Point3D position, final Color color) { + this(position, color, 1.0); + } + + /** + * Returns the color of this light source. + * + * @return the light color + */ + public Color getColor() { + return color; + } + + /** + * Returns the intensity multiplier of this light source. + * + * @return the intensity multiplier + */ + public double getIntensity() { + return intensity; + } + + /** + * Returns the position of this light source. + * + * @return the position in world space + */ + public Point3D getPosition() { + return position; + } + + /** + * Sets the color of this light source. + * + * @param color the new light color + */ + public void setColor(final Color color) { + this.color = color; + } + + /** + * Sets the intensity multiplier of this light source. + * + * @param intensity the new intensity multiplier (1.0 = normal brightness) + */ + public void setIntensity(final double intensity) { + this.intensity = intensity; + } + + /** + * Sets the position of this light source. + * + * @param position the new position in world space + */ + public void setPosition(final Point3D position) { + this.position = position; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.java new file mode 100644 index 0000000..b368e6e --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.java @@ -0,0 +1,180 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.lighting; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; + +import java.util.ArrayList; +import java.util.List; + +/** + * Manages light sources in the scene and calculates lighting for polygons. + * + *

This class implements flat shading using the Lambert cosine law. For each + * polygon face, it calculates the surface normal and determines how much light + * each source contributes based on the angle between the normal and the light + * direction.

+ * + *

The lighting calculation considers:

+ *
    + *
  • Distance from polygon center to each light source
  • + *
  • Angle between surface normal and light direction
  • + *
  • Color and intensity of each light source
  • + *
+ * + *

Usage example:

+ *
{@code
+ * LightingManager lighting = new LightingManager();
+ *
+ * // Add light sources
+ * lighting.addLight(new LightSource(new Point3D(100, -50, 200), Color.YELLOW));
+ * lighting.addLight(new LightSource(new Point3D(-100, 50, 200), Color.BLUE));
+ *
+ * // Set ambient light (base illumination)
+ * lighting.setAmbientLight(new Color(30, 30, 30));
+ *
+ * // Calculate shaded color for a polygon
+ * Color shadedColor = lighting.calculateLighting(
+ *     polygonCenter,
+ *     surfaceNormal,
+ *     baseColor
+ * );
+ * }
+ * + * @see LightSource represents a single light source + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon + */ +public class LightingManager { + + private final List lights = new ArrayList<>(); + private Color ambientLight = new Color(10, 10, 10); + + /** + * Creates a new lighting manager with no light sources. + */ + public LightingManager() { + } + + /** + * Adds a light source to the scene. + * + * @param light the light source to add + */ + public void addLight(final LightSource light) { + lights.add(light); + } + + /** + * Calculates the shaded color for a polygon based on lighting. + * + * @param polygonCenter the center point of the polygon in world space + * @param normal the surface normal vector (should be normalized) + * @param baseColor the original color of the polygon + * @return the shaded color after applying lighting + */ + public Color calculateLighting(final Point3D polygonCenter, + final Point3D normal, + final Color baseColor) { + int totalR = 0; + int totalG = 0; + int totalB = 0; + + // Add ambient light contribution + totalR += ambientLight.r; + totalG += ambientLight.g; + totalB += ambientLight.b; + + // Calculate contribution from each light source + for (final LightSource light : lights) { + final Point3D lightPos = light.getPosition(); + final Color lightColor = light.getColor(); + final double lightIntensity = light.getIntensity(); + + // Calculate vector from polygon to light + final double lightDirX = lightPos.x - polygonCenter.x; + final double lightDirY = lightPos.y - polygonCenter.y; + final double lightDirZ = lightPos.z - polygonCenter.z; + + // Normalize the light direction + final double lightDist = Math.sqrt( + lightDirX * lightDirX + + lightDirY * lightDirY + + lightDirZ * lightDirZ + ); + + if (lightDist < 0.0001) + continue; + + final double invLightDist = 1.0 / lightDist; + final double normLightDirX = lightDirX * invLightDist; + final double normLightDirY = lightDirY * invLightDist; + final double normLightDirZ = lightDirZ * invLightDist; + + // Calculate dot product (Lambert cosine law) + final double dotProduct = normal.x * normLightDirX + + normal.y * normLightDirY + + normal.z * normLightDirZ; + + // Only add light if surface faces the light + if (dotProduct > 0) { + // Apply distance attenuation (inverse square law, simplified) + final double attenuation = 1.0 / (1.0 + 0.0001 * lightDist * lightDist); + final double intensity = dotProduct * attenuation * lightIntensity; + + // Add light color contribution + totalR += (int) (lightColor.r * intensity); + totalG += (int) (lightColor.g * intensity); + totalB += (int) (lightColor.b * intensity); + } + } + + // Clamp values to valid range and apply to base color + final int r = Math.min(255, (totalR * baseColor.r) / 255); + final int g = Math.min(255, (totalG * baseColor.g) / 255); + final int b = Math.min(255, (totalB * baseColor.b) / 255); + + return new Color(r, g, b, baseColor.a); + } + + /** + * Returns the ambient light color. + * + * @return the ambient light color + */ + public Color getAmbientLight() { + return ambientLight; + } + + /** + * Returns all light sources in the scene. + * + * @return list of light sources + */ + public List getLights() { + return lights; + } + + /** + * Removes a light source from the scene. + * + * @param light the light source to remove + */ + public void removeLight(final LightSource light) { + lights.remove(light); + } + + /** + * Sets the ambient light color for the scene. + * + *

Ambient light provides base illumination that affects all surfaces + * equally, regardless of their orientation.

+ * + * @param ambientLight the ambient light color + */ + public void setAmbientLight(final Color ambientLight) { + this.ambientLight = ambientLight; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/package-info.java new file mode 100755 index 0000000..b9700ac --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/package-info.java @@ -0,0 +1,26 @@ +/** + * Rasterization-based real-time software renderer for the Sixth 3D engine. + * + *

This package provides a complete rasterization pipeline that renders 3D scenes + * to a 2D pixel buffer using traditional approaches:

+ *
    + *
  • Wireframe rendering - lines and wireframe shapes
  • + *
  • Solid polygon rendering - filled polygons with flat shading
  • + *
  • Textured polygon rendering - polygons with texture mapping and mipmap support
  • + *
  • Depth sorting - back-to-front painter's algorithm using Z-index ordering
  • + *
+ * + *

Key classes in this package:

+ *
    + *
  • {@link eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection} - root container for all 3D shapes in a scene
  • + *
  • {@link eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator} - collects and depth-sorts shapes for rendering
  • + *
  • {@link eu.svjatoslav.sixth.e3d.renderer.raster.Color} - RGBA color representation with predefined constants
  • + *
+ * + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic basic shape primitives (lines, polygons) + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite composite shapes (boxes, grids, text) + * @see eu.svjatoslav.sixth.e3d.renderer.raster.texture texture and mipmap support + */ + +package eu.svjatoslav.sixth.e3d.renderer.raster; + diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java new file mode 100644 index 0000000..a75d798 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java @@ -0,0 +1,150 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes; + +import eu.svjatoslav.sixth.e3d.gui.RenderingContext; +import eu.svjatoslav.sixth.e3d.math.TransformStack; +import eu.svjatoslav.sixth.e3d.math.Vertex; +import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Base class for shapes defined by an array of vertex coordinates. + * + *

This is the foundation for all primitive renderable shapes such as lines, + * solid polygons, and textured polygons. Each shape has a fixed number of vertices + * ({@link Vertex} objects) that define its geometry in 3D space.

+ * + *

During each render frame, the {@link #transform} method projects all vertices + * from world space to screen space. If all vertices are visible (in front of the camera), + * the shape is queued in the {@link RenderAggregator} for depth-sorted painting via + * the {@link #paint} method.

+ * + *

Creating a custom coordinate shape:

+ *
{@code
+ * public class Triangle extends AbstractCoordinateShape {
+ *     private final Color color;
+ *
+ *     public Triangle(Point3D p1, Point3D p2, Point3D p3, Color color) {
+ *         super(new Vertex(p1), new Vertex(p2), new Vertex(p3));
+ *         this.color = color;
+ *     }
+ *
+ *     public void paint(RenderingContext ctx) {
+ *         // Custom painting logic using ctx.graphics and
+ *         // coordinates[i].transformedCoordinate for screen positions
+ *     }
+ * }
+ * }
+ * + * @see AbstractShape the parent class for all shapes + * @see Vertex wraps a 3D coordinate with its transformed (screen-space) position + * @see RenderAggregator collects and depth-sorts shapes before painting + */ +public abstract class AbstractCoordinateShape extends AbstractShape { + + /** + * Global counter used to assign unique IDs to shapes, ensuring deterministic + * rendering order for shapes at the same depth. + */ + private static final AtomicInteger lastShapeId = new AtomicInteger(); + + /** + * Unique identifier for this shape instance, used as a tiebreaker when + * sorting shapes with identical Z-depth values. + */ + public final int shapeId; + + /** + * The vertex coordinates that define this shape's geometry. + * Each vertex contains both the original world-space coordinate and + * a transformed screen-space coordinate computed during {@link #transform}. + */ + public final Vertex[] coordinates; + + /** + * Average Z-depth of this shape in screen space after transformation. + * Used by the {@link RenderAggregator} to sort shapes back-to-front + * for correct painter's algorithm rendering. + */ + public double onScreenZ; + + /** + * Creates a shape with the specified number of vertices, each initialized + * to the origin (0, 0, 0). + * + * @param pointsCount the number of vertices in this shape + */ + public AbstractCoordinateShape(final int pointsCount) { + coordinates = new Vertex[pointsCount]; + for (int i = 0; i < pointsCount; i++) + coordinates[i] = new Vertex(); + + shapeId = lastShapeId.getAndIncrement(); + } + + /** + * Creates a shape from the given vertices. + * + * @param vertexes the vertices defining this shape's geometry + */ + public AbstractCoordinateShape(final Vertex... vertexes) { + coordinates = vertexes; + + shapeId = lastShapeId.getAndIncrement(); + } + + /** + * Returns the average Z-depth of this shape in screen space. + * + * @return the average Z-depth value, used for depth sorting + */ + public double getZ() { + return onScreenZ; + } + + /** + * Paints this shape onto the rendering context's pixel buffer. + * + *

This method is called after all shapes have been transformed and sorted + * by depth. Implementations should use the transformed screen-space coordinates + * from {@link Vertex#transformedCoordinate} to draw pixels.

+ * + * @param renderBuffer the rendering context containing the pixel buffer and graphics context + */ + public abstract void paint(RenderingContext renderBuffer); + + /** + * {@inheritDoc} + * + *

Transforms all vertices to screen space by applying the current transform stack. + * Computes the average Z-depth and, if all vertices are visible (in front of the camera), + * queues this shape for rendering.

+ */ + @Override + public void transform(final TransformStack transforms, + final RenderAggregator aggregator, + final RenderingContext renderingContext) { + + double accumulatedZ = 0; + boolean paint = true; + + for (final Vertex geometryPoint : coordinates) { + geometryPoint.calculateLocationRelativeToViewer(transforms, renderingContext); + + accumulatedZ += geometryPoint.transformedCoordinate.z; + + if (!geometryPoint.transformedCoordinate.isVisible()) + paint = false; + } + + if (paint) { + onScreenZ = accumulatedZ / coordinates.length; + aggregator.queueShapeForRendering(this); + } + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractShape.java new file mode 100644 index 0000000..f0d37b1 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractShape.java @@ -0,0 +1,81 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes; + +import eu.svjatoslav.sixth.e3d.gui.RenderingContext; +import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.sixth.e3d.math.TransformStack; +import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator; + +/** + * Base class for all renderable shapes in the Sixth 3D engine. + * + *

Every shape that can be rendered must extend this class and implement the + * {@link #transform(TransformStack, RenderAggregator, RenderingContext)} method, + * which projects the shape from world space into screen space during each render frame.

+ * + *

Shapes can optionally have a {@link MouseInteractionController} attached to receive + * mouse click and hover events when the user interacts with the shape in the 3D view.

+ * + *

Shape hierarchy overview:

+ *
+ * AbstractShape
+ *   +-- AbstractCoordinateShape   (shapes with vertex coordinates: lines, polygons)
+ *   +-- AbstractCompositeShape    (groups of sub-shapes: boxes, grids, text canvases)
+ * 
+ * + * @see AbstractCoordinateShape for shapes defined by vertex coordinates + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape for compound shapes + * @see MouseInteractionController for handling mouse events on shapes + */ +public abstract class AbstractShape { + + /** + * Optional controller that receives mouse interaction events (click, enter, exit) + * when the user interacts with this shape in the 3D view. + * Set to {@code null} if mouse interaction is not needed. + */ + public MouseInteractionController mouseInteractionController; + + /** + * Assigns a mouse interaction controller to this shape. + * + *

Example usage:

+ *
{@code
+     * shape.setMouseInteractionController(new MouseInteractionController() {
+     *     public boolean mouseClicked(int button) {
+     *         System.out.println("Shape clicked!");
+     *         return true;
+     *     }
+     *     public boolean mouseEntered() { return false; }
+     *     public boolean mouseExited() { return false; }
+     * });
+     * }
+ * + * @param mouseInteractionController the controller to handle mouse events, + * or {@code null} to disable mouse interaction + */ + public void setMouseInteractionController( + final MouseInteractionController mouseInteractionController) { + this.mouseInteractionController = mouseInteractionController; + } + + /** + * Transforms this shape from world space to screen space and queues it for rendering. + * + *

This method is called once per frame for each shape in the scene. Implementations + * should apply the current transform stack to their vertices, compute screen-space + * coordinates, and if the shape is visible, add it to the {@link RenderAggregator} + * for depth-sorted painting.

+ * + * @param transforms the current stack of transforms (world-to-camera transformations) + * @param aggregator collects transformed shapes for depth-sorted rendering + * @param renderingContext provides frame dimensions, graphics context, and frame metadata + */ + public abstract void transform(final TransformStack transforms, + final RenderAggregator aggregator, + final RenderingContext renderingContext); + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java new file mode 100644 index 0000000..8ceefc2 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java @@ -0,0 +1,141 @@ +/* + * 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; + +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.shapes.AbstractCoordinateShape; +import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; +import eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureBitmap; + +/** + * Base class for textures always facing the viewer. + *

+ * This class implements the "billboard" rendering technique where the texture + * remains oriented towards the camera regardless of 3D position. The visible size + * is calculated based on distance from viewer (z-coordinate) and scale factor. + *

+ * The texture mapping algorithm: + * 1. Calculates screen coverage based on perspective + * 2. Clips to viewport boundaries + * 3. Maps texture pixels to screen pixels using proportional scaling + */ +public class Billboard extends AbstractCoordinateShape { + + private static final double SCALE_MULTIPLIER = 0.005; + public final Texture texture; + + /** + * Scale of the texture object. + *

+ * Object rendered visible size on the screen depends on underlying texture size and scale. + *

+ * 0 means that object will be infinitely small. + * 1 in recommended value to maintain sharpness of the texture as seen by the viewer. + */ + private double scale; + + public Billboard(final Point3D point, final double scale, + final Texture texture) { + super(new Vertex(point)); + this.texture = texture; + setScale(scale); + } + + /** + * Paint the texture on the screen (targetRenderingArea) + * + * @param targetRenderingArea the screen to paint on + */ + @Override + public void paint(final RenderingContext targetRenderingArea) { + + // distance from camera/viewer to center of the texture + final double z = coordinates[0].transformedCoordinate.z; + + // compute forward oriented texture visible distance from center + final double visibleHorizontalDistanceFromCenter = (targetRenderingArea.width + * scale * texture.primaryBitmap.width) / z; + + final double visibleVerticalDistanceFromCenter = (targetRenderingArea.width + * scale * texture.primaryBitmap.height) / z; + + // compute visible pixel density, and get appropriate bitmap + final double zoom = (visibleHorizontalDistanceFromCenter * 2) + / texture.primaryBitmap.width; + + final TextureBitmap textureBitmap = texture.getZoomedBitmap(zoom); + + final Point2D onScreenCoordinate = coordinates[0].onScreenCoordinate; + + // compute Y + final int onScreenUncappedYStart = (int) (onScreenCoordinate.y - visibleVerticalDistanceFromCenter); + final int onScreenUncappedYEnd = (int) (onScreenCoordinate.y + visibleVerticalDistanceFromCenter); + final int onScreenUncappedHeight = onScreenUncappedYEnd - onScreenUncappedYStart; + + int onScreenCappedYStart = onScreenUncappedYStart; + int onScreenCappedYEnd = onScreenUncappedYEnd; + + // cap Y to upper screen border + if (onScreenCappedYStart < 0) + onScreenCappedYStart = 0; + + // cap Y to lower screen border + if (onScreenCappedYEnd > targetRenderingArea.height) + onScreenCappedYEnd = targetRenderingArea.height; + + // compute X + final int onScreenUncappedXStart = (int) (onScreenCoordinate.x - visibleHorizontalDistanceFromCenter); + final int onScreenUncappedXEnd = (int) (onScreenCoordinate.x + visibleHorizontalDistanceFromCenter); + final int onScreenUncappedWidth = onScreenUncappedXEnd - onScreenUncappedXStart; + + // cap X to left screen border + int onScreenCappedXStart = onScreenUncappedXStart; + if (onScreenCappedXStart < 0) + onScreenCappedXStart = 0; + + // cap X to right screen border + int onScreenCappedXEnd = onScreenUncappedXEnd; + if (onScreenCappedXEnd > targetRenderingArea.width) + onScreenCappedXEnd = targetRenderingArea.width; + + final byte[] targetRenderingAreaBytes = targetRenderingArea.pixels; + + final int textureWidth = textureBitmap.width; + + for (int y = onScreenCappedYStart; y < onScreenCappedYEnd; y++) { + + final int sourceBitmapScanlinePixel = ((textureBitmap.height * (y - onScreenUncappedYStart)) / onScreenUncappedHeight) + * textureWidth; + + int targetRenderingAreaOffset = ((y * targetRenderingArea.width) + onScreenCappedXStart) * 4; + + for (int x = onScreenCappedXStart; x < onScreenCappedXEnd; x++) { + + final int sourceBitmapPixelAddress = (sourceBitmapScanlinePixel + ((textureWidth * (x - onScreenUncappedXStart)) / onScreenUncappedWidth)) * 4; + + textureBitmap.drawPixel(sourceBitmapPixelAddress, targetRenderingAreaBytes, targetRenderingAreaOffset); + + targetRenderingAreaOffset += 4; + } + } + } + + /** + * Set the scale of the texture + * + * @param scale the scale of the texture + */ + public void setScale(final double scale) { + this.scale = scale * SCALE_MULTIPLIER; + } + + public Point3D getLocation() { + return coordinates[0].coordinate; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/GlowingPoint.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/GlowingPoint.java new file mode 100644 index 0000000..f770b73 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/GlowingPoint.java @@ -0,0 +1,99 @@ +/* + * 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; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +import static java.lang.Math.pow; +import static java.lang.Math.sqrt; + +/** + * A glowing 3D point rendered with a circular gradient texture. + *

+ * This class creates and reuses textures for glowing points of the same color. + * The texture is a circle with an alpha gradient from center to edge, ensuring + * a consistent visual appearance regardless of viewing angle. + *

+ * The static set of glowing points enables texture sharing and garbage + * collection of unused textures via WeakHashMap. + */ +public class GlowingPoint extends Billboard { + + private static final int TEXTURE_RESOLUTION_PIXELS = 100; + /** + * A set of all existing glowing points. + * Used to reuse textures of glowing points of the same color. + */ + private static final Set glowingPoints = Collections.newSetFromMap(new WeakHashMap<>()); + private final Color color; + + public GlowingPoint(final Point3D point, final double pointSize, + final Color color) { + super(point, computeScale(pointSize), getTexture(color)); + this.color = color; + + synchronized (glowingPoints) { + glowingPoints.add(this); + } + } + + + private static double computeScale(double pointSize) { + return pointSize / ((double) (TEXTURE_RESOLUTION_PIXELS / 50f)); + } + + /** + * Returns a texture for a glowing point of the given color. + * The texture is a circle with a gradient from transparent to the given color. + */ + private static Texture getTexture(final Color color) { + // attempt to reuse texture from existing glowing point of the same color + synchronized (glowingPoints) { + for (GlowingPoint glowingPoint : glowingPoints) + if (color.equals(glowingPoint.color)) + return glowingPoint.texture; + } + + // existing texture not found, creating new one + return createTexture(color); + } + + /** + * Creates a texture for a glowing point of the given color. + * The texture is a circle with a gradient from transparent to the given color. + */ + private static Texture createTexture(final Color color) { + final Texture texture = new Texture(TEXTURE_RESOLUTION_PIXELS, TEXTURE_RESOLUTION_PIXELS, 1); + int halfResolution = TEXTURE_RESOLUTION_PIXELS / 2; + + for (int x = 0; x < TEXTURE_RESOLUTION_PIXELS; x++) + for (int y = 0; y < TEXTURE_RESOLUTION_PIXELS; y++) { + int address = texture.primaryBitmap.getAddress(x, y); + + final int distanceFromCenter = (int) sqrt(pow(halfResolution - x, 2) + pow(halfResolution - y, 2)); + + int alpha = 255 - ((270 * distanceFromCenter) / halfResolution); + if (alpha < 0) + alpha = 0; + + texture.primaryBitmap.bytes[address] = (byte) alpha; + address++; + texture.primaryBitmap.bytes[address] = (byte) color.b; + address++; + texture.primaryBitmap.bytes[address] = (byte) color.g; + address++; + texture.primaryBitmap.bytes[address] = (byte) color.r; + } + + return texture; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java new file mode 100644 index 0000000..9a71bb0 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java @@ -0,0 +1,342 @@ +/* + * 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.line; + +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.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape; + + +/** + * A 3D line segment with perspective-correct width and alpha blending. + *

+ * This class represents a line between two 3D points, rendered with a specified + * width that adjusts based on perspective (distance from the viewer). + * The line is drawn using interpolators to handle edge cases and alpha blending for + * transparency effects. + *

+ * The rendering algorithm: + * 1. For thin lines (below a threshold), draws single-pixel lines with alpha + * adjustment based on perspective. + * 2. For thicker lines, creates four interpolators to define the line's + * rectangular area and fills it scanline by scanline. + *

+ * Note: The width is scaled by the LINE_WIDTH_MULTIPLIER and adjusted based on + * the distance from the viewer (z-coordinate) to maintain a consistent visual size. + */ +public class Line extends AbstractCoordinateShape { + + private static final double MINIMUM_WIDTH_THRESHOLD = 1; + + private static final double LINE_WIDTH_MULTIPLIER = 0.2d; + + /** + * width of the line. + */ + public final double width; + final LineInterpolator[] lineInterpolators = new LineInterpolator[4]; + + /** + * Color of the line. + */ + public Color color; + + public Line(final Line parentLine) { + this(parentLine.coordinates[0].coordinate.clone(), + parentLine.coordinates[1].coordinate.clone(), + new Color(parentLine.color), parentLine.width); + } + + public Line(final Point3D point1, final Point3D point2, final Color color, + final double width) { + + super( + new Vertex(point1), + new Vertex(point2) + ); + + this.color = color; + this.width = width; + + for (int i = 0; i < lineInterpolators.length; i++) + lineInterpolators[i] = new LineInterpolator(); + + } + + private void drawHorizontalLine(final LineInterpolator line1, + final LineInterpolator line2, final int y, + final RenderingContext renderBuffer) { + + int x1 = line1.getX(y); + int x2 = line2.getX(y); + + double d1 = line1.getD(); + double d2 = line2.getD(); + + if (x1 > x2) { + final int tmp = x1; + x1 = x2; + x2 = tmp; + + final double tmp2 = d1; + d1 = d2; + d2 = tmp2; + } + + final int unclippedWidth = x2 - x1; + final double dinc = (d2 - d1) / unclippedWidth; + + if (x1 < 0) { + d1 += (dinc * (-x1)); + x1 = 0; + } + + if (x2 >= renderBuffer.width) + x2 = renderBuffer.width - 1; + + final int drawnWidth = x2 - x1; + + int offset = ((y * renderBuffer.width) + x1) * 4; + final byte[] offSreenBufferBytes = renderBuffer.pixels; + + final int lineAlpha = color.a; + + final int colorB = color.b; + final int colorG = color.g; + final int colorR = color.r; + + for (int i = 0; i < drawnWidth; i++) { + + final double alphaMultiplier = 1d - Math.abs(d1); + + final int realLineAlpha = (int) (lineAlpha * alphaMultiplier); + final int backgroundAlpha = 255 - realLineAlpha; + + offSreenBufferBytes[offset] = (byte) 255; + offset++; + offSreenBufferBytes[offset] = (byte) ((((offSreenBufferBytes[offset] & 0xff) * backgroundAlpha) + (colorB * realLineAlpha)) / 256); + offset++; + offSreenBufferBytes[offset] = (byte) ((((offSreenBufferBytes[offset] & 0xff) * backgroundAlpha) + (colorG * realLineAlpha)) / 256); + offset++; + offSreenBufferBytes[offset] = (byte) ((((offSreenBufferBytes[offset] & 0xff) * backgroundAlpha) + (colorR * realLineAlpha)) / 256); + offset++; + + d1 += dinc; + } + + } + + private void drawSinglePixelHorizontalLine(final RenderingContext buffer, + final int alpha) { + + final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate; + final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate; + + int xStart = (int) onScreenPoint1.x; + int xEnd = (int) onScreenPoint2.x; + + int lineHeight; + int yBase; + + if (xStart > xEnd) { + final int tmp = xStart; + xStart = xEnd; + xEnd = tmp; + lineHeight = (int) (onScreenPoint1.y - onScreenPoint2.y); + yBase = (int) onScreenPoint2.y; + } else { + yBase = (int) onScreenPoint1.y; + lineHeight = (int) (onScreenPoint2.y - onScreenPoint1.y); + } + + final int lineWidth = xEnd - xStart; + if (lineWidth == 0) + return; + + final byte[] offSreenBufferBytes = buffer.pixels; + final int backgroundAlpha = 255 - alpha; + + final int blueWithAlpha = color.b * alpha; + final int greenWithAplha = color.g * alpha; + final int redWithAlpha = color.r * alpha; + + for (int relativeX = 0; relativeX <= lineWidth; relativeX++) { + final int x = xStart + relativeX; + + if ((x >= 0) && (x < buffer.width)) { + + final int y = yBase + ((relativeX * lineHeight) / lineWidth); + if ((y >= 0) && (y < buffer.height)) { + int ramOffset = ((y * buffer.width) + x) * 4; + + offSreenBufferBytes[ramOffset] = (byte) 255; + ramOffset++; + offSreenBufferBytes[ramOffset] = (byte) ((((offSreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + blueWithAlpha) / 256); + ramOffset++; + offSreenBufferBytes[ramOffset] = (byte) ((((offSreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + greenWithAplha) / 256); + ramOffset++; + offSreenBufferBytes[ramOffset] = (byte) ((((offSreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + redWithAlpha) / 256); + } + } + } + + } + + private void drawSinglePixelVerticalLine(final RenderingContext buffer, + final int alpha) { + + final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate; + final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate; + + int yStart = (int) onScreenPoint1.y; + int yEnd = (int) onScreenPoint2.y; + + int lineWidth; + int xBase; + + if (yStart > yEnd) { + final int tmp = yStart; + yStart = yEnd; + yEnd = tmp; + lineWidth = (int) (onScreenPoint1.x - onScreenPoint2.x); + xBase = (int) onScreenPoint2.x; + } else { + xBase = (int) onScreenPoint1.x; + lineWidth = (int) (onScreenPoint2.x - onScreenPoint1.x); + } + + final int lineHeight = yEnd - yStart; + if (lineHeight == 0) + return; + + final byte[] offScreenBufferBytes = buffer.pixels; + final int backgroundAlpha = 255 - alpha; + + final int blueWithAlpha = color.b * alpha; + final int greenWithAlpha = color.g * alpha; + final int redWithAlpha = color.r * alpha; + + for (int relativeY = 0; relativeY <= lineHeight; relativeY++) { + final int y = yStart + relativeY; + + if ((y >= 0) && (y < buffer.height)) { + + final int x = xBase + ((relativeY * lineWidth) / lineHeight); + if ((x >= 0) && (x < buffer.width)) { + int ramOffset = ((y * buffer.width) + x) * 4; + + offScreenBufferBytes[ramOffset] = (byte) 255; + ramOffset++; + offScreenBufferBytes[ramOffset] = (byte) ((((offScreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + blueWithAlpha) / 256); + ramOffset++; + offScreenBufferBytes[ramOffset] = (byte) ((((offScreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + greenWithAlpha) / 256); + ramOffset++; + offScreenBufferBytes[ramOffset] = (byte) ((((offScreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + redWithAlpha) / 256); + } + } + } + } + + private int getLineInterpolator(final int startPointer, final int y) { + + for (int i = startPointer; i < lineInterpolators.length; i++) + if (lineInterpolators[i].containsY(y)) + return i; + return -1; + } + + @Override + public void paint(final RenderingContext buffer) { + + final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate; + final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate; + + final double xp = onScreenPoint2.x - onScreenPoint1.x; + final double yp = onScreenPoint2.y - onScreenPoint1.y; + + final double point1radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width) + / coordinates[0].transformedCoordinate.z; + final double point2radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width) + / coordinates[1].transformedCoordinate.z; + + if ((point1radius < MINIMUM_WIDTH_THRESHOLD) + || (point2radius < MINIMUM_WIDTH_THRESHOLD)) { + + double averageRadius = (point1radius + point2radius) / 2; + + if (averageRadius > 1) + averageRadius = 1; + + final int alpha = (int) (color.a * averageRadius); + if (alpha < 2) + return; + + if (Math.abs(xp) > Math.abs(yp)) + drawSinglePixelHorizontalLine(buffer, alpha); + else + drawSinglePixelVerticalLine(buffer, alpha); + return; + } + + final double lineLength = Math.sqrt((xp * xp) + (yp * yp)); + + final double yinc1 = (point1radius * xp) / lineLength; + final double yinc2 = (point2radius * xp) / lineLength; + + final double xdec1 = (point1radius * yp) / lineLength; + final double xdec2 = (point2radius * yp) / lineLength; + + final double p1x1 = onScreenPoint1.x - xdec1; + final double p1y1 = onScreenPoint1.y + yinc1; + + final double p1x2 = onScreenPoint1.x + xdec1; + final double p1y2 = onScreenPoint1.y - yinc1; + + final double p2x1 = onScreenPoint2.x - xdec2; + final double p2y1 = onScreenPoint2.y + yinc2; + + final double p2x2 = onScreenPoint2.x + xdec2; + final double p2y2 = onScreenPoint2.y - yinc2; + + lineInterpolators[0].setPoints(p1x1, p1y1, 1d, p2x1, p2y1, 1d); + lineInterpolators[1].setPoints(p1x2, p1y2, -1d, p2x2, p2y2, -1d); + + lineInterpolators[2].setPoints(p1x1, p1y1, 1d, p1x2, p1y2, -1d); + lineInterpolators[3].setPoints(p2x1, p2y1, 1d, p2x2, p2y2, -1d); + + double ymin = p1y1; + if (p1y2 < ymin) + ymin = p1y2; + if (p2y1 < ymin) + ymin = p2y1; + if (p2y2 < ymin) + ymin = p2y2; + if (ymin < 0) + ymin = 0; + + double ymax = p1y1; + if (p1y2 > ymax) + ymax = p1y2; + if (p2y1 > ymax) + ymax = p2y1; + if (p2y2 > ymax) + ymax = p2y2; + if (ymax >= buffer.height) + ymax = buffer.height - 1; + + for (int y = (int) ymin; y <= ymax; y++) { + final int li1 = getLineInterpolator(0, y); + if (li1 != -1) { + final int li2 = getLineInterpolator(li1 + 1, y); + if (li2 != -1) + drawHorizontalLine(lineInterpolators[li1], lineInterpolators[li2], y, buffer); + } + } + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineAppearance.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineAppearance.java new file mode 100644 index 0000000..c27aad6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineAppearance.java @@ -0,0 +1,50 @@ +/* + * 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.line; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; + + +/** + * Factory for creating Line objects with consistent appearance settings. + *

+ * This class encapsulates common line styling parameters (width and color) to + * avoid redundant configuration. It provides multiple constructors for + * flexibility and ensures default values are used when not specified. + */ +public class LineAppearance { + + private final double lineWidth; + + private Color color = new Color(100, 100, 255, 255); + + public LineAppearance() { + lineWidth = 1; + } + + public LineAppearance(final double lineWidth) { + this.lineWidth = lineWidth; + } + + public LineAppearance(final double lineWidth, final Color color) { + this.lineWidth = lineWidth; + this.color = color; + } + + public Line getLine(final Point3D point1, final Point3D point2) { + return new Line(point1, point2, color, lineWidth); + } + + public Line getLine(final Point3D point1, final Point3D point2, + final Color color) { + return new Line(point1, point2, color, lineWidth); + } + + public double getLineWidth() { + return lineWidth; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineInterpolator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineInterpolator.java new file mode 100644 index 0000000..57f8dee --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineInterpolator.java @@ -0,0 +1,68 @@ +/* + * 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.line; + +/** + * Interpolates between two points along a line for scanline rendering. + *

+ * This class calculates screen coordinates and depth values (d) for a given Y + * position. It supports perspective-correct interpolation by tracking the + * distance between points and using it to compute step increments. + *

+ * The comparison logic prioritizes interpolators with greater vertical coverage + * to optimize scanline ordering. + */ +public class LineInterpolator { + + private double x1, y1, d1, x2, y2, d2; + + private double d; + private int height; + private int width; + private double dinc; + + public boolean containsY(final int y) { + + if (y1 < y2) { + if (y >= y1) + return y <= y2; + } else if (y >= y2) + return y <= y1; + + return false; + } + + public double getD() { + return d; + } + + public int getX(final int y) { + if (height == 0) + return (int) (x2 + x1) / 2; + + final int distanceFromY1 = y - (int) y1; + + d = d1 + ((dinc * distanceFromY1) / height); + + return (int) x1 + ((width * distanceFromY1) / height); + } + + public void setPoints(final double x1, final double y1, final double d1, + final double x2, final double y2, final double d2) { + + this.x1 = x1; + this.y1 = y1; + this.d1 = d1; + + this.x2 = x2; + this.y2 = y2; + this.d2 = d2; + + height = (int) y2 - (int) y1; + width = (int) x2 - (int) x1; + + dinc = d2 - d1; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java new file mode 100644 index 0000000..20fb6f4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java @@ -0,0 +1,126 @@ +/* + * 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.solidpolygon; + +import eu.svjatoslav.sixth.e3d.geometry.Point2D; + +/** + * Interpolates the x coordinate along a 2D line edge for scanline-based polygon rasterization. + * + *

{@code LineInterpolator} represents one edge of a polygon in screen space, defined by + * two {@link Point2D} endpoints. Given a scanline y coordinate, it computes the corresponding + * x coordinate via linear interpolation. This is a core building block for the solid polygon + * rasterizer, which fills triangles by sweeping horizontal scanlines and using two + * {@code LineInterpolator} instances to find the left and right x boundaries at each y level.

+ * + *

Instances are {@link Comparable}, sorted by absolute height (tallest first) and then + * by width. This ordering is used during rasterization to select the primary (longest) edge + * of the triangle for the outer scanline loop.

+ * + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon + * @see Point2D + */ +public class LineInterpolator implements Comparable { + + /** + * The first endpoint of this edge. + */ + Point2D p1; + + /** + * The second endpoint of this edge. + */ + Point2D p2; + + /** + * The vertical span (p2.y - p1.y), which may be negative. + */ + private int height; + + /** + * The horizontal span (p2.x - p1.x), which may be negative. + */ + private int width; + + /** + * The absolute value of the vertical span, used for sorting. + */ + private int absoluteHeight; + + @Override + public boolean equals(final Object o) { + if (o == null) return false; + + return o instanceof LineInterpolator && compareTo((LineInterpolator) o) == 0; + } + + @Override + public int compareTo(final LineInterpolator o) { + if (absoluteHeight < o.absoluteHeight) + return 1; + if (absoluteHeight > o.absoluteHeight) + return -1; + + return Integer.compare(o.width, width); + + } + + @Override + public int hashCode() { + int result = width; + result = 31 * result + absoluteHeight; + return result; + } + + /** + * Tests whether the given y coordinate falls within the vertical span of this edge. + * + * @param y the scanline y coordinate to test + * @return {@code true} if {@code y} is between the y coordinates of the two endpoints (inclusive) + */ + public boolean containsY(final int y) { + + if (p1.y <= p2.y) { + if (y >= p1.y) + return y <= p2.y; + } else if (y >= p2.y) + return y <= p1.y; + + return false; + } + + /** + * Computes the interpolated x coordinate for the given scanline y value. + * + *

If the edge is horizontal (height is zero), returns the average of the + * two endpoint x coordinates.

+ * + * @param y the scanline y coordinate + * @return the interpolated x coordinate on this edge at the given y + */ + public int getX(final int y) { + + if (height == 0) + return (int) (p2.x + p1.x) / 2; + + return (int) (p1.x + ((width * (y - p1.y)) / height)); + } + + /** + * Sets the two endpoints of this edge and precomputes the width, height, and absolute height. + * + * @param p1 the first endpoint + * @param p2 the second endpoint + */ + public void setPoints(final Point2D p1, final Point2D p2) { + this.p1 = p1; + this.p2 = p2; + height = (int) (p2.y - p1.y); + width = (int) (p2.x - p1.x); + + absoluteHeight = Math.abs(height); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java new file mode 100644 index 0000000..eb53029 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java @@ -0,0 +1,296 @@ +/* + * 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.solidpolygon; + +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.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.sixth.e3d.math.Vertex; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape; + +import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon; + +/** + * A solid-color triangle renderer with mouse interaction support. + *

+ * This class implements a high-performance triangle rasterizer using scanline + * algorithms. It handles: + * - Perspective-correct edge interpolation + * - Alpha blending with background pixels + * - Viewport clipping + * - Mouse hover detection via point-in-polygon tests + * - Optional flat shading based on light sources + *

+ * The static drawPolygon method is designed for reuse by other polygon types. + */ +public class SolidPolygon extends AbstractCoordinateShape { + + private final static LineInterpolator polygonBoundary1 = new LineInterpolator(); + private final static LineInterpolator polygonBoundary2 = new LineInterpolator(); + private final static LineInterpolator polygonBoundary3 = new LineInterpolator(); + private final Point3D cachedNormal = new Point3D(); + private final Point3D cachedCenter = new Point3D(); + private Color color; + private boolean shadingEnabled = false; + private LightingManager lightingManager; + private boolean backfaceCulling = false; + + public SolidPolygon(final Point3D point1, final Point3D point2, + final Point3D point3, final Color color) { + super( + new Vertex(point1), + new Vertex(point2), + new Vertex(point3) + ); + this.color = color; + } + + public static void drawHorizontalLine(final LineInterpolator line1, + final LineInterpolator line2, final int y, + final RenderingContext renderBuffer, final Color color) { + + int x1 = line1.getX(y); + int x2 = line2.getX(y); + + if (x1 > x2) { + final int tmp = x1; + x1 = x2; + x2 = tmp; + } + + if (x1 < 0) + x1 = 0; + + if (x2 >= renderBuffer.width) + x2 = renderBuffer.width - 1; + + final int width = x2 - x1; + + int offset = ((y * renderBuffer.width) + x1) * 4; + final byte[] offScreenBufferBytes = renderBuffer.pixels; + + final int polygonAlpha = color.a; + final int b = color.b; + final int g = color.g; + final int r = color.r; + + if (polygonAlpha == 255) + for (int i = 0; i < width; i++) { + offScreenBufferBytes[offset] = (byte) 255; + offset++; + offScreenBufferBytes[offset] = (byte) b; + offset++; + offScreenBufferBytes[offset] = (byte) g; + offset++; + offScreenBufferBytes[offset] = (byte) r; + offset++; + } + else { + final int backgroundAlpha = 255 - polygonAlpha; + + final int blueWithAlpha = b * polygonAlpha; + final int greenWithAlpha = g * polygonAlpha; + final int redWithAlpha = r * polygonAlpha; + + for (int i = 0; i < width; i++) { + offScreenBufferBytes[offset] = (byte) 255; + offset++; + offScreenBufferBytes[offset] = (byte) ((((offScreenBufferBytes[offset] & 0xff) * backgroundAlpha) + blueWithAlpha) / 256); + offset++; + offScreenBufferBytes[offset] = (byte) ((((offScreenBufferBytes[offset] & 0xff) * backgroundAlpha) + greenWithAlpha) / 256); + offset++; + offScreenBufferBytes[offset] = (byte) ((((offScreenBufferBytes[offset] & 0xff) * backgroundAlpha) + redWithAlpha) / 256); + offset++; + } + + } + + } + + public static void drawPolygon(final RenderingContext context, + final Point2D onScreenPoint1, final Point2D onScreenPoint2, + final Point2D onScreenPoint3, + final MouseInteractionController mouseInteractionController, + final Color color) { + + onScreenPoint1.roundToInteger(); + onScreenPoint2.roundToInteger(); + onScreenPoint3.roundToInteger(); + + if (mouseInteractionController != null) + if (context.getMouseEvent() != null) + if (pointWithinPolygon(context.getMouseEvent().coordinate, + onScreenPoint1, onScreenPoint2, onScreenPoint3)) + context.setCurrentObjectUnderMouseCursor(mouseInteractionController); + + if (color.isTransparent()) + return; + + // find top-most point + int yTop = (int) onScreenPoint1.y; + + if (onScreenPoint2.y < yTop) + yTop = (int) onScreenPoint2.y; + + if (onScreenPoint3.y < yTop) + yTop = (int) onScreenPoint3.y; + + if (yTop < 0) + yTop = 0; + + // find bottom-most point + int yBottom = (int) onScreenPoint1.y; + + if (onScreenPoint2.y > yBottom) + yBottom = (int) onScreenPoint2.y; + + if (onScreenPoint3.y > yBottom) + yBottom = (int) onScreenPoint3.y; + + if (yBottom >= context.height) + yBottom = context.height - 1; + + // paint + polygonBoundary1.setPoints(onScreenPoint1, onScreenPoint2); + polygonBoundary2.setPoints(onScreenPoint1, onScreenPoint3); + polygonBoundary3.setPoints(onScreenPoint2, onScreenPoint3); + + // Inline sort for 3 elements to avoid array allocation + LineInterpolator a = polygonBoundary1; + LineInterpolator b = polygonBoundary2; + LineInterpolator c = polygonBoundary3; + LineInterpolator t; + if (a.compareTo(b) > 0) { t = a; a = b; b = t; } + if (b.compareTo(c) > 0) { t = b; b = c; c = t; } + if (a.compareTo(b) > 0) { t = a; a = b; b = t; } + + for (int y = yTop; y < yBottom; y++) + if (a.containsY(y)) { + if (b.containsY(y)) + drawHorizontalLine(a, b, y, context, color); + else if (c.containsY(y)) + drawHorizontalLine(a, c, y, context, color); + } else if (b.containsY(y)) + if (c.containsY(y)) + drawHorizontalLine(b, c, y, context, color); + } + + public Color getColor() { + return color; + } + + public void setColor(final Color color) { + this.color = color; + } + + /** + * Returns the lighting manager used for shading calculations. + * + * @return the lighting manager, or null if shading is not enabled + */ + public LightingManager getLightingManager() { + return lightingManager; + } + + /** + * Checks if shading is enabled for this polygon. + * + * @return true if shading is enabled, false otherwise + */ + public boolean isShadingEnabled() { + return shadingEnabled; + } + + /** + * Enables or disables shading for this polygon. + * + * @param shadingEnabled true to enable shading, false to disable + * @param lightingManager the lighting manager to use for shading calculations + */ + public void setShadingEnabled(final boolean shadingEnabled, final LightingManager lightingManager) { + this.shadingEnabled = shadingEnabled; + this.lightingManager = lightingManager; + } + + public boolean isBackfaceCullingEnabled() { + return backfaceCulling; + } + + public void setBackfaceCulling(final boolean backfaceCulling) { + this.backfaceCulling = backfaceCulling; + } + + private void calculateNormal(final Point3D result) { + final Point3D v1 = coordinates[0].coordinate; + final Point3D v2 = coordinates[1].coordinate; + final Point3D v3 = coordinates[2].coordinate; + + final double ax = v2.x - v1.x; + final double ay = v2.y - v1.y; + final double az = v2.z - v1.z; + + final double bx = v3.x - v1.x; + final double by = v3.y - v1.y; + final double bz = v3.z - v1.z; + + double nx = ay * bz - az * by; + double ny = az * bx - ax * bz; + double nz = ax * by - ay * bx; + + final double length = Math.sqrt(nx * nx + ny * ny + nz * nz); + if (length > 0.0001) { + nx /= length; + ny /= length; + nz /= length; + } + + result.x = nx; + result.y = ny; + result.z = nz; + } + + private void calculateCenter(final Point3D result) { + final Point3D v1 = coordinates[0].coordinate; + final Point3D v2 = coordinates[1].coordinate; + final Point3D v3 = coordinates[2].coordinate; + + result.x = (v1.x + v2.x + v3.x) / 3.0; + result.y = (v1.y + v2.y + v3.y) / 3.0; + result.z = (v1.z + v2.z + v3.z) / 3.0; + } + + @Override + public void paint(final RenderingContext renderBuffer) { + + final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate; + final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate; + final Point2D onScreenPoint3 = coordinates[2].onScreenCoordinate; + + if (backfaceCulling) { + final double signedArea = (onScreenPoint2.x - onScreenPoint1.x) + * (onScreenPoint3.y - onScreenPoint1.y) + - (onScreenPoint3.x - onScreenPoint1.x) + * (onScreenPoint2.y - onScreenPoint1.y); + if (signedArea >= 0) + return; + } + + Color paintColor = color; + + if (shadingEnabled && lightingManager != null) { + calculateCenter(cachedCenter); + calculateNormal(cachedNormal); + paintColor = lightingManager.calculateLighting(cachedCenter, cachedNormal, color); + } + + drawPolygon(renderBuffer, onScreenPoint1, onScreenPoint2, + onScreenPoint3, mouseInteractionController, paintColor); + + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java new file mode 100644 index 0000000..5b6c198 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java @@ -0,0 +1,124 @@ +/* + * 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 static java.lang.Math.abs; + +/** + * Interpolator for textured polygon edges with perspective correction. + *

+ * This class maps screen coordinates to texture coordinates while maintaining + * perspective accuracy. + * It's used to create texture-mapped scanlines that adjust for depth (z) to + * prevent texture distortion. + *

+ * The comparison logic ensures proper scanline ordering based on vertical + * coverage and horizontal span. + */ +public class PolygonBorderInterpolator implements + Comparable { + + // on-screen coordinates + Point2D onScreenPoint1; + Point2D onScreenPoint2; + + double distanceFromY1; + private int onScreenHeight; + private int onScreenWidth; + private int onscreenAbsoluteHeight; + private double textureWidth; + private double textureHeight; + // texture coordinates + private Point2D texturePoint1; + private Point2D texturePoint2; + + + @Override + public boolean equals(final Object o) { + if (o == null) return false; + + return o instanceof PolygonBorderInterpolator && compareTo((PolygonBorderInterpolator) o) == 0; + } + + @Override + public int hashCode() { + int result = onScreenWidth; + result = 31 * result + onscreenAbsoluteHeight; + return result; + } + + @Override + public int compareTo(final PolygonBorderInterpolator otherInterpolator) { + if (onscreenAbsoluteHeight < otherInterpolator.onscreenAbsoluteHeight) + return 1; + if (onscreenAbsoluteHeight > otherInterpolator.onscreenAbsoluteHeight) + return -1; + + if (onScreenWidth < otherInterpolator.onScreenWidth) + return 1; + if (onScreenWidth > otherInterpolator.onScreenWidth) + return -1; + + return 0; + } + + public boolean containsY(final int y) { + + if (onScreenPoint1.y < onScreenPoint2.y) { + if (y >= onScreenPoint1.y) + return y <= onScreenPoint2.y; + } else if (y >= onScreenPoint2.y) + return y <= onScreenPoint1.y; + + return false; + } + + public double getTX() { + + if (onScreenHeight == 0) + return (texturePoint2.x + texturePoint1.x) / 2d; + + return texturePoint1.x + ((textureWidth * distanceFromY1) / onScreenHeight); + } + + public double getTY() { + + if (onScreenHeight == 0) + return (texturePoint2.y + texturePoint1.y) / 2d; + + return texturePoint1.y + ((textureHeight * distanceFromY1) / onScreenHeight); + } + + public int getX() { + + if (onScreenHeight == 0) + return (int) ((onScreenPoint2.x + onScreenPoint1.x) / 2d); + + return (int) (onScreenPoint1.x + ((onScreenWidth * distanceFromY1) / onScreenHeight)); + } + + public void setCurrentY(final int y) { + distanceFromY1 = y - onScreenPoint1.y; + } + + public void setPoints(final Point2D onScreenPoint1, final Point2D onScreenPoint2, + final Point2D texturePoint1, final Point2D texturePoint2) { + + this.onScreenPoint1 = onScreenPoint1; + this.onScreenPoint2 = onScreenPoint2; + this.texturePoint1 = texturePoint1; + this.texturePoint2 = texturePoint2; + + onScreenHeight = (int) (onScreenPoint2.y - onScreenPoint1.y); + onScreenWidth = (int) (onScreenPoint2.x - onScreenPoint1.x); + onscreenAbsoluteHeight = abs(onScreenHeight); + + textureWidth = texturePoint2.x - texturePoint1.x; + textureHeight = texturePoint2.y - texturePoint1.y; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java new file mode 100644 index 0000000..45e9b98 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java @@ -0,0 +1,249 @@ +/* + * 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.gui.RenderingContext; +import eu.svjatoslav.sixth.e3d.math.Vertex; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape; +import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; +import eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureBitmap; + +import java.awt.Color; + +import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon; + +/** + * Textured polygon. + *

+ * + *

+ * This is how perspective-correct texture rendering is implemented:
+ * If polygon is sufficiently small, it is rendered without perspective correction.
+ * Otherwise, it is sliced into smaller polygons.
+ * 
+ */ + +public class TexturedPolygon extends AbstractCoordinateShape { + + private static final PolygonBorderInterpolator polygonBorder1 = new PolygonBorderInterpolator(); + private static final PolygonBorderInterpolator polygonBorder2 = new PolygonBorderInterpolator(); + private static final PolygonBorderInterpolator polygonBorder3 = new PolygonBorderInterpolator(); + + public final Texture texture; + + /** + * If true then polygon borders will be drawn. + * It is used for debugging purposes. + */ + public boolean showBorders = false; + private boolean backfaceCulling = false; + + private double totalTextureDistance = -1; + + public TexturedPolygon(Vertex p1, Vertex p2, Vertex p3, final Texture texture) { + + super(p1, p2, p3); + this.texture = texture; + } + + private void computeTotalTextureDistance() { + // compute total texture distance + totalTextureDistance = coordinates[0].textureCoordinate.getDistanceTo(coordinates[1].textureCoordinate); + totalTextureDistance += coordinates[0].textureCoordinate.getDistanceTo(coordinates[2].textureCoordinate); + totalTextureDistance += coordinates[1].textureCoordinate.getDistanceTo(coordinates[2].textureCoordinate); + } + + private void drawHorizontalLine(final PolygonBorderInterpolator line1, + final PolygonBorderInterpolator line2, final int y, + final RenderingContext renderBuffer, + final TextureBitmap textureBitmap) { + + line1.setCurrentY(y); + line2.setCurrentY(y); + + int x1 = line1.getX(); + int x2 = line2.getX(); + + final double tx2, ty2; + final double tx1, ty1; + + if (x1 <= x2) { + + tx1 = line1.getTX() * textureBitmap.multiplicationFactor; + ty1 = line1.getTY() * textureBitmap.multiplicationFactor; + + tx2 = line2.getTX() * textureBitmap.multiplicationFactor; + ty2 = line2.getTY() * textureBitmap.multiplicationFactor; + + } else { + final int tmp = x1; + x1 = x2; + x2 = tmp; + + tx1 = line2.getTX() * textureBitmap.multiplicationFactor; + ty1 = line2.getTY() * textureBitmap.multiplicationFactor; + + tx2 = line1.getTX() * textureBitmap.multiplicationFactor; + ty2 = line1.getTY() * textureBitmap.multiplicationFactor; + } + + final double realWidth = x2 - x1; + final double realX1 = x1; + + if (x1 < 0) + x1 = 0; + + if (x2 >= renderBuffer.width) + x2 = renderBuffer.width - 1; + + int renderBufferOffset = ((y * renderBuffer.width) + x1) * 4; + final byte[] renderBufferBytes = renderBuffer.pixels; + + final double twidth = tx2 - tx1; + final double theight = ty2 - ty1; + + for (int x = x1; x < x2; x++) { + + final double distance = x - realX1; + + final double tx = tx1 + ((twidth * distance) / realWidth); + final double ty = ty1 + ((theight * distance) / realWidth); + + final int textureOffset = textureBitmap.getAddress((int) tx, + (int) ty); + + textureBitmap.drawPixel(textureOffset, renderBufferBytes, + renderBufferOffset); + + renderBufferOffset += 4; + } + + } + + @Override + public void paint(final RenderingContext renderBuffer) { + + final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate; + final Point2D projectedPoint2 = coordinates[1].onScreenCoordinate; + final Point2D projectedPoint3 = coordinates[2].onScreenCoordinate; + + if (backfaceCulling) { + final double signedArea = (projectedPoint2.x - projectedPoint1.x) + * (projectedPoint3.y - projectedPoint1.y) + - (projectedPoint3.x - projectedPoint1.x) + * (projectedPoint2.y - projectedPoint1.y); + if (signedArea >= 0) + return; + } + + projectedPoint1.roundToInteger(); + projectedPoint2.roundToInteger(); + projectedPoint3.roundToInteger(); + + if (mouseInteractionController != null) + if (renderBuffer.getMouseEvent() != null) + if (pointWithinPolygon( + renderBuffer.getMouseEvent().coordinate, projectedPoint1, + projectedPoint2, projectedPoint3)) + renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController); + + // Show polygon boundaries (for debugging) + if (showBorders) + showBorders(renderBuffer); + + // find top-most point + int yTop = (int) projectedPoint1.y; + + if (projectedPoint2.y < yTop) + yTop = (int) projectedPoint2.y; + + if (projectedPoint3.y < yTop) + yTop = (int) projectedPoint3.y; + + if (yTop < 0) + yTop = 0; + + // find bottom-most point + int yBottom = (int) projectedPoint1.y; + + if (projectedPoint2.y > yBottom) + yBottom = (int) projectedPoint2.y; + + if (projectedPoint3.y > yBottom) + yBottom = (int) projectedPoint3.y; + + if (yBottom >= renderBuffer.height) + yBottom = renderBuffer.height - 1; + + // paint + double totalVisibleDistance = projectedPoint1.getDistanceTo(projectedPoint2); + totalVisibleDistance += projectedPoint1.getDistanceTo(projectedPoint3); + totalVisibleDistance += projectedPoint2.getDistanceTo(projectedPoint3); + + if (totalTextureDistance == -1) + computeTotalTextureDistance(); + final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d; + + final TextureBitmap zoomedBitmap = texture.getZoomedBitmap(scaleFactor); + + polygonBorder1.setPoints(projectedPoint1, projectedPoint2, + coordinates[0].textureCoordinate, + coordinates[1].textureCoordinate); + polygonBorder2.setPoints(projectedPoint1, projectedPoint3, + coordinates[0].textureCoordinate, + coordinates[2].textureCoordinate); + polygonBorder3.setPoints(projectedPoint2, projectedPoint3, + coordinates[1].textureCoordinate, + coordinates[2].textureCoordinate); + + // Inline sort for 3 elements to avoid array allocation + PolygonBorderInterpolator a = polygonBorder1; + PolygonBorderInterpolator b = polygonBorder2; + PolygonBorderInterpolator c = polygonBorder3; + PolygonBorderInterpolator t; + if (a.compareTo(b) > 0) { t = a; a = b; b = t; } + if (b.compareTo(c) > 0) { t = b; b = c; c = t; } + if (a.compareTo(b) > 0) { t = a; a = b; b = t; } + + for (int y = yTop; y < yBottom; y++) + if (a.containsY(y)) { + if (b.containsY(y)) + drawHorizontalLine(a, b, y, renderBuffer, zoomedBitmap); + else if (c.containsY(y)) + drawHorizontalLine(a, c, y, renderBuffer, zoomedBitmap); + } else if (b.containsY(y)) + if (c.containsY(y)) + drawHorizontalLine(b, c, y, renderBuffer, zoomedBitmap); + + } + + public boolean isBackfaceCullingEnabled() { + return backfaceCulling; + } + + public void setBackfaceCulling(final boolean backfaceCulling) { + this.backfaceCulling = backfaceCulling; + } + + private void showBorders(final RenderingContext renderBuffer) { + + final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate; + final Point2D projectedPoint2 = coordinates[1].onScreenCoordinate; + final Point2D projectedPoint3 = coordinates[2].onScreenCoordinate; + + renderBuffer.graphics.setColor(Color.YELLOW); + renderBuffer.graphics.drawLine((int) projectedPoint1.x, + (int) projectedPoint1.y, (int) projectedPoint2.x, + (int) projectedPoint2.y); + renderBuffer.graphics.drawLine((int) projectedPoint3.x, + (int) projectedPoint3.y, (int) projectedPoint2.x, + (int) projectedPoint2.y); + renderBuffer.graphics.drawLine((int) projectedPoint1.x, + (int) projectedPoint1.y, (int) projectedPoint3.x, + (int) projectedPoint3.y); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java new file mode 100644 index 0000000..f394a29 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java @@ -0,0 +1,90 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.Billboard; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas; +import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; + +/** + * A text label rendered as a billboard texture that always faces the camera. + * + *

This shape renders a single line of text onto a {@link Texture} using the font metrics + * defined in {@link TextCanvas} ({@link TextCanvas#FONT}, {@link TextCanvas#FONT_CHAR_WIDTH_TEXTURE_PIXELS}, + * {@link TextCanvas#FONT_CHAR_HEIGHT_TEXTURE_PIXELS}), then displays the texture as a + * forward-oriented billboard via its {@link Billboard} superclass. The result + * is a text label that remains readable from any viewing angle.

+ * + *

Usage example:

+ *
{@code
+ * // Create a red text label at position (0, -50, 300)
+ * ForwardOrientedTextBlock label = new ForwardOrientedTextBlock(
+ *     new Point3D(0, -50, 300),
+ *     1.0,
+ *     2,
+ *     "Hello, World!",
+ *     Color.RED
+ * );
+ * shapeCollection.addShape(label);
+ * }
+ * + * @see Billboard + * @see TextCanvas + * @see Texture + */ +public class ForwardOrientedTextBlock extends Billboard { + + /** + * Creates a new forward-oriented text block at the given 3D position. + * + * @param point the 3D position where the text label is placed + * @param scale the scale factor controlling the rendered size of the text + * @param maxUpscaleFactor the maximum mipmap upscale factor for the backing texture + * @param text the text string to render + * @param textColor the color of the rendered text + */ + public ForwardOrientedTextBlock(final Point3D point, final double scale, + final int maxUpscaleFactor, final String text, + final eu.svjatoslav.sixth.e3d.renderer.raster.Color textColor) { + super(point, scale, getTexture(text, maxUpscaleFactor, textColor)); + + } + + /** + * Creates a {@link Texture} containing the rendered text string. + * + *

The texture dimensions are calculated from the text length and the font metrics + * defined in {@link TextCanvas}. Each character is drawn individually at the appropriate + * horizontal offset using {@link TextCanvas#FONT}.

+ * + * @param text the text string to render into the texture + * @param maxUpscaleFactor the maximum mipmap upscale factor for the texture + * @param textColor the color of the rendered text + * @return a new {@link Texture} containing the rendered text + */ + public static Texture getTexture(final String text, + final int maxUpscaleFactor, + final eu.svjatoslav.sixth.e3d.renderer.raster.Color textColor) { + + final Texture texture = new Texture(text.length() + * TextCanvas.FONT_CHAR_WIDTH_TEXTURE_PIXELS, TextCanvas.FONT_CHAR_HEIGHT_TEXTURE_PIXELS, + maxUpscaleFactor); + + // texture.graphics.setColor(Color.BLUE); + // texture.graphics.fillRect(0, 0, texture.primaryBitmap.width, + // texture.primaryBitmap.width); + + texture.graphics.setFont(TextCanvas.FONT); + texture.graphics.setColor(textColor.toAwtColor()); + + for (int c = 0; c < text.length(); c++) + texture.graphics.drawChars(new char[]{text.charAt(c),}, 0, 1, + (c * TextCanvas.FONT_CHAR_WIDTH_TEXTURE_PIXELS), + (int) (TextCanvas.FONT_CHAR_HEIGHT_TEXTURE_PIXELS / 1.45)); + + return texture; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java new file mode 100644 index 0000000..c7f71d7 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java @@ -0,0 +1,180 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite; + +import eu.svjatoslav.sixth.e3d.geometry.Point2D; +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.math.Transform; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas; + +import java.util.List; + +/** + * A 2D graph visualization rendered in 3D space. + * + *

Plots a series of {@link Point2D} data points as a connected line graph, overlaid on a + * grid with horizontal and vertical grid lines, axis labels, and a title. The graph is + * rendered in the XY plane at the specified 3D location, with all dimensions scaled by + * a configurable scale factor.

+ * + *

The graph uses the following default configuration:

+ *
    + *
  • X-axis range: {@code 0} to {@code 20} (world units before scaling)
  • + *
  • Y-axis range: {@code -2} to {@code 2}
  • + *
  • Grid spacing: {@code 0.5} in both horizontal and vertical directions
  • + *
  • Grid color: semi-transparent blue ({@code rgba(100, 100, 250, 100)})
  • + *
  • Plot color: semi-transparent red ({@code rgba(255, 0, 0, 100)})
  • + *
+ * + *

Usage example:

+ *
{@code
+ * // Prepare data points
+ * List data = new ArrayList<>();
+ * for (double x = 0; x <= 20; x += 0.1) {
+ *     data.add(new Point2D(x, Math.sin(x)));
+ * }
+ *
+ * // Create a graph at position (0, 0, 500) with scale factor 10
+ * Graph graph = new Graph(10.0, data, "sin(x)", new Point3D(0, 0, 500));
+ *
+ * // Add to the scene
+ * shapeCollection.addShape(graph);
+ * }
+ * + * @see Line + * @see TextCanvas + * @see AbstractCompositeShape + */ +public class Graph extends AbstractCompositeShape { + + /** The width of the graph in unscaled world units. */ + private final double width; + /** The minimum Y-axis value. */ + private final double yMin; + /** The maximum Y-axis value. */ + private final double yMax; + /** The spacing between vertical grid lines along the X-axis. */ + private final double horizontalStep; + /** The spacing between horizontal grid lines along the Y-axis. */ + private final double verticalStep; + /** The color used for grid lines. */ + private final Color gridColor; + /** The width of grid lines in world units (after scaling). */ + private final double lineWidth; + /** The color used for the data plot line. */ + private final Color plotColor; + + /** + * Creates a new graph visualization at the specified 3D location. + * + *

The graph is constructed with grid lines, axis labels, plotted data, and a title + * label. All spatial dimensions are multiplied by the given scale factor.

+ * + * @param scale the scale factor applied to all spatial dimensions of the graph + * @param data the list of 2D data points to plot; consecutive points are connected by lines + * @param label the title text displayed above the graph + * @param location the 3D position of the graph's origin in the scene + */ + public Graph(final double scale, final List data, + final String label, final Point3D location) { + super(location); + + width = 20; + + yMin = -2; + yMax = 2; + + horizontalStep = 0.5; + verticalStep = 0.5; + + gridColor = new Color(100, 100, 250, 100); + + lineWidth = 0.1 * scale; + plotColor = new Color(255, 0, 0, 100); + + addVerticalLines(scale); + addXLabels(scale); + addHorizontalLinesAndLabels(scale); + plotData(scale, data); + + final Point3D labelLocation = new Point3D(width / 2, yMax + 0.5, 0) + .scaleUp(scale); + + final TextCanvas labelCanvas = new TextCanvas(new Transform( + labelLocation), label, Color.WHITE, Color.TRANSPARENT); + + addShape(labelCanvas); + } + + private void addHorizontalLinesAndLabels(final double scale) { + for (double y = yMin; y <= yMax; y += verticalStep) { + + final Point3D p1 = new Point3D(0, y, 0).scaleUp(scale); + + final Point3D p2 = new Point3D(width, y, 0).scaleUp(scale); + + final Line line = new Line(p1, p2, gridColor, lineWidth); + + addShape(line); + + final Point3D labelLocation = new Point3D(-0.5, y, 0) + .scaleUp(scale); + + final TextCanvas label = new TextCanvas( + new Transform(labelLocation), String.valueOf(y), + Color.WHITE, Color.TRANSPARENT); + + addShape(label); + + } + } + + private void addVerticalLines(final double scale) { + for (double x = 0; x <= width; x += horizontalStep) { + + final Point3D p1 = new Point3D(x, yMin, 0).scaleUp(scale); + final Point3D p2 = new Point3D(x, yMax, 0).scaleUp(scale); + + final Line line = new Line(p1, p2, gridColor, lineWidth); + + addShape(line); + + } + } + + private void addXLabels(final double scale) { + for (double x = 0; x <= width; x += horizontalStep * 2) { + final Point3D labelLocation = new Point3D(x, yMin - 0.4, 0) + .scaleUp(scale); + + final TextCanvas label = new TextCanvas( + new Transform(labelLocation), String.valueOf(x), + Color.WHITE, Color.TRANSPARENT); + + addShape(label); + } + } + + private void plotData(final double scale, final List data) { + Point3D previousPoint = null; + for (final Point2D point : data) { + + final Point3D p3d = new Point3D(point.x, point.y, 0).scaleUp(scale); + + if (previousPoint != null) { + + final Line line = new Line(previousPoint, p3d, plotColor, + 0.4 * scale); + + addShape(line); + } + + previousPoint = p3d; + } + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/LightSourceMarker.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/LightSourceMarker.java new file mode 100755 index 0000000..a33c52d --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/LightSourceMarker.java @@ -0,0 +1,43 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.GlowingPoint; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A visual marker that indicates a light source position in the 3D scene. + * + *

Rendered as a glowing point that provides a clear, lightweight visual + * indicator useful for debugging light placement in the scene.

+ * + *

Usage example:

+ *
{@code
+ * // Place a yellow light source marker at position (100, -50, 200)
+ * LightSourceMarker marker = new LightSourceMarker(
+ *     new Point3D(100, -50, 200),
+ *     Color.YELLOW
+ * );
+ * shapeCollection.addShape(marker);
+ * }
+ * + * @see GlowingPoint + * @see AbstractCompositeShape + */ +public class LightSourceMarker extends AbstractCompositeShape { + + /** + * Creates a new light source marker at the specified location. + * + * @param location the 3D position of the marker in the scene + * @param color the color of the glowing point + */ + public LightSourceMarker(final Point3D location, final Color color) { + super(location); + addShape(new GlowingPoint(new Point3D(0, 0, 0), 15, color)); + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java new file mode 100644 index 0000000..1ababb2 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java @@ -0,0 +1,185 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite; + +import eu.svjatoslav.sixth.e3d.geometry.Point2D; +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.math.Transform; +import eu.svjatoslav.sixth.e3d.math.Vertex; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; +import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; + +/** + * A rectangular shape with texture mapping, composed of two textured triangles. + * + *

This composite shape creates a textured rectangle in 3D space by splitting it into + * two {@link TexturedPolygon} triangles that share a common {@link Texture}. The rectangle + * is centered at the origin of its local coordinate system, with configurable world-space + * dimensions and independent texture resolution.

+ * + *

The contained {@link Texture} object is accessible via {@link #getTexture()}, allowing + * dynamic rendering to the texture surface (e.g., drawing text, images, or procedural content) + * after construction.

+ * + *

Usage example:

+ *
{@code
+ * // Create a 200x100 textured rectangle at position (0, 0, 300)
+ * Transform transform = new Transform(new Point3D(0, 0, 300));
+ * TexturedRectangle rect = new TexturedRectangle(transform, 200, 100, 2);
+ *
+ * // Draw onto the texture dynamically
+ * Texture tex = rect.getTexture();
+ * tex.graphics.setColor(java.awt.Color.RED);
+ * tex.graphics.fillRect(0, 0, 50, 50);
+ *
+ * // Add to the scene
+ * shapeCollection.addShape(rect);
+ * }
+ * + * @see TexturedPolygon + * @see Texture + * @see AbstractCompositeShape + */ +public class TexturedRectangle extends AbstractCompositeShape { + + /** Top-left corner position in local 3D coordinates. */ + public Point3D topLeft; + /** Top-right corner position in local 3D coordinates. */ + public Point3D topRight; + /** Bottom-right corner position in local 3D coordinates. */ + public Point3D bottomRight; + /** Bottom-left corner position in local 3D coordinates. */ + public Point3D bottomLeft; + /** Top-left corner mapping in texture coordinates (pixels). */ + public Point2D textureTopLeft; + /** Top-right corner mapping in texture coordinates (pixels). */ + public Point2D textureTopRight; + /** Bottom-right corner mapping in texture coordinates (pixels). */ + public Point2D textureBottomRight; + /** Bottom-left corner mapping in texture coordinates (pixels). */ + public Point2D textureBottomLeft; + private Texture texture; + + /** + * Creates a textured rectangle with only a transform, without initializing geometry. + * + *

After construction, call {@link #initialize(double, double, int, int, int)} to + * set up the rectangle's dimensions, texture, and triangle geometry.

+ * + * @param transform the position and orientation of this rectangle in the scene + */ + public TexturedRectangle(final Transform transform) { + super(transform); + } + + /** + * Creates a textured rectangle where the texture resolution matches the world-space size. + * + *

This is a convenience constructor equivalent to calling + * {@link #TexturedRectangle(Transform, int, int, int, int, int)} with + * {@code textureWidth = width} and {@code textureHeight = height}.

+ * + * @param transform the position and orientation of this rectangle in the scene + * @param width the width of the rectangle in world units (also used as texture width in pixels) + * @param height the height of the rectangle in world units (also used as texture height in pixels) + * @param maxTextureUpscale the maximum mipmap upscale factor for the texture + */ + public TexturedRectangle(final Transform transform, final int width, + final int height, final int maxTextureUpscale) { + this(transform, width, height, width, height, maxTextureUpscale); + } + + /** + * Creates a fully initialized textured rectangle with independent world-space size and texture resolution. + * + * @param transform the position and orientation of this rectangle in the scene + * @param width the width of the rectangle in world units + * @param height the height of the rectangle in world units + * @param textureWidth the width of the backing texture in pixels + * @param textureHeight the height of the backing texture in pixels + * @param maxTextureUpscale the maximum mipmap upscale factor for the texture + */ + public TexturedRectangle(final Transform transform, final int width, + final int height, final int textureWidth, final int textureHeight, + final int maxTextureUpscale) { + + super(transform); + + initialize(width, height, textureWidth, textureHeight, + maxTextureUpscale); + } + + /** + * Returns the backing texture for this rectangle. + * + *

The returned {@link Texture} can be used to draw dynamic content onto the + * rectangle's surface via its {@code graphics} field (a {@link java.awt.Graphics2D} instance).

+ * + * @return the texture mapped onto this rectangle + */ + public Texture getTexture() { + return texture; + } + + /** + * Initializes the rectangle geometry, texture, and the two constituent textured triangles. + * + *

The rectangle is centered at the local origin: corners span from + * {@code (-width/2, -height/2, 0)} to {@code (width/2, height/2, 0)}. + * Two {@link TexturedPolygon} triangles are created to cover the full rectangle, + * sharing a single {@link Texture} instance.

+ * + * @param width the width of the rectangle in world units + * @param height the height of the rectangle in world units + * @param textureWidth the width of the backing texture in pixels + * @param textureHeight the height of the backing texture in pixels + * @param maxTextureUpscale the maximum mipmap upscale factor for the texture + */ + public void initialize(final double width, final double height, + final int textureWidth, final int textureHeight, + final int maxTextureUpscale) { + + topLeft = new Point3D(-width / 2, -height / 2, 0); + topRight = new Point3D(width / 2, -height / 2, 0); + bottomRight = new Point3D(width / 2, height / 2, 0); + bottomLeft = new Point3D(-width / 2, height / 2, 0); + + texture = new Texture(textureWidth, textureHeight, maxTextureUpscale); + + textureTopRight = new Point2D(textureWidth, 0); + textureTopLeft = new Point2D(0, 0); + textureBottomRight = new Point2D(textureWidth, textureHeight); + textureBottomLeft = new Point2D(0, textureHeight); + + + + + final TexturedPolygon texturedPolygon1 = new TexturedPolygon( + new Vertex(topLeft, textureTopLeft), + new Vertex(topRight, textureTopRight), + new Vertex(bottomRight, textureBottomRight), texture); + + texturedPolygon1 + .setMouseInteractionController(mouseInteractionController); + + final TexturedPolygon texturedPolygon2 = new TexturedPolygon( + new Vertex(topLeft, textureTopLeft), + new Vertex(bottomLeft, textureBottomLeft), + new Vertex(bottomRight, textureBottomRight), texture); + + texturedPolygon2 + .setMouseInteractionController(mouseInteractionController); + + addShape(texturedPolygon1); + addShape(texturedPolygon2); + } + +// public void initialize(final int width, final int height, +// final int maxTextureUpscale) { +// initialize(width, height, width, height, maxTextureUpscale); +// } + +} 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 new file mode 100644 index 0000000..e35c451 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java @@ -0,0 +1,399 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.gui.RenderingContext; +import eu.svjatoslav.sixth.e3d.gui.ViewSpaceTracker; +import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.sixth.e3d.math.Transform; +import eu.svjatoslav.sixth.e3d.math.TransformStack; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator; +import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager; +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.TexturedPolygon; +import eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer; + +import java.util.ArrayList; +import java.util.List; + +/** + * A composite shape that groups multiple sub-shapes into a single logical unit. + * + *

Use {@code AbstractCompositeShape} to build complex 3D objects by combining + * primitive shapes (lines, polygons, textured polygons) into a group that can be + * positioned, rotated, and manipulated as one entity. Sub-shapes can be organized + * into named groups for selective visibility toggling.

+ * + *

Usage example - creating a custom composite shape:

+ *
{@code
+ * // Create a composite shape at position (0, 0, 200)
+ * AbstractCompositeShape myObject = new AbstractCompositeShape(
+ *     new Point3D(0, 0, 200)
+ * );
+ *
+ * // Add sub-shapes
+ * myObject.addShape(new Line(
+ *     new Point3D(-50, 0, 0), new Point3D(50, 0, 0),
+ *     Color.RED, 2.0
+ * ));
+ *
+ * // Add shapes to a named group for toggling visibility
+ * myObject.addShape(labelShape, "labels");
+ * myObject.hideGroup("labels");  // hide all shapes in "labels" group
+ * myObject.showGroup("labels");  // show them again
+ *
+ * // Add to scene
+ * viewPanel.getRootShapeCollection().addShape(myObject);
+ * }
+ * + *

Level-of-detail slicing:

+ *

Textured polygons within the composite shape are automatically sliced into smaller + * triangles based on distance from the viewer. This provides perspective-correct texture + * mapping without requiring hardware support. The slicing factor adapts dynamically.

+ * + *

Extending this class:

+ *

Override {@link #beforeTransformHook} to customize shape appearance or behavior + * on each frame (e.g., animations, dynamic geometry updates).

+ * + * @see SubShape wrapper for individual sub-shapes with group and visibility support + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape the base shape class + * @see eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer the level-of-detail polygon slicer + */ +public class AbstractCompositeShape extends AbstractShape { + private final List originalSubShapes = new ArrayList<>(); + private final ViewSpaceTracker viewSpaceTracker; + double currentSliceFactor = 5; + private List renderedSubShapes = new ArrayList<>(); + private boolean slicingOutdated = true; + private Transform transform; + private LightingManager lightingManager; + + /** + * Creates a composite shape at the world origin with no rotation. + */ + public AbstractCompositeShape() { + this(new Transform()); + } + + /** + * Creates a composite shape at the specified location with no rotation. + * + * @param location the position in world space + */ + public AbstractCompositeShape(final Point3D location) { + this(new Transform(location)); + } + + /** + * Creates a composite shape with the specified transform (position and orientation). + * + * @param transform the initial transform defining position and rotation + */ + public AbstractCompositeShape(final Transform transform) { + this.transform = transform; + viewSpaceTracker = new ViewSpaceTracker(); + } + + /** + * Adds a sub-shape to this composite shape without a group identifier. + * + * @param shape the shape to add + */ + public void addShape(final AbstractShape shape) { + addShape(shape, null); + } + + /** + * Adds a sub-shape to this composite shape with an optional group identifier. + * + *

Grouped shapes can be shown, hidden, or removed together using + * {@link #showGroup}, {@link #hideGroup}, and {@link #removeGroup}.

+ * + * @param shape the shape to add + * @param groupId the group identifier, or {@code null} for ungrouped shapes + */ + public void addShape(final AbstractShape shape, final String groupId) { + final SubShape subShape = new SubShape(shape); + subShape.setGroup(groupId); + subShape.setVisible(true); + originalSubShapes.add(subShape); + slicingOutdated = true; + } + + /** + * This method should be overridden by anyone wanting to customize shape + * before it is rendered. + */ + public void beforeTransformHook(final TransformStack transformPipe, + final RenderingContext context) { + } + + /** + * Returns the world-space position of this composite shape. + * + * @return the translation component of this shape's transform + */ + public Point3D getLocation() { + return transform.getTranslation(); + } + + /** + * Returns the list of all sub-shapes (including hidden ones). + * + * @return the internal list of sub-shapes + */ + public List getOriginalSubShapes() { + return originalSubShapes; + } + + /** + * Returns the view-space tracker that monitors the distance + * and angle between the camera and this shape for level-of-detail adjustments. + * + * @return the view-space tracker for this shape + */ + public ViewSpaceTracker getViewSpaceTracker() { + return viewSpaceTracker; + } + + /** + * Hides all sub-shapes belonging to the specified group. + * Hidden shapes are not rendered but remain in the collection. + * + * @param groupIdentifier the group to hide + * @see #showGroup(String) + * @see #removeGroup(String) + */ + public void hideGroup(final String groupIdentifier) { + for (int i = 0; i < originalSubShapes.size(); i++) { + final SubShape subShape = originalSubShapes.get(i); + if (subShape.matchesGroup(groupIdentifier)) { + subShape.setVisible(false); + slicingOutdated = true; + } + } + } + + private boolean isReslicingNeeded(double proposedNewSliceFactor, double currentSliceFactor) { + + if (slicingOutdated) + return true; + + // reslice if there is significant difference between proposed and current slice factor + if (proposedNewSliceFactor > currentSliceFactor) { + final double tmp = proposedNewSliceFactor; + proposedNewSliceFactor = currentSliceFactor; + currentSliceFactor = tmp; + } + + return (currentSliceFactor / proposedNewSliceFactor) > 1.5d; + } + + /** + * Permanently removes all sub-shapes belonging to the specified group. + * + * @param groupIdentifier the group to remove + * @see #hideGroup(String) + */ + public void removeGroup(final String groupIdentifier) { + final java.util.Iterator iterator = originalSubShapes + .iterator(); + + while (iterator.hasNext()) { + final SubShape subShape = iterator.next(); + if (subShape.matchesGroup(groupIdentifier)) { + iterator.remove(); + slicingOutdated = true; + } + } + } + + /** + * Returns all sub-shapes belonging to the specified group. + * + * @param groupIdentifier the group identifier to match + * @return list of matching sub-shapes + */ + public List getGroup(final String groupIdentifier) { + final List result = new ArrayList<>(); + for (int i = 0; i < originalSubShapes.size(); i++) { + final SubShape subShape = originalSubShapes.get(i); + if (subShape.matchesGroup(groupIdentifier)) + result.add(subShape); + } + return result; + } + + private void resliceIfNeeded() { + + final double proposedSliceFactor = viewSpaceTracker.proposeSliceFactor(); + + if (isReslicingNeeded(proposedSliceFactor, currentSliceFactor)) { + currentSliceFactor = proposedSliceFactor; + reslice(); + } + } + + /** + * Paint solid elements of this composite shape into given color. + */ + public void setColor(final Color color) { + for (final SubShape subShape : getOriginalSubShapes()) { + final AbstractShape shape = subShape.getShape(); + + if (shape instanceof SolidPolygon) + ((SolidPolygon) shape).setColor(color); + + if (shape instanceof Line) + ((Line) shape).color = color; + } + } + + /** + * Assigns a group identifier to all sub-shapes that currently have no group. + * + * @param groupIdentifier the group to assign to ungrouped shapes + */ + public void setGroupForUngrouped(final String groupIdentifier) { + for (int i = 0; i < originalSubShapes.size(); i++) { + final SubShape subShape = originalSubShapes.get(i); + if (subShape.isUngrouped()) + subShape.setGroup(groupIdentifier); + } + } + + @Override + public void setMouseInteractionController( + final MouseInteractionController mouseInteractionController) { + super.setMouseInteractionController(mouseInteractionController); + + for (final SubShape subShape : originalSubShapes) + subShape.getShape().setMouseInteractionController( + mouseInteractionController); + + slicingOutdated = true; + + } + + /** + * Replaces this shape's transform (position and orientation). + * + * @param transform the new transform to apply + */ + public void setTransform(final Transform transform) { + this.transform = transform; + } + + /** + * Sets the lighting manager for this composite shape and enables shading on all SolidPolygon sub-shapes. + * + * @param lightingManager the lighting manager to use for shading calculations + */ + public void setLightingManager(final LightingManager lightingManager) { + this.lightingManager = lightingManager; + applyShadingToPolygons(); + } + + /** + * Enables or disables shading for all SolidPolygon sub-shapes. + * + * @param shadingEnabled true to enable shading, false to disable + */ + public void setShadingEnabled(final boolean shadingEnabled) { + for (final SubShape subShape : getOriginalSubShapes()) { + final AbstractShape shape = subShape.getShape(); + if (shape instanceof SolidPolygon) { + ((SolidPolygon) shape).setShadingEnabled(shadingEnabled, lightingManager); + } + } + } + + private void applyShadingToPolygons() { + if (lightingManager == null) + return; + + for (final SubShape subShape : getOriginalSubShapes()) { + final AbstractShape shape = subShape.getShape(); + if (shape instanceof SolidPolygon) { + ((SolidPolygon) shape).setShadingEnabled(true, lightingManager); + } + } + } + + public void setBackfaceCulling(final boolean backfaceCulling) { + for (final SubShape subShape : getOriginalSubShapes()) { + final AbstractShape shape = subShape.getShape(); + if (shape instanceof SolidPolygon) { + ((SolidPolygon) shape).setBackfaceCulling(backfaceCulling); + } else if (shape instanceof TexturedPolygon) { + ((TexturedPolygon) shape).setBackfaceCulling(backfaceCulling); + } + } + } + + /** + * Makes all sub-shapes belonging to the specified group visible. + * + * @param groupIdentifier the group to show + * @see #hideGroup(String) + */ + public void showGroup(final String groupIdentifier) { + for (int i = 0; i < originalSubShapes.size(); i++) { + final SubShape subShape = originalSubShapes.get(i); + if (subShape.matchesGroup(groupIdentifier)) { + subShape.setVisible(true); + slicingOutdated = true; + } + } + } + + private void reslice() { + slicingOutdated = false; + + final List result = new ArrayList<>(); + + final Slicer slicer = new Slicer(currentSliceFactor); + for (int i = 0; i < originalSubShapes.size(); i++) { + final SubShape subShape = originalSubShapes.get(i); + if (subShape.isVisible()) { + if (subShape.getShape() instanceof TexturedPolygon) + slicer.slice((TexturedPolygon) subShape.getShape()); + else + result.add(subShape.getShape()); + } + } + + result.addAll(slicer.getResult()); + + renderedSubShapes = result; + } + + @Override + public void transform(final TransformStack transformPipe, + final RenderAggregator aggregator, final RenderingContext context) { + + // add current composite shape transform to the end of the transform + // pipeline + transformPipe.addTransform(transform); + + viewSpaceTracker.analyze(transformPipe, context); + + beforeTransformHook(transformPipe, context); + + // hack, to get somewhat perspective correct textures + resliceIfNeeded(); + + // transform rendered subshapes + for (final AbstractShape shape : renderedSubShapes) + shape.transform(transformPipe, aggregator, context); + + transformPipe.dropTransform(); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java new file mode 100644 index 0000000..6530b2d --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java @@ -0,0 +1,93 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base; + +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape; + +/** + * Wrapper around an {@link AbstractShape} within an {@link AbstractCompositeShape}, + * adding group membership and visibility control. + * + *

Sub-shapes can be organized into named groups so they can be shown, hidden, + * or removed together. This is useful for toggling parts of a composite shape, + * such as showing/hiding labels, highlights, or selection borders.

+ * + * @see AbstractCompositeShape#addShape(AbstractShape, String) + * @see AbstractCompositeShape#hideGroup(String) + * @see AbstractCompositeShape#showGroup(String) + */ +public class SubShape { + + private final AbstractShape shape; + private boolean visible = true; + private String groupIdentifier; + + /** + * Creates a sub-shape wrapper around the given shape. + * + * @param shape the shape to wrap + */ + public SubShape(AbstractShape shape) { + this.shape = shape; + } + + /** + * Returns {@code true} if this sub-shape has no group assigned. + * + * @return {@code true} if ungrouped + */ + public boolean isUngrouped() { + return groupIdentifier == null; + } + + /** + * Checks whether this sub-shape belongs to the specified group. + * + * @param groupIdentifier the group identifier to match against, or {@code null} to match ungrouped shapes + * @return {@code true} if this sub-shape belongs to the specified group + */ + public boolean matchesGroup(final String groupIdentifier) { + if (this.groupIdentifier == null) + return groupIdentifier == null; + + return this.groupIdentifier.equals(groupIdentifier); + } + + /** + * Assigns this sub-shape to a group. + * + * @param groupIdentifier the group identifier, or {@code null} to make it ungrouped + */ + public void setGroup(final String groupIdentifier) { + this.groupIdentifier = groupIdentifier; + } + + /** + * Returns the wrapped shape. + * + * @return the underlying shape + */ + public AbstractShape getShape() { + return shape; + } + + /** + * Returns whether this sub-shape is currently visible and will be rendered. + * + * @return {@code true} if visible + */ + public boolean isVisible() { + return visible; + } + + /** + * Sets the visibility of this sub-shape. + * + * @param visible {@code true} to make the shape visible, {@code false} to hide it + */ + public void setVisible(boolean visible) { + this.visible = visible; + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCube.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCube.java new file mode 100755 index 0000000..6fa3482 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCube.java @@ -0,0 +1,45 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; + +/** + * A solid cube centered at a given point with equal side length along all axes. + * This is a convenience subclass of {@link SolidPolygonRectangularBox} that + * constructs a cube from a center point and a half-side length. + * + *

The cube extends {@code size} units in each direction from the center, + * resulting in a total edge length of {@code 2 * size}.

+ * + *

Usage example:

+ *
{@code
+ * SolidPolygonCube cube = new SolidPolygonCube(
+ *         new Point3D(0, 0, 300), 50, Color.GREEN);
+ * shapeCollection.addShape(cube);
+ * }
+ * + * @see SolidPolygonRectangularBox + * @see Color + */ +public class SolidPolygonCube extends SolidPolygonRectangularBox { + + /** + * Constructs a solid cube centered at the given point. + * + * @param center the center point of the cube in 3D space + * @param size the half-side length; the cube extends this distance from + * the center along each axis, giving a total edge length of + * {@code 2 * size} + * @param color the fill color applied to all faces of the cube + */ + public SolidPolygonCube(final Point3D center, final double size, + final Color color) { + super(new Point3D(center.x - size, center.y - size, center.z - size), + new Point3D(center.x + size, center.y + size, center.z + size), + color); + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java new file mode 100644 index 0000000..a926a94 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java @@ -0,0 +1,98 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A solid cylinder oriented along the Y-axis. + * + *

The cylinder has circular top and bottom caps connected by a curved side + * surface made of rectangular panels. The number of segments determines the + * smoothness of the curved surface.

+ * + *

Usage example:

+ *
{@code
+ * // Create a cylinder with radius 50, height 100, and 16 segments
+ * SolidPolygonCylinder cylinder = new SolidPolygonCylinder(
+ *     new Point3D(0, 0, 200), 50, 100, 16, Color.RED);
+ * shapeCollection.addShape(cylinder);
+ * }
+ * + * @see SolidPolygonCube + * @see SolidPolygonSphere + * @see SolidPolygon + */ +public class SolidPolygonCylinder extends AbstractCompositeShape { + + /** + * Constructs a solid cylinder centered at the given point. + * + * @param center the center point of the cylinder in 3D space. + * The cylinder is centered on the Y-axis, extending + * {@code height/2} above and below this point. + * @param radius the radius of the cylinder + * @param height the total height of the cylinder + * @param segments the number of segments around the circumference. + * Higher values create smoother cylinders. Minimum is 3. + * @param color the fill color applied to all polygons + */ + public SolidPolygonCylinder(final Point3D center, final double radius, + final double height, final int segments, + final Color color) { + super(); + + final double halfHeight = height / 2.0; + final double bottomY = center.y - halfHeight; + final double topY = center.y + halfHeight; + + Point3D bottomCenter = new Point3D(center.x, bottomY, center.z); + Point3D topCenter = new Point3D(center.x, topY, center.z); + + Point3D[] bottomRing = new Point3D[segments]; + Point3D[] topRing = new Point3D[segments]; + + for (int i = 0; i < segments; i++) { + double angle = 2.0 * Math.PI * i / segments; + double x = center.x + radius * Math.cos(angle); + double z = center.z + radius * Math.sin(angle); + bottomRing[i] = new Point3D(x, bottomY, z); + topRing[i] = new Point3D(x, topY, z); + } + + for (int i = 0; i < segments; i++) { + int next = (i + 1) % segments; + + addShape(new SolidPolygon( + new Point3D(bottomCenter.x, bottomCenter.y, bottomCenter.z), + new Point3D(bottomRing[i].x, bottomRing[i].y, bottomRing[i].z), + new Point3D(bottomRing[next].x, bottomRing[next].y, bottomRing[next].z), + color)); + + addShape(new SolidPolygon( + new Point3D(topCenter.x, topCenter.y, topCenter.z), + new Point3D(topRing[next].x, topRing[next].y, topRing[next].z), + new Point3D(topRing[i].x, topRing[i].y, topRing[i].z), + color)); + + addShape(new SolidPolygon( + new Point3D(bottomRing[i].x, bottomRing[i].y, bottomRing[i].z), + new Point3D(topRing[i].x, topRing[i].y, topRing[i].z), + new Point3D(bottomRing[next].x, bottomRing[next].y, bottomRing[next].z), + color)); + + addShape(new SolidPolygon( + new Point3D(bottomRing[next].x, bottomRing[next].y, bottomRing[next].z), + new Point3D(topRing[i].x, topRing[i].y, topRing[i].z), + new Point3D(topRing[next].x, topRing[next].y, topRing[next].z), + color)); + } + + setBackfaceCulling(true); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java new file mode 100644 index 0000000..97b18d8 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java @@ -0,0 +1,68 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A solid square-based pyramid with the base centered at a given point. + * + *

The pyramid has a square base and four triangular faces meeting at an apex. + * The base has side length of {@code 2 * baseSize} and the height extends + * {@code height} units above the base center to the apex.

+ * + *

Usage example:

+ *
{@code
+ * SolidPolygonPyramid pyramid = new SolidPolygonPyramid(
+ *         new Point3D(0, 0, 300), 50, 100, Color.BLUE);
+ * shapeCollection.addShape(pyramid);
+ * }
+ * + * @see SolidPolygonCube + * @see SolidPolygonSphere + * @see SolidPolygon + */ +public class SolidPolygonPyramid extends AbstractCompositeShape { + + /** + * Constructs a solid square-based pyramid with base centered at the given point. + * + * @param baseCenter the center point of the pyramid's base in 3D space + * @param baseSize the half-width of the square base; the base extends + * this distance from the center along X and Z axes, + * giving a total base edge length of {@code 2 * baseSize} + * @param height the height of the pyramid from base center to apex + * @param color the fill color applied to all faces of the pyramid + */ + public SolidPolygonPyramid(final Point3D baseCenter, final double baseSize, + final double height, final Color color) { + super(); + + final double halfBase = baseSize; + final double apexY = baseCenter.y - height; + final double baseY = baseCenter.y; + + Point3D frontLeft = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z - halfBase); + Point3D frontRight = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z - halfBase); + Point3D backRight = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z + halfBase); + Point3D backLeft = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z + halfBase); + Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z); + + // Polygons that touch apex + addShape(new SolidPolygon(frontLeft, frontRight, apex, color)); + addShape(new SolidPolygon(frontRight, backRight, apex, color)); + addShape(new SolidPolygon(backRight, backLeft, apex, color)); + addShape(new SolidPolygon(backLeft, frontLeft, apex, color)); + + // Pyramid bottom + addShape(new SolidPolygon( backLeft, backRight, frontLeft, color)); + addShape(new SolidPolygon( frontRight, frontLeft, backRight, color)); + + setBackfaceCulling(true); + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java new file mode 100755 index 0000000..800def5 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java @@ -0,0 +1,92 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A solid (filled) rectangular box composed of 12 triangular polygons (2 per face, + * covering all 6 faces). Each face is rendered as a pair of {@link SolidPolygon} + * triangles with the same color. + * + *

The box can be constructed either from a center point and a uniform size + * (producing a cube), or from two diagonally opposite corner points (producing + * an arbitrary axis-aligned rectangular box).

+ * + *

The vertices are labeled p1 through p8, representing the eight corners of + * the box. The triangles are arranged to cover the bottom, top, front, back, + * left, and right faces.

+ * + *

Usage example:

+ *
{@code
+ * // From center and size:
+ * SolidPolygonRectangularBox box1 = new SolidPolygonRectangularBox(
+ *         new Point3D(0, 0, 200), 100, Color.RED);
+ *
+ * // From two corner points:
+ * SolidPolygonRectangularBox box2 = new SolidPolygonRectangularBox(
+ *         new Point3D(-50, -25, 100), new Point3D(50, 25, 200), Color.BLUE);
+ *
+ * shapeCollection.addShape(box1);
+ * }
+ * + * @see SolidPolygonCube + * @see SolidPolygon + * @see AbstractCompositeShape + */ +public class SolidPolygonRectangularBox extends AbstractCompositeShape { + + /** + * Constructs a solid rectangular box between two diagonally opposite corner + * points in 3D space. The eight vertices of the box are derived from the + * coordinate components of {@code p1} and {@code p7}. All six faces are + * tessellated into two triangles each, for a total of 12 solid polygons. + * + * @param p1 the first corner point (minimum coordinates by convention) + * @param p7 the diagonally opposite corner point (maximum coordinates) + * @param color the fill color applied to all 12 triangular polygons + */ + public SolidPolygonRectangularBox(final Point3D p1, final Point3D p7, final Color color) { + super(); + + final Point3D p2 = new Point3D(p7.x, p1.y, p1.z); + final Point3D p3 = new Point3D(p7.x, p1.y, p7.z); + final Point3D p4 = new Point3D(p1.x, p1.y, p7.z); + + final Point3D p5 = new Point3D(p1.x, p7.y, p1.z); + final Point3D p6 = new Point3D(p7.x, p7.y, p1.z); + final Point3D p8 = new Point3D(p1.x, p7.y, p7.z); + + // Bottom face (y = minY) + addShape(new SolidPolygon(p1, p2, p3, color)); + addShape(new SolidPolygon(p1, p3, p4, color)); + + // Top face (y = maxY) + addShape(new SolidPolygon(p5, p8, p7, color)); + addShape(new SolidPolygon(p5, p7, p6, color)); + + // Front face (z = minZ) + addShape(new SolidPolygon(p1, p5, p6, color)); + addShape(new SolidPolygon(p1, p6, p2, color)); + + // Back face (z = maxZ) + addShape(new SolidPolygon(p3, p7, p8, color)); + addShape(new SolidPolygon(p3, p8, p4, color)); + + // Left face (x = minX) + addShape(new SolidPolygon(p1, p4, p8, color)); + addShape(new SolidPolygon(p1, p8, p5, color)); + + // Right face (x = maxX) + addShape(new SolidPolygon(p2, p6, p7, color)); + addShape(new SolidPolygon(p2, p7, p3, color)); + + setBackfaceCulling(true); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.java new file mode 100644 index 0000000..6f0d64d --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.java @@ -0,0 +1,84 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A solid sphere composed of triangular polygons. + * + *

The sphere is constructed using a latitude-longitude grid (UV sphere). + * The number of segments determines the smoothness - more segments create + * a smoother sphere but require more polygons.

+ * + *

Usage example:

+ *
{@code
+ * // Create a sphere with radius 50 and 16 segments (smooth)
+ * SolidPolygonSphere sphere = new SolidPolygonSphere(
+ *     new Point3D(0, 0, 200), 50, 16, Color.RED);
+ * shapeCollection.addShape(sphere);
+ * }
+ * + * @see SolidPolygonCube + * @see SolidPolygon + * @see AbstractCompositeShape + */ +public class SolidPolygonSphere extends AbstractCompositeShape { + + /** + * Constructs a solid sphere centered at the given point. + * + * @param center the center point of the sphere in 3D space + * @param radius the radius of the sphere + * @param segments the number of segments (latitude/longitude divisions). + * Higher values create smoother spheres. Minimum is 3. + * @param color the fill color applied to all triangular polygons + */ + public SolidPolygonSphere(final Point3D center, final double radius, + final int segments, final Color color) { + super(); + + final int rings = segments; + final int sectors = segments * 2; + + for (int i = 0; i < rings; i++) { + double lat0 = Math.PI * (-0.5 + (double) i / rings); + double lat1 = Math.PI * (-0.5 + (double) (i + 1) / rings); + + for (int j = 0; j < sectors; j++) { + double lon0 = 2 * Math.PI * (double) j / sectors; + double lon1 = 2 * Math.PI * (double) (j + 1) / sectors; + + Point3D p0 = sphericalToCartesian(center, radius, lat0, lon0); + Point3D p1 = sphericalToCartesian(center, radius, lat0, lon1); + Point3D p2 = sphericalToCartesian(center, radius, lat1, lon0); + Point3D p3 = sphericalToCartesian(center, radius, lat1, lon1); + + if (i > 0) { + addShape(new SolidPolygon(p0, p2, p1, color)); + } + + if (i < rings - 1) { + addShape(new SolidPolygon(p2, p3, p1, color)); + } + } + } + + setBackfaceCulling(true); + } + + private Point3D sphericalToCartesian(final Point3D center, + final double radius, + final double lat, + final double lon) { + double x = center.x + radius * Math.cos(lat) * Math.cos(lon); + double y = center.y + radius * Math.sin(lat); + double z = center.z + radius * Math.cos(lat) * Math.sin(lon); + return new Point3D(x, y, z); + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java new file mode 100644 index 0000000..a616dae --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java @@ -0,0 +1,188 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas; + +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.renderer.raster.shapes.AbstractCoordinateShape; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; + +import java.awt.*; + +import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon.drawPolygon; +import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas.FONT_CHAR_HEIGHT; +import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas.FONT_CHAR_WIDTH; +import static java.lang.String.valueOf; + +/** + * Represents a single character on the text canvas. + */ +public class CanvasCharacter extends AbstractCoordinateShape { + + private static final int MAX_FONT_SIZE = 500; + + /** + * Cached fonts. + */ + private static final Font[] fonts = new Font[MAX_FONT_SIZE]; + + /** + * The character to be rendered. + */ + private char value; + + /** + * The foreground color of the character. + */ + private eu.svjatoslav.sixth.e3d.renderer.raster.Color foregroundColor; + + /** + * The background color of the character. + */ + private eu.svjatoslav.sixth.e3d.renderer.raster.Color backgroundColor; + + public CanvasCharacter(final Point3D centerLocation, final char character, + final eu.svjatoslav.sixth.e3d.renderer.raster.Color foregroundColor, + final eu.svjatoslav.sixth.e3d.renderer.raster.Color backgroundColor) { + + // There are 5 coordinates: center, upper left, upper right, lower right, lower left + super(5); + + value = character; + this.foregroundColor = foregroundColor; + this.backgroundColor = backgroundColor; + + + coordinates[0].coordinate = centerLocation; + + final double halfWidth = FONT_CHAR_WIDTH / 2d; + final double halfHeight = FONT_CHAR_HEIGHT / 2d; + + // upper left + coordinates[1].coordinate = centerLocation.clone().translateX(-halfWidth) + .translateY(-halfHeight); + + // upper right + coordinates[2].coordinate = centerLocation.clone().translateX(halfWidth) + .translateY(-halfHeight); + + // lower right + coordinates[3].coordinate = centerLocation.clone().translateX(halfWidth) + .translateY(halfHeight); + + // lower left + coordinates[4].coordinate = centerLocation.clone().translateX(-halfWidth) + .translateY(halfHeight); + } + + /** + * Returns a font of the specified size. + *

+ * If the font of the specified size is already cached, it will be + * returned. Otherwise, a new font will be created, cached and returned. + * + * @param size the size of the font + * @return the font + */ + public static Font getFont(final int size) { + if (fonts[size] != null) + return fonts[size]; + + final Font font = new Font("Courier", Font.BOLD, size); + fonts[size] = font; + return font; + } + + /** + * Returns color of the background. + */ + public eu.svjatoslav.sixth.e3d.renderer.raster.Color getBackgroundColor() { + return backgroundColor; + } + + /** + * Sets color of the background. + */ + public void setBackgroundColor( + final eu.svjatoslav.sixth.e3d.renderer.raster.Color backgroundColor) { + this.backgroundColor = backgroundColor; + } + + /** + * Returns color of the foreground. + * + * @return the color + */ + public eu.svjatoslav.sixth.e3d.renderer.raster.Color getForegroundColor() { + return foregroundColor; + } + + /** + * Sets color of the foreground. + * + * @param foregroundColor the color + */ + public void setForegroundColor( + final eu.svjatoslav.sixth.e3d.renderer.raster.Color foregroundColor) { + this.foregroundColor = foregroundColor; + } + + /** + * Paints the character on the screen. + * @param renderingContext the rendering context + */ + @Override + public void paint(final RenderingContext renderingContext) { + + // Draw background rectangle first. It is composed of two triangles. + drawPolygon(renderingContext, + coordinates[1].onScreenCoordinate, + coordinates[2].onScreenCoordinate, + coordinates[3].onScreenCoordinate, + mouseInteractionController, + backgroundColor); + + drawPolygon(renderingContext, + coordinates[1].onScreenCoordinate, + coordinates[3].onScreenCoordinate, + coordinates[4].onScreenCoordinate, + mouseInteractionController, + backgroundColor); + + final int desiredFontSize = (int) ((renderingContext.width * 4.5) / onScreenZ); + + // do not render too large characters + if (desiredFontSize >= MAX_FONT_SIZE) + return; + + final Point2D onScreenLocation = coordinates[0].onScreenCoordinate; + + // screen borders check + if (onScreenLocation.x < 0) + return; + if (onScreenLocation.y < 0) + return; + + if (onScreenLocation.x > renderingContext.width) + return; + if (onScreenLocation.y > renderingContext.height) + return; + + // draw the character + renderingContext.graphics.setFont(getFont(desiredFontSize)); + renderingContext.graphics.setColor(foregroundColor.toAwtColor()); + renderingContext.graphics.drawString( + valueOf(value), + (int) onScreenLocation.x - (int) (desiredFontSize / 3.2), + (int) onScreenLocation.y + (int) (desiredFontSize / 2.5)); + + } + + public void setValue(final char value) { + this.value = value; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/RenderMode.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/RenderMode.java new file mode 100644 index 0000000..b4fc5e7 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/RenderMode.java @@ -0,0 +1,28 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas; + +/** + * Defines how text is rendered on a {@link TextCanvas}. + * + *

The render mode controls the trade-off between rendering quality and performance. + * {@link TextCanvas} automatically selects the optimal mode based on the viewer's + * distance and viewing angle relative to the text surface.

+ * + * @see TextCanvas + */ +public enum RenderMode { + /** + * Text is rendered as pixels on textured polygon. + * This mode works in any orientation. Even if polygon is rotated. + */ + TEXTURE, + + /** + * Text is rendered as high quality, anti-aliased tiles. + * This mode works only if text is facing the camera almost directly. + */ + CHARACTERS +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.java new file mode 100644 index 0000000..14e563b --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.java @@ -0,0 +1,463 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.gui.RenderingContext; +import eu.svjatoslav.sixth.e3d.gui.TextPointer; +import eu.svjatoslav.sixth.e3d.math.Transform; +import eu.svjatoslav.sixth.e3d.math.TransformStack; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.TexturedRectangle; + +import java.awt.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; + +import static eu.svjatoslav.sixth.e3d.renderer.raster.Color.BLACK; +import static eu.svjatoslav.sixth.e3d.renderer.raster.Color.WHITE; +import static java.lang.Math.PI; +import static java.lang.Math.abs; + +/** + * A text rendering surface in 3D space that displays a grid of characters. + * + *

{@code TextCanvas} extends {@link TexturedRectangle} and renders a 2D grid of + * characters (rows and columns) onto a texture-mapped rectangle. Each character cell + * supports independent foreground and background colors through {@link CanvasCharacter}.

+ * + *

Characters are rendered using a monospace font at a fixed size + * ({@value #FONT_CHAR_WIDTH_TEXTURE_PIXELS} x {@value #FONT_CHAR_HEIGHT_TEXTURE_PIXELS} + * texture pixels per character). The canvas automatically switches between two + * {@linkplain RenderMode render modes} based on the viewer's distance and viewing angle:

+ *
    + *
  • {@link RenderMode#TEXTURE} -- renders all characters to a shared texture bitmap. + * This is efficient for distant or obliquely viewed text.
  • + *
  • {@link RenderMode#CHARACTERS} -- renders each character as an individual textured + * polygon with higher quality anti-aliased tiles. Used when the viewer is close + * and looking at the text nearly head-on.
  • + *
+ * + *

Usage example

+ *
{@code
+ * Transform location = new Transform(new Point3D(0, 0, 500));
+ * TextCanvas canvas = new TextCanvas(location, "Hello, World!",
+ *         Color.WHITE, Color.BLACK);
+ * shapeCollection.addShape(canvas);
+ *
+ * // Or create a blank canvas and write to it
+ * TextCanvas blank = new TextCanvas(location, new TextPointer(10, 40),
+ *         Color.GREEN, Color.BLACK);
+ * blank.locate(0, 0);
+ * blank.print("Line 1");
+ * blank.locate(1, 0);
+ * blank.print("Line 2");
+ * }
+ * + * @see RenderMode + * @see CanvasCharacter + * @see TexturedRectangle + */ +public class TextCanvas extends TexturedRectangle { + + /** + * Font character width in world coordinates. + */ + public static final int FONT_CHAR_WIDTH = 8; + + /** + * Font character height in world coordinates. + */ + public static final int FONT_CHAR_HEIGHT = 16; + + /** + * Font character width in texture pixels. + */ + public static final int FONT_CHAR_WIDTH_TEXTURE_PIXELS = 16; + + /** + * Font character height in texture pixels. + */ + public static final int FONT_CHAR_HEIGHT_TEXTURE_PIXELS = 32; + + + public static final Font FONT = CanvasCharacter.getFont((int) (FONT_CHAR_HEIGHT_TEXTURE_PIXELS / 1.066)); + private static final String GROUP_TEXTURE = "texture"; + private static final String GROUP_CHARACTERS = "characters"; + private final TextPointer size; + private final TextPointer cursorLocation = new TextPointer(); + CanvasCharacter[][] lines; + private RenderMode renderMode = null; + private Color backgroundColor = BLACK; + private Color foregroundColor = WHITE; + + /** + * Creates a text canvas initialized with the given text string. + * + *

The canvas dimensions are automatically computed from the text content + * (number of lines determines rows, the longest line determines columns).

+ * + * @param location the 3D transform positioning this canvas in the scene + * @param text the initial text content (may contain newlines for multiple rows) + * @param foregroundColor the default text color + * @param backgroundColor the default background color + */ + public TextCanvas(final Transform location, final String text, + final Color foregroundColor, final Color backgroundColor) { + this(location, getTextDimensions(text), foregroundColor, + backgroundColor); + setText(text); + } + + /** + * Creates a blank text canvas with the specified dimensions. + * + *

The canvas is initialized with spaces in every cell, filled with the + * specified background color. Characters can be written using + * {@link #putChar(char)}, {@link #print(String)}, or {@link #setText(String)}.

+ * + * @param dimensions the grid size as a {@link TextPointer} where + * {@code row} is the number of rows and {@code column} is the number of columns + * @param location the 3D transform positioning this canvas in the scene + * @param foregroundColor the default text color + * @param backgroundColor the default background color + */ + public TextCanvas(final Transform location, final TextPointer dimensions, + final Color foregroundColor, final Color backgroundColor) { + super(location); + getViewSpaceTracker().enableOrientationTracking(); + + size = dimensions; + final int columns = dimensions.column; + final int rows = dimensions.row; + + this.backgroundColor = backgroundColor; + this.foregroundColor = foregroundColor; + + // initialize underlying textured rectangle + initialize( + columns * FONT_CHAR_WIDTH, + rows * FONT_CHAR_HEIGHT, + columns * FONT_CHAR_WIDTH_TEXTURE_PIXELS, + rows * FONT_CHAR_HEIGHT_TEXTURE_PIXELS, + 0); + + getTexture().primaryBitmap.fillColor(backgroundColor); + getTexture().resetResampledBitmapCache(); + + setGroupForUngrouped(GROUP_TEXTURE); + + lines = new CanvasCharacter[rows][]; + for (int row = 0; row < rows; row++) { + lines[row] = new CanvasCharacter[columns]; + + for (int column = 0; column < columns; column++) { + final Point3D characterCoordinate = getCharLocation(row, column); + + final CanvasCharacter character = new CanvasCharacter( + characterCoordinate, ' ', foregroundColor, + backgroundColor); + addShape(character); + lines[row][column] = character; + } + + } + + setGroupForUngrouped(GROUP_CHARACTERS); + + setRenderMode(RenderMode.TEXTURE); + } + + /** + * Computes the row and column dimensions needed to fit the given text. + * + * @param text the text content (may contain newlines) + * @return a {@link TextPointer} where {@code row} is the number of lines and + * {@code column} is the length of the longest line + */ + public static TextPointer getTextDimensions(final String text) { + + final BufferedReader reader = new BufferedReader(new StringReader(text)); + + int rows = 0; + int columns = 0; + + while (true) { + final String line; + try { + line = reader.readLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (line == null) + return new TextPointer(rows, columns); + + rows++; + columns = Math.max(columns, line.length()); + } + } + + @Override + public void beforeTransformHook(final TransformStack transformPipe, + final RenderingContext context) { + ensureOptimalRenderMode(context); + } + + private void ensureOptimalRenderMode(RenderingContext context) { + + // if the text is too far away, use texture + final double textRelativeSize = context.width / getViewSpaceTracker().getDistanceToCamera(); + if (textRelativeSize < 2d) { + setRenderMode(RenderMode.TEXTURE); + return; + } + + // if user is looking at the text from the side, use texture + final double piHalf = PI / 2; + final double deviation = abs(getViewSpaceTracker().getAngleXZ() + + piHalf) + + abs(getViewSpaceTracker().getAngleYZ() + piHalf); + + final double maxDeviation = 0.5; + setRenderMode(deviation > maxDeviation ? RenderMode.TEXTURE : RenderMode.CHARACTERS); + } + + /** + * Clears the entire canvas, resetting all characters to spaces with the default colors. + * + *

Both the character grid and the backing texture bitmap are reset.

+ */ + public void clear() { + for (final CanvasCharacter[] line : lines) + for (final CanvasCharacter character : line) { + character.setValue(' '); + character.setBackgroundColor(backgroundColor); + character.setForegroundColor(foregroundColor); + } + + // set background color + getTexture().primaryBitmap.fillColor(backgroundColor); + getTexture().resetResampledBitmapCache(); + } + + private void drawCharToTexture(final int row, final int column, + final char character, final Color foreground) { + final Graphics2D graphics = getTexture().graphics; + + getTexture().primaryBitmap.drawRectangle( + column * FONT_CHAR_WIDTH_TEXTURE_PIXELS, + row * FONT_CHAR_HEIGHT_TEXTURE_PIXELS, + (column * FONT_CHAR_WIDTH_TEXTURE_PIXELS) + FONT_CHAR_WIDTH_TEXTURE_PIXELS, + (row * FONT_CHAR_HEIGHT_TEXTURE_PIXELS) + FONT_CHAR_HEIGHT_TEXTURE_PIXELS, + backgroundColor); + + graphics.setFont(FONT); + graphics.setColor(foreground.toAwtColor()); + graphics.drawChars( + new char[]{character,}, 0, 1, + (column * FONT_CHAR_WIDTH_TEXTURE_PIXELS), + (row * FONT_CHAR_HEIGHT_TEXTURE_PIXELS) + (int) (FONT_CHAR_HEIGHT_TEXTURE_PIXELS / 1.23f)); + getTexture().resetResampledBitmapCache(); + } + + /** + * Computes the 3D world coordinate for the center of the character cell at the given row and column. + * + * @param row the row index (0-based, from the top) + * @param column the column index (0-based, from the left) + * @return the 3D coordinate of the character cell center, relative to the canvas origin + */ + public Point3D getCharLocation(final int row, final int column) { + final Point3D coordinate = topLeft.clone(); + + coordinate.translateY((row * FONT_CHAR_HEIGHT) + + (FONT_CHAR_HEIGHT / 3.2)); + + coordinate.translateX((column * FONT_CHAR_WIDTH) + + (FONT_CHAR_WIDTH / 2)); + + return coordinate; + } + + /** + * Returns the dimensions of this text canvas. + * + * @return a {@link TextPointer} where {@code row} is the number of rows + * and {@code column} is the number of columns + */ + public TextPointer getSize() { + return size; + } + + /** + * Moves the internal cursor to the specified row and column. + * + *

Subsequent calls to {@link #putChar(char)} and {@link #print(String)} will + * begin writing at this position.

+ * + * @param row the target row (0-based) + * @param column the target column (0-based) + */ + public void locate(final int row, final int column) { + cursorLocation.row = row; + cursorLocation.column = column; + } + + /** + * Prints a string starting at the current cursor location, advancing the cursor after each character. + * + *

When the cursor reaches the end of a row, it wraps to the beginning of the next row.

+ * + * @param text the text to print + * @see #locate(int, int) + */ + public void print(final String text) { + for (int i = 0; i < text.length(); i++) + putChar(text.charAt(i)); + } + + /** + * Writes a character at the current cursor location and advances the cursor. + * + *

The cursor moves one column to the right. If it exceeds the row width, + * it wraps to column 0 of the next row.

+ * + * @param character the character to write + */ + public void putChar(final char character) { + putChar(cursorLocation, character); + + cursorLocation.column++; + if (cursorLocation.column >= size.column) { + cursorLocation.column = 0; + cursorLocation.row++; + } + } + + /** + * Writes a character at the specified row and column using the current foreground and background colors. + * + *

If the row or column is out of bounds, the call is silently ignored.

+ * + * @param row the row index (0-based) + * @param column the column index (0-based) + * @param character the character to write + */ + public void putChar(final int row, final int column, final char character) { + if ((row >= lines.length) || (row < 0)) + return; + + final CanvasCharacter[] line = lines[row]; + + if ((column >= line.length) || (column < 0)) + return; + + final CanvasCharacter canvasCharacter = line[column]; + canvasCharacter.setValue(character); + canvasCharacter.setBackgroundColor(backgroundColor); + canvasCharacter.setForegroundColor(foregroundColor); + drawCharToTexture(row, column, character, + foregroundColor); + } + + /** + * Writes a character at the position specified by a {@link TextPointer}. + * + * @param location the row and column position + * @param character the character to write + */ + public void putChar(final TextPointer location, final char character) { + putChar(location.row, location.column, character); + } + + /** + * Sets the default background color for subsequent character writes. + * + * @param backgroundColor the new background color + */ + public void setBackgroundColor( + final eu.svjatoslav.sixth.e3d.renderer.raster.Color backgroundColor) { + this.backgroundColor = backgroundColor; + } + + /** + * Sets the default foreground (text) color for subsequent character writes. + * + * @param foregroundColor the new foreground color + */ + public void setForegroundColor( + final eu.svjatoslav.sixth.e3d.renderer.raster.Color foregroundColor) { + this.foregroundColor = foregroundColor; + } + + private void setRenderMode(final RenderMode mode) { + if (mode == renderMode) + return; + + switch (mode) { + case CHARACTERS: + hideGroup(GROUP_TEXTURE); + showGroup(GROUP_CHARACTERS); + break; + case TEXTURE: + hideGroup(GROUP_CHARACTERS); + showGroup(GROUP_TEXTURE); + break; + } + + renderMode = mode; + } + + /** + * Replaces the entire canvas content with the given multi-line text string. + * + *

Each line of text (separated by newlines) is written to consecutive rows, + * starting from row 0. Characters beyond the canvas width are ignored.

+ * + * @param text the text to display (may contain newline characters) + */ + public void setText(final String text) { + final BufferedReader reader = new BufferedReader(new StringReader(text)); + + int row = 0; + + while (true) { + final String line; + try { + line = reader.readLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (line == null) + return; + + int column = 0; + for (int i = 0; i < line.length(); i++) { + putChar(row, column, line.charAt(i)); + column++; + } + row++; + } + } + + /** + * Sets the foreground color of all existing characters on the canvas. + * + *

This updates the color of every {@link CanvasCharacter} in the grid, + * but does not affect the backing texture. It is primarily useful in + * {@link RenderMode#CHARACTERS} mode.

+ * + * @param color the new foreground color for all characters + */ + public void setTextColor(final Color color) { + for (final CanvasCharacter[] line : lines) + for (final CanvasCharacter character : line) + character.setForegroundColor(color); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/package-info.java new file mode 100644 index 0000000..cb4f6be --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/package-info.java @@ -0,0 +1,9 @@ +/** + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + *

+ * + * Text canvas is a 2D canvas that can be used to render text. + */ + +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid2D.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid2D.java new file mode 100644 index 0000000..333d920 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid2D.java @@ -0,0 +1,80 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.geometry.Rectangle; +import eu.svjatoslav.sixth.e3d.math.Transform; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A 2D grid of line segments lying in the XY plane (Z = 0 in local space). + * The grid is divided into configurable numbers of cells along the X and Y axes, + * producing a regular rectangular mesh of lines. + * + *

This shape is useful for rendering floors, walls, reference planes, or any + * flat surface that needs a grid overlay. The grid is positioned and oriented + * in world space using a {@link Transform}.

+ * + *

Usage example:

+ *
{@code
+ * Transform transform = new Transform(new Point3D(0, 100, 0));
+ * Rectangle rect = new Rectangle(new Point2D(-500, -500), new Point2D(500, 500));
+ * LineAppearance appearance = new LineAppearance(1, Color.GRAY);
+ * Grid2D grid = new Grid2D(transform, rect, 10, 10, appearance);
+ * shapeCollection.addShape(grid);
+ * }
+ * + * @see Grid3D + * @see LineAppearance + * @see AbstractCompositeShape + */ +public class Grid2D extends AbstractCompositeShape { + + /** + * Constructs a 2D grid in the XY plane with the specified dimensions and + * number of divisions. + * + * @param transform the transform defining the grid's position and orientation + * in world space + * @param rectangle the rectangular dimensions of the grid in local XY space + * @param xDivisionCount the number of divisions (cells) along the X axis; + * produces {@code xDivisionCount + 1} vertical lines + * @param yDivisionCount the number of divisions (cells) along the Y axis; + * produces {@code yDivisionCount + 1} horizontal lines + * @param appearance the line appearance (color, width) used for all grid lines + */ + public Grid2D(final Transform transform, final Rectangle rectangle, + final int xDivisionCount, final int yDivisionCount, + final LineAppearance appearance) { + + super(transform); + + final double stepY = rectangle.getHeight() / yDivisionCount; + final double stepX = rectangle.getWidth() / xDivisionCount; + + for (int ySlice = 0; ySlice <= yDivisionCount; ySlice++) { + final double y = (ySlice * stepY) + rectangle.getLowerY(); + + for (int xSlice = 0; xSlice <= xDivisionCount; xSlice++) { + final double x = (xSlice * stepX) + rectangle.getLowerX(); + + final Point3D p1 = new Point3D(x, y, 0); + final Point3D p2 = new Point3D(x + stepX, y, 0); + final Point3D p3 = new Point3D(x, y + stepY, 0); + + if (xSlice < xDivisionCount) + addShape(appearance.getLine(p1, p2)); + + if (ySlice < yDivisionCount) + addShape(appearance.getLine(p1, p3)); + } + + } + + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid3D.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid3D.java new file mode 100755 index 0000000..f7ea422 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid3D.java @@ -0,0 +1,99 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A 3D grid of line segments filling a rectangular volume defined by two + * opposite corner points. Lines run along all three axes (X, Y, and Z) at + * regular intervals determined by the step size. + * + *

At each grid intersection point, up to three line segments are created + * (one along each axis), forming a three-dimensional lattice. The corner + * points are automatically normalized so that {@code p1} holds the minimum + * coordinates and {@code p2} holds the maximum coordinates.

+ * + *

This shape is useful for visualizing 3D space, voxel boundaries, or + * spatial reference grids in a scene.

+ * + *

Usage example:

+ *
{@code
+ * LineAppearance appearance = new LineAppearance(1, Color.GRAY);
+ * Point3D corner1 = new Point3D(-100, -100, -100);
+ * Point3D corner2 = new Point3D(100, 100, 100);
+ * Grid3D grid = new Grid3D(corner1, corner2, 50, appearance);
+ * shapeCollection.addShape(grid);
+ * }
+ * + * @see Grid2D + * @see LineAppearance + * @see AbstractCompositeShape + */ +public class Grid3D extends AbstractCompositeShape { + + /** + * Constructs a 3D grid filling the volume between two corner points. + * The corner points are copied and normalized internally so that grid + * generation always proceeds from minimum to maximum coordinates. + * + * @param p1t the first corner point defining the volume (copied, not modified) + * @param p2t the diagonally opposite corner point (copied, not modified) + * @param step the spacing between grid lines along each axis; must be + * positive + * @param appearance the line appearance (color, width) used for all grid lines + */ + public Grid3D(final Point3D p1t, final Point3D p2t, final double step, + final LineAppearance appearance) { + + super(); + + final Point3D p1 = new Point3D(p1t); + final Point3D p2 = new Point3D(p2t); + + if (p1.x > p2.x) { + final double tmp = p1.x; + p1.x = p2.x; + p2.x = tmp; + } + + if (p1.y > p2.y) { + final double tmp = p1.y; + p1.y = p2.y; + p2.y = tmp; + } + + if (p1.z > p2.z) { + final double tmp = p1.z; + p1.z = p2.z; + p2.z = tmp; + } + + for (double x = p1.x; x <= p2.x; x += step) + for (double y = p1.y; y <= p2.y; y += step) + for (double z = p1.z; z <= p2.z; z += step) { + + final Point3D p3 = new Point3D(x, y, z); + + if ((x + step) <= p2.x) { + final Point3D point3d2 = new Point3D(x + step, y, z); + addShape(appearance.getLine(p3, point3d2)); + } + + if ((y + step) <= p2.y) { + final Point3D point3d3 = new Point3D(x, y + step, z); + addShape(appearance.getLine(p3, point3d3)); + } + + if ((z + step) <= p2.z) { + final Point3D point3d4 = new Point3D(x, y, z + step); + addShape(appearance.getLine(p3, point3d4)); + } + + } + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java new file mode 100755 index 0000000..b776db4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java @@ -0,0 +1,89 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.sixth.e3d.geometry.Box; +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A wireframe box (rectangular parallelepiped) composed of 12 line segments + * representing the edges of the box. The box is axis-aligned, defined by two + * opposite corner points. + * + *

The wireframe consists of four edges along each axis: four edges parallel + * to X, four parallel to Y, and four parallel to Z.

+ * + *

Usage example:

+ *
{@code
+ * LineAppearance appearance = new LineAppearance(2, Color.GREEN);
+ * Point3D corner1 = new Point3D(-50, -50, -50);
+ * Point3D corner2 = new Point3D(50, 50, 50);
+ * WireframeBox box = new WireframeBox(corner1, corner2, appearance);
+ * shapeCollection.addShape(box);
+ * }
+ * + * @see WireframeCube + * @see Box + * @see LineAppearance + * @see AbstractCompositeShape + */ +public class WireframeBox extends AbstractCompositeShape { + + /** + * Constructs a wireframe box from a {@link Box} geometry object. + * + * @param box the axis-aligned box defining the two opposite corners + * @param appearance the line appearance (color, width) used for all 12 edges + */ + public WireframeBox(final Box box, + final LineAppearance appearance) { + + this(box.p1, box.p2, appearance); + } + + /** + * Constructs a wireframe box from two opposite corner points. The corners + * do not need to be in any particular min/max order; the constructor uses + * each coordinate independently to form all eight vertices of the box. + * + * @param p1 the first corner point of the box + * @param p2 the diagonally opposite corner point of the box + * @param appearance the line appearance (color, width) used for all 12 edges + */ + public WireframeBox(final Point3D p1, final Point3D p2, + final LineAppearance appearance) { + super(); + + addShape(appearance.getLine(new Point3D(p1.x, p1.y, p1.z), new Point3D( + p2.x, p1.y, p1.z))); + addShape(appearance.getLine(new Point3D(p1.x, p2.y, p1.z), new Point3D( + p2.x, p2.y, p1.z))); + addShape(appearance.getLine(new Point3D(p1.x, p1.y, p1.z), new Point3D( + p1.x, p2.y, p1.z))); + addShape(appearance.getLine(new Point3D(p2.x, p1.y, p1.z), new Point3D( + p2.x, p2.y, p1.z))); + + addShape(appearance.getLine(new Point3D(p1.x, p1.y, p2.z), new Point3D( + p2.x, p1.y, p2.z))); + addShape(appearance.getLine(new Point3D(p1.x, p2.y, p2.z), new Point3D( + p2.x, p2.y, p2.z))); + addShape(appearance.getLine(new Point3D(p1.x, p1.y, p2.z), new Point3D( + p1.x, p2.y, p2.z))); + addShape(appearance.getLine(new Point3D(p2.x, p1.y, p2.z), new Point3D( + p2.x, p2.y, p2.z))); + + addShape(appearance.getLine(new Point3D(p1.x, p1.y, p1.z), new Point3D( + p1.x, p1.y, p2.z))); + addShape(appearance.getLine(new Point3D(p1.x, p2.y, p1.z), new Point3D( + p1.x, p2.y, p2.z))); + addShape(appearance.getLine(new Point3D(p2.x, p1.y, p1.z), new Point3D( + p2.x, p1.y, p2.z))); + addShape(appearance.getLine(new Point3D(p2.x, p2.y, p1.z), new Point3D( + p2.x, p2.y, p2.z))); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.java new file mode 100755 index 0000000..5f31fd3 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.java @@ -0,0 +1,45 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance; + +/** + * A wireframe cube (equal-length sides) centered at a given point in 3D space. + * This is a convenience subclass of {@link WireframeBox} that constructs an + * axis-aligned cube from a center point and a half-side length. + * + *

The cube extends {@code size} units in each direction from the center, + * resulting in a total edge length of {@code 2 * size}.

+ * + *

Usage example:

+ *
{@code
+ * LineAppearance appearance = new LineAppearance(1, Color.CYAN);
+ * WireframeCube cube = new WireframeCube(new Point3D(0, 0, 200), 50, appearance);
+ * shapeCollection.addShape(cube);
+ * }
+ * + * @see WireframeBox + * @see LineAppearance + */ +public class WireframeCube extends WireframeBox { + + /** + * Constructs a wireframe cube centered at the given point. + * + * @param center the center point of the cube in 3D space + * @param size the half-side length; the cube extends this distance from + * the center along each axis, giving a total edge length + * of {@code 2 * size} + * @param appearance the line appearance (color, width) used for all 12 edges + */ + public WireframeCube(final Point3D center, final double size, + final LineAppearance appearance) { + super(new Point3D(center.x - size, center.y - size, center.z - size), + new Point3D(center.x + size, center.y + size, center.z + size), + appearance); + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeDrawing.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeDrawing.java new file mode 100755 index 0000000..a015852 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeDrawing.java @@ -0,0 +1,75 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A freeform polyline drawing tool that connects sequential points with line + * segments. Points are added one at a time via {@link #addPoint(Point3D)}; + * each new point is connected to the previously added point by a line. + * + *

The first point added establishes the starting position without drawing + * a line. Each subsequent point creates a new line segment from the previous + * point to the new one.

+ * + *

This shape is useful for drawing paths, trails, trajectories, or + * arbitrary wireframe shapes that are defined as a sequence of vertices.

+ * + *

Usage example:

+ *
{@code
+ * LineAppearance appearance = new LineAppearance(2, Color.YELLOW);
+ * WireframeDrawing drawing = new WireframeDrawing(appearance);
+ * drawing.addPoint(new Point3D(0, 0, 0));
+ * drawing.addPoint(new Point3D(100, 50, 0));
+ * drawing.addPoint(new Point3D(200, 0, 0));
+ * shapeCollection.addShape(drawing);
+ * }
+ * + * @see LineAppearance + * @see AbstractCompositeShape + */ +public class WireframeDrawing extends AbstractCompositeShape { + + /** The line appearance used for all segments in this drawing. */ + final private LineAppearance lineAppearance; + + /** The most recently added point, used as the start of the next line segment. */ + Point3D currentPoint; + + /** + * Constructs a new empty wireframe drawing with the given line appearance. + * + * @param lineAppearance the line appearance (color, width) used for all + * line segments added to this drawing + */ + public WireframeDrawing(final LineAppearance lineAppearance) { + super(); + this.lineAppearance = lineAppearance; + } + + /** + * Adds a new point to the drawing. If this is the first point, it sets the + * starting position. Otherwise, a line segment is created from the previous + * point to this new point. + * + *

The point is defensively copied, so subsequent modifications to the + * passed {@code point3d} object will not affect the drawing.

+ * + * @param point3d the point to add to the polyline + */ + public void addPoint(final Point3D point3d) { + if (currentPoint != null) { + final Line line = lineAppearance.getLine(currentPoint, point3d); + addShape(line); + } + + currentPoint = new Point3D(point3d); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeSphere.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeSphere.java new file mode 100755 index 0000000..6c885f4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeSphere.java @@ -0,0 +1,87 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +import java.util.ArrayList; + +/** + * A wireframe sphere approximation built from rings of connected line segments. + * The sphere is generated using parametric spherical coordinates, producing a + * latitude-longitude grid of vertices connected by lines. + * + *

The sphere is divided into 20 longitudinal slices and 20 latitudinal rings + * (using a step of {@code PI / 10} radians). Adjacent vertices within each ring + * are connected, and corresponding vertices between consecutive rings are also + * connected, forming a mesh that approximates a sphere surface.

+ * + *

Usage example:

+ *
{@code
+ * LineAppearance appearance = new LineAppearance(1, Color.WHITE);
+ * WireframeSphere sphere = new WireframeSphere(new Point3D(0, 0, 300), 100f, appearance);
+ * shapeCollection.addShape(sphere);
+ * }
+ * + * @see LineAppearance + * @see AbstractCompositeShape + */ +public class WireframeSphere extends AbstractCompositeShape { + + /** Stores the vertices of the previously generated ring for inter-ring connections. */ + ArrayList previousRing = new ArrayList<>(); + + /** + * Constructs a wireframe sphere at the given location with the specified radius. + * The sphere is approximated by a grid of line segments generated from + * parametric spherical coordinates. + * + * @param location the center point of the sphere in 3D space + * @param radius the radius of the sphere + * @param lineFactory the line appearance (color, width) used for all line segments + */ + public WireframeSphere(final Point3D location, final float radius, + final LineAppearance lineFactory) { + super(location); + + final double step = Math.PI / 10; + + final Point3D center = new Point3D(); + + int ringIndex = 0; + + for (double j = 0d; j <= (Math.PI * 2); j += step) { + + Point3D oldPoint = null; + int pointIndex = 0; + + for (double i = 0; i <= (Math.PI * 2); i += step) { + final Point3D newPoint = new Point3D(0, 0, radius); + newPoint.rotate(center, i, j); + + if (oldPoint != null) + addShape(lineFactory.getLine(newPoint, oldPoint)); + + if (ringIndex > 0) { + final Point3D previousRingPoint = previousRing + .get(pointIndex); + addShape(lineFactory.getLine(newPoint, previousRingPoint)); + + previousRing.set(pointIndex, newPoint); + } else + previousRing.add(newPoint); + + oldPoint = newPoint; + pointIndex++; + } + + ringIndex++; + } + + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/BorderLine.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/BorderLine.java new file mode 100644 index 0000000..1e5e1ff --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/BorderLine.java @@ -0,0 +1,81 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.slicer; + +import eu.svjatoslav.sixth.e3d.geometry.Point2D; +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.math.Vertex; + +/** + * Represents an edge (border line) of a triangle in the polygon slicing pipeline. + * + *

A {@code BorderLine} connects two {@link Vertex} endpoints and carries an + * identification {@link #count} used by the {@link Slicer} to determine which edge + * of the original triangle this line corresponds to (1, 2, or 3). This identification + * is essential for correct recursive subdivision -- when the longest edge is split, + * the slicer uses the count to decide how to partition the triangle into two + * smaller triangles.

+ * + * @see Slicer + * @see Vertex + */ +public class BorderLine { + + /** + * The edge identifier (1, 2, or 3) indicating which edge of the original triangle + * this border line represents. Used by {@link Slicer} during recursive subdivision. + */ + public int count; + + /** + * The first vertex endpoint of this edge. + */ + Vertex c1; + + /** + * The second vertex endpoint of this edge. + */ + Vertex c2; + + /** + * Creates an uninitialized border line for reuse. + */ + public BorderLine() { + } + + /** + * Sets the endpoints and edge identifier for this border line. + * + * @param c1 the first vertex endpoint + * @param c2 the second vertex endpoint + * @param count the edge identifier (1, 2, or 3) + */ + public void set(final Vertex c1, final Vertex c2, final int count) { + this.c1 = c1; + this.c2 = c2; + this.count = count; + } + + /** + * Computes the 3D Euclidean distance between the two endpoint vertices. + * + * @return the length of this edge in world-space units + */ + public double getLength() { + return c1.coordinate.getDistanceTo(c2.coordinate); + } + + /** + * Computes the midpoint vertex of this edge by averaging both the 3D coordinates + * and the 2D texture coordinates of the two endpoints. + * + * @return a new {@link Vertex} at the midpoint, with interpolated texture coordinates + */ + public Vertex getMiddlePoint() { + return new Vertex( + new Point3D().computeMiddlePoint(c1.coordinate, c2.coordinate), + new Point2D().setToMiddle(c1.textureCoordinate, c2.textureCoordinate)); + } +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java new file mode 100644 index 0000000..20b80e9 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java @@ -0,0 +1,141 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.slicer; + +import eu.svjatoslav.sixth.e3d.math.Vertex; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon; + +import java.util.ArrayList; +import java.util.List; + +/** + * Recursively subdivides textured polygons into smaller triangles for + * perspective-correct rendering and level-of-detail management. + * + *

When a textured polygon covers a large area of the screen, rendering it as + * a single triangle can produce visible texture distortion due to affine (non-perspective) + * texture interpolation. The {@code Slicer} addresses this by recursively splitting + * triangles along their longest edge until no edge exceeds {@link #maxDistance}.

+ * + *

The subdivision algorithm works as follows:

+ *
    + *
  1. For a given triangle, compute the lengths of all three edges.
  2. + *
  3. Sort edges by length and find the longest one.
  4. + *
  5. If the longest edge is shorter than {@code maxDistance}, emit the triangle as-is.
  6. + *
  7. Otherwise, split the longest edge at its midpoint (interpolating both 3D and + * texture coordinates) and recurse on the two resulting sub-triangles.
  8. + *
+ * + *

This class is used by + * {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape} + * to break large composite shapes into appropriately-sized sub-polygons.

+ * + * @see BorderLine + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon + */ +public class Slicer { + + private static final BorderLine line1 = new BorderLine(); + private static final BorderLine line2 = new BorderLine(); + private static final BorderLine line3 = new BorderLine(); + + /** + * Maximum distance between two points. + * If the distance is greater than this value, the polygon will be sliced. + * Otherwise, it will be added to the result. + */ + private final double maxDistance; + + /** + * Result of slicing. + */ + private final List result = new ArrayList<>(); + + /** + * Creates a new slicer with the specified maximum edge length. + * + * @param maxDistance the maximum allowed edge length in world-space units; + * edges longer than this will be subdivided + */ + public Slicer(final double maxDistance) { + this.maxDistance = maxDistance; + } + + private void considerSlicing(final Vertex c1, + final Vertex c2, + final Vertex c3, + final TexturedPolygon originalPolygon) { + + line1.set(c1, c2, 1); + line2.set(c2, c3, 2); + line3.set(c3, c1, 3); + + // Inline sort for 3 elements by length to avoid array allocation + BorderLine a = line1; + BorderLine b = line2; + BorderLine c = line3; + BorderLine t; + if (a.getLength() > b.getLength()) { t = a; a = b; b = t; } + if (b.getLength() > c.getLength()) { t = b; b = c; c = t; } + if (a.getLength() > b.getLength()) { t = a; a = b; b = t; } + + final BorderLine longestLine = c; + + if (longestLine.getLength() < maxDistance) { + final TexturedPolygon polygon = new TexturedPolygon(c1, c2, c3, + originalPolygon.texture); + + polygon.setMouseInteractionController(originalPolygon.mouseInteractionController); + + getResult().add(polygon); + return; + } + + final Vertex middle = longestLine.getMiddlePoint(); + + switch (longestLine.count) { + case 1: + considerSlicing(c1, middle, c3, originalPolygon); + considerSlicing(middle, c2, c3, originalPolygon); + return; + case 2: + considerSlicing(c1, c2, middle, originalPolygon); + considerSlicing(middle, c3, c1, originalPolygon); + return; + case 3: + considerSlicing(c1, c2, middle, originalPolygon); + considerSlicing(middle, c2, c3, originalPolygon); + } + + } + + /** + * Returns the list of subdivided polygons produced by the slicing process. + * + * @return an unmodifiable view of the resulting {@link TexturedPolygon} list + */ + public List getResult() { + return result; + } + + /** + * Slices the given textured polygon into smaller triangles. + * + *

After calling this method, retrieve the resulting sub-polygons via + * {@link #getResult()}. The original polygon's texture reference and + * mouse interaction controller are preserved on all sub-polygons.

+ * + * @param originalPolygon the polygon to subdivide + */ + public void slice(final TexturedPolygon originalPolygon) { + + considerSlicing( + originalPolygon.coordinates[0], + originalPolygon.coordinates[1], + originalPolygon.coordinates[2], + originalPolygon); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java new file mode 100644 index 0000000..bf319b7 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java @@ -0,0 +1,383 @@ +/* + * 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.gui.RenderingContext; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.awt.image.WritableRaster; + +import static java.util.Arrays.fill; + +/** + * Represents a 2D texture with mipmap support for level-of-detail rendering. + * + *

A {@code Texture} contains a primary bitmap at native resolution, along with + * cached upscaled and downscaled versions (mipmaps) that are lazily generated on demand. + * This mipmap chain enables efficient texture sampling at varying distances from the camera, + * avoiding aliasing artifacts for distant surfaces and pixelation for close-up views.

+ * + *

The texture also exposes a {@link java.awt.Graphics2D} context backed by the primary + * bitmap's {@link java.awt.image.BufferedImage}, allowing dynamic rendering of text, + * shapes, or other 2D content directly onto the texture surface. Anti-aliasing is + * enabled by default on this graphics context.

+ * + *

Mipmap levels

+ *
    + *
  • Primary bitmap -- the native resolution; always available.
  • + *
  • Downsampled bitmaps -- up to 8 levels, each half the size of the previous. + * Used when the texture is rendered at zoom levels below 1.0.
  • + *
  • Upsampled bitmaps -- configurable count (set at construction time), each + * double the size of the previous. Used when the texture is rendered at zoom levels + * above 2.0.
  • + *
+ * + *

Usage example

+ *
{@code
+ * Texture tex = new Texture(256, 256, 3);
+ * // Draw content using the Graphics2D context
+ * tex.graphics.setColor(java.awt.Color.RED);
+ * tex.graphics.fillRect(0, 0, 256, 256);
+ * // Invalidate cached mipmaps after modifying the primary bitmap
+ * tex.resetResampledBitmapCache();
+ * // Retrieve the appropriate mipmap for a given zoom level
+ * TextureBitmap bitmap = tex.getZoomedBitmap(0.5);
+ * }
+ * + * @see TextureBitmap + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon + */ +public class Texture { + + /** + * The primary (native resolution) bitmap for this texture. + * All dynamic drawing via {@link #graphics} modifies this bitmap's backing data. + */ + public final TextureBitmap primaryBitmap; + + /** + * A {@link java.awt.Graphics2D} context for drawing 2D content onto the primary bitmap. + * Anti-aliasing for both geometry and text is enabled by default. + */ + public final java.awt.Graphics2D graphics; + + /** + * Cached upsampled (enlarged) versions of the primary bitmap. + * Index 0 is 2x the primary, index 1 is 4x, and so on. + * Entries are lazily populated on first access. + */ + TextureBitmap[] upSampled; + + /** + * Cached downsampled (reduced) versions of the primary bitmap. + * Index 0 is 1/2 the primary, index 1 is 1/4, and so on. + * Entries are lazily populated on first access. + */ + TextureBitmap[] downSampled = new TextureBitmap[8]; + + /** + * Creates a new texture with the specified dimensions and upscale capacity. + * + *

The underlying {@link java.awt.image.BufferedImage} is created using + * {@link eu.svjatoslav.sixth.e3d.gui.RenderingContext#bufferedImageType} for + * compatibility with the raster rendering pipeline.

+ * + * @param width the width of the primary bitmap in pixels + * @param height the height of the primary bitmap in pixels + * @param maxUpscale the maximum number of upscaled mipmap levels to support + * (each level doubles the resolution) + */ + public Texture(final int width, final int height, final int maxUpscale) { + upSampled = new TextureBitmap[maxUpscale]; + + final BufferedImage bufferedImage = new BufferedImage(width, height, + RenderingContext.bufferedImageType); + + final WritableRaster raster = bufferedImage.getRaster(); + final DataBufferByte dbi = (DataBufferByte) raster.getDataBuffer(); + graphics = (Graphics2D) bufferedImage.getGraphics(); + + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + primaryBitmap = new TextureBitmap(width, height, dbi.getData(), 1); + } + + /** + * Determines the appropriate downscale factor index for a given zoom level. + * + *

Iterates through the downsampled mipmap levels (each halving the size) + * and returns the index of the first level whose effective size falls below + * the requested zoom.

+ * + * @param zoom the zoom level (typically less than 1.0 for downscaling) + * @return the index into the {@code downSampled} array to use, clamped to the + * maximum available level + */ + public int detectDownscaleFactorForZoom(final double zoom) { + double size = 1; + for (int i = 0; i < downSampled.length; i++) { + size = size / 2; + if (size < zoom) + return i; + } + + return downSampled.length - 1; + } + + /** + * Determines the appropriate upscale factor index for a given zoom level. + * + *

Iterates through the upsampled mipmap levels (each doubling the size) + * and returns the index of the first level whose effective size exceeds + * the requested zoom.

+ * + * @param zoom the zoom level (typically greater than 2.0 for upscaling) + * @return the index into the {@code upSampled} array to use, clamped to the + * maximum available level + */ + public int detectUpscaleFactorForZoom(final double zoom) { + double size = 2; + for (int i = 0; i < upSampled.length; i++) { + size = size * 2; + if (size > zoom) + return i; + } + + return upSampled.length - 1; + } + + /** + * Downscale given bitmap by factor of 2. + * + * @param originalBitmap Bitmap to downscale. + * @return Downscaled bitmap. + */ + public TextureBitmap downscaleBitmap(final TextureBitmap originalBitmap) { + int newWidth = originalBitmap.width / 2; + int newHeight = originalBitmap.height / 2; + + // Enforce minimum width and height + if (newWidth < 1) + newWidth = 1; + if (newHeight < 1) + newHeight = 1; + + final TextureBitmap downScaled = new TextureBitmap(newWidth, newHeight, + originalBitmap.multiplicationFactor / 2d); + + final ColorAccumulator accumulator = new ColorAccumulator(); + + for (int y = 0; y < newHeight; y++) + for (int x = 0; x < newWidth; x++) { + accumulator.reset(); + accumulator.accumulate(originalBitmap, x * 2, y * 2); + accumulator.accumulate(originalBitmap, (x * 2) + 1, y * 2); + accumulator.accumulate(originalBitmap, x * 2, (y * 2) + 1); + accumulator + .accumulate(originalBitmap, (x * 2) + 1, (y * 2) + 1); + accumulator.storeResult(downScaled, x, y); + } + + return downScaled; + } + + /** + * Returns a downscaled bitmap at the specified mipmap level, creating it lazily if needed. + * + *

Level 0 is half the primary resolution, level 1 is a quarter, and so on. + * Each level is derived by downscaling the previous level by a factor of 2.

+ * + * @param scaleFactor the downscale level index (0 = 1/2 size, 1 = 1/4 size, etc.) + * @return the cached or newly created downscaled {@link TextureBitmap} + * @see #downscaleBitmap(TextureBitmap) + */ + public TextureBitmap getDownscaledBitmap(final int scaleFactor) { + if (downSampled[scaleFactor] == null) { + + TextureBitmap largerBitmap; + if (scaleFactor == 0) + largerBitmap = primaryBitmap; + else + largerBitmap = getDownscaledBitmap(scaleFactor - 1); + + downSampled[scaleFactor] = downscaleBitmap(largerBitmap); + } + + return downSampled[scaleFactor]; + } + + /** + * Returns the bitmap that should be used for rendering at the given zoom + * + * @param scaleFactor The upscale factor + * @return The bitmap + */ + public TextureBitmap getUpscaledBitmap(final int scaleFactor) { + if (upSampled[scaleFactor] == null) { + + TextureBitmap smallerBitmap; + if (scaleFactor == 0) + smallerBitmap = primaryBitmap; + else + smallerBitmap = getUpscaledBitmap(scaleFactor - 1); + + upSampled[scaleFactor] = upscaleBitmap(smallerBitmap); + } + + return upSampled[scaleFactor]; + } + + /** + * Returns the bitmap that should be used for rendering at the given zoom + * + * @param zoomLevel The zoom level + * @return The bitmap + */ + public TextureBitmap getZoomedBitmap(final double zoomLevel) { + + if (zoomLevel < 1) { + final int downscaleFactor = detectDownscaleFactorForZoom(zoomLevel); + return getDownscaledBitmap(downscaleFactor); + } else if (zoomLevel > 2) { + final int upscaleFactor = detectUpscaleFactorForZoom(zoomLevel); + + if (upscaleFactor < 0) + return primaryBitmap; + + return getUpscaledBitmap(upscaleFactor); + } + + // System.out.println(zoomLevel); + return primaryBitmap; + } + + /** + * Resets the cache of resampled bitmaps + */ + public void resetResampledBitmapCache() { + fill(upSampled, null); + + fill(downSampled, null); + } + + /** + * Upscales the given bitmap by a factor of 2 + * + * @param originalBitmap The bitmap to upscale + * @return The upscaled bitmap + */ + public TextureBitmap upscaleBitmap(final TextureBitmap originalBitmap) { + final int newWidth = originalBitmap.width * 2; + final int newHeight = originalBitmap.height * 2; + + final TextureBitmap upScaled = new TextureBitmap(newWidth, newHeight, + originalBitmap.multiplicationFactor * 2d); + + final ColorAccumulator accumulator = new ColorAccumulator(); + + for (int y = 0; y < originalBitmap.height; y++) + for (int x = 0; x < originalBitmap.width; x++) { + accumulator.reset(); + accumulator.accumulate(originalBitmap, x, y); + accumulator.storeResult(upScaled, x * 2, y * 2); + + accumulator.reset(); + accumulator.accumulate(originalBitmap, x, y); + accumulator.accumulate(originalBitmap, x + 1, y); + accumulator.storeResult(upScaled, (x * 2) + 1, y * 2); + + accumulator.reset(); + accumulator.accumulate(originalBitmap, x, y); + accumulator.accumulate(originalBitmap, x, y + 1); + accumulator.storeResult(upScaled, x * 2, (y * 2) + 1); + + accumulator.reset(); + accumulator.accumulate(originalBitmap, x, y); + accumulator.accumulate(originalBitmap, x + 1, y); + accumulator.accumulate(originalBitmap, x, y + 1); + accumulator.accumulate(originalBitmap, x + 1, y + 1); + accumulator.storeResult(upScaled, (x * 2) + 1, (y * 2) + 1); + } + + return upScaled; + } + + /** + * A helper class that accumulates color values for a given area of a bitmap + */ + public static class ColorAccumulator { + // Accumulated color values + public int r, g, b, a; + + // Number of pixels that have been accumulated + public int pixelCount = 0; + + /** + * Accumulates the color values of the given pixel + * + * @param bitmap The bitmap + * @param x The x coordinate of the pixel + * @param y The y coordinate of the pixel + */ + public void accumulate(final TextureBitmap bitmap, final int x, + final int y) { + int address = bitmap.getAddress(x, y); + + a += bitmap.bytes[address] & 0xff; + address++; + + b += bitmap.bytes[address] & 0xff; + address++; + + g += bitmap.bytes[address] & 0xff; + address++; + + r += bitmap.bytes[address] & 0xff; + + pixelCount++; + } + + /** + * Resets the accumulator + */ + public void reset() { + a = 0; + r = 0; + g = 0; + b = 0; + pixelCount = 0; + } + + /** + * Stores the accumulated color values in the given bitmap + * + * @param bitmap The bitmap + * @param x The x coordinate of the pixel + * @param y The y coordinate of the pixel + */ + public void storeResult(final TextureBitmap bitmap, final int x, + final int y) { + int address = bitmap.getAddress(x, y); + + bitmap.bytes[address] = (byte) (a / pixelCount); + address++; + + bitmap.bytes[address] = (byte) (b / pixelCount); + address++; + + bitmap.bytes[address] = (byte) (g / pixelCount); + address++; + + bitmap.bytes[address] = (byte) (r / pixelCount); + } + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.java new file mode 100644 index 0000000..a2d253d --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.java @@ -0,0 +1,253 @@ +/* + * 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; + +/** + * Represents a single resolution level of a texture as a raw byte array. + * + *

Each pixel is stored as 4 consecutive bytes in ABGR order: + * Alpha, Blue, Green, Red. This byte ordering matches the + * {@link java.awt.image.BufferedImage#TYPE_4BYTE_ABGR} format used by + * the rendering pipeline.

+ * + *

{@code TextureBitmap} is used internally by {@link Texture} to represent + * individual mipmap levels. The {@link #multiplicationFactor} records the + * scale ratio relative to the primary (native) resolution -- for example, + * a value of 0.5 means this bitmap is half the original size, and 2.0 + * means it is double.

+ * + *

This class provides low-level pixel operations including:

+ *
    + *
  • Alpha-blended pixel transfer to a target raster ({@link #drawPixel(int, byte[], int)})
  • + *
  • Direct pixel writes using engine {@link Color} ({@link #drawPixel(int, int, Color)})
  • + *
  • Filled rectangle drawing ({@link #drawRectangle(int, int, int, int, Color)})
  • + *
  • Full-surface color fill ({@link #fillColor(Color)})
  • + *
+ * + * @see Texture + * @see Color + */ +public class TextureBitmap { + + /** + * Raw pixel data in ABGR byte order (Alpha, Blue, Green, Red). + * The array length is {@code width * height * 4}. + */ + public final byte[] bytes; + + /** + * The width of this bitmap in pixels. + */ + public final int width; + + /** + * The height of this bitmap in pixels. + */ + public final int height; + + /** + * The scale factor of this bitmap relative to the primary (native) texture resolution. + * A value of 1.0 indicates the native resolution, 0.5 indicates half-size, 2.0 indicates double-size, etc. + */ + public double multiplicationFactor; + + /** + * Creates a texture bitmap backed by an existing byte array. + * + *

This constructor is typically used when the bitmap data is obtained from + * a {@link java.awt.image.BufferedImage}'s raster, allowing direct access to + * the image's pixel data without copying.

+ * + * @param width the bitmap width in pixels + * @param height the bitmap height in pixels + * @param bytes the raw pixel data array (must be at least {@code width * height * 4} bytes) + * @param multiplicationFactor the scale factor relative to the native texture resolution + */ + public TextureBitmap(final int width, final int height, final byte[] bytes, + final double multiplicationFactor) { + + this.width = width; + this.height = height; + this.bytes = bytes; + this.multiplicationFactor = multiplicationFactor; + } + + /** + * Creates a texture bitmap with a newly allocated byte array. + * + *

The pixel data array is initialized to all zeros (fully transparent black).

+ * + * @param width the bitmap width in pixels + * @param height the bitmap height in pixels + * @param multiplicationFactor the scale factor relative to the native texture resolution + */ + public TextureBitmap(final int width, final int height, + final double multiplicationFactor) { + + this(width, height, new byte[width * height * 4], multiplicationFactor); + } + + /** + * Transfer (render) one pixel from current {@link TextureBitmap} to target raster bitmap. + * + * @param sourceBitmapPixelAddress Pixel address within current {@link TextureBitmap} as indicated by its offset. + * @param targetBitmap Bitmap of the target image where pixel should be rendered to. + * @param targetBitmapPixelAddress Pixel location within target image where pixel should be rendered to. + */ + public void drawPixel(int sourceBitmapPixelAddress, + final byte[] targetBitmap, int targetBitmapPixelAddress) { + + final int textureAlpha = bytes[sourceBitmapPixelAddress] & 0xff; + + if (textureAlpha == 0) + return; + + if (textureAlpha == 255) { + // skip reading of background for fully opaque pixels + targetBitmap[targetBitmapPixelAddress] = (byte) 255; + + targetBitmapPixelAddress++; + sourceBitmapPixelAddress++; + targetBitmap[targetBitmapPixelAddress] = bytes[sourceBitmapPixelAddress]; + + targetBitmapPixelAddress++; + sourceBitmapPixelAddress++; + targetBitmap[targetBitmapPixelAddress] = bytes[sourceBitmapPixelAddress]; + + targetBitmapPixelAddress++; + sourceBitmapPixelAddress++; + targetBitmap[targetBitmapPixelAddress] = bytes[sourceBitmapPixelAddress]; + return; + } + + final int backgroundAlpha = 255 - textureAlpha; + sourceBitmapPixelAddress++; + + targetBitmap[targetBitmapPixelAddress] = (byte) 255; + targetBitmapPixelAddress++; + + targetBitmap[targetBitmapPixelAddress] = (byte) ((((targetBitmap[targetBitmapPixelAddress] & 0xff) * backgroundAlpha) + ((bytes[sourceBitmapPixelAddress] & 0xff) * textureAlpha)) / 256); + sourceBitmapPixelAddress++; + targetBitmapPixelAddress++; + + targetBitmap[targetBitmapPixelAddress] = (byte) ((((targetBitmap[targetBitmapPixelAddress] & 0xff) * backgroundAlpha) + ((bytes[sourceBitmapPixelAddress] & 0xff) * textureAlpha)) / 256); + sourceBitmapPixelAddress++; + targetBitmapPixelAddress++; + + targetBitmap[targetBitmapPixelAddress] = (byte) ((((targetBitmap[targetBitmapPixelAddress] & 0xff) * backgroundAlpha) + ((bytes[sourceBitmapPixelAddress] & 0xff) * textureAlpha)) / 256); + } + + /** + * Draws a single pixel at the specified coordinates using the given color. + * + *

The color components are written directly without alpha blending. + * Coordinates are clamped to the bitmap bounds by {@link #getAddress(int, int)}.

+ * + * @param x the x coordinate of the pixel + * @param y the y coordinate of the pixel + * @param color the color to write + */ + public void drawPixel(final int x, final int y, final Color color) { + int address = getAddress(x, y); + + bytes[address] = (byte) color.a; + + address++; + bytes[address] = (byte) color.b; + + address++; + bytes[address] = (byte) color.g; + + address++; + bytes[address] = (byte) color.r; + } + + /** + * Fills a rectangular region with the specified color. + * + *

If {@code x1 > x2}, the coordinates are swapped to ensure correct rendering. + * The same applies to {@code y1} and {@code y2}. The rectangle is exclusive of the + * right and bottom edges.

+ * + * @param x1 the left x coordinate + * @param y1 the top y coordinate + * @param x2 the right x coordinate (exclusive) + * @param y2 the bottom y coordinate (exclusive) + * @param color the fill color + */ + public void drawRectangle(int x1, final int y1, int x2, final int y2, + final Color color) { + + if (x1 > x2) { + final int tmp = x1; + x1 = x2; + x2 = tmp; + } + + if (y1 > y2) { + final int tmp = x1; + x1 = x2; + x2 = tmp; + } + + for (int y = y1; y < y2; y++) + for (int x = x1; x < x2; x++) + drawPixel(x, y, color); + } + + /** + * Fills the entire bitmap with the specified color. + * + *

Every pixel in the bitmap is set to the given color value, + * overwriting all existing content.

+ * + * @param color the color to fill the entire bitmap with + */ + public void fillColor(final Color color) { + int address = 0; + while (address < bytes.length) { + bytes[address] = (byte) color.a; + address++; + + bytes[address] = (byte) color.b; + address++; + + bytes[address] = (byte) color.g; + address++; + + bytes[address] = (byte) color.r; + address++; + } + } + + /** + * Computes the byte offset into the {@link #bytes} array for the pixel at ({@code x}, {@code y}). + * + *

Coordinates are clamped to the valid range {@code [0, width-1]} and + * {@code [0, height-1]} so that out-of-bounds accesses are safely handled + * by sampling the nearest edge pixel.

+ * + * @param x the x coordinate of the pixel + * @param y the y coordinate of the pixel + * @return the byte offset of the first component (alpha) for the specified pixel + */ + public int getAddress(int x, int y) { + if (x < 0) + x = 0; + + if (x >= width) + x = width - 1; + + if (y < 0) + y = 0; + + if (y >= height) + y = height - 1; + + return ((y * width) + x) * 4; + } +} diff --git a/src/main/resources/eu/svjatoslav/sixth/e3d/examples/hourglass.png b/src/main/resources/eu/svjatoslav/sixth/e3d/examples/hourglass.png new file mode 100644 index 0000000000000000000000000000000000000000..47a1638610ced7ff9f6bf98d7f98c8d923343836 GIT binary patch literal 2161 zcmZ`)X*AT07yivy!fO~?3<@QZJ+e#_jb<>tvai{)4w5Wm-)6*1j9p|I*-F{B5XKTo zNwz4m56PPlMvO6Def)p;pL?GBocrASoO8e2a~~QT>OnY!IRF5FVDwR@CoKL4Y^*0a zAS`V6gqX3&TSx$?PyLJT#Bx%D0!;OEfSQk@YkwwP^i6L8K$sK&;9~)Rb|T@w13;h> z0Q`Fg0N{@SK+w0K%>;4MV0Hdm4+R|mi6U}&=83}Yt8Wtk0DtlQ0SL$|5CVWx?idu( z+`9Lr)m>xrRajJKz*L-D&Y;?Bh4m7G&~iybOv^_hwCb;%(}$v&T?IS3739_%u)*Be;jdzLQuT5= zd0GLT+N(#MJBZE0sQK`@R0&Qv9G>?0v1H-3aJ3z;-ia&8$;mr04Tya6n^LBf8^vh^ z^D#VfMbmI8WNt_(IET`zkSS9vtEgD(o<3yxC^M4+2Kv90Azr&GDn80N|IT^h^y!UI za!E-;w@-`oKtt6T6MiC*h)?L$ATbr4DiUPwxs)Zn#ZZ1U$c4zg&!imGPO@4cWsDuEIzDQ(Nc!vlg!@N*FSL^V<#R8$Dz zxL;GK*UQHxBqRtp_-w9bz_%>r{y2f|*RZJlQZdTz+IU{6p7zDOob2WapBBo_aQ-50 z&uM@XCy6oUXAyl{G~wkmA_C>(OU}wNm$PdHh!n(IhW|QC(uBW{9uoPbDK1X!92gaw zs>Ns>`9U2MEUvipuEC(^PFf5ymmggmKk84>U9Fj|eHsljEWH`!J$T@Q`VL{^496CN0pC}4z8a#fA&VXM77wYf>;}(p#p{~r@KN!~ ztdH0lBpAOkm`zg2<>kPcn5bjJ=E?;Kes=9akmhx7&ej6;C8926WML$6 zd3jkY^PQbIhLNqEo%JChq8|(tRaZj>t(IRf@W*F`z6!x$Hdbb~wz~^GOrb?x(f#09 zSS_$cx?ihz_;YVz$hP`slf><>v$I1Y$^Et4FV;2*e#i>HRQYn7$*9N+63sb4$7 z&268l^_e%`+4Pc>vgyo7@T9C+RPtaWRD=9H$*Gc7wdb~KZrA8O^!e}3 z@;a!Z&r}9&xDh)Xf;byUPYvE81wXH;iCW^Gnwy)8UijJK$Ru84o%@@b)7+1Tu^2mJUxdn5k)WVLPSIXIj(>aac1Hi0dBdHFaQ z;anxHf!p6w$QiuQ($X^C{IhnfikVN@LEI4f&=2uYSl-Rm&BPDG4uL>S?%CT@0)9+B zU7sNzF0ux>e`$`qI*HgZa1IQlPRLr-jBJ-%LZuV>l7x9xrxhWFP??+ei%9K3W>{?b zZF6(F6c54AEGJK<8jVITgfgbeBnJnzBPm$tTleHc3ZFEQory>jepOXf{HkU%Zq1bO zMqV+p!E;opP6&p=DvWa~|LwBk7i-hDRI4AKW0cb>481R&qtz;NSy)K0f$0SN zvzITaR;Ren&RCl`oVDCqmJt^qleu@(dD4@@!ozK79jax|5L|k}+%W{eyo4PYJ33eB zCmK?yrxStr;La!6C8jlBmyRdK=TGW!Y=t1U4kG-Gvkzg`6&1Wfl(ekJY}J-m!(dLF zs^5OK3Rzf4Z=^(FVxEt7Uqv9~FpOOGfq@Tw9ZgMyPoJ_J+iCXUn>d^T^olT9oUI^y z1X5Bj0&xL6=!EVaD2j(ExXqPhxN2ZDtWkf5LGRkz-Hn}BS>$vHbrV#uPps{V$KHyZF1E5O7&ZSw&t+P5!c~xw1O^iV9p=O;$+- luA~HGGCKGl!@c{ip6;Rl-{9f!8+~E`Fla+mjn18z{{SeC1P}lK literal 0 HcmV?d00001 diff --git a/src/test/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLineTest.java b/src/test/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLineTest.java new file mode 100644 index 0000000..d4c3069 --- /dev/null +++ b/src/test/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLineTest.java @@ -0,0 +1,116 @@ +/* + * Sixth - System for data storage, computation, exploration and interaction. + * Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + * +*/ + +package eu.svjatoslav.sixth.e3d.gui.textEditorComponent; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TextLineTest { + + @Test + public void testAddIdent() { + TextLine textLine = new TextLine("test"); + textLine.addIdent(4); + assertEquals(" test", textLine.toString()); + + textLine = new TextLine(); + textLine.addIdent(4); + assertEquals("", textLine.toString()); + } + + @Test + public void testCutFromBeginning() { + TextLine textLine = new TextLine("test"); + textLine.cutFromBeginning(2); + assertEquals("st", textLine.toString()); + + textLine = new TextLine("test"); + textLine.cutFromBeginning(4); + assertEquals("", textLine.toString()); + + textLine = new TextLine("test"); + textLine.cutFromBeginning(5); + assertEquals("", textLine.toString()); + + textLine = new TextLine("test"); + textLine.cutFromBeginning(100); + assertEquals("", textLine.toString()); + } + + @Test + public void testCutSubString() { + TextLine textLine = new TextLine("test"); + assertEquals("es", textLine.cutSubString(1, 3)); + assertEquals("tt", textLine.toString()); + + textLine = new TextLine("test"); + assertEquals("st ", textLine.cutSubString(2, 5)); + assertEquals("te", textLine.toString()); + } + + @Test + public void testGetCharForLocation() { + final TextLine textLine = new TextLine("test"); + assertEquals('s', textLine.getCharForLocation(2)); + assertEquals('t', textLine.getCharForLocation(3)); + assertEquals(' ', textLine.getCharForLocation(4)); + } + + @Test + public void testGetIdent() { + final TextLine textLine = new TextLine(" test"); + assertEquals(3, textLine.getIdent()); + } + + @Test + public void testGetLength() { + final TextLine textLine = new TextLine("test"); + assertEquals(4, textLine.getLength()); + } + + @Test + public void testInsertCharacter() { + TextLine textLine = new TextLine("test"); + textLine.insertCharacter(1, 'o'); + assertEquals("toest", textLine.toString()); + + textLine = new TextLine("test"); + textLine.insertCharacter(5, 'o'); + assertEquals("test o", textLine.toString()); + + } + + @Test + public void testIsEmpty() { + TextLine textLine = new TextLine(""); + assertEquals(true, textLine.isEmpty()); + + textLine = new TextLine(" "); + assertEquals(true, textLine.isEmpty()); + + textLine = new TextLine("l"); + assertEquals(false, textLine.isEmpty()); + } + + @Test + public void testRemoveCharacter() { + TextLine textLine = new TextLine("test"); + textLine.removeCharacter(0); + assertEquals("est", textLine.toString()); + + textLine = new TextLine("test"); + textLine.removeCharacter(3); + assertEquals("tes", textLine.toString()); + + textLine = new TextLine("test"); + textLine.removeCharacter(4); + assertEquals("test", textLine.toString()); + } + +} -- 2.20.1

+ * See also: Let's remove Quaternions from every 3D Engine + * + * @param center center point. + * @param angleXZ angle around XZ axis. + * @param angleYZ angle around YZ axis. + */ + public Point3D rotate(final Point3D center, final double angleXZ, + final double angleYZ) { + final double s1 = sin(angleXZ); + final double c1 = cos(angleXZ); + + final double s2 = sin(angleYZ); + final double c2 = cos(angleYZ); + + x -= center.x; + y -= center.y; + z -= center.z; + + final double y1 = (z * s2) + (y * c2); + final double z1 = (z * c2) - (y * s2); + + final double x1 = (z1 * s1) + (x * c1); + final double z2 = (z1 * c1) - (x * s1); + + x = x1 + center.x; + y = y1 + center.y; + z = z2 + center.z; + + return this; + } + + /** + * Rotate current point around the origin by the given angles. + * + * @param angleXZ angle around the XZ plane (yaw), in radians + * @param angleYZ angle around the YZ plane (pitch), in radians + * @return this point (mutated) + */ + public Point3D rotate(final double angleXZ, final double angleYZ) { + return rotate(new Point3D(0, 0, 0), angleXZ, angleYZ); + } + + /** + * Round current point coordinates to integer values. + */ + public void roundToInteger() { + x = (int) x; + y = (int) y; + z = (int) z; + } + + /** + * Scale down current point by factor. + * All coordinates will be divided by factor. + * + * @param factor factor to scale by. + * @return current point. + */ + public Point3D scaleDown(final double factor) { + x /= factor; + y /= factor; + z /= factor; + return this; + } + + /** + * Scale up current point by factor. + * All coordinates will be multiplied by factor. + * + * @param factor factor to scale by. + * @return current point. + */ + public Point3D scaleUp(final double factor) { + x *= factor; + y *= factor; + z *= factor; + return this; + } + + /** + * Set current point coordinates to given values. + * + * @param x X coordinate. + * @param y Y coordinate. + * @param z Z coordinate. + */ + public void setValues(final double x, final double y, final double z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Subtract other point from current point. Value of other point will not be changed. + * + * @return current point. + */ + public Point3D subtract(final Point3D otherPoint) { + x -= otherPoint.x; + y -= otherPoint.y; + z -= otherPoint.z; + return this; + } + + @Override + public String toString() { + return "x:" + x + " y:" + y + " z:" + z; + } + + /** + * Translate current point along X axis by given increment. + * + * @return current point. + */ + public Point3D translateX(final double xIncrement) { + x += xIncrement; + return this; + } + + /** + * Translate current point along Y axis by given increment. + * + * @return current point. + */ + public Point3D translateY(final double yIncrement) { + y += yIncrement; + return this; + } + + /** + * Translate current point along Z axis by given increment. + * + * @return current point. + */ + public Point3D translateZ(final double zIncrement) { + z += zIncrement; + return this; + } + + /** + * Here we assume that Z coordinate is distance to the viewer. + * If Z is positive, then point is in front of the viewer, and therefore it is visible. + * + * @return point visibility status. + */ + public boolean isVisible() { + return z > 0; + } + + /** + * Resets point coordinates to zero along all axes. + * + * @return current point. + */ + public Point3D zero() { + x = 0; + y = 0; + z = 0; + return this; + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java new file mode 100644 index 0000000..d5f558e --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java @@ -0,0 +1,61 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.geometry; + +/** + * Utility class for polygon operations. + */ +public class Polygon { + + + /** + * Checks if point is on the right side of the line. + * @param point point to check + * @param lineP1 line start point + * @param lineP2 line end point + * @return true if point is on the right side of the line + */ + private static boolean intersectsLine(final Point2D point, Point2D lineP1, + Point2D lineP2) { + + // Sort line points by y coordinate. + if (lineP1.y > lineP2.y) { + final Point2D tmp = lineP1; + lineP1 = lineP2; + lineP2 = tmp; + } + + // Check if point is within line y range. + if (point.y < lineP1.y || point.y > lineP2.y) + return false; + + // Check if point is on the line. + final double xp = lineP2.x - lineP1.x; + final double yp = lineP2.y - lineP1.y; + + final double crossX = lineP1.x + ((xp * (point.y - lineP1.y)) / yp); + + return point.x >= crossX; + } + + public static boolean pointWithinPolygon(final Point2D point, + final Point2D p1, final Point2D p2, final Point2D p3) { + + int intersectionCount = 0; + + if (intersectsLine(point, p1, p2)) + intersectionCount++; + + if (intersectsLine(point, p2, p3)) + intersectionCount++; + + if (intersectsLine(point, p3, p1)) + intersectionCount++; + + return intersectionCount == 1; + + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java new file mode 100644 index 0000000..966d366 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java @@ -0,0 +1,63 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.geometry; + +import static java.lang.Math.abs; +import static java.lang.Math.min; + +/** + * Rectangle class. + */ +public class Rectangle { + + /** + * Rectangle points. + */ + public Point2D p1, p2; + + /** + * Creates new rectangle with given size. + * The rectangle will be centered at the origin. + * The rectangle will be square. + * + * @param size The size of the rectangle. + */ + public Rectangle(final double size) { + p2 = new Point2D(size / 2, size / 2); + p1 = p2.clone().invert(); + } + + /** + * @param p1 The first point of the rectangle. + * @param p2 The second point of the rectangle. + */ + public Rectangle(final Point2D p1, final Point2D p2) { + this.p1 = p1; + this.p2 = p2; + } + + public double getHeight() { + return abs(p1.y - p2.y); + } + + /** + * @return The leftmost x coordinate of the rectangle. + */ + public double getLowerX() { + return min(p1.x, p2.x); + } + + public double getLowerY() { + return min(p1.y, p2.y); + } + + /** + * @return rectangle width. + */ + public double getWidth() { + return abs(p1.x - p2.x); + } + +} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/package-info.java new file mode 100644 index 0000000..9423419 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/package-info.java @@ -0,0 +1,5 @@ +package eu.svjatoslav.sixth.e3d.geometry; + +/** + * Goal is to provide basic geometry classes. + */ \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java new file mode 100644 index 0000000..0baf2fa --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java @@ -0,0 +1,223 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.gui; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.math.Transform; + +/** + * Represents the viewer's camera in the 3D world, with position, orientation, and movement. + * + *