From: Svjatoslav Agejenko Date: Thu, 12 Mar 2026 18:07:52 +0000 (+0200) Subject: Initial commit X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=023800d9f03fadd4b048d7ec1628469a10a21ebb;p=sixth-3d.git Initial commit --- 023800d9f03fadd4b048d7ec1628469a10a21ebb 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 0000000..7094240 Binary files /dev/null and b/doc/example.png differ 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. + *

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

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:

+ * + * + *

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

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

+ * + * + * @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 0000000..47a1638 Binary files /dev/null and b/src/main/resources/eu/svjatoslav/sixth/e3d/examples/hourglass.png differ 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()); + } + +}