│ ├── Cell.java
│ ├── Matrix.java
│ └── Star.java
-├── terrain_demo/ - Procedural terrain generation demo
+├── terrain_demo/ - Procedural terrain generation demo
│ ├── DiamondSquareTerrain.java
│ ├── FractalTree.java
+│ ├── Branch.java
│ └── TerrainDemo.java
├── galaxy_demo/ - Galaxy simulation demo
│ ├── Galaxy.java
│ └── PointCloudDemo.java
-├── RandomPolygonsDemo.java
-├── OctreeDemo.java
-├── graph_demo/ - 3D graph visualization demos
+├── graph_demo/ - 3D graph visualization demos
│ ├── MathGraphsDemo.java
│ └── SurfaceGraph3D.java
+├── benchmark/ - Graphics benchmark
+│ ├── GraphicsBenchmark.java
+│ ├── BenchmarkTest.java
+│ ├── TestResult.java
+│ ├── SolidCubesTest.java
+│ ├── LitSolidCubesTest.java
+│ ├── TexturedCubesTest.java
+│ ├── TexturedCube.java
+│ ├── WireframeCubesTest.java
+│ └── StarGridTest.java
+├── essentials/ - Essential/tutorial demos
+│ ├── MinimalExample.java
+│ ├── CoordinateSystemDemo.java
+│ ├── WindingOrderDemo.java
+│ ├── ShapeGalleryDemo.java
+│ ├── CSGDemo.java
+│ └── LightSourceDemo.java
+├── RGBCubeDemo.java
+├── OctreeDemo.java
+├── SineHeightmap.java
├── TextEditorDemo.java
├── TextEditorDemo2.java
├── RainingNumbersDemo.java
-├── WindingOrderDemo.java - Tests winding order & backface culling
└── package-info.java
```
#+attr_html: :class responsive-img
#+attr_latex: :width 1000px
-[[file:overview.png]]
+[[file:Screenshots/Procedural terrain.png]]
+
The goal of this project is to show off capabilities and API usage of
[[https://www3.svjatoslav.eu/projects/sixth-3d/][Sixth 3D]] engine.
See the [[https://www3.svjatoslav.eu/projects/sixth-3d/csg/][CSG documentation]] for a detailed explanation of the boolean operations.
+** Light sources
+:PROPERTIES:
+:CUSTOM_ID: light-sources-demo
+:ID: 6f7a8b9c-0d1e-2345-f678-901234567890
+:END:
+
+: eu.svjatoslav.sixth.e3d.examples.essentials.LightSourceDemo
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[file:Screenshots/Essentials/Lights.png]]
+
+Demonstrates how multiple light sources illuminate a 3D surface and how
+their contributions combine to create complex lighting effects. A white
+sphere serves as the canvas, allowing light colors to mix visibly on its
+surface.
+
+The white base color is crucial: colored lights tint the surface based on
+their own color. A yellow light creates warm yellow highlights, while a
+blue light adds cool blue tones. Where both lights influence the same
+surface area, their contributions add together, creating blended colors.
+
+*Light configuration:*
+
+| Light | Position | Color | Intensity | Description |
+|--------+----------------------+--------+-----------+-----------------------------------|
+| Yellow | (0, -300, 0) | YELLOW | 2.0 | Directly above sphere (bright) |
+| Blue | (-200, 50, -200) | BLUE | 0.5 | Left/below/close to camera (dim) |
+
+The yellow light positioned directly above creates strong top-down
+illumination with high intensity (2.0). The blue light from the left-front
+side provides softer fill lighting at lower intensity (0.5). Low ambient
+light (30, 30, 30) ensures dramatic contrast between lit and shadowed
+areas.
+
+*Creating light sources:*
+
+#+BEGIN_SRC java
+import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightSource;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+// Create a bright yellow light above the sphere
+LightSource yellowLight = new LightSource(
+ new Point3D(0, -300, 0), // position: directly above
+ Color.YELLOW, // color
+ 2.0 // intensity: extra bright
+);
+
+// Create a dim blue light from the left-front
+LightSource blueLight = new LightSource(
+ new Point3D(-200, 50, -200),
+ Color.BLUE,
+ 0.5 // intensity: dim
+);
+#+END_SRC
+
+Each light source has three properties:
+
+| Property | Description |
+|------------+--------------------------------------|
+| Position | 3D world coordinates of the light |
+| Color | RGB color of emitted light |
+| Intensity | Brightness multiplier (1.0 = normal) |
+
+Multiple light sources add their contributions together, allowing for
+complex lighting setups like the screenshot above showing a sphere lit
+by two lights.
+
+*Key concepts demonstrated:*
+
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightSource.html][LightSource]] creation with position, color, and intensity
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/LightSourceMarker.html][LightSourceMarker]] for visualizing light positions in the scene
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setShadingEnabled(boolean)][setShadingEnabled(true)]] on shapes to enable lighting response
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.html#setAmbientLight(eu.svjatoslav.sixth.e3d.renderer.raster.Color)][Ambient light]] configuration for base illumination
+- Multiple lights contributing to surface shading simultaneously
+
+Small glowing dots mark each light source position, making it easy to
+understand where the illumination originates. These markers use the
+same color as their associated light source.
+
+See the [[https://www3.svjatoslav.eu/projects/sixth-3d/shading/][Shading & Lighting documentation]] for a detailed explanation of
+the Lambert cosine law, distance attenuation, and ambient light.
+
* Advanced examples
:PROPERTIES:
:CUSTOM_ID: example-scenes
including cursor keys. To navigate around the world again, the window must be
unfocused first by pressing the ESC key.
-
-+ TODO:
- + Improve focus handling:
- + Perhaps add shortcut to navigate world without exiting entire
- stack of focus.
- + Possibility to retain and reuse recently focused elements.
- + Store user location in the world and view direction with the
- focused window. So that when returning focus to far away object,
- user is redirected also to proper location in the world.
- + Possibility to store recently visited locations in the world and
- return to them.
-
** Text editors demo gallery
:PROPERTIES:
:CUSTOM_ID: text-editors-demo-gallery
#+attr_latex: :width 1000px
[[file:Screenshots/Mathematical formulas.png]]
-+ TODO: instead of projecting 2D visualizations onto 3D space,
- visualize some formula using all 3 dimensions available.
-
** Sine heightmap and sphere
:PROPERTIES:
:CUSTOM_ID: sine-heightmaps-and-sphere
the scene to raytrace the current view through the compressed voxel
data structure.
-
** Point cloud galaxy
:PROPERTIES:
:CUSTOM_ID: point-cloud-galaxy
This demo showcases the engine's ability to efficiently render large
numbers of billboard-style particles with per-point coloring.
-
** Raining numbers
:PROPERTIES:
:CUSTOM_ID: raining-numbers
- World wrapping for infinite effects
- Per-instance color randomization
+** RGB cube
+:PROPERTIES:
+:CUSTOM_ID: rgb-cube
+:ID: a1b2c3d4-e5f6-7890-1234-567890abcdef
+:END:
+
+: eu.svjatoslav.sixth.e3d.examples.RGBCubeDemo
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[file:Screenshots/RGB cube.png]]
+
+Volumetric RGB color cube visualization composed of 10×10×10 square
+plates (1,000 total) arranged as flat XY planes along the Z axis. Each
+plate is built from two textured triangles using a solid-color texture
+with a black border.
+
+*Color mapping:*
+
+Each square's color is determined by its 3D grid position:
+
+| Axis | Color channel | Range | Description |
+|------+---------------+----------+--------------------------------|
+| X | Red | 0–255 | Increases left to right |
+| Y | Green | 0–255 | Increases top to bottom |
+| Z | Blue | 0–255 | Increases front to back |
+
+*Visual characteristics:*
+
+- 10 parallel Z-layers, each containing a 10×10 grid of square plates
+- Each plate: two textured triangles with a 32×32 pixel solid-color texture
+- Black border (3 pixels) around each plate for visual separation
+- Square size: 40×40 world units
+- Cube extends ±200 units from origin in each axis
+- Colors range from black (0,0,0) at one corner to white (255,255,255) at opposite corner
+
+*Usage:*
+
+Navigate around and through the cube to explore color relationships:
+- Outer faces show primary/secondary color gradients
+- Interior reveals color mixing in 3D space
+- Fly through layers to see how blue component changes with depth
+
+This demo demonstrates:
+- Textured triangle rendering with per-cell color assignment
+- 3D color space visualization
+- Systematic grid-based geometry generation
** Procedural terrain
:PROPERTIES:
*Usage:*
1. Launch from demo launcher or run main class directly
-2. Intro dialog appears with instructions
-3. Click "OK" to begin benchmark
-4. Press *Space* to skip current test (reduces measurement precision)
-5. Results dialog appears after all tests complete
-6. Click "Copy to Clipboard" to save results for comparison
-
-*Camera motion:*
+2. Intro dialog appears with three options:
+ - *Short Test* — runs all tests once with current thread count (30 seconds per test)
+ - *Extended Test* — runs all tests multiple times with increasing thread counts (5 seconds per test) to measure threading scaling
+ - *Cancel* — abort without running
+3. Press *Space* to skip current test (reduces measurement precision)
+4. Results dialog appears after all tests complete
+5. Click "Copy to Clipboard" to save results for comparison
-The camera follows a deterministic orbital path ensuring reproducible
-results:
-- Circular orbit at 1,200 unit radius around scene center (0,0,0)
-- Sinusoidal vertical wobble (±800 units) at 1.8934× orbit frequency
-- Continuous rotation
-
-*Example benchmark report:*
+*Short test example:*
#+begin_example
================================================================================
Resolution: 1920x1080
Cubes: 4096 (16x16x16 grid)
Duration: 30 seconds per test
+Mode: Short
--------------------------------------------------------------------------------
SYSTEM INFORMATION
================================================================================
#+end_example
+*Extended test:*
+
+The extended test measures how rendering performance scales with thread count.
+It starts with 1 thread, then increases by 1.5× (rounded, minimum +1) until
+reaching the CPU core count. Each test scene runs for 5 seconds per thread
+count step. The result is a matrix showing average FPS for each scene × thread
+count combination.
+
+*Extended test example:*
+
+#+begin_example
+================================================================================
+ GRAPHICS BENCHMARK RESULTS
+================================================================================
+Date: 2026-05-12 01:13:41
+Resolution: 1920x1080
+Cubes: 4096 (16x16x16 grid)
+Duration: 5 seconds per test
+Mode: Extended (multi-thread)
+
+--------------------------------------------------------------------------------
+SYSTEM INFORMATION
+--------------------------------------------------------------------------------
+CPU Name: AMD Ryzen AI 9 HX 370 w/ Radeon 890M
+Arch: amd64
+CPU cores: 24
+
+--------------------------------------------------------------------------------
+THREAD SCALING RESULTS (Avg FPS)
+--------------------------------------------------------------------------------
+Test \ Threads 1 2 3 5 8 12 18 24
+--------------------------------------------------------------------------------
+Solid Cubes 21.03 26.52 29.29 31.13 30.93 26.34 29.10 25.49
+Lit Solid Cubes 28.21 34.07 36.08 26.46 25.49 31.42 25.17 25.33
+Textured Cubes 17.08 25.52 25.51 25.98 25.24 24.61 19.51 26.37
+Wireframe Cubes 18.80 29.55 32.02 27.89 26.42 31.89 30.71 31.64
+Star Grid 185.09 255.10 266.75 267.00 281.60 297.08 291.88 310.74
+================================================================================
+#+end_example
+
The benchmark captures CPU name, architecture, and core count for
-cross-system comparisons. Results are sorted by test execution order
-and show average FPS with two decimal precision.
+cross-system comparisons. The thread scaling matrix makes it easy to
+identify the optimal thread count for each rendering workload.
* Source code
:PROPERTIES:
+++ /dev/null
-/*
- * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.sixth.e3d.examples;
-
-import eu.svjatoslav.sixth.e3d.geometry.Point3D;
-import eu.svjatoslav.sixth.e3d.gui.ViewFrame;
-import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
-import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
-import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
-import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
-import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonArrow;
-import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCone;
-import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.Grid3D;
-
-import static eu.svjatoslav.sixth.e3d.geometry.Point3D.point;
-import static eu.svjatoslav.sixth.e3d.renderer.raster.Color.hex;
-
-/**
- * Demo showcasing the SolidPolygonArrow shape with various colors, sizes,
- * orientations, and transparency levels.
- *
- * <p>This demo displays arrows pointing in different directions to demonstrate
- * the flexibility of the arrow shape. A 3D grid provides spatial reference.</p>
- */
-public class ArrowDemo {
-
- /**
- * Entry point for the arrow demo.
- *
- * @param args command line arguments (ignored)
- */
- public static void main(final String[] args) {
- final ViewFrame viewFrame = new ViewFrame("Arrow Demo");
- final ViewPanel viewPanel = viewFrame.getViewPanel();
- final ShapeCollection shapes = viewPanel.getRootShapeCollection();
-
- // Position camera to view the scene
- viewPanel.getCamera().getTransform().setTranslation(point(0, -200, -600));
-
- // Add a 3D grid for spatial reference
- final LineAppearance gridAppearance = new LineAppearance(1, hex("64646450"));
- final Grid3D grid = new Grid3D(
- point(-300, -200, -300),
- point(300, 200, 300),
- 100,
- gridAppearance
- );
- shapes.addShape(grid);
-
- // Create arrows pointing in different directions with different properties
-
- // Red arrow pointing up (negative Y direction)
- final SolidPolygonArrow redArrow = new SolidPolygonArrow(
- point(0, 150, 0),
- point(0, -150, 0),
- 8, Color.RED
- );
- shapes.addShape(redArrow);
-
- // Green arrow pointing along positive X
- final SolidPolygonArrow greenArrow = new SolidPolygonArrow(
- point(-200, 0, 0),
- point(0, 0, 0),
- 6, Color.GREEN
- );
- shapes.addShape(greenArrow);
-
- // Blue arrow pointing along positive Z
- final SolidPolygonArrow blueArrow = new SolidPolygonArrow(
- point(0, 0, -200),
- point(0, 0, 0),
- 6, Color.BLUE
- );
- shapes.addShape(blueArrow);
-
- // Yellow arrow pointing diagonally
- final SolidPolygonArrow yellowArrow = new SolidPolygonArrow(
- point(100, 100, 100),
- point(300, -100, 300),
- 10, Color.YELLOW
- );
- shapes.addShape(yellowArrow);
-
- // Semi-transparent cyan arrow (50% opacity)
- final SolidPolygonArrow transparentCyanArrow = new SolidPolygonArrow(
- point(-150, 50, -100),
- point(-50, -100, 100),
- 8, hex("00FFFF80")
- );
- shapes.addShape(transparentCyanArrow);
-
- // Semi-transparent magenta arrow (25% opacity)
- final SolidPolygonArrow transparentMagentaArrow = new SolidPolygonArrow(
- point(50, 200, 50),
- point(50, 0, 50),
- 12, hex("FF00FF40")
- );
- shapes.addShape(transparentMagentaArrow);
-
- // Small white arrows forming a circle pattern
- for (int i = 0; i < 8; i++) {
- final double angle = i * Math.PI / 4;
- final double radius = 250;
- final double startX = radius * Math.cos(angle);
- final double startZ = radius * Math.sin(angle);
- final double endX = (radius + 80) * Math.cos(angle);
- final double endZ = (radius + 80) * Math.sin(angle);
-
- final SolidPolygonArrow circleArrow = new SolidPolygonArrow(
- point(startX, -80, startZ),
- point(endX, -80, endZ),
- 4, Color.WHITE
- );
- shapes.addShape(circleArrow);
- }
-
- // A standalone cone to demonstrate SolidPolygonCone
- final SolidPolygonCone standaloneCone = new SolidPolygonCone(
- point(-300, 0, 0),
- 40,
- 80,
- 16,
- hex("FF8000FF")
- );
- shapes.addShape(standaloneCone);
-
- viewPanel.repaintDuringNextViewUpdate();
- }
-}
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ *
+ */
+
+package eu.svjatoslav.sixth.e3d.examples;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.ViewFrame;
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureGenerator;
+
+import static eu.svjatoslav.sixth.e3d.geometry.Point3D.point;
+
+/**
+ * Demo showing a 3D cube of colored square plates where each axis represents a color channel.
+ * <p>
+ * Creates a 10x10x10 grid of square plates arranged as flat XY planes.
+ * Each plate is rendered as two textured triangles using a solid-color texture
+ * with a black border. Color is determined by the plate's 3D grid position:
+ * </p>
+ * <ul>
+ * <li>X position → Red intensity (0-255)</li>
+ * <li>Y position → Green intensity (0-255)</li>
+ * <li>Z position → Blue intensity (0-255)</li>
+ * </ul>
+ * <p>
+ * The black border around each plate visually separates individual cells.
+ * The result is a volumetric RGB color cube that can be explored by flying around.
+ * </p>
+ */
+public class RGBCubeDemo {
+
+ /** Number of squares per dimension. */
+ private static final int GRID_SIZE = 10;
+
+ /** Size of each square in world units. */
+ private static final double SQUARE_SIZE = 40;
+
+ /** Spacing between square centers (20% gap = 1.25 × square size). */
+ private static final double SPACING = SQUARE_SIZE * 1.25;
+
+ /** Half the grid size for centering calculations. */
+ private static final double HALF_GRID = GRID_SIZE / 2.0;
+
+ /** Camera distance from the cube center. */
+ private static final double CAMERA_DISTANCE = 600;
+
+ /** Texture size in pixels for each square. */
+ private static final int TEXTURE_SIZE = 32;
+
+ /** Width of the black border around each square in pixels. */
+ private static final int BORDER_WIDTH = 3;
+
+ /** Border color (black) applied to all squares. */
+ private static final Color BORDER_COLOR = new Color(0, 0, 0);
+
+ /**
+ * Entry point for the RGB cube demo.
+ *
+ * @param args command line arguments (ignored)
+ */
+ public static void main(final String[] args) {
+ final ViewFrame viewFrame = new ViewFrame("RGB Cube");
+
+ viewFrame.getViewPanel().getCamera().getTransform().set(-405.73, -407.15, -713.60, -0.64, -0.42, 0.00);
+
+
+ final ShapeCollection geometryCollection = viewFrame.getViewPanel()
+ .getRootShapeCollection();
+
+ createRGBCube(geometryCollection);
+
+ viewFrame.getViewPanel().repaintDuringNextViewUpdate();
+ }
+
+ /**
+ * Creates the RGB color cube composed of colored square plates.
+ * <p>
+ * Generates 10 Z-layers, each containing a 10x10 grid of square plates.
+ * Each plate consists of two textured triangles with a solid-color texture
+ * bordered in black. Color is calculated from normalized grid position.
+ * </p>
+ *
+ * @param shapeCollection the collection to add plates to
+ */
+ private static void createRGBCube(final ShapeCollection shapeCollection) {
+ final double maxIndex = GRID_SIZE - 1;
+
+ for (int gz = 0; gz < GRID_SIZE; gz++) {
+ for (int gy = 0; gy < GRID_SIZE; gy++) {
+ for (int gx = 0; gx < GRID_SIZE; gx++) {
+ createSquare(shapeCollection, gx, gy, gz, maxIndex);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a single textured square plate at the specified grid position.
+ * <p>
+ * The plate is built from two textured triangles positioned in world space
+ * and colored based on normalized grid coordinates. A black border surrounds
+ * each plate to make individual cells visually distinct.
+ * </p>
+ *
+ * @param shapeCollection the collection to add the plate to
+ * @param gx grid X position (0 to GRID_SIZE-1)
+ * @param gy grid Y position (0 to GRID_SIZE-1)
+ * @param gz grid Z position (0 to GRID_SIZE-1)
+ * @param maxIndex maximum grid index (GRID_SIZE - 1)
+ */
+ private static void createSquare(final ShapeCollection shapeCollection,
+ final int gx, final int gy, final int gz,
+ final double maxIndex) {
+ final double x = (gx - HALF_GRID) * SPACING + SPACING / 2;
+ final double y = (gy - HALF_GRID) * SPACING + SPACING / 2;
+ final double z = (gz - HALF_GRID) * SPACING + SPACING / 2;
+
+ final int r = (int) ((gx / maxIndex) * 255);
+ final int g = (int) ((gy / maxIndex) * 255);
+ final int b = (int) ((gz / maxIndex) * 255);
+
+ final Color fillColor = new Color(r, g, b);
+
+ final Texture texture = TextureGenerator.solidWithBorder(
+ TEXTURE_SIZE, fillColor, BORDER_COLOR, BORDER_WIDTH, 0);
+
+ final double halfSize = SQUARE_SIZE / 2;
+
+ final Point3D p1 = point(x - halfSize, y - halfSize, z);
+ final Point3D p2 = point(x + halfSize, y - halfSize, z);
+ final Point3D p3 = point(x + halfSize, y + halfSize, z);
+ final Point3D p4 = point(x - halfSize, y + halfSize, z);
+
+ final Point2D t00 = new Point2D(0, 0);
+ final Point2D t10 = new Point2D(TEXTURE_SIZE, 0);
+ final Point2D t01 = new Point2D(0, TEXTURE_SIZE);
+ final Point2D t11 = new Point2D(TEXTURE_SIZE, TEXTURE_SIZE);
+
+ // Correct winding order for CCW in Y-down screen coords (front-facing)
+ // Following WindingOrderDemo pattern: upper → lower-left → lower-right
+ // Triangle 1: p1 (upper-left) → p4 (lower-left) → p3 (lower-right)
+ final TexturedTriangle tri1 = new TexturedTriangle(
+ new Vertex(p1, t00),
+ new Vertex(p4, t01),
+ new Vertex(p3, t11),
+ texture
+ );
+ tri1.setBackfaceCulling(false);
+
+ // Triangle 2: p1 (upper-left) → p3 (lower-right) → p2 (upper-right)
+ final TexturedTriangle tri2 = new TexturedTriangle(
+ new Vertex(p1, t00),
+ new Vertex(p3, t11),
+ new Vertex(p2, t10),
+ texture
+ );
+ tri2.setBackfaceCulling(false);
+
+ shapeCollection.addShape(tri1);
+ shapeCollection.addShape(tri2);
+ }
+}
private static final double WOBBLE_AMPLITUDE = 800;
private static final int TEST_DURATION_MS = 30000;
+ /** Benchmark mode: short (single run) or extended (multiple thread counts). */
+ private enum BenchmarkMode { SHORT, EXTENDED }
+
private ViewFrame viewFrame;
private ViewPanel viewPanel;
private ShapeCollection shapes;
private boolean pendingTestTransition = false;
private final List<TestResult> results = new ArrayList<>();
private final List<BenchmarkTest> tests = new ArrayList<>();
+ private BenchmarkMode mode;
+
+ // Extended mode state
+ private int currentThreadCount = 1;
+ private int threadCountIndex = 0;
+ private final List<Integer> threadCounts = new ArrayList<>();
/**
* Entry point for the graphics benchmark.
*/
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
- if (showIntroDialog()) {
- new GraphicsBenchmark();
+ BenchmarkMode choice = showIntroDialog();
+ if (choice != null) {
+ new GraphicsBenchmark(choice);
}
});
}
- private static boolean showIntroDialog() {
+ private static BenchmarkMode showIntroDialog() {
String message =
"<html><div style='width:400px; font-family: sans-serif;'>" +
"<h2>Graphics Benchmark</h2>" +
"<span style='color: gray;'>(skipping reduces measurement precision)</span></li>" +
"<li>A summary will be shown at the end</li>" +
"</ul>" +
+ "<p><b>Extended test</b> runs all tests multiple times with increasing " +
+ "thread counts (1 to " + Runtime.getRuntime().availableProcessors() + " cores) " +
+ "to measure threading scaling.</p>" +
"</div></html>";
- int choice = JOptionPane.showConfirmDialog(
- null,
- message,
- "Graphics Benchmark",
- JOptionPane.OK_CANCEL_OPTION,
- JOptionPane.INFORMATION_MESSAGE
- );
+ JButton shortButton = new JButton("Short Test");
+ JButton extendedButton = new JButton("Extended Test");
+ JButton cancelButton = new JButton("Cancel");
+
+ JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 0));
+ buttonPanel.add(shortButton);
+ buttonPanel.add(extendedButton);
+ buttonPanel.add(cancelButton);
+
+ final BenchmarkMode[] result = {null};
+
+ shortButton.addActionListener(e -> {
+ result[0] = BenchmarkMode.SHORT;
+ Window w = SwingUtilities.getWindowAncestor(shortButton);
+ if (w != null) w.dispose();
+ });
+ extendedButton.addActionListener(e -> {
+ result[0] = BenchmarkMode.EXTENDED;
+ Window w = SwingUtilities.getWindowAncestor(extendedButton);
+ if (w != null) w.dispose();
+ });
+ cancelButton.addActionListener(e -> {
+ Window w = SwingUtilities.getWindowAncestor(cancelButton);
+ if (w != null) w.dispose();
+ });
+
+ JPanel panel = new JPanel(new BorderLayout(0, 10));
+ panel.add(new JLabel(message), BorderLayout.CENTER);
+ panel.add(buttonPanel, BorderLayout.SOUTH);
+ panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
- return choice == JOptionPane.OK_OPTION;
+ JDialog dialog = new JDialog((Frame) null, "Graphics Benchmark", true);
+ dialog.setContentPane(panel);
+ dialog.pack();
+ dialog.setLocationRelativeTo(null);
+ dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+ dialog.setVisible(true);
+
+ return result[0];
}
/**
* Constructs and runs the graphics benchmark.
+ *
+ * @param mode SHORT for single run, EXTENDED for multi-thread-count run
*/
- public GraphicsBenchmark() {
+ public GraphicsBenchmark(BenchmarkMode mode) {
+ this.mode = mode;
+ if (mode == BenchmarkMode.EXTENDED) {
+ buildThreadCountList();
+ }
registerTests();
initializeWindow();
}
+ /**
+ * Builds the list of thread counts for extended mode.
+ * Starts at 1, each step multiplies by 1.5 and rounds to nearest integer,
+ * always increasing by at least 1. Stops when reaching available processors.
+ */
+ private void buildThreadCountList() {
+ int maxCores = Runtime.getRuntime().availableProcessors();
+ int count = 1;
+ threadCounts.add(count);
+ while (count < maxCores) {
+ int next = (int) Math.round(count * 1.5);
+ if (next <= count) {
+ next = count + 1;
+ }
+ if (next > maxCores) {
+ next = maxCores;
+ }
+ threadCounts.add(next);
+ count = next;
+ }
+ }
+
private void initializeWindow() {
viewFrame = new ViewFrame("Graphics Benchmark", WINDOW_WIDTH, WINDOW_HEIGHT);
viewPanel = viewFrame.getViewPanel();
}
private void startNextTest() {
- int nextIndex = results.size();
- if (nextIndex >= tests.size()) {
- scheduleBenchmarkFinish();
- return;
+ int testIndex;
+ if (mode == BenchmarkMode.EXTENDED) {
+ // In extended mode, cycle through: test0@threads[0], test1@threads[0], ..., testN@threads[0],
+ // then test0@threads[1], test1@threads[1], ..., testN@threads[1], etc.
+ testIndex = results.size() % tests.size();
+ int roundNumber = results.size() / tests.size();
+
+ if (roundNumber >= threadCounts.size()) {
+ scheduleBenchmarkFinish();
+ return;
+ }
+
+ currentThreadCount = threadCounts.get(roundNumber);
+ viewPanel.setNumRenderThreads(currentThreadCount);
+ } else {
+ testIndex = results.size();
+ if (testIndex >= tests.size()) {
+ scheduleBenchmarkFinish();
+ return;
+ }
}
- currentTest = tests.get(nextIndex);
+ currentTest = tests.get(testIndex);
testFinished = false;
orbitAngle = 0;
frameCount = 0;
testFinished = true;
long elapsed = System.currentTimeMillis() - testStartTime;
double durationSeconds = elapsed / 1000.0;
- results.add(new TestResult(currentTest.getName(), frameCount, durationSeconds));
+ int threads = (mode == BenchmarkMode.EXTENDED) ? currentThreadCount : viewPanel.getNumRenderThreads();
+ results.add(new TestResult(currentTest.getName(), frameCount, durationSeconds, threads));
pendingTestTransition = true;
}
sb.append("Resolution: ").append(WINDOW_WIDTH).append("x").append(WINDOW_HEIGHT).append("\n");
sb.append("Cubes: ").append(GRID_SIZE * GRID_SIZE * GRID_SIZE)
.append(" (").append(GRID_SIZE).append("x").append(GRID_SIZE).append("x").append(GRID_SIZE).append(" grid)\n");
- sb.append("Duration: ").append(TEST_DURATION_MS / 1000).append(" seconds per test\n");
+ sb.append("Duration: ").append(
+ mode == BenchmarkMode.EXTENDED ? "5" : String.valueOf(TEST_DURATION_MS / 1000))
+ .append(" seconds per test\n");
+ sb.append("Mode: ").append(mode == BenchmarkMode.EXTENDED ? "Extended (multi-thread)" : "Short").append("\n");
sb.append("\n");
sb.append(thinSeparator).append("\n");
sb.append("SYSTEM INFORMATION\n");
sb.append("Arch: ").append(System.getProperty("os.arch")).append("\n");
sb.append("CPU cores: ").append(runtime.availableProcessors()).append("\n");
sb.append("\n");
+
+ if (mode == BenchmarkMode.EXTENDED) {
+ formatExtendedResults(sb, thinSeparator);
+ } else {
+ formatShortResults(sb, thinSeparator);
+ }
+
+ sb.append(separator).append("\n");
+
+ return sb.toString();
+ }
+
+ private void formatShortResults(StringBuilder sb, String thinSeparator) {
sb.append(thinSeparator).append("\n");
sb.append(String.format("%-28s %s%n", "Test", "Avg FPS"));
sb.append(thinSeparator).append("\n");
for (TestResult result : results) {
sb.append(String.format("%-28s %.2f%n", result.testName, result.averageFps));
}
+ }
- sb.append(separator).append("\n");
+ private void formatExtendedResults(StringBuilder sb, String thinSeparator) {
+ // Build a 2D matrix: rows = test names, columns = thread counts
+ // Collect unique test names in order and thread counts in order
+ List<String> testNames = tests.stream().map(BenchmarkTest::getName).toList();
- return sb.toString();
+ // Column width: enough for FPS values like "1234.56"
+ int nameWidth = 28;
+ int colWidth = 10;
+
+ // Header row
+ sb.append(thinSeparator).append("\n");
+ sb.append("THREAD SCALING RESULTS (Avg FPS)\n");
+ sb.append(thinSeparator).append("\n");
+
+ StringBuilder header = new StringBuilder(String.format("%-" + nameWidth + "s", "Test \\ Threads"));
+ for (int tc : threadCounts) {
+ header.append(String.format("%" + colWidth + "d", tc));
+ }
+ sb.append(header).append("\n");
+ sb.append(thinSeparator).append("\n");
+
+ // Data rows
+ for (String testName : testNames) {
+ StringBuilder row = new StringBuilder(String.format("%-" + nameWidth + "s", testName));
+ for (int tc : threadCounts) {
+ double fps = findFps(testName, tc);
+ if (fps >= 0) {
+ row.append(String.format("%" + colWidth + ".2f", fps));
+ } else {
+ row.append(String.format("%" + colWidth + "s", "-"));
+ }
+ }
+ sb.append(row).append("\n");
+ }
+ }
+
+ /**
+ * Finds the FPS for a given test name and thread count from the results.
+ * @return average FPS, or -1 if not found
+ */
+ private double findFps(String testName, int threadCount) {
+ for (TestResult result : results) {
+ if (result.testName.equals(testName) && result.threadCount == threadCount) {
+ return result.averageFps;
+ }
+ }
+ return -1;
}
private void showResultsDialog() {
frameCount++;
long elapsed = System.currentTimeMillis() - testStartTime;
- if (elapsed >= TEST_DURATION_MS && !testFinished) {
+ int testDuration = (mode == BenchmarkMode.EXTENDED) ? 5000 : TEST_DURATION_MS;
+ if (elapsed >= testDuration && !testFinished) {
finishCurrentTest();
}
/** Average frames per second achieved during the test. */
public final double averageFps;
+ /** Number of render threads used during this test. */
+ public final int threadCount;
+
/**
* Creates a test result record.
* @param testName the name of the test
* @param frameCount total frames rendered
* @param durationSeconds test duration in seconds
+ * @param threadCount number of render threads used
*/
- public TestResult(String testName, long frameCount, double durationSeconds) {
+ public TestResult(String testName, long frameCount, double durationSeconds, int threadCount) {
this.testName = testName;
this.frameCount = frameCount;
this.durationSeconds = durationSeconds;
this.averageFps = frameCount / durationSeconds;
+ this.threadCount = threadCount;
}
-}
\ No newline at end of file
+}
package eu.svjatoslav.sixth.e3d.examples.benchmark;
import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureGenerator;
import java.util.ArrayList;
import java.util.List;
private void initializeTextures() {
textures = new Texture[TEXTURE_COUNT];
for (int i = 0; i < TEXTURE_COUNT; i++) {
- textures[i] = createGlowTexture(
+ final Color color = new Color(
50 + random.nextInt(200),
50 + random.nextInt(200),
50 + random.nextInt(200)
);
+ textures[i] = TextureGenerator.glowingBorder(64, color, 6, 120, true, 2);
}
}
}
}
- private Texture createGlowTexture(int r, int g, int b) {
- int texSize = 64;
- Texture texture = new Texture(texSize, texSize, 2);
-
- java.awt.Graphics2D gr = texture.graphics;
- gr.setBackground(new java.awt.Color(r, g, b, 30));
- gr.clearRect(0, 0, texSize, texSize);
-
- int glowWidth = 6;
- for (int i = 0; i < glowWidth; i++) {
- int intensity = (int) (120.0 * (glowWidth - i) / glowWidth);
- java.awt.Color glowColor = new java.awt.Color(
- Math.min(255, r + intensity),
- Math.min(255, g + intensity),
- Math.min(255, b + intensity),
- 50 - i * 8
- );
- gr.setColor(glowColor);
- gr.drawRect(i, i, texSize - 1 - 2 * i, texSize - 1 - 2 * i);
- }
-
- gr.dispose();
- texture.resetResampledBitmapCache();
- return texture;
- }
-}
\ No newline at end of file
+ }
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ *
+ */
+package eu.svjatoslav.sixth.e3d.examples.essentials;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.ViewFrame;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightSource;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.LightSourceMarker;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonSphere;
+
+import static eu.svjatoslav.sixth.e3d.geometry.Point3D.origin;
+import static eu.svjatoslav.sixth.e3d.geometry.Point3D.point;
+import static eu.svjatoslav.sixth.e3d.renderer.raster.Color.BLUE;
+import static eu.svjatoslav.sixth.e3d.renderer.raster.Color.WHITE;
+import static eu.svjatoslav.sixth.e3d.renderer.raster.Color.YELLOW;
+import static eu.svjatoslav.sixth.e3d.renderer.raster.Color.hex;
+
+/**
+ * Demo showcasing light sources and their effect on shaded surfaces.
+ *
+ * <p>This demo displays a white sphere illuminated by two colored light sources:
+ * a bright yellow light on the right side and a dim blue light on the left side.
+ * The white sphere base color allows the light colors to mix visibly on the
+ * surface, demonstrating how multiple light sources contribute to shading.</p>
+ *
+ * <p><b>Run this demo:</b></p>
+ * <pre>{@code
+ * java -cp target/sixth-3d-demos.jar eu.svjatoslav.sixth.e3d.examples.essentials.LightSourceDemo
+ * }</pre>
+ *
+ * <p><b>Key concepts demonstrated:</b></p>
+ * <ul>
+ * <li>LightSource creation with position, color, and intensity</li>
+ * <li>LightSourceMarker for visualizing light positions</li>
+ * <li>ShadingEnabled on SolidPolygonSphere</li>
+ * <li>Ambient light configuration</li>
+ * <li>Multiple lights adding their contributions</li>
+ * </ul>
+ *
+ * @see LightSource
+ * @see LightSourceMarker
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager
+ */
+public class LightSourceDemo {
+
+ /**
+ * Sphere radius in world units.
+ */
+ private static final double SPHERE_RADIUS = 100;
+
+ /**
+ * Number of segments for smooth sphere rendering.
+ */
+ private static final int SPHERE_SEGMENTS = 16;
+
+ /**
+ * Distance from sphere center to light sources.
+ */
+ private static final double LIGHT_DISTANCE = 200;
+
+ /**
+ * Entry point for the light sources demo.
+ *
+ * @param args command line arguments (ignored)
+ */
+ public static void main(final String[] args) {
+ final ViewFrame viewFrame = new ViewFrame("Light Sources Demo");
+ final ViewPanel viewPanel = viewFrame.getViewPanel();
+ final ShapeCollection shapes = viewPanel.getRootShapeCollection();
+
+ // Position camera to view sphere from front
+ viewPanel.getCamera().getTransform().set(0, 0, -500, 0, 0, 0);
+
+ // Low ambient light for dramatic contrast between lit and unlit areas
+ viewPanel.getLightingManager().setAmbientLight(hex("1E1E1E"));
+
+ // Create white sphere with shading enabled
+ createSphere(shapes);
+
+ // Create yellow light on the right side (bright)
+ createYellowLight(viewPanel, shapes);
+
+ // Create blue light on the left side (dim)
+ createBlueLight(viewPanel, shapes);
+
+ viewPanel.repaintDuringNextViewUpdate();
+ }
+
+ /**
+ * Creates the white sphere centered at origin.
+ *
+ * <p>The white base color allows light colors to be visible on the surface.
+ * Shading is enabled so the sphere responds to light sources, and backface
+ * culling is enabled since it's a closed mesh.</p>
+ *
+ * @param shapes the shape collection to add the sphere to
+ */
+ private static void createSphere(final ShapeCollection shapes) {
+ final SolidPolygonSphere sphere = new SolidPolygonSphere(
+ origin(),
+ SPHERE_RADIUS,
+ SPHERE_SEGMENTS,
+ WHITE);
+
+ sphere.setShadingEnabled(true);
+ sphere.setBackfaceCulling(true);
+ shapes.addShape(sphere);
+ }
+
+ /**
+ * Creates a bright yellow light source directly above the sphere.
+ *
+ * <p>Position: directly above (-Y in Sixth 3D coordinate system).
+ * Intensity is set to 2.0 for extra brightness.</p>
+ *
+ * @param viewPanel the view panel to add the light to its lighting manager
+ * @param shapes the shape collection to add the light marker to
+ */
+ private static void createYellowLight(final ViewPanel viewPanel, final ShapeCollection shapes) {
+ // Position: directly above the sphere
+ final Point3D yellowPosition = point(0, -LIGHT_DISTANCE * 1.5, 0);
+
+ final LightSource yellowLight = new LightSource(
+ yellowPosition,
+ YELLOW,
+ 2.0 // extra bright
+ );
+
+ viewPanel.getLightingManager().addLight(yellowLight);
+
+ // Visual marker showing light position
+ shapes.addShape(new LightSourceMarker(yellowPosition, YELLOW));
+ }
+
+ /**
+ * Creates a dim blue light source on the left side of the sphere.
+ *
+ * <p>Position: left (-X), below (+Y), close to camera (-Z).
+ * Intensity is set to 0.5 for dim illumination.</p>
+ *
+ * @param viewPanel the view panel to add the light to its lighting manager
+ * @param shapes the shape collection to add the light marker to
+ */
+ private static void createBlueLight(final ViewPanel viewPanel, final ShapeCollection shapes) {
+ // Position: left, below, close to camera
+ final Point3D bluePosition = point(-LIGHT_DISTANCE, LIGHT_DISTANCE / 4, -LIGHT_DISTANCE);
+
+ final LightSource blueLight = new LightSource(
+ bluePosition,
+ BLUE,
+ 1.5 // dim
+ );
+
+ viewPanel.getLightingManager().addLight(blueLight);
+
+ // Visual marker showing light position
+ shapes.addShape(new LightSourceMarker(bluePosition, BLUE));
+ }
+}
\ No newline at end of file
new DemoEntry("CSG demo",
"Boolean operations: union, subtract, intersect on 3D shapes",
new ShowCSG()),
+
+ new DemoEntry("Light sources",
+ "Sphere illuminated by 2 colored lights (yellow + blue)",
+ new ShowLightSources()),
};
private static final DemoEntry[] OTHER_DEMOS = {
new DemoEntry("Procedural Terrain",
"Procedural mountains with 3 colored light sources",
new ShowTerrainDemo()),
+ new DemoEntry("RGB cube",
+ "10x10x10 grid of textured square plates (X=Red, Y=Green, Z=Blue)",
+ new ShowRGBCube()),
new DemoEntry("Graphics benchmark",
"Automated performance measuring FPS across modes",
new ShowGraphicsBenchmark()),
CSGDemo.main(null);
}
}
+
+ private static class ShowRGBCube extends AbstractAction {
+ ShowRGBCube() {
+ putValue(NAME, "RGB cube");
+ }
+
+ @Override
+ public void actionPerformed(final ActionEvent e) {
+ RGBCubeDemo.main(null);
+ }
+ }
+
+ private static class ShowLightSources extends AbstractAction {
+ ShowLightSources() {
+ putValue(NAME, "Light sources");
+ }
+
+ @Override
+ public void actionPerformed(final ActionEvent e) {
+ LightSourceDemo.main(null);
+ }
+ }
}
\ No newline at end of file