feat(demos): add RGB cube demo, light source demo, and extended benchmark feat
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Tue, 12 May 2026 19:09:30 +0000 (22:09 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Tue, 12 May 2026 19:09:30 +0000 (22:09 +0300)
Add RGBCubeDemo: volumetric 10x10x10 color cube with textured triangles
and per-cell color mapping (X=Red, Y=Green, Z=Blue).

Add LightSourceDemo: white sphere illuminated by two colored lights
(yellow + blue) demonstrating multiple light contribution and shading.

Extend GraphicsBenchmark with short/extended test modes. Extended mode
measures thread scaling from 1 to available cores with a 1.5x step
factor, outputting a matrix of FPS per test x thread count.

Update AGENTS.md to reflect current code organization. Remove
ArrowDemo.java and doc/overview.png. Add screenshots for lights and
RGB cube. Move inline TODOs from index.org to sixth-3d/TODO.org.

Update TexturedCubesTest to use TextureGenerator.glowingBorder instead
of inline AWT texture creation.

12 files changed:
AGENTS.md
doc/Screenshots/Essentials/Lights.png [new file with mode: 0644]
doc/Screenshots/RGB cube.png [new file with mode: 0644]
doc/index.org
doc/overview.png [deleted file]
src/main/java/eu/svjatoslav/sixth/e3d/examples/ArrowDemo.java [deleted file]
src/main/java/eu/svjatoslav/sixth/e3d/examples/RGBCubeDemo.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/GraphicsBenchmark.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TestResult.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TexturedCubesTest.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/essentials/LightSourceDemo.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/launcher/ApplicationListPanel.java

index d5e1345..363b0b6 100644 (file)
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -52,22 +52,40 @@ src/main/java/eu/svjatoslav/sixth/e3d/examples/
 │   ├── 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
 ```
 
diff --git a/doc/Screenshots/Essentials/Lights.png b/doc/Screenshots/Essentials/Lights.png
new file mode 100644 (file)
index 0000000..91272de
Binary files /dev/null and b/doc/Screenshots/Essentials/Lights.png differ
diff --git a/doc/Screenshots/RGB cube.png b/doc/Screenshots/RGB cube.png
new file mode 100644 (file)
index 0000000..2f98f24
Binary files /dev/null and b/doc/Screenshots/RGB cube.png differ
index d0cd0c3..e4290c0 100644 (file)
@@ -37,7 +37,8 @@
 
 #+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.
@@ -324,6 +325,90 @@ from simple primitives through boolean combinations.
 
 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
@@ -395,18 +480,6 @@ When any window is focused, all keyboard input is redirected to that window,
 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
@@ -435,9 +508,6 @@ See also this [[https://hackers-1995.vercel.app/][similar-looking web-based demo
 #+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
@@ -475,7 +545,6 @@ partitioned [[https://en.wikipedia.org/wiki/Octree][octree]] compresses the data
 the scene to raytrace the current view through the compressed voxel
 data structure.
 
-
 ** Point cloud galaxy
 :PROPERTIES:
 :CUSTOM_ID: point-cloud-galaxy
@@ -508,7 +577,6 @@ usage and improving rendering performance.
 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
@@ -547,6 +615,53 @@ This demo demonstrates:
 - 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:
@@ -685,21 +800,15 @@ functionality.
 *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
 ================================================================================
@@ -709,6 +818,7 @@ Date:        2026-03-16 19:17:51
 Resolution:  1920x1080
 Cubes:       4096 (16x16x16 grid)
 Duration:    30 seconds per test
+Mode:        Short
 
 --------------------------------------------------------------------------------
 SYSTEM INFORMATION
@@ -728,9 +838,49 @@ Star Grid                    318.97
 ================================================================================
 #+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:
diff --git a/doc/overview.png b/doc/overview.png
deleted file mode 100644 (file)
index ba977ab..0000000
Binary files a/doc/overview.png and /dev/null differ
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/ArrowDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/ArrowDemo.java
deleted file mode 100644 (file)
index 3b45854..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/RGBCubeDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/RGBCubeDemo.java
new file mode 100644 (file)
index 0000000..752a225
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * 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);
+    }
+}
index f44bd6e..cbe2377 100644 (file)
@@ -51,6 +51,9 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
     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;
@@ -65,6 +68,12 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
     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.
@@ -72,13 +81,14 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
      */
     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>" +
@@ -89,27 +99,88 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
                 "<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();
@@ -133,13 +204,29 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
     }
 
     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;
@@ -157,7 +244,8 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         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;
     }
@@ -198,7 +286,10 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         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");
@@ -207,6 +298,19 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         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");
@@ -214,10 +318,55 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         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() {
@@ -312,7 +461,8 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         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();
         }
 
index 9c7eef2..669ffb0 100644 (file)
@@ -22,16 +22,21 @@ public class TestResult {
     /** 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
+}
index 887b29d..1b2107f 100644 (file)
@@ -6,8 +6,10 @@
 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;
@@ -80,11 +82,12 @@ public class TexturedCubesTest implements BenchmarkTest {
     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);
         }
     }
 
@@ -96,29 +99,4 @@ public class TexturedCubesTest implements BenchmarkTest {
         }
     }
 
-    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
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/essentials/LightSourceDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/essentials/LightSourceDemo.java
new file mode 100644 (file)
index 0000000..07b683b
--- /dev/null
@@ -0,0 +1,164 @@
+/*
+ * 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
index 86ed1da..30d2620 100644 (file)
@@ -49,6 +49,10 @@ class ApplicationListPanel extends JPanel {
             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 = {
@@ -89,6 +93,9 @@ class ApplicationListPanel extends JPanel {
             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()),
@@ -342,4 +349,26 @@ class ApplicationListPanel extends JPanel {
             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